::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );
BOOL APIENTRY DllMain( HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { if( ul_reason_for_call == DLL_PROCESS_ATTACH ) { // Increase reference count via LoadLibrary char lib_name[MAX_PATH]; ::GetModuleFileName( hModule, lib_name, MAX_PATH ); ::LoadLibrary( lib_name ); // Safely remove hook ::UnhookWindowsHookEx( g_hHook ); } return TRUE; }那么会发生什么呢?首先我们通过Windows 钩子将DLL映射到远程进程。然后,在DLL被实际映射之后,我们解开钩子。通常当第一个消息到达钩子作用线程时,DLL此时也不会被映射。这里的处理技巧是调用LoadLibrary通过增加 DLLs的引用计数来防止映射不成功。
HINSTANCE LoadLibrary( LPCTSTR lpLibFileName // 库模块文件名的地址 ); BOOL FreeLibrary( HMODULE hLibModule // 要加载的库模块的句柄 );
DWORD WINAPI ThreadProc( LPVOID lpParameter // 线程数据 );
HANDLE hThread; char szLibPath[_MAX_PATH]; // “LibSpy.dll”模块的名称 (包括全路径); void* pLibRemote; // 远程进程中的地址,szLibPath 将被拷贝到此处; DWORD hLibModule; // 要加载的模块的基地址(HMODULE) HMODULE hKernel32 = ::GetModuleHandle("Kernel32"); // 初始化szLibPath //... // 1. 在远程进程中为szLibPath 分配内存 // 2. 将szLibPath 写入分配的内存 pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath), MEM_COMMIT, PAGE_READWRITE ); ::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath, sizeof(szLibPath), NULL ); // 将"LibSpy.dll" 加载到远程进程(使用CreateRemoteThread 和 LoadLibrary) hThread = ::CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32, "LoadLibraryA" ), pLibRemote, 0, NULL ); ::WaitForSingleObject( hThread, INFINITE ); // 获取所加载的模块的句柄 ::GetExitCodeThread( hThread, &hLibModule ); // 清除 ::CloseHandle( hThread ); ::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );假设我们实际想要注入的代码――SendMessage ――被放在DllMain (DLL_PROCESS_ATTACH)中,现在它已经被执行。那么现在应该从目标进程中将DLL 卸载:
// 从目标进程中卸载"LibSpy.dll" (使用 CreateRemoteThread 和 FreeLibrary) hThread = ::CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32, "FreeLibrary" ), (void*)hLibModule, 0, NULL ); ::WaitForSingleObject( hThread, INFINITE ); // 清除 ::CloseHandle( hThread );进程间通信
HANDLE CreateRemoteThread( HANDLE hProcess, // 传入创建新线程的进程句柄 LPSECURITY_ATTRIBUTES lpThreadAttributes, // 安全属性指针 DWORD dwStackSize, // 字节为单位的初始线程堆栈 LPTHREAD_START_ROUTINE lpStartAddress, // 指向线程函数的指针 LPVOID lpParameter, // 新线程使用的参数 DWORD dwCreationFlags, // 创建标志 LPDWORD lpThreadId // 指向返回的线程ID );如果你比较它与 CreateThread(MSDN)的声明,你会注意到如下的差别:
switch( expression ) { case constant1: statement1; goto END; case constant2: statement2; goto END; case constant3: statement2; goto END; } switch( expression ) { case constant4: statement4; goto END; case constant5: statement5; goto END; case constant6: statement6; goto END; } END:或者将它们修改成一个 if-else if 结构语句(参见附录E)。
int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR lpString ); int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString ); 参数说明: hProcess:编辑框控件所属的进程句柄; hWnd:包含密码的编辑框控件句柄; lpString:接收文本的缓冲指针; 返回值:返回值是拷贝的字符数;
INJDATA typedef LRESULT (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM); typedef struct { HWND hwnd; // 编辑框句柄 SENDMESSAGE fnSendMessage; // 指向user32.dll 中 SendMessageA 的指针 char psText[128]; // 接收密码的缓冲 } INJDATA;
static DWORD WINAPI ThreadFunc (INJDATA *pData) { pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password sizeof(pData->psText), (LPARAM)pData->psText ); return 0; } // 该函数在ThreadFunc之后标记内存地址 // int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc. static void AfterThreadFunc (void) { }
static LRESULT CALLBACK NewProc( HWND hwnd, // 窗口句柄 UINT uMsg, // 消息标示符 WPARAM wParam, // 第一个消息参数 LPARAM lParam ) // 第二个消息参数 { INJDATA* pData = (INJDATA*) NewProc; // pData 指向 NewProc pData--; // 现在pData 指向INJDATA; // 回想一下INJDATA 被置于远程进程NewProc之前; //----------------------------- // 此处是子类化代码 // ........ //----------------------------- // 调用原窗口过程; // fnOldProc (由SetWindowLong 返回) 被(远程)ThreadFunc初始化 // 并被保存在(远程)INJDATA;中 return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); }但这里还有一个问题,见第一行代码:
INJDATA* pData = (INJDATA*) NewProc;
static LRESULT CALLBACK NewProc( HWND hwnd, // 窗口句柄 UINT uMsg, // 消息标示符 WPARAM wParam, // 第一个消息参数 LPARAM lParam ) // 第二个消息参数 { // 计算INJDATA 结构的位置 // 在远程进程中记住这个INJDATA // 被放在NewProc之前 INJDATA* pData; _asm { call dummy dummy: pop ecx // <- ECX 包含当前的EIP sub ecx, 9 // <- ECX 包含NewProc的地址 mov pData, ecx } pData--; //----------------------------- // 此处是子类化代码 // ........ //----------------------------- // 调用原来的窗口过程 return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); }那么,接下来该怎么办呢?事实上,每个进程都有一个特殊的寄存器,它指向下一条要执行的指令的内存位置。即所谓的指令指针,在32位 Intel 和 AMD 处理器上被表示为 EIP。因为 EIP是一个专用寄存器,你无法象操作一般常规存储器(如:EAX,EBX等)那样通过编程存取它。也就是说没有操作代码来寻址 EIP,以便直接读取或修改其内容。但是,EIP 仍然还是可以通过间接方法修改的(并且随时可以修改),通过JMP,CALL和RET这些指令实现。下面我们就通过例子来解释通过 CALL/RET 子例程调用机制在32位 Intel 和 AMD 处理器上是如何工作的。
Address OpCode/Params Decoded instruction -------------------------------------------------- :00401000 55 push ebp ; entry point of ; NewProc :00401001 8BEC mov ebp, esp :00401003 51 push ecx :00401004 E800000000 call 00401009 ; *a* call dummy :00401009 59 pop ecx ; *b* :0040100A 83E909 sub ecx, 00000009 ; *c* :0040100D 894DFC mov [ebp-04], ecx ; mov pData, ECX :00401010 8B45FC mov eax, [ebp-04] :00401013 83E814 sub eax, 00000014 ; pData--; ..... ..... :0040102D 8BE5 mov esp, ebp :0040102F 5D pop ebp :00401030 C21000 ret 0010
static LRESULT CALLBACK NewProc( HWND hwnd, // 窗口句柄 UINT uMsg, // 消息标示符 WPARAM wParam, // 第一个消息参数 LPARAM lParam ) // 第二个消息参数 { INJDATA* pData = 0xA0B0C0D0; // 虚构值 //----------------------------- // 子类化代码 // ........ //----------------------------- // 调用原来的窗口过程 return pData->fnCallWindowProc( pData->fnOldProc, hwnd,uMsg,wParam,lParam ); }此处 0xA0B0C0D0 只是远程进程地址空间中真实(绝对)INJDATA地址的占位符。前面讲过,你无法在编译时知道该地址。但你可以在调用 VirtualAllocEx (为INJDATA)之后得到 INJDATA 在远程进程中的位置。编译我们的 NewProc 后,可以得到如下结果:
Address OpCode/Params Decoded instruction -------------------------------------------------- :00401000 55 push ebp :00401001 8BEC mov ebp, esp :00401003 C745FCD0C0B0A0 mov [ebp-04], A0B0C0D0 :0040100A ... .... :0040102D 8BE5 mov esp, ebp :0040102F 5D pop ebp :00401030 C21000 ret 0010因此,其编译的代码(十六进制)将是:
558BECC745FCD0C0B0A0......8BE55DC21000.现在你可以象下面这样继续:
558BECC745FCD0C0B0A0......8BE55DC21000 <- 原来的NewProc (注1) 558BECC745FC00008A00......8BE55DC21000 <- 修改后的NewProc,使用的是INJDATA的实际地址。也就是说,你用真正的 INJDATA(注2) 地址替代了虚拟值 A0B0C0D0(注2)。
解决方案 | OS | 进程 |
I、Hooks | Win9x 和 WinNT | 仅仅与 USER32.DLL (注3)链接的进程 |
II、CreateRemoteThread & LoadLibrary | 仅 WinNT(注4) | 所有进程(注5), 包括系统服务(注6) |
III、CreateRemoteThread & WriteProcessMemory |
仅 WinNT | 所有进程, 包括系统服务 |
const int cbCodeSize = ((LPBYTE) AfterThreadFunc - (LPBYTE) ThreadFunc)你实际上计算的是指向 ThreadFunc 的JMPs 和AfterThreadFunc之间的“距离” (通常它们会紧挨着,不用考虑距离问题)。现在假设 ThreadFunc 的地址位于004014C0 而伴随的 JMP指令位于 00401020。
:00401020 jmp 004014C0 ... :004014C0 push EBP ; ThreadFunc 的实际地址 :004014C1 mov EBP, ESP ...那么
WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);将拷贝“JMP 004014C0”指令(以及随后cbCodeSize范围内的所有指令)到远程进程――不是实际的 ThreadFunc。远程进程要执行的第一件事情将是“JMP 004014C0” 。它将会在其最后几条指令当中――远程进程和所有进程均如此。但 JMP指令的这个“规则”也有例外。如果某个函数被声明为静态的,它将会被直接调用,即使增量链接也是如此。这就是为什么规则#4要将 ThreadFunc 和 AfterThreadFunc 声明为静态或禁用增量链接的缘故。(有关增量链接的其它信息参见 Matt Pietrek的文章“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools” )
void Dummy(void) { BYTE var[256]; var[0] = 0; var[1] = 1; var[255] = 255; }编译后的汇编如下:
:00401000 push ebp :00401001 mov ebp, esp :00401003 sub esp, 00000100 ; change ESP as storage for ; local variables is needed :00401006 mov byte ptr [esp], 00 ; var[0] = 0; :0040100A mov byte ptr [esp+01], 01 ; var[1] = 1; :0040100F mov byte ptr [esp+FF], FF ; var[255] = 255; :00401017 mov esp, ebp ; restore stack pointer :00401019 pop ebp :0040101A ret注意上述例子中,堆栈指针是如何被修改的?而如果某个函数需要4KB以上局部变量内存空间又会怎么样呢?其实,堆栈指针并不是被直接修改,而是通过另一个函数调用来修改的。就是这个额外的函数调用使得我们的 ThreadFunc “被破坏”了,因为其远程拷贝会调用一个不存在的东西。
sub esp, 0x1000 ; "分配" 第一次 4 Kb test [esp], eax ; 承诺一个新页内存(如果还没有承诺) sub esp, 0x1000 ; "分配" 第二次4 Kb test [esp], eax ; ... sub esp, 0x1000 test [esp], eax注意4KB堆栈指针是如何被修改的,更重要的是,每一步之后堆栈底是如何被“触及”(要经过检查)。这样保证在“分配”(承诺)另一页面之前,当前页面承诺的范围也包含堆栈底。
int Dummy( int arg1 ) { int ret =0; switch( arg1 ) { case 1: ret = 1; break; case 2: ret = 2; break; case 3: ret = 3; break; case 4: ret = 0xA0B0; break; } return ret; }编译后变成下面这个样子:
地址 操作码/参数 解释后的指令 -------------------------------------------------- ; arg1 -> ECX :00401000 8B4C2404 mov ecx, dword ptr [esp+04] :00401004 33C0 xor eax, eax ; EAX = 0 :00401006 49 dec ecx ; ECX -- :00401007 83F903 cmp ecx, 00000003 :0040100A 771E ja 0040102A ; JMP 到表***中的地址之一 ; 注意 ECX 包含的偏移 :0040100C FF248D2C104000 jmp dword ptr [4*ecx+0040102C] :00401013 B801000000 mov eax, 00000001 ; case 1: eax = 1; :00401018 C3 ret :00401019 B802000000 mov eax, 00000002 ; case 2: eax = 2; :0040101E C3 ret :0040101F B803000000 mov eax, 00000003 ; case 3: eax = 3; :00401024 C3 ret :00401025 B8B0A00000 mov eax, 0000A0B0 ; case 4: eax = 0xA0B0; :0040102A C3 ret :0040102B 90 nop ; 地址表*** :0040102C 13104000 DWORD 00401013 ; jump to case 1 :00401030 19104000 DWORD 00401019 ; jump to case 2 :00401034 1F104000 DWORD 0040101F ; jump to case 3 :00401038 25104000 DWORD 00401025 ; jump to case 4注意如何实现这个开关语句?
操作码 指令 描述 FF /4 JMP r/m32 Jump near, absolute indirect, address given in r/m32
:004014C0 push EBP ; ThreadFunc 的入口点 :004014C1 mov EBP, ESP ... :004014C5 call 0041550 ; 这里将使远程进程崩溃 ... :00401502 ret如果 CALL 是由编译器添加的指令(因为某些“禁忌” 开关如/GZ是打开的),它将被定位在 ThreadFunc 的开始的某个地方或者结尾处。
![]() |
![]() |
![]() |
0人
|
了这篇文章 |
点击图片可刷新验证码请点击后输入验证码博客过2级,无需填写验证码
同时赞一个