逆向工程核心原理-第27章-代码注入
参考了SYJ学长的博客:https://bbs.pediy.com/thread-267065.htm
27.1 代码注入
代码注入是一种向目标进程插入独立运行代码并是指运行的技术, 它一般调用CreateRemoteThread()API以远程线程形式运行插入的代码, 所以也被称为线程注入.
下图是代码注入技术的实现原理
首先向target.exe插入代码与数据, 此过程中:
- 代码以线程过程(Thread Procedure)形式插入
- 数据以线程参数的形式插入
也就是说, 代码与数据是分别注入的.
27.2 DLL注入与代码注入
请看下面的简单代码, 其作用是弹出Windows消息框
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
| DWORD WINAPI ThreadProc(LPVOID lParam) { MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK) }
这里讲一下MessageBoxA函数
作用: 弹出Windows消息框
函数原型: int MessageBoxA( [in, optional] HWND hWnd, [in, optional] LPCSTR lpText, [in, optional] LPCSTR lpCaption, [in] UINT uType );
参数解析: hWnd: 该消息框的所有者窗口的句柄, 即该窗口的父窗口句柄, 如果为NULL, 则该窗口没有所有者窗口
lpText: 要显示的文本字符串指针 LPCSTR类型L代表long, P代表pointer, C代表const, STR代表string, 前面也记过笔记但是又忘记了
lpCaption: 消息框的标题字符串指针
uType: 对话框的内容和行为, 比如有无按钮之类 以下为该参数可取的值 MB_ABORTRETRYIGNORE 包含三个按钮Abort, Retry和Ignore MB_CANCELTRYCONTINUE 包含三个按钮取消, 重试, 继续 MB_HELP 消息框添加帮助按钮 MB_OK 包含一个OK按钮 MB_OKCANCEL 包含两个按钮 OK和Cancel MB_RETRYCANCEL 包含两个按钮 重试和取消 MB_YESNO 包含两个按钮 Yes和No MB_YESNOCANCEL 包含三个按钮 Yes, No和Cancel
|
DLL注入
如果是DLL注入技术, 则会将上述代码放入某个DLL文件, 再通过将DLL注入到目标进程后调用DllMain函数然后再在其中调用这段代码
书中没给DLL注入程序, 所以就直接截书上的图了
我们可以看到在DLL注入完成后调用MessageBoxA的整个调用过程
在10001002地址处有一条PUSH 10009290
指令, 紧接着是PUSH 1000929C
指令.
在OD的Dump窗口中查看地址10009290
与1000929C
, 如下图所示
可以看到上面的10009290
与1000929C
, 在这里都有对应的数据(两个字符串), 所需要的数据都保存在导入的DLL当中
我们再回过头去看看调用MessageBoxA的汇编语句CALL DWORD PTR DS : [100080F0]
指令, 该指令调用的是user32.dll中的MessageBoxA()函数, 转到100080F0
处查看
可以看到该区域为DLL的IAT(导入地址表)上面还有导入的kernel32.dll的LCMapStringW函数和ntdll.dll的RtlSizeHeap函数. 可以看到MessageBoxA的地址数据也存放在DLL当中
我们从上面对DLL注入的分析可以看到, DLL注入的代码(整个调用MessageBoxA过程, 不是MessageBoxA的库函数)与数据都存放在DLL当中, 当DLL被插入到进程内存中时, 代码和数据都进入到了目标进程的内存空间, 所以代码能够正常运行
代码注入
而代码注入则不同, 它不像DLL注入能把代码和数据打包在一起, 如果想要注入的代码能够正常运行, 还需要想办法把代码所需要使用的数据也一同注入进去, 比如上面调用MessageBoxA()所需要使用的两个字符串参数数据, 没有它们MessageBoxA()将无法成功调用
使用代码注入的原因
使用代码注入实现的功能跟DLL注入类似, 却要比DLL注入要麻烦, 那为什么要使用代码注入呢
1. 占用内存少
要注入的代码与数据较少, 就不需要将它们做成DLL的形式再注入. 此时直接采用代码注入的方式同样能获得与DLL注入相同的效果, 且占用的内存会更少
2. 难以查找痕迹
采用DLL注入的方式会再目标进程的内存中留下相关痕迹, 很容易让人判断出目标进程是否被执行过注入操作, 但采用代码注入方式几乎不会留下任何痕迹, 恶意代码中大量使用代码注入技术
3. 其他
不需要另外的DLL文件, 只要有代码注入程序即可.
总结: DLL注入技术主要用在代码量大且复杂的时候, 而代码注入技术则适用于代码量小且简单的时候(想想, 如果要用到100个数据是使用代码注入, 数据的注入会非常麻烦)
27.3 练习示例
示例为CodeInjection.exe用于代码注入, 它向notepad.exe进程注入简单的代码, 注入后会弹出消息框
27.3.1 运行notepad.exe
首先运行notepad.exe 然后使用ProcessExplorer查看notepad.exe进程的PID
可以看到notepad的PID为29592
27.3.2 运行CodeInjection.exe
在命令行窗口中输入命令与参数, 注意要用管理员身份打开, 否则会The token does not have the specified privilege.报错
运行成功后发现notepad.exe弹出了一个窗口
7.4 CodeInjection.cpp
下面的代码略去了异常处理部分
27.4.1 CodeInjection.exe的main()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| int main(int argc, char *argv[]) { DWORD dwPID = 0;
if ( argc != 2 ) { printf("\n USAGE : %s pid\n", argv[0]); }
dwPID = (DWORD)atol(argv[1]); InjectCode(dwPID);
return 0; }
|
27.4.2 ThreadProc()函数
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
| 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; }
|
上述代码实际被注入的部分是ThreadProc()函数, 其中使用了很多的函数指针, 经过整理后会非常简单
1 2 3
| hMod = LoadLibraryA("user32.dll"); pFunc = GetProcAddress(hMod, "MessageBoxA"); pFunc(NULL, "www.reversecore.com", "ReverseCore", MB_OK);
|
上面的代码并不是故意写得那么乱, 而是代码注入的方式所导致的.
重要的是ThreadProc()代码的概念: 代码注入技术的核心内容是注入可独立运行的代码, 为此, 需要同时注入代码与数据.
ThreadProc所用的到的数据都是通过lParam参数传递的.
我们看看普通程序
1 2 3 4 5 6
| DWORD WINAPI ThreadProc(LPVOID lparam) { MessageBoxA(NULL, "www.reversecore.com", "ReverseCore", MB_OK);
return 0; }
|
使用调试器调试生成的文件
可以看见, 如果将该段代码直接注入, 则代码无法正常运行, 因为代码中引用的地址(10009290
, 1000929C
, 100080F0
)并不存在于目标进程中, 要使代码能够正常运行, 必须向相应地址同时注入相关字符串以及API地址, 并且通过编程方式使上图代码也能够准确引用被注入数据的地址
为了满足这些条件, ThreadProc函数中使用THREAD_PARAM(线程参数)结构体来接收2个API地址与4个字符串数据. 其中2个API分别是LoadLibraryA与GetProcAddress(), 只要有了这两个API, 就能够调用目标进程加载的库的所有库函数.
大部分用户模式进程都会加载kernel32.dll, 所以直接传递LoadLibraryA()与GetProcAddress()的地址不会有什么问题(因为是固定的).
调试我们CodeInject.exe中的Threadproc
可以看到[EBP + 8]就是我们的参数结构体
重要的数据都是从[EBP + 8]中获取的, 所以这个ThreadProc()函数是可以独立运行的代码
27.4.3 InjectCode()函数
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
| 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; }
|
分析代码注入一些理解
首先是对CreateRemoteThread()有了一些不同的看法, 之前DLL注入的时候对于CreateRemoteThread()的印象是凭空创建了一个线程(注意凭空), 但是看了代码注入后, 发现CreateRemoteThread()并不是凭空, 它有点像是一个IP寄存器, 我们把这个IP寄存器拨到内存的指定位置, 然后创建一个线程来跑这一段代码. 而这一段代码需要数据支撑, 所以它所需要的数据也必须在这个内存当中.
代码注入的大致流程: 给注入的代码在目标进程内分配空间 → 给需要的数据在目标进程内分配空间
→调用CreateRemoteThread()创建线程, 执行该代码
27.5 代码注入调试练习
27.5.1 调试notepad.exe
先用OD调试notepad.exe文件, 使用F9让notepad.exe处于”Runing”运行中状态
27.5.3 设置OllyDbg选项
代码注入的本质其实就是在程序中创建一个新进程, 所以我们在调试选项中开启在新线程时停止, 就可以在Threadproc(线程过程)函数处停下来了
27.5.3 运行CodeInjection.exe
然后使用ProcessExplorer查看进程的PID
然后再终端处运行CodeInjection.exe(注意终端要使用管理员身份打开)
然后就会在我们注入的代码处停止下来
执行到7B0004后面
我们查看参数结构体的内存
至此完成调试
1 2 3 4 5 6
| 首先在这里谈谈自己的理解 DLL注入: 调用了CreateRemoteThread(), 创建的线程运行LoadLibraryA(), 加载了DLL后, 运行DllMain 函数, 代码和数据都保存再DLL中, 所以不用考虑所用数据的问题
代码注入: 同样调用了CreateRemoteThread(), 创建的线程运行的是自己写的函数, 所需的数据都在原本的 程序内所以需要以参数的形式传递到函数中去, 而且需要提前保存在目标进程的内存空间中
|