【逆向工程核心原理:代码注入】

代码注入与内存操作:从原理到实战的逆向工程核心技术 在软件安全与逆向工程领域,进程内存管理和代码执行控制是两项基础且关键的技术概念。理解进程虚拟地址空间的布局(如代码段、数据段、堆栈)及其权限管理(读、写、执行),是掌握动态分析技术的基石。其核心原理在于,操作系统为每个进程提供独立的受保护空间,而代码注入技术正是通过合法或非预期的方式,在此空间内植入并执行外部代码,从而实现对程序运行时行为的主动干预。这项技术的核心价值在于,它使得安全研究人员和开发者能够深入观察、分析和验证软件的动态行为,例如进行漏洞利用验证、恶意软件行为分析或安全工具开发。从应用场景来看, 阅读详情

概念: 代码注入是一种像目标进程插入独立运行代码并使之运行的技术,它一般调用CreateRemoteThread() API 实现远程线程形式运行插入的代码,也被称为线程注入。
代码注入

与DLL注入的区别: DLL注入后,代码和数据都在DLL的数据区,DLL代码可以直接使用。代码注入仅仅是向目标进程插入必要的代码,若想让代码正常运行,必须手动提供代码所需要的数据。

代码注入的优点:
1、占用内存少:注入的代码少,不用包装成DLL。
2、难以查找痕迹:DLL注入可以观察到进程中多了DLL模块
3、其他:就是弔


代码注入(C语言)

0x01、解释一下源代码
// CodeInjection.cpp
#include "windows.h"
#include "stdio.h"
// 这个结构用来传输模块名和模块中的函数名字
/*
FARPROC:是一个系统自带的宏,是函数指针类型(对,你没看错)
pFunc:这个数组用来存储两个API函数地址(往后看就会明白)
szBuf:是存模块名字和字符串这些数据资源
*/
typedef struct _THREAD_PARAM 
{
    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
    char    szBuf[4][128];          // "user32.dll", "MessageBoxA", "www.reversecore.com", "ReverseCore"
} THREAD_PARAM, *PTHREAD_PARAM;

// 定义一个函数指针,返回值是HMODULE
typedef HMODULE (WINAPI *PFLOADLIBRARYA)
(
    LPCSTR lpLibFileName
);

// 定义一个函数指针,返回值是FARPROC(函数指针类型)
typedef FARPROC (WINAPI *PFGETPROCADDRESS)
(
    HMODULE hModule,
    LPCSTR lpProcName
);

// 定义一个函数指针,用来调用MessageBox
typedef int (WINAPI *PFMESSAGEBOXA)
(
    HWND hWnd,
    LPCSTR lpText,
    LPCSTR lpCaption,
    UINT uType
);

// 要注入的代码,lParam参数的值就是最上面的结构体
DWORD WINAPI ThreadProc(LPVOID lParam)
{
	// lParam从LPVOID强转成PHREAD_PRARM的指针类型
    PTHREAD_PARAM   pParam      = (PTHREAD_PARAM)lParam;
    HMODULE         hMod        = NULL;
    FARPROC         pFunc       = NULL;
	// 解析第一个函数:pParam->pFunc[0] 是函数地址;pParam->szBuf[0]是参数,就是模块名字
	// LoadLibrary()
    hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);    // "user32.dll"
    if( !hMod )
        return 1;
	// 解析第二个函数:pParam->pFunc[1] 是函数地址;pParam->szBuf[0] 是参数,就是函数名字
	// GetProcAddress()
	// pFunc:返回值是MessageBoxA的函数地址
    pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);  // "MessageBoxA"
    if( !pFunc )
        return 1;
	// pParam->szBuf[2],pParam->szBuf[3] 是MessageBoxA要显示的字符串
	// MessageBoxA()
    ((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);

    return 0;
}

// 实施注入的函数
BOOL InjectCode(DWORD dwPID)
{
    HMODULE         hMod            = NULL;
    // 这个结构体对象就是要传的数据(lParam)
    THREAD_PARAM    param           = {0,};
    HANDLE          hProcess        = NULL;
    HANDLE          hThread         = NULL;
    LPVOID          pRemoteBuf[2]   = {0,};
    DWORD           dwSize          = 0;
	// 获取模块句柄
    hMod = GetModuleHandleA("kernel32.dll");
	// 把模块中的两个API地址找到,放到要传的结构体中
    // set THREAD_PARAM
    param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
    param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");
    // 继续准备字符串数据
    strcpy_s(param.szBuf[0], "user32.dll");
    strcpy_s(param.szBuf[1], "MessageBoxA");
    strcpy_s(param.szBuf[2], "www.reversecore.com");
    strcpy_s(param.szBuf[3], "ReverseCore");

    // Open Process(打开要注入程序的进程,dwPID是要注入进程的PID)
    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS,   // dwDesiredAccess
                                  FALSE,                // bInheritHandle
                                  dwPID)) )             // dwProcessId
    {
        printf("OpenProcess() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    // Allocation for THREAD_PARAM
    // 在进程中开辟一段空间,大小是dwSize,也就是传入结构体数据的大小
    // 开辟空间的地址是pRemoteBuf[0]
    dwSize = sizeof(THREAD_PARAM);
    if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess,          // hProcess
                                      NULL,                 // lpAddress
                                      dwSize,               // dwSize
                                      MEM_COMMIT,           // flAllocationType
                                      PAGE_READWRITE)) )    // flProtect
    {
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }
	// 把要传入的数据写到开辟空间pRemoteBuf[0]
    if( !WriteProcessMemory(hProcess,                       // hProcess
                            pRemoteBuf[0],                  // lpBaseAddress
                            (LPVOID)&param,                 // lpBuffer
                            dwSize,                         // nSize
                            NULL) )                         // [out] lpNumberOfBytesWritten
    {
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    // Allocation for ThreadProc()
    // 开辟一段空间用来存代码
    dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
    if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess,          // hProcess
                                      NULL,                 // lpAddress
                                      dwSize,               // dwSize
                                      MEM_COMMIT,           // flAllocationType
                                      PAGE_EXECUTE_READWRITE)) )    // flProtect
    {
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }
	// 把要注入的代码数据写道pRemoteBuf[1]中
    if( !WriteProcessMemory(hProcess,                       // hProcess
                            pRemoteBuf[1],                  // lpBaseAddress
                            (LPVOID)ThreadProc,             // lpBuffer
                            dwSize,                         // nSize
                            NULL) )                         // [out] lpNumberOfBytesWritten
    {
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }
	// 在要注入的程序中创建远程线程
	// 远程线程的执行函数是注入的代码,参数是pRemoteBuf,也就是刚写入数据的地址
    if( !(hThread = CreateRemoteThread(hProcess,            // hProcess
                                       NULL,                // lpThreadAttributes
                                       0,                   // dwStackSize
                                       (LPTHREAD_START_ROUTINE)pRemoteBuf[1],     // lpStartAddress
                                       pRemoteBuf[0],       // lpParameter
                                       0,                   // dwCreationFlags
                                       NULL)) )             // lpThreadId
    {
        printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }
	// 等待线程结束
    WaitForSingleObject(hThread, INFINITE);	

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) 
{
    TOKEN_PRIVILEGES tp;
    HANDLE hToken;
    LUID luid;

    if( !OpenProcessToken(GetCurrentProcess(),
                          TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, 
			              &hToken) )
    {
        printf("OpenProcessToken error: %u\n", GetLastError());
        return FALSE;
    }

    if( !LookupPrivilegeValue(NULL,           // lookup privilege on local system
                              lpszPrivilege,  // privilege to lookup 
                              &luid) )        // receives LUID of privilege
    {
        printf("LookupPrivilegeValue error: %u\n", GetLastError() ); 
        return FALSE; 
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    if( bEnablePrivilege )
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    else
        tp.Privileges[0].Attributes = 0;

    // Enable the privilege or disable all privileges.
    if( !AdjustTokenPrivileges(hToken, 
                               FALSE, 
                               &tp, 
                               sizeof(TOKEN_PRIVILEGES), 
                               (PTOKEN_PRIVILEGES) NULL, 
                               (PDWORD) NULL) )
    { 
        printf("AdjustTokenPrivileges error: %u\n", GetLastError() ); 
        return FALSE; 
    } 

    if( GetLastError() == ERROR_NOT_ALL_ASSIGNED )
    {
        printf("The token does not have the specified privilege. \n");
        return FALSE;
    } 

    return TRUE;
}

int main(int argc, char *argv[])
{
    DWORD dwPID     = 0;

	if( argc != 2 )
	{
	    printf("\n USAGE  : %s <pid>\n", argv[0]);
		return 1;
	}

	// change privilege
	if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
        return 1;

    // code injection
    dwPID = (DWORD)atol(argv[1]);
    InjectCode(dwPID);

	return 0;
}

注:
1、windows10中,源码只有编译成32位release版本才可以注入成功注入notepad。
2、windows10中,源码只有编译成64位release版本才可以注入成功注入64位程序。

0x02、注入流程
  1. 命令行运行程序,传入进程参数
  2. InjectCode打开指定进程,在进程中开辟空间,写入ThreadProc代码;再开辟空间,写入代码需要的数据;在指定进程中开启远程线程,执行注入的代码程序;等待线程结束。
0x03、演示执行过程
  • 编译代码(32bit)
    编译

  • 运行notepad并查看PID
    PID

  • 执行InjectCode.exe

  • 执行注入

  • 结果
    结果

0x04、调试演示
  • 载入notepad进程(32bit),F9运行

  • 设置新线程断点
    设置

  • 执行InjectCode.exe

  • 调试器断下

注入的ThreadProc代码
调试结果参数数据:
参数

0x05、解释使用的API
// 打开进程
HANDLE WINAPI OpenProcess(
  __in  DWORD dwDesiredAccess,	// 要获得该进程的权限
  __in  BOOL bInheritHandle,	// 表示所得到的进程句柄是否可以被继承
  __in  DWORD dwProcessId	// 要打开进程的PID
);
/*
PROCESS_ALL_ACCESS  //所有能获得的权限
PROCESS_CREATE_PROCESS  //需要创建一个进程
PROCESS_CREATE_THREAD   //需要创建一个线程
PROCESS_DUP_HANDLE      //重复使用DuplicateHandle句柄
PROCESS_QUERY_INFORMATION   //获得进程信息的权限,如它的退出代码、优先级
PROCESS_QUERY_LIMITED_INFORMATION  /*获得某些信息的权限,如果获得了PROCESS_QUERY_INFORMATION,也拥有PROCESS_QUERY_LIMITED_INFORMATION权限
PROCESS_SET_INFORMATION    //设置某些信息的权限,如进程优先级
PROCESS_SET_QUOTA          //设置内存限制的权限,使用SetProcessWorkingSetSize
PROCESS_SUSPEND_RESUME     //暂停或恢复进程的权限
PROCESS_TERMINATE          //终止一个进程的权限,使用TerminateProcess
PROCESS_VM_OPERATION       //操作进程内存空间的权限(可用VirtualProtectEx和WriteProcessMemory)
PROCESS_VM_READ            //读取进程内存空间的权限,可使用ReadProcessMemory
PROCESS_VM_WRITE           //读取进程内存空间的权限,可使用WriteProcessMemory
SYNCHRONIZE                //等待进程终止
*/
LPVOID WINAPI VirtualAllocEx(
  __in      HANDLE hProcess,	// 要开辟空间的进程句柄
  __in_opt  LPVOID lpAddress,	// 想要获取的地址区域
  __in      SIZE_T dwSize,	// 要分配的内存区域的大小(以字节为单位)
  __in      DWORD flAllocationType,	// 内存分配的类型
  __in      DWORD flProtect	//	内存页保护
);

BOOL WINAPI WriteProcessMemory(
  __in   HANDLE hProcess,	// 写入进程的句柄
  __in   LPVOID lpBaseAddress,	// 	要写入的地址
  __in   LPCVOID lpBuffer,	// 指向写入的内容
  __in   SIZE_T nSize,	// 写入大小
  __out  SIZE_T* lpNumberOfBytesWritten	// 返回实际写入的字节
);

HANDLE WINAPI CreateRemoteThread(
  __in   HANDLE hProcess,	// 要在指定进程创建远程线程
  __in   LPSECURITY_ATTRIBUTES lpThreadAttributes,	// 线程安全描述字,指向SECURITY_ATTRIBUTES结构的指针
  __in   SIZE_T dwStackSize,	// 线程栈大小,以字节表示。如果此参数为 0 ,则新线程将使用可执行文件的默认大小
  __in   LPTHREAD_START_ROUTINE lpStartAddress,	// 一个LPTHREAD_START_ROUTINE类型的指针,指向在远程进程中执行的函数地址
  __in   LPVOID lpParameter,	// 传入函数需要的参数
  __in   DWORD dwCreationFlags,	// 创建线程的其它标志,为0,创建后,线程会立即运行
  __out  LPDWORD lpThreadId	// 返回线程ID
);


代码注入(汇编)

0x01、要注入的汇编代码

把要注入的代码ThreadProc和需要使用的数据弄成汇编代码
ThreadProc()

00401000    55              push ebp
00401001    8BEC            mov ebp,esp
00401003    8B75 08         mov esi,dword ptr ss:[ebp+0x8]
00401006    68 6C6C0000     push 0x6C6C
0040100B    68 33322E64     push 0x642E3233
00401010    68 75736572     push 0x72657375
00401015    54              push esp
00401016    FF16            call dword ptr ds:[esi]
00401018    68 6F784100     push 0x41786F
0040101D    68 61676542     push 0x42656761
00401022    68 4D657373     push 0x7373654D
00401027    54              push esp
00401028    50              push eax
00401029    FF56 04         call dword ptr ds:[esi+0x4]
0040102C    6A 00           push 0x0
0040102E    E8 0C000000     call asmtest.0040103F

00401033   .  52 65 76 65 72 73 65 43 6F 72 65 00   ascii "ReverseCore",0            ; 字符串 "ReverseCore"

0040103F   >  E8 14000000   call asmtest.00401058                    ;  call

00401044   .  77 77 77 2E 7>ascii "www.reversecor"					; 字符串"www.reversecore.com"
00401052   >  65            gs:
00401053      2E            db 2E                                    ;  CHAR '.'
00401054   .  63 6F 6D 00   ascii "com",0

00401058    6A 00            PUSH 0                             ; - hWnd (0)
0040105A    FFD0             CALL EAX                           ; MessageBoxA(0, "www.reversecore.com", "ReverseCore", 0)
0040105C    33C0             XOR EAX,EAX
0040105E    8BE5             MOV ESP,EBP
00401060    5D               POP EBP
00401061    C3               RETN

0x02、分析汇编代码
// ThreadProc栈帧
push ebp
mov ebp,esp
// 第一个参数,创建线程会给ThreadProc传个参数,THREAD_PARAM结构
mov esi,dword ptr ss:[ebp+0x8]
// 压入字符串"user32.dll",因为x86是小端存储,所以压栈的字符串都是反的
push 0x6C6C // "\0\0ll"
push 0x642E3233 // "d.23"
push 0x72657375 // "resu"
// "user32.dll" 字符串地址
push esp
// 调用THREAD_PARAM第一个参数,LoadLibrary函数, 参数就是 "user32.dll",调用结束后EAX是user32.dll句柄
call dword ptr ds:[esi]
// 压入参数 "MessageBoxA"
push 0x41786F
push 0x42656761
push 0x7373654D
// 字符串 "MessageBoxA"
push esp
// 加载的user32.dll模块的句柄
push eax
// 调用第二个函数 GetProcAddress, 调用结束后EAX是MessageBox的函数地址
call dword ptr ds:[esi+0x4]
// 准备调用MessageBox函数
push 0x0 // 第四个参数
call asmtest.0040103F // 利用call指令把下面字符串 "ReverseCore" 的地址压入栈,作为MessageBox的第三个参数,

// 字符串 "ReverseCore"
52 65 76 65 72 73 65 43 6F 72 65 00

call asmtest.00401058 // 利用call指令压入第二个参数 "www.reversecore.com", 这条指令的地址就是0040103F

// 字符串 "www.reversecore.com"
77 77 77 2E 72 65 76 65 72 73 65 63 6F 72 65 2E 63 6F 6D 00

PUSH 0// 第一个参数,这条指令的地址是401058
CALL EAX ; 调用MessageBoxA(0, "www.reversecore.com", "ReverseCore", 0)
// 把MessageBox函数返回值清零
XOR EAX,EAX
// 还原栈帧,函数返回
MOV ESP,EBP
POP EBP
RETN

0x03、扣出二进制代码执行注入

把汇编代码的二进制抠出来,放到注入程序中,用数组方式存储:

#include "windows.h"
#include "stdio.h"

typedef struct _THREAD_PARAM 
{
// 相比于c代码注入,这里的字符串数据少了,因为提前把它们写到了汇编代码里
    FARPROC pFunc[2];               // LoadLibraryA(), GetProcAddress()
} THREAD_PARAM, *PTHREAD_PARAM;

BYTE g_InjectionCode[] = 
{
    0x55, 0x8B, 0xEC, 0x8B, 0x75, 0x08, 0x68, 0x6C, 0x6C, 0x00,
    0x00, 0x68, 0x33, 0x32, 0x2E, 0x64, 0x68, 0x75, 0x73, 0x65,
    0x72, 0x54, 0xFF, 0x16, 0x68, 0x6F, 0x78, 0x41, 0x00, 0x68,
    0x61, 0x67, 0x65, 0x42, 0x68, 0x4D, 0x65, 0x73, 0x73, 0x54,
    0x50, 0xFF, 0x56, 0x04, 0x6A, 0x00, 0xE8, 0x0C, 0x00, 0x00,
    0x00, 0x52, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x43, 0x6F,
    0x72, 0x65, 0x00, 0xE8, 0x14, 0x00, 0x00, 0x00, 0x77, 0x77,
    0x77, 0x2E, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x65, 0x63,
    0x6F, 0x72, 0x65, 0x2E, 0x63, 0x6F, 0x6D, 0x00, 0x6A, 0x00,
    0xFF, 0xD0, 0x33, 0xC0, 0x8B, 0xE5, 0x5D, 0xC3
};

/*
004010ED    55               PUSH EBP
004010EE    8BEC             MOV EBP,ESP
004010F0    8B75 08          MOV ESI,DWORD PTR SS:[EBP+8]       ; ESI = pParam           
004010F3    68 6C6C0000      PUSH 6C6C                      
004010F8    68 33322E64      PUSH 642E3233
004010FD    68 75736572      PUSH 72657375
00401102    54               PUSH ESP                           ; - "user32.dll"
00401103    FF16             CALL DWORD PTR DS:[ESI]            ; LoadLibraryA("user32.dll")
00401105    68 6F784100      PUSH 41786F
0040110A    68 61676542      PUSH 42656761
0040110F    68 4D657373      PUSH 7373654D
00401114    54               PUSH ESP                           ; - "MessageBoxA"
00401115    50               PUSH EAX                           ; - hMod
00401116    FF56 04          CALL DWORD PTR DS:[ESI+4]          ; GetProcAddress(hMod, "MessageBoxA")
00401119    6A 00            PUSH 0                             ; - MB_OK (0)
0040111B    E8 0C000000      CALL 0040112C
00401120                     <ASCII>                            ; - "ReverseCore", 0
0040112C    E8 14000000      CALL 00401145
00401131                     <ASCII>                            ; - "www.reversecore.com", 0
00401145    6A 00            PUSH 0                             ; - hWnd (0)
00401147    FFD0             CALL EAX                           ; MessageBoxA(0, "www.reversecore.com", "ReverseCore", 0)
00401149    33C0             XOR EAX,EAX                        
0040114B    8BE5             MOV ESP,EBP
0040114D    5D               POP EBP                            
0040114E    C3               RETN
*/

BOOL SetPrivilege(LPCTSTR lpszPrivilege, BOOL bEnablePrivilege) 
{
    TOKEN_PRIVILEGES tp;
    HANDLE hToken;
    LUID luid;

    if( !OpenProcessToken(GetCurrentProcess(),
                          TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, 
			              &hToken) )
    {
        printf("OpenProcessToken error: %u\n", GetLastError());
        return FALSE;
    }

    if( !LookupPrivilegeValue(NULL,           // lookup privilege on local system
                              lpszPrivilege,  // privilege to lookup 
                              &luid) )        // receives LUID of privilege
    {
        printf("LookupPrivilegeValue error: %u\n", GetLastError() ); 
        return FALSE; 
    }

    tp.PrivilegeCount = 1;
    tp.Privileges[0].Luid = luid;
    if( bEnablePrivilege )
        tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
    else
        tp.Privileges[0].Attributes = 0;

    // Enable the privilege or disable all privileges.
    if( !AdjustTokenPrivileges(hToken, 
                               FALSE, 
                               &tp, 
                               sizeof(TOKEN_PRIVILEGES), 
                               (PTOKEN_PRIVILEGES) NULL, 
                               (PDWORD) NULL) )
    { 
        printf("AdjustTokenPrivileges error: %u\n", GetLastError() ); 
        return FALSE; 
    } 

    if( GetLastError() == ERROR_NOT_ALL_ASSIGNED )
    {
        printf("The token does not have the specified privilege. \n");
        return FALSE;
    } 

    return TRUE;
}

BOOL InjectCode(DWORD dwPID)
{
    HMODULE         hMod            = NULL;
    THREAD_PARAM    param           = {0,};
    HANDLE          hProcess        = NULL;
    HANDLE          hThread         = NULL;
    LPVOID          pRemoteBuf[2]   = {0,};

    hMod = GetModuleHandleA("kernel32.dll");

    // set THREAD_PARAM
    param.pFunc[0] = GetProcAddress(hMod, "LoadLibraryA");
    param.pFunc[1] = GetProcAddress(hMod, "GetProcAddress");

    // Open Process
    if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS,               // dwDesiredAccess
                                  FALSE,                            // bInheritHandle
                                  dwPID)) )                         // dwProcessId
    {
        printf("OpenProcess() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    // Allocation for THREAD_PARAM
    if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess,                  // hProcess
                                         NULL,                      // lpAddress
                                         sizeof(THREAD_PARAM),      // dwSize
                                         MEM_COMMIT,                // flAllocationType
                                         PAGE_READWRITE)) )         // flProtect
    {
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !WriteProcessMemory(hProcess,                               // hProcess
                            pRemoteBuf[0],                          // lpBaseAddress
                            (LPVOID)&param,                         // lpBuffer
                            sizeof(THREAD_PARAM),                   // nSize
                            NULL) )                                 // [out] lpNumberOfBytesWritten
    {
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    // Allocation for ThreadProc()
    if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess,                  // hProcess
                                         NULL,                      // lpAddress
                                         sizeof(g_InjectionCode),   // dwSize
                                         MEM_COMMIT,                // flAllocationType
                                         PAGE_EXECUTE_READWRITE)) ) // flProtect
    {
        printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !WriteProcessMemory(hProcess,                               // hProcess
                            pRemoteBuf[1],                          // lpBaseAddress
                            (LPVOID)&g_InjectionCode,               // lpBuffer
                            sizeof(g_InjectionCode),                // nSize
                            NULL) )                                 // [out] lpNumberOfBytesWritten
    {
        printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    if( !(hThread = CreateRemoteThread(hProcess,                    // hProcess
                                       NULL,                        // lpThreadAttributes
                                       0,                           // dwStackSize
                                       (LPTHREAD_START_ROUTINE)pRemoteBuf[1],
                                       pRemoteBuf[0],               // lpParameter
                                       0,                           // dwCreationFlags
                                       NULL)) )                     // lpThreadId
    {
        printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError());
        return FALSE;
    }

    WaitForSingleObject(hThread, INFINITE);	

    CloseHandle(hThread);
    CloseHandle(hProcess);

    return TRUE;
}

int main(int argc, char *argv[])
{
    DWORD dwPID     = 0;

	if( argc != 2 )
	{
	    printf("\n USAGE  : %s <pid>\n", argv[0]);
		return 1;
	}

	// change privilege
	if( !SetPrivilege(SE_DEBUG_NAME, TRUE) )
        return 1;

    // code injection
    dwPID = (DWORD)atol(argv[1]);
    InjectCode(dwPID);

	return 0;
}


注:
windows10 32位Debug和Release都可以注入成功32位notepad,注入64位notepad失败
windows10 64位Debug和Release都可以注入失败64位notepad,(猜测是因为汇编代码是32位的,字符串压栈造成不能注入,以后改进试试)

0x04、注入结果

执行结果

0x05、64位改写(但是没成功)
00007FFA204CD97C | 55                       | push rbp                                |
00007FFA204CD97D | 48:8BEC                  | mov rbp,rsp                             |
00007FFA204CD980 | 48:8B75 10               | mov rsi,qword ptr ss:[rbp+0x10]            |
00007FFA204CD984 | 68 6C6C0000              | push 6C6C                               |
00007FFA204CD989 | 48:BB 7573657233322E64   | mov rbx,642E323372657375                | rbx:"LdrpInitializeProcess"
00007FFA204CD993 | 53                       | push rbx                                | rbx:"LdrpInitializeProcess"
00007FFA204CD994 | 54                       | push rsp                                |
00007FFA204CD995 | FF16                     | call qword ptr ds:[rsi]                 |
00007FFA204CD997 | 68 6F784100              | push 41786F                             |
00007FFA204CD99C | 48:BB 4D65737361676542   | mov rbx,426567617373654D                | rbx:"LdrpInitializeProcess"
00007FFA204CD9A6 | 53                       | push rbx                                | rbx:"LdrpInitializeProcess"
00007FFA204CD9A7 | 54                       | push rsp                                |
00007FFA204CD9A8 | 50                       | push rax                                |
00007FFA204CD9A9 | FF56 08                  | call qword ptr ds:[rsi+8]               |
00007FFA204CD9AC | 6A 00                    | push 0                                  |
00007FFA204CD9AE | 52                       | push rdx                                |
00007FFA204CD9AF | E8 0C000000              | call ntdll.7FFA204CD9C0                 |
00007FFA204CD9B4 | 52                       | push rdx                                |
00007FFA204CD9B5 | 65:76 65                 | jbe ntdll.7FFA204CDA1D                  |
00007FFA204CD9B8 | 72 73                    | jb ntdll.7FFA204CDA2D                   |
00007FFA204CD9BA | 6543:6F                  | outsd                                   |
00007FFA204CD9BD | 72 65                    | jb ntdll.7FFA204CDA24                   |
00007FFA204CD9BF | 00                       | db 0                                    |
00007FFA204CD9C0 | E8 14000000              | call ntdll.7FFA204CD9D9                 |
00007FFA204CD9C5 | 77 77                    | ja ntdll.7FFA204CDA3E                   |
00007FFA204CD9C7 | 77 2E                    | ja ntdll.7FFA204CD9F7                   |
00007FFA204CD9C9 | 72 65                    | jb ntdll.7FFA204CDA30                   |
00007FFA204CD9CB | 76 65                    | jbe ntdll.7FFA204CDA32                  |
00007FFA204CD9CD | 72 73                    | jb ntdll.7FFA204CDA42                   |
00007FFA204CD9CF | 65:636F 72               | movsxd ebp,dword ptr gs:[rdi+72]        |
00007FFA204CD9D3 | 652E:636F 6D             | movsxd ebp,dword ptr gs:[rdi+6D]        |
00007FFA204CD9D8 | 00                       | db 0                                    |
00007FFA204CD9D9 | 6A 00                    | push 0                                  |
00007FFA204CD9DB | FFD0                     | call rax                                |
00007FFA204CD9DD | 48:33C0                  | xor rax,rax                             |
00007FFA204CD9E0 | 48:8BE5                  | mov rsp,rbp                             |
00007FFA204CD9E3 | 5D                       | pop rbp                                 |
00007FFA204CD9E4 | C3                       | ret                                     |

		0x55,0x48,0x8B,0xEC,0x48,0x8B,0x75,0x10,0x68,0x6C,
    0x6C,0x00,0x00,0x48,0xBB,0x75,0x73,0x65,0x72,0x33,
    0x32,0x2E,0x64,0x53,0x54,0xFF,0x16,0x68,0x6F,0x78,
    0x41,0x00,0x48,0xBB,0x4D,0x65,0x73,0x73,0x61,0x67,
    0x65,0x42,0x53,0x54,0x50,0xFF,0x56,0x08,0x6A,0x00,
    0x52,0xE8,0x0C,0x00,0x00,0x00,0x52,0x65,0x76,0x65,
    0x72,0x73,0x65,0x43,0x6F,0x72,0x65,0x00,0xE8,0x14,
    0x00,0x00,0x00,0x77,0x77,0x77,0x2E,0x72,0x65,0x76,
    0x65,0x72,0x73,0x65,0x63,0x6F,0x72,0x65,0x2E,0x63,
    0x6F,0x6D,0x00,0x6A,0x00,0xFF,0xD0,0x48,0x33,0xC0,
    0x48,0x8B,0xE5,0x5D,0xC3

网络安全学习资源分享:

给大家分享一份全套的网络安全学习资料,给那些想学习 网络安全的小伙伴们一点帮助!

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

因篇幅有限,仅展示部分资料,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,需要点击下方链接即可前往获取

读者福利 | 优快云大礼包:《网络安全入门&进阶学习资源包》免费分享 (安全链接,放心点击)

👉1.成长路线图&学习规划👈

要学习一门新的技术,作为新手一定要先学习成长路线图,方向不对,努力白费。

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

在这里插入图片描述
在这里插入图片描述

👉2.网安入门到进阶视频教程👈

很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩。(全套教程文末领取哈)
在这里插入图片描述

在这里插入图片描述

👉3.SRC&黑客文档👈

大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录

SRC技术文籍:

在这里插入图片描述

黑客资料由于是敏感资源,这里不能直接展示哦!(全套教程文末领取哈)

👉4.护网行动资料👈

其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!

在这里插入图片描述

👉5.黑客必读书单👈

在这里插入图片描述

👉6.网络安全岗面试题合集👈

当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。
在这里插入图片描述
所有资料共282G,朋友们如果有需要全套《网络安全入门+进阶学习资源包》,可以扫描下方二维码或链接免费领取~

读者福利 | 优快云大礼包:《网络安全入门&进阶学习资源包》免费分享 (安全链接,放心点击)

逆向工程初体验:手把手教你用Cheat Engine 7.5分析并注入汇编代码 本文详细介绍了如何使用Cheat Engine 7.5进行逆向工程,包括分析程序内存、注入汇编代码等核心操作。通过实战案例演示了如何查找和修改游戏数值,深入解析了CPU指令执行原理,并强调了代码注入技术的合法使用边界。适合对CE修改器和代码注入技术感兴趣的开发者学习参考。 阅读详情

相关推荐

Cheat Engine逆向工程入门:从内存扫描到代码注入实战指南

动态分析是软件逆向工程的核心技术之一,它允许工程师在程序运行时观察和干预其内部状态。其基本原理是通过附加到目标进程,实时监控内存数据的变化,从而定位关键变量和函数。这项技术的核心价值在于能够在不修改原始程序文件的情况下,深入理解程序逻辑、调试复杂问题或进行安全研究。在游戏修改、软件调试和安全漏洞分析等场景中,动态分析工具发挥着不可替代的作用。以Cheat Engine为例,它通过直观的内存扫描功能,让用户能够快速定位如生命值、金币数量等游戏数据,并进一步利用指针扫描和代码注入技术,实现从数据修改到逻辑篡改的

weixin_30954265的博客 367

关于反射的总结——代码体现

package reflect; import java.lang.reflect.*; public class ReflectDemo { /*定义一个标准的JavaBean,名叫Person,包含属性name、age。使用反射 * 的方式创建一个实例、调用构造函数初始化name、age,使用反射方式 * 调用setName方法对名称进行设置,不使用setAge方法直接使用反射方式对a

camel@Y blog 571

从Cheat Engine代码注入到逆向工程:掌握程序行为的底层原理与实践

代码注入是一种通过向运行中的进程内存写入自定义指令来改变或扩展程序行为的技术。其核心原理在于利用跳板机制,在程序原有执行流中插入跳转指令,引导CPU执行注入的代码片段,之后再返回原流程。这项技术的价值在于,它允许开发者和安全研究人员在不拥有源代码的情况下,深入理解、监控甚至修改软件的内部逻辑。从游戏模组开发、软件功能扩展,到安全漏洞分析与恶意代码检测,代码注入都是关键的实践手段。在游戏修改领域,诸如Cheat Engine等工具常被用于实现‘无敌’、‘锁定资源’等功能,这本质上就是通过注入技术干预游戏的内存

weixin_30924087的博客 342

Android网络编程之Http通信(cmwap处理)

先附上一个Demo的截图:     需要Demo的可以直接下载来看看:http://download.youkuaiyun.com/detail/shinay/4380308 在Android开发Http程序的时候,可以选择HttpURLConnection和HttpClient接口进行编程,下面就说下这两种方式的写法: HttpURLConnection get方式: /**

哇哈哈。。。 5714

Cheat Engine教程:从内存扫描到代码注入的进阶技巧

本文深入解析Cheat Engine从内存扫描到代码注入的进阶技巧,包括精确值扫描、多级指针解析和自动汇编脚本编写。通过实战案例演示如何破解复杂游戏机制,帮助用户掌握游戏修改的核心技术,提升逆向工程能力。

weixin_30790841的博客 350

从CheatEngine入门:掌握内存扫描与指针追踪的逆向工程基础

内存管理是计算机系统的核心概念之一,程序运行时的所有数据都存储在内存中,并通过地址进行访问。理解内存地址、数据存储格式(如4字节整数、浮点数)以及指针机制,是进行软件分析、调试和性能优化的基础。这些底层原理在游戏修改、安全分析和系统编程中具有重要价值。通过工具实践,可以直观地观察数据在内存中的变化,从而深化对程序执行流程和数据结构操作的理解。本文以CheatEngine为例,详细介绍了精确数值扫描、模糊搜索、指针扫描等关键技术,并探讨了其在动态地址定位和代码逻辑分析中的应用。学习这些技能不仅能提升问题排查能

weixin_33795743的博客 400

计算机毕业设计—34665 SpringBoot医院人事工资管理系统(源码免费领)定制程序、java、PHP、python、小程序、文案全套、毕设成品等

随着科学技术的飞速发展,社会的方方面面、各行各业都在努力与现代的先进技术接轨,通过科技手段来提高自身的优势,医院当然也不例外。医院人事工资管理系统是以实际运用为开发背景,运用软件工程原理和开发方法,采用Java技术构建的一个管理系统。整个开发过程首先对软件系统进行需求分析,得出系统的主要功能。

2401_85644395的博客 899

UE4SS入门指南:五步实现虚幻引擎游戏LUA脚本注入与Mod开发

在游戏开发与修改领域,运行时注入技术允许开发者在不拥有源代码的情况下,动态修改和扩展应用程序的行为。其核心原理是通过钩子(Hook)和内存访问技术,拦截程序执行流程或读写内存数据,从而实现对软件逻辑的深度定制。这项技术的价值在于为游戏模组(Mod)开发、功能测试和逆向工程研究提供了强大而灵活的工具。在虚幻引擎游戏社区,UE4SS作为一款流行的运行时注入器与LUA脚本引擎,已成为为《赛博朋克2077》、《艾尔登法环》等热门游戏创建功能模组的关键工具。通过它,爱好者可以编写LUA脚本,实现从修改游戏参数到增加全

weixin_34302798的博客 351

向量化计算

本文对比了标量处理和向量化处理在图像亮度调整任务中的性能差异。以100万像素图像为例,标量处理每次循环处理1个像素,耗时约0.00167秒;而使用AVX-512指令集的向量化处理可同时处理64个像素,耗时仅0.000104秒,速度提升16倍。关键优势在于能效:向量化处理单位像素能耗降低35.5倍,能量延迟积改善570倍。这种能效提升源于硬件并行设计分摊了控制开销,使64倍计算量仅增加少量能耗。文章还列举了科学计算、多媒体处理等领域的实际应用案例,并讨论了数据对齐、条件分支等实现挑战。最后通过CUDA矩阵乘法

weixin_40398522的博客 1055

逆向思维入门:用Cheat Engine教程关拆解游戏内存修改的基本原理

本文通过Cheat Engine(CE)教程关拆解游戏内存修改的基本原理,采用逆向思维解析内存扫描、差分分析、浮点数据处理等核心技术。适合新手学习CE工具的使用,掌握从数据修改到代码注入的进阶技巧,同时强调技术应用的伦理边界。

weixin_30487201的博客 482

Windows平台ADB工具实战指南:安卓开发与微信跳一跳辅助设计

Android Debug Bridge(ADB)是连接开发环境与安卓设备的核心调试桥梁,其在Windows平台通过adb.exe命令行工具实现对设备的精确控制。该工具链由三大关键组件构成:adb.exe(主控程序)、(提供底层API接口调用支持)、(负责USB设备通信协议解析)。三者协同工作,构建起PC与设备间的稳定通信通道。# 示例:通过ADB查看连接设备列表此命令背后涉及客户端-服务器架构调度,adb.exe作为客户端向adbd。

weixin_28721743的博客 1107

Cheat Engine核心原理与实战:从内存扫描到代码注入的逆向工程入门

内存扫描与调试是理解程序运行机制的基础技术,其核心原理在于实时访问和修改进程在内存中的数据。通过对比内存快照的变化,可以精准定位目标变量地址,这一过程不仅涉及对数据类型(如4字节整数、浮点数)的识别,还常需处理动态内存分配带来的地址变化问题,此时指针映射技术成为关键。掌握这些技术对于软件调试、游戏机制研究及安全分析具有重要价值,尤其在单机游戏或独立软件的逆向分析场景中,它提供了一个安全的实践环境。本文以Cheat Engine这一经典工具为例,深入剖析其扫描机制、指针寻址及代码注入等高级功能,旨在帮助读者构

weixin_30493401的博客 365

【SEED Labs 2.0】V*N Tunneling Lab

本文为 SEED Labs 2.0 - V*N Tunneling Lab 的实验记录。本实验工作量较大,但难度不大,重点在于搞清是谁发给谁,走的什么路径。另外,本次实验由于网络中的设备较多,修改了环境变量方便分清哪个窗口是哪个设备。这样操作起来就方便多了。...

Chenyang的博客 6974

Cheat Engine 内存修改原理与逆向工程入门指南

内存是程序运行时数据和指令的临时存储空间,理解内存管理是掌握计算机底层原理的关键。程序运行时,其状态数据(如游戏中的生命值、金币)都存储在内存的特定地址中。通过内存扫描技术,可以定位并修改这些数据,这不仅是实现特定功能(如游戏修改)的手段,更是深入理解程序运行机制、进行软件动态分析的实践入口。以 Cheat Engine 为代表的工具,将复杂的内存地址查找、指针解析和代码注入过程可视化与流程化,极大地降低了学习门槛。掌握其核心的“精确/模糊数值扫描”、“指针分析”和“汇编调试”功能,能够帮助开发者或爱好者从

weixin_29315091的博客 299

对工时评估的思考:当AI颠覆了coding效率

在AI时代,“人日”作为软件工时度量衡已然失效。开发的价值核心正从“编码实现”转向“深度思考”与“精准沟通”——即如何将人类的模糊需求精确地传达给AI。当最有价值的“思想工作”变得最难量化时,传统的项目管理范式随之动摇。我们必须超越对过程的算计,开始探索衡量思想、沟通与业务价值的新标尺。

架构师刘7的博客 1017

MicroPython跨平台移植实战:从源码解析到STM32+FreeRTOS部署

本文详细解析了MicroPython的跨平台移植实战,重点介绍了如何将其从源码开始,成功部署到STM32微控制器并运行于FreeRTOS实时操作系统。内容涵盖源码结构解析、编译环境搭建、内存管理协调、FreeRTOS任务集成、数学库(math)启用与优化,以及自定义模块开发等核心步骤,为开发者提供了一套完整的、可操作的嵌入式Python移植方案。

vodka的博客 1014

3步掌握DLL注入技术:从零实现游戏内存修改

DLL注入、进程内存、游戏修改、Windows API、远程线程 你是否想过,为什么有些程序能够在不修改源文件的情况下改变游戏行为?答案就在DLL注入技术中。今天,我们通过一个开源项目来探索这项技术的核心原理,让你在30分钟内理解并实践Windows系统下的进程注入技术。 ## 从游戏场景到技术实现 想象一下这个场景:你正在玩一款经典游戏,但某些关卡难度过高,你希望能获得一些辅助功能。传统的

gitblog_00259的博客 329

【2021最新版】Dubbo面试题总结(40道题含答案解析)

文章目录1、为什么要用Dubbo?2、Dubbo的整体架构设计有哪些分层?3、默认使用的是什么通信框架,还有别的选择吗?4、服务调用是阻塞的吗?5、一般使用什么注册中心?还有别的选择吗?6、默认使用什么序列化框架,你知道的还有哪些?7、服务提供者能实现失效踢出是什么原理?8、服务上线怎么不影响旧版本?9、如何解决服务调用链过长的问题?10、说说核心的配置有哪些?11、Dubbo推荐用什么协议?12、同一个服务多个注册的情况下可以直连某一个服务吗?13、画一画服务注册与发现的流程图?14、Dubbo集群容错有

程序媛小琬的博客 1万+

逆向工程入门实战:从汇编基础到补丁注入的完整学习路径

逆向工程是理解程序底层运行机制的核心技术,其原理基于对编译后二进制代码的静态分析与动态调试。掌握汇编语言与栈帧结构,能帮助开发者从代码编写者转变为程序行为的解剖者,在漏洞挖掘、恶意代码分析、遗留系统维护等场景中具有关键价值。本文围绕一套系统化的逆向练习,从汇编基础、工具链配置、补丁注入到典型卡点突破,完整梳理了从入门到进阶的实操链路,适合已掌握C/C++并希望深入底层安全领域的开发者参考。

weixin_42530732的博客 222

基于深度残差U-Net与多尺度注意力机制的医学图像分割系统

本文提出了一种创新的医学图像分割系统,通过融合残差连接、通道注意力机制和空间金字塔池化技术,构建了高性能的UResnet分割网络。系统采用编码器-解码器架构,创新性地引入多层次特征增强模块,显著提升了医学图像中复杂组织结构的分割精度。实验结果表明,该方案在多个医学图像数据集上均取得了优异性能,边界分割精度提升15%,训练收敛速度提升30%,同时减少20%计算量。系统为临床诊断提供了可靠的技术支持,未来可扩展至3D医学图像分割和联邦学习等方向。

Henry的博客 813

GD32测量pwm波频率以及占空比

1、需求 最近做的一个项目,其中有一个传感器输出是pwm波形,频率代表速度,频率越高速度越快。不同的占空比代表不同的故障类型。占空比10%~90%,频率范围20hz ~1khz。 基于以上的需求,单片机要实现两个功能: 1、测量频率 2、测量占空比 这怎么玩呢?从没搞过啊。 2、需求分析 经过查找资料以及参考正点原子的教程,发现单片机的定时器的输入捕获功能能够实现该功能。 2.1分析 如上图所示,测量占空比和测量频率实际上就是测量波形的长度。 对于占空比,就是测量T2与T1时刻的差值 对于频率,就是测量T

Jack Yi的博客 9065

逆向工程核心原理,64位系统下HOOK隐藏进程

(2)原书中使用Jmp imm32相对地址跳转,此时只能适应2GB字节空间,而实际64位系统下,系统API空间、用户动态库函数地址空间相差非常大,故相对地址跳转不合适。64位下应注入procexp64.exe进程,虽然它是通过32位父进程启动的,64位系统下实际使用改进程查看进程信息。// 0x000,下一个条目偏移(0 表示末尾)// 0x054(64 位为 8 字节)// 0x068(64 位为 8 字节)// 0x054(64 位为 8 字节)// 0x068(64 位为 8 字节)

jx02的专栏 236

AI产品经理面试宝典第94天:RAG知识库构建与预处理策略面试指南

本文深入解析RAG知识库构建的核心挑战与预处理策略,涵盖文档格式转换、问答对生成及自动化测试全流程,为AI产品经理提供面试必备的实战方法论与避坑指南,助你精准应对技术深度类问题。

打造全国最全的AI Agent开发知识领域的博客 311

Ghidra逆向工程原理与企业级实战指南

Ghidra并非简单替代IDA的反编译工具,而是基于可扩展架构与确定性分析的逆向工程基础设施。其核心在于将二进制分析解耦为Loader、Analyzer、Decompiler和脚本引擎四层可编程组件,实现从原始字节到语义化伪代码的三次抽象转换(二进制→Pcode→SSA数据流图→C-like伪代码)。这种设计赋予Ghidra跨架构适配能力与分析结果可重现性,特别适用于物联网固件、工控SDK、加壳PE等复杂场景。结合Java运行时契约管理、Python自动化编排及Apache 2.0合规实践,Ghidra真正

weixin_34364071的博客 256

CMake入门教程【基础篇】在Windows、Linux上安装CMake

CMake如何下载与安装

小鱼酱的博客 2727

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

上一篇: 为什么运维转网安特别简单?3 个核心优势 + 1 条落地路径,看完就懂
下一篇: 什么是代码审计?代码审计,流程、方式、范围详解,看这一篇就够了!
网络安全_入门教程
博客等级 码龄4年 1万+粉丝 2151原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值