在前一节中介绍了通过远线程不带参数的方式提前注入进程,现在介绍种远线程携带参数的方法。(转载请指明出处)
1.2 执行注入的进程需要传信息给被注入进程
因为同样采用的是远线程注入,所以大致的思路是一样的,只是在细节上要注意一些处理。总体来说分为以下几个步骤:
1 将需要传递的信息写入被注入进程的地址空间。
2 将远线程函数体写入被注入进程的空间。
3 在被注入进程中执行该远线程函数,让该线程利用我们之前写入的参数完成任务。
在被注入进程的地址空间中写入“需要传递”的信息不存在什么问题,因为该信息是”死的“”数据“,我们写入什么内容就是什么内容,它就是二进制数据。但是写入函数执行体就存在一定的问题。首先我们要考虑用什么语言来写这个函数?我是VC程序员,当然优先选择C /C。可是使用这些语言往往会存在问题,因为我们不知道编译器对我们的代码可能做了什么手脚。下面我来验证下
代码语言:javascript复制 typedef struct _RemoteThreadRountineParam_
{
//LPLoadLibrary lpLoadLibraryW;
//LPGetProcAddr lpGetProcAddress;
WCHAR wszDllPath[MAX_PATH];
CHAR szFuncName[MAX_FUNCNAMELENGTH];
HANDLE hEvent;
}RemoteThreadRountineParam,*pRemoteThreadRountineParam;
代码语言:javascript复制DWORD WINAPI RemoteThreadRoutine_Error( LPVOID lpParam )
{
if ( NULL == lpParam )
{
return 0;
}
pRemoteThreadRountineParam lpRmtParam = (pRemoteThreadRountineParam) lpParam;
if ( NULL == lpRmtParam->lpLoadLibraryW )
{
return 0;
}
if ( NULL == lpRmtParam->lpGetProcAddress )
{
return 0;
}
HMODULE hHookDll = LoadLibraryW(lpRmtParam->wszDllPath);
if ( NULL == hHookDll )
{
return 0;
}
LPExportFun lpExportFunAddr = (LPExportFun)GetProcAddress( hHookDll, lpRmtParam->szFuncName );
if ( NULL == lpExportFunAddr )
{
return 0;
}
lpExportFunAddr( lpRmtParam->hEvent );
return 0;
}
回想前一节中,我们将DLL的绝对路径写入被注入进程的空间作为远线程的唯一参数,而本节的远线程需要很多参数,所以我们要定义一个结构体RemoteThreadRountineParam。它包含的成员是:要注入的DLL的绝对路径、这DLL中的导出函数名,以及这个导出函数需要的参数——Event句柄。远线程执行的函数体是RemoteThreadRoutine_Error,其参数是一个指向RemoteThreadRountineParam结构体对象的一个指针,正如其名字——它是Error的。其执行的逻辑也是很简单的:加载DLL,寻找导出函数和执行导出函数。之后我们所有带参数的注入逻辑都将采用这个最基本的处理流程,只是细节处理上存在一定的区别。
代码语言:javascript复制do {
// 写入线程例程代码
// 分配内存空间
pBufferRemoteFun = VirtualAllocEx( hProcess, NULL, dwFunMemSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE );
if ( NULL == pBufferRemoteFun ) {
break;
}
// 将信息写入傀儡进程的内存地址空间
if ( FALSE == WriteProcessMemory( hProcess, pBufferRemoteFun, pRmtRoutine, dwFunMemSize, NULL ) ) {
break;
}
do {
pBufferParam = VirtualAllocEx( hProcess, NULL, dwParamMemSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE );
if ( NULL == pBufferParam ) {
break;
}
if ( FALSE == WriteProcessMemory( hProcess, pBufferParam, &RmtThdRtParam, dwParamMemSize, NULL ) ) {
break;
}
do {
// 注入线程
pRemoteThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pBufferRemoteFun, pBufferParam, 0, NULL );
if ( NULL == pRemoteThread ) {
break;
}
这段逻辑分别对应于刚介绍的三个步骤。其中dwFunMemSize是我们定义的一个”足够大“的空间大小,因为我这儿没有计算准确的函数执行体大小(其实我也不知道怎么去计算这个大小)。因为我们的函数执行体代码是要执行的,所以我们申请的空间是具有EXECUTE属性的。pBufferRemoteFun是指向远线程函数执行体的在”远程“的空间。pRmtRoutine是指向远线程函数执行体的在”本地“的空间。其他没有什么好介绍的,我们将主要的注意力放在pRmtRoutine。
最简单的方式是
代码语言:javascript复制char* pRmtRoutine = (char*)RemoteThreadRoutine_Error;
但是我们debug的结果是 “‘0x000a432c’指令引用的‘0x0000a432c’内存。该内存不能为‘written’”。可以见得我们写入的远线程代码存在问题。现在我们用windbg调试下。在调试前,我们先调整下VC代码为
代码语言:javascript复制// 注入线程
pRemoteThread = CreateRemoteThread( hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pBufferRemoteFun, pBufferParam, 0, NULL );
if ( NULL == pRemoteThread ) {
break;
}
ResumeThread( hThread );
// 等待远线程激活事件
if ( WAIT_OBJECT_0 != WaitForSingleObject( hEvent, 10 * 1000 ) ) {
// 等待出错,退出进程
//::TerminateProcess( hProcess, 0 );
}
else {
// 等待成功,恢复进程
ResumeThread( hThread );
}
注意此处,我不会在线程执行失败后立即TerminateProcess,否则我们windbg准备调试被注入进程时,被注入进程可能已经就被杀掉了。而且因为之前我们是以挂起方式创建被注入进程的,所以在执行完创建远线程后,要ResumeThread主线程。否则我们在远线程挂了后,windbg没法挂到任何一个线程上。调试的过程是:
1 用VC在CreateRemoteThread上下断点,F5,断到这个函数执行前。记下pBufferRemoteFun的值。
2 用windbg附加到被注入进程上。
3 在VC中F5,让被注入进程出现错误,以让windbg捕获。
4 在windbg中F5。
会出现以下信息
代码语言:javascript复制(d20.8d8): Break instruction exception - code 80000003 (first chance)
eax=002d1eb4 ebx=7ffdb000 ecx=00000003 edx=00000008 esi=002d1f48 edi=002d1eb4
eip=7c92120e esp=0007fb20 ebp=0007fc94 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202
_no_process_!DbgBreakPoint:
7c92120e cc int 3
看不出什么信息。但是我们可以借助pBufferRemoteFun的值(假设为0x000a0000 ),在windbg命令框中输入0x000a0000 0x000a0100。出现如下信息
代码语言:javascript复制000a0000 e927430000 jmp 000a432c
000a0005 e902360000 jmp 000a360c
000a000a e9ad6d0000 jmp 000a6dbc
……
这是神马?这是debug环境下增量编译(incremental linking)的一种表现。这儿说说增量编译,增量编译如同在“调用”和“函数执行逻辑”之间插入一个“地址转换层”。比如我们有个函数A,我们调用A的汇编是Call 0x00ABCDEF,那么修改A函数代码后编译,这个0x00ABCDEF地址会发生改变。因为Call指令分为两步,其中第二步是jmp到A函数逻辑的入口点,jmp的偏移是需要计算的。于是我们频繁修改A的函数逻辑,会导致频繁的计算A函数逻辑偏移地址(想想整个PE文件中所有调用都要再算一次jmp偏移是不是很浪费)。于是一种解决方案是,调用A时就Call一个固定地址,该地址指令是jmp到一个固定的地址,这个地址保存的是真实调用A的代码。这样每次编译只用修改“转换层”中的jmp偏移即可。
回到我们的问题,用u 0x000a432c可以发现这个内存空间不存在汇编代码。因为我们写错内容了,我们要写入的远线程函数的逻辑代码。那么我们将jmp过滤掉。
代码语言:javascript复制 // JMP过渡
if ( (char)0xE9 == *pRmtRoutine ) {
DWORD dwOffset = 0;
memcpy_s( &dwOffset, sizeof(DWORD), pRmtRoutine 1, sizeof(DWORD) );
// 偏移,1为0xE9,4为dwoffset大小
pRmtRoutine = pRmtRoutine 1 4 dwOffset;
}
再试一下我们程序。还是报错,这次错误是“'0x000a0053'指令引用的'0x00424788'内存。该内存不能为'read'”。使用刚才的调试方法,我们发现我们注入的代码如下
代码语言:javascript复制000a0000 55 push ebp
000a0001 8bec mov ebp,esp
000a0003 81ece4000000 sub esp,0E4h
000a0009 53 push ebx
000a000a 56 push esi
000a000b 57 push edi
000a000c 8dbd1cffffff lea edi,[ebp-0E4h]
000a0012 b939000000 mov ecx,39h
000a0017 b8cccccccc mov eax,0CCCCCCCCh
000a001c f3ab rep stos dword ptr es:[edi]
000a001e 837d0800 cmp dword ptr [ebp 8],0
000a0022 7507 jne 000a002b
000a0024 33c0 xor eax,eax
000a0026 e983000000 jmp 000a00ae
000a002b 8b4508 mov eax,dword ptr [ebp 8]
000a002e 8945f8 mov dword ptr [ebp-8],eax
000a0031 8b45f8 mov eax,dword ptr [ebp-8]
000a0034 833800 cmp dword ptr [eax],0
000a0037 7504 jne 000a003d
000a0039 33c0 xor eax,eax
000a003b eb71 jmp 000a00ae
000a003d 8b45f8 mov eax,dword ptr [ebp-8]
000a0040 83780400 cmp dword ptr [eax 4],0
000a0044 7504 jne 000a004a
000a0046 33c0 xor eax,eax
000a0048 eb64 jmp 000a00ae
000a004a 8b45f8 mov eax,dword ptr [ebp-8]
000a004d 83c008 add eax,8
000a0050 8bf4 mov esi,esp
000a0052 50 push eax
000a0053 ff1588474200 call dword ptr ds:[424788h]
000a0059 3bf4 cmp esi,esp
000a005b e8c0bbffff call 0009bc20
000a0060 8945ec mov dword ptr [ebp-14h],eax
000a0063 837dec00 cmp dword ptr [ebp-14h],0
000a0067 7504 jne 000a006d
000a0069 33c0 xor eax,eax
000a006b eb41 jmp 000a00ae
000a006d 8b45f8 mov eax,dword ptr [ebp-8]
000a0070 0510020000 add eax,210h
000a0075 8bf4 mov esi,esp
000a0077 50 push eax
000a0078 8b4dec mov ecx,dword ptr [ebp-14h]
000a007b 51 push ecx
000a007c ff1504474200 call dword ptr ds:[424704h]
000a0082 3bf4 cmp esi,esp
000a0084 e897bbffff call 0009bc20
000a0089 8945e0 mov dword ptr [ebp-20h],eax
000a008c 837de000 cmp dword ptr [ebp-20h],0
000a0090 7504 jne 000a0096
000a0092 33c0 xor eax,eax
000a0094 eb18 jmp 000a00ae
000a0096 8bf4 mov esi,esp
000a0098 8b45f8 mov eax,dword ptr [ebp-8]
000a009b 8b8850020000 mov ecx,dword ptr [eax 250h]
000a00a1 51 push ecx
000a00a2 ff55e0 call dword ptr [ebp-20h]
000a00a5 3bf4 cmp esi,esp
000a00a7 e874bbffff call 0009bc20
000a00ac 33c0 xor eax,eax
000a00ae 5f pop edi
000a00af 5e pop esi
000a00b0 5b pop ebx
000a00b1 81c4e4000000 add esp,0E4h
000a00b7 3bec cmp ebp,esp
000a00b9 e862bbffff call 0009bc20
000a00be 8be5 mov esp,ebp
000a00c0 5d pop ebp
000a00c1 c20400 ret 4
我们查看之前报错的0x000a0053行call dword ptr ds:[424704h],这个函数地址不是被注入进程空间的函数地址,像之后000a007c ff1504474200 call dword ptr ds:[424704h]也是会报错的。那么这两个函数是啥?我在VC中Alt 8查看远线程函数的汇编代码,可以发现call dword ptr ds:[424704h]对应于LoadLibraryW这个函数。0x00424704h保存的是0x7c80aeeb。我们用windbg加载并运行注入进程的PE文件,break后查看相关地址命令
代码语言:javascript复制0:002> u 0x7c80aeeb
kernel32!LoadLibraryW:
7c80aeeb 8bff mov edi,edi
7c80aeed 55 push ebp
7c80aeee 8bec mov ebp,esp
7c80aef0 6a00 push 0
7c80aef2 6a00 push 0
7c80aef4 ff7508 push dword ptr [ebp 8]
7c80aef7 e8f96bffff call kernel32!LoadLibraryExW (7c801af5)
7c80aefc 5d pop ebp
代码语言:javascript复制0:002> u 0x7c801af5
kernel32!LoadLibraryExW:
7c801af5 e9f2e7cf84 jmp 015002ec
7c801afa 807ce8d509 cmp byte ptr [eax ebp*8-2Bh],9
7c801aff 0000 add byte ptr [eax],al
7c801b01 33ff xor edi,edi
7c801b03 897dd8 mov dword ptr [ebp-28h],edi
7c801b06 897dd4 mov dword ptr [ebp-2Ch],edi
7c801b09 897de0 mov dword ptr [ebp-20h],edi
7c801b0c 897de4 mov dword ptr [ebp-1Ch],edi
可以想到,0x7c80aeeb是Kernel32.dll文件在该进程中LoadLibrary的函数入口地址。所以我们call dword ptr ds:[424704h]时,被注入进程中424704h保存的是啥是不确定的。但是,如我在前一节介绍的,windows程序加载kernel32.dll的基地址一般是一样的,于是我们要是将0x7c80aeeb这个值直接传给远线程,应该就可以了。同样的问题存在于我们之前对GetProcAddress的调用,于是我们将这些函数地址以参数形式传入被注入进程。
代码语言:javascript复制RemoteThreadRountineParam RmtThdRtParam;
RmtThdRtParam.lpGetProcAddress = GetProcAddress;
RmtThdRtParam.lpLoadLibraryW = LoadLibraryW;
wmemset( RmtThdRtParam.wszDllPath, 0, MAX_PATH );
wcscpy_s( RmtThdRtParam.wszDllPath, MAX_PATH, lpDllPath );
std::string strFuncName = "ExportFun";
memset( RmtThdRtParam.szFuncName, 0 , MAX_FUNCNAMELENGTH );
memcpy_s( RmtThdRtParam.szFuncName, MAX_FUNCNAMELENGTH, strFuncName.c_str(), strFuncName.length() );
远线程的代码改为
代码语言:javascript复制 HMODULE hHookDll = (lpRmtParam->lpLoadLibraryW)(lpRmtParam->wszDllPath);
if ( NULL == hHookDll )
{
return 0;
}
LPExportFun lpExportFunAddr = (LPExportFun)(lpRmtParam->lpGetProcAddress)( hHookDll, lpRmtParam->szFuncName );
if ( NULL == lpExportFunAddr )
{
return 0;
}
lpExportFunAddr( lpRmtParam->hEvent );
F5。还是报错。这次的错误是“'0x0009bc20'指令引用的'0x0009bc20'内存。该内存不能为'written'”。继续使用之前的调试方法,发现我们注入的代码中有如下一行
代码语言:javascript复制000a0053 8b4df8 mov ecx,dword ptr [ebp-8]
000a0056 8b11 mov edx,dword ptr [ecx]
000a0058 ffd2 call edx
000a005a 3bf4 cmp esi,esp
000a005c e8bfbbffff call 0009bc20
000a0061 8945ec mov dword ptr [ebp-14h],eax
000a0064 837dec00 cmp dword ptr [ebp-14h],0
000a0068 7504 jne 000a006e
我们再在VS中查看我们的远线程反汇编代码有如下
代码语言:javascript复制00415A2C E8 BF BB FF FF call @ILT 1515(__RTC_CheckEsp) (4115F0h)
E8BFBBFFFF这条指令是引起被注入进程崩溃的原因,这指令是RTC检查函数,默认情况下VC会给我们的代码做些手脚,这个就是个例子。我们对远线程代码关闭RTC检查。
代码语言:javascript复制#pragma runtime_checks( "scu", off )
DWORD WINAPI RemoteThreadRoutine_Error( LPVOID lpParam )
{
……
}
#pragma runtime_checks( "scu", restore )
运行之,OK了。 这个过程很忐忑,但是如果不想研究这个,可以选择内嵌汇编方式。因为RTC检查不会在Release版本中做,所以我们可以将远线程函数本地执行一次,在函数的入口处int 3一下,然后用windbg或ollydbg启动之,断在函数入口点,然后我们把其汇编东东扒拉下来就行了。当然会写汇编的同学就直接动手写汇编代码就行了。
代码语言:javascript复制__declspec(naked) DWORD WINAPI RemoteThreadRoutineASM( LPVOID lpParam )
{
__asm
{
push esi
// 检测指针参数
mov esi, [esp 8]
pushad
pushfd
test esi, esi
jz short End
// 检测参数第一个成员
mov eax, [esi 4]
test eax, eax
jz short End
// 检测参数第二个成员
mov eax, [esi]
test eax, eax
jz short End
lea ecx, [esi 8]
push ecx
call eax
test eax,eax
jz short End
lea edx, [esi 210h]
push edx
push eax
mov eax, [esi 4]
call eax
test eax,eax
jz short End
mov ecx, [esi 250h]
push ecx
call eax
End:
xor eax, eax
popfd
popad
pop esi
retn 8
}
}
一定要加__declspec(naked),否则起不来哦! (转载请指明出处)