代码注入
代码注入是一种想目标进程插入具备独立运行能力的代码并使之运行的技术,由于一般是通过调用CreateRemoteThread APi以远程线程形式运行插入的代码,因此也被称为线程注入。
例子,如若需要将下列用于弹出Windows消息框的代码注入的到目标进程:
1 2 3 4 5
| DOWRD WINAPI ThreadProc(LPVOID lparam) { MessageBox(NULL, "www.reversecore.com", "ReverseCore", MB_OK); return 0; }
|
使用DLL注入的方式实现
在DLL注入技术里,会将代码放入某个DLL文件,在将整个DLL文件注入到目标进程中,使用OD载入目标程序,开启调试选项中的 “ 中断在新模块载入处” 的选项,使程序处于运行状态,使用DLL注入器将DLL注入,会来到如下代码:
在0x10001002 - 0x10001007处的两条指令
1 2 3 4
| 0x10001002 push 10009290 //字符串ReverseCore的起始位置 0x10001007 push 1000929c //字符串www.reversecore.com的位置 //... 0x1000100E call DOWRD ptr ds:[100080F0] //API入口
|
关注DLL注入方式中有关地址的部分,可以发现:DLL代码使用的所有数据都是位于DLL自己载入内存时的数据区域。这样有什么坏处?
- 需要将整个DLL的内容装进内存,占用内存大
- 痕迹明显,很容易被察觉
- …
使用代码注入的方式实现
而代码注入则是指注入必要的代码,并且代码所使用的数据一同注入,并且在完成注入代码的运行时需要明确指出数据的地址。 克服了一定的DLL注入的缺点
demo演示:
使用代码注入器–CodeInjection.exe输入目标进程对应的pid完成代码注入,实现弹窗。
源码剖析
先贴一下CodeInjection的完整代码,后面展开分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
|
#include "windows.h" #include "stdio.h"
typedef struct _THREAD_PARAM { FARPROC pFunc[2]; char szBuf[4][128]; } THREAD_PARAM, *PTHREAD_PARAM;
typedef HMODULE (WINAPI *PFLOADLIBRARYA) ( LPCSTR lpLibFileName );
typedef FARPROC (WINAPI *PFGETPROCADDRESS) ( HMODULE hModule, LPCSTR lpProcName );
typedef int (WINAPI *PFMESSAGEBOXA) ( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType );
DWORD WINAPI ThreadProc(LPVOID lParam) { PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam; HMODULE hMod = NULL; FARPROC pFunc = NULL;
hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]); if( !hMod ) return 1;
pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]); if( !pFunc ) return 1;
((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);
return 0; }
BOOL InjectCode(DWORD dwPID) { HMODULE hMod = NULL; THREAD_PARAM param = {0,}; HANDLE hProcess = NULL; HANDLE hThread = NULL; LPVOID pRemoteBuf[2] = {0,}; DWORD dwSize = 0;
hMod = GetModuleHandleA("kernel32.dll");
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");
if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) { printf("OpenProcess() fail : err_code = %d\n", GetLastError()); return FALSE; }
dwSize = sizeof(THREAD_PARAM); if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE)) ) { printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError()); return FALSE; }
if( !WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)¶m, dwSize, NULL) ) { printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError()); return FALSE; }
dwSize = (DWORD)InjectCode - (DWORD)ThreadProc; if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) ) { printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError()); return FALSE; }
if( !WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL) ) { printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError()); return FALSE; }
if( !(hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL)) ) { 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, lpszPrivilege, &luid) ) { 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;
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; }
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) ) return 1;
dwPID = (DWORD)atol(argv[1]); InjectCode(dwPID);
return 0; }
|
main
首先是CodeInjection的main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| int main(int argc, char *argv[]) { DWORD dwPID = 0;
if( argc != 2 ) { printf("\n USAGE : %s <pid>\n", argv[0]); return 1; }
if( !SetPrivilege(SE_DEBUG_NAME, TRUE) ) return 1;
dwPID = (DWORD)atol(argv[1]); InjectCode(dwPID);
return 0; }
|
无非做了参数合法验证,由于涉及跨进程内存操作,因此需要提升权限SetPrivilege,进程权限提升在之前有做过文章描述。随后是转化传递进来的pid,转化为int类型,然后进入InjectCode函数
1 2 3 4 5 6
| HMODULE hMod = NULL; THREAD_PARAM param = {0,}; HANDLE hProcess = NULL; HANDLE hThread = NULL; LPVOID pRemoteBuf[2] = {0,}; DWORD dwSize = 0;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| typedef HMODULE (WINAPI *PFLOADLIBRARYA) ( LPCSTR lpLibFileName );
typedef struct _THREAD_PARAM //传递给线程的参数 { FARPROC pFunc[2]; char szBuf[4][128]; } THREAD_PARAM, *PTHREAD_PARAM;
typedef FARPROC (WINAPI *PFGETPROCADDRESS) ( HMODULE hModule, LPCSTR lpProcName );
typedef int (WINAPI *PFMESSAGEBOXA) ( HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType );
|
线程参数写入
接着InjectCode函数的源码分析,下边完成的是线程参数的写入
1 2 3 4 5 6 7 8
| hMod = GetModuleHandleA("kernel32.dll");
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");
|
重点关注线程参数的设置:
- 线程中需要用到的kernel32.LoadLibraryA & kernel32.GetProcAddress
- 线程中需要用到的一些字符串,user32.dll,MessageBoxA,www.reversecore.com,ReverseCore。
根据线程参数也猜得出大致线程的行为,获取kernel32.LoadLibraryA
后将user32.dll载入,kernel32.GetProcAddress
获取user32.MessageBoxA
API地址,完成MessageBoxA调用,实现弹窗
1 2 3 4 5
| if ( !(hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPID)) ) { printf("OpenProcess() fail : err_code = %d\n", GetLastError()); return FALSE; }
|
以PROCESS_ALL_ACCESS打开目标进程,打开成功后返回目标进程的句柄到hProcess
1
| dwSize = sizeof(THREAD_PARAM);
|
计算将要传递进目标进程中作为线程参数的结构体大小,计算结果存放在dwSize中
1 2 3 4 5 6
| if( !(pRemoteBuf[0] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_READWRITE)) ) { printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError()); return FALSE; }
|
在目标进程hProcess开辟一段大小为dwSize的内存空间,对于这块内存的详情为MEM_COMMIT,保护形式为PAGE_READWRITE,也就是具备读写权限。
关于MEM_COMMIT的描述:
为指定地址空间提交物理内存。这个函数初始化内在为零,试图提交已提交的内存页不会导致函数失败。这意味着您可以在不确定当前页的当前提交状态的情况下提交一系列页面。如果尚未保留内存页,则设置此值会导致函数同时保留并提交内存页。
函数执行成功后在目标进程中开辟的内存空间起始地址放入pRemoteBuf[0]
往下分析:
1 2 3 4 5
| if( !WriteProcessMemory(hProcess, pRemoteBuf[0], (LPVOID)¶m, dwSize, NULL) ) { printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError()); return FALSE; }
|
往先前在目标进程中开辟出来的内存空间写入线程参数param
。
至此完成线程参数写入目标进程空间
线程代码写入
接下来需要写入线程的代码,同样使用VirtualAllocEx API
需要注意的点
在目标进程中分配线程代码所需要的空间需要进行计算得出。
源码中计算方式,dwSize = (DWORD)InjectCode - (DWORD)ThreadProc;
之所以能够这样计算是由于作者使用的MS Visual C++中使用Release模式编译程序源码,源码中函数顺序和二进制代码中的前后顺序是一致的,源码文件是按照ThreadProc、InjectCode顺序编写的,所以生成InjectCode.exe中两函数也按照这个顺序排列。(特性,记住,会用就好)
因此我们得到了需要在目标进程中开辟另外一块空间,写入线程的代码,在目标进程中使用CreateRemoteThread
设置回调函数为即将写入的ThreadProc
,在传入ThreadProc
需要的参数param
,如此一来,当在目标进程中开辟成功线程,新线程会自行调用ThreadProc
言归正传,得到线程代码所需要的空间大小dwSize
,接下来就是申请空间了:
1 2 3 4 5 6
| if( !(pRemoteBuf[1] = VirtualAllocEx(hProcess, NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE)) ) { printf("VirtualAllocEx() fail : err_code = %d\n", GetLastError()); return FALSE; }
|
方式类似于第一次调用VirtualAllocEx
进行线程所需参数空间一样,区别在于开辟出来的空间的权限,这块空间是需要执行权限的,因为这是代码的位置。开辟出来的空间起始地址放在pRemoteBuf[1]
.
1 2 3 4 5 6
| if( !WriteProcessMemory(hProcess, pRemoteBuf[1], (LPVOID)ThreadProc, dwSize, NULL) ) { printf("WriteProcessMemory() fail : err_code = %d\n", GetLastError()); return FALSE; }
|
这里需要理解一下,ThreadProc
+ dwSize
这两个内容配合,完成ThreadProc
代码的完整写入
1 2 3 4 5
| if( !(hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pRemoteBuf[1], pRemoteBuf[0], 0, NULL)) ) { printf("CreateRemoteThread() fail : err_code = %d\n", GetLastError()); return FALSE; }
|
一切准备就绪,在目标进程中开启新线程,线程中执行写入的ThreadProc
代码。
ThreadProc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| DWORD WINAPI ThreadProc(LPVOID lParam) { PTHREAD_PARAM pParam = (PTHREAD_PARAM)lParam; HMODULE hMod = NULL; FARPROC pFunc = NULL;
hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]); if( !hMod ) return 1;
pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]); if( !pFunc ) return 1;
((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);
return 0; }
|
参数只有一个lParam
是一个结构体
1 2 3 4 5
| typedef struct _THREAD_PARAM { FARPROC pFunc[2]; char szBuf[4][128]; } THREAD_PARAM, *PTHREAD_PARAM;
|
其中THREAD_PARAM.pFunc
存放的是两个API的调用地址:LoadLibraryA(), GetProcAddress(),都是kernel32.dll的API。
THREAD_PARAM.szBuf
存放的是线程执行需要用到的4个字符串
分别是:user32.dll
,MessageBoxA
,www.reversecore.com
,ReverseCore
。
因此:
1
| hMod = ((PFLOADLIBRARYA)pParam->pFunc[0])(pParam->szBuf[0]);
|
完成的是使用kernel32.LoadLibraryA
将user32.dll进行载入,载入后模块句柄存放在hMod中。
1
| pFunc = (FARPROC)((PFGETPROCADDRESS)pParam->pFunc[1])(hMod, pParam->szBuf[1]);
|
完成的是使用kernel32.GetProcAddress
获取user32.MessageBoxA
的地址,存放到pFunc
中
1
| ((PFMESSAGEBOXA)pFunc)(NULL, pParam->szBuf[2], pParam->szBuf[3], MB_OK);
|
这一步完成的是对MessageBoxA的调用。可以看到,上述许多位置都用到了类型转换,保证程序的运行。
执行完线程内容,回到InjectCode,完成痕迹擦出
1 2 3 4 5 6
| WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread); CloseHandle(hProcess);
return TRUE;
|
回顾
动手调试,很容易可以看出代码注入区别于DLL的方式在于所有完成ThreadPro过程的重要数据都是从[ebp+8]接受使用的,使用的地址是相对地址而不是DLL那样的硬编码地址。另外CodeInject中的ThreadProc也是可以独立运行的代码。