上回写到用CreateRemoteThread注入dll,这次换个方式直接注入代码执行
首先看下CreateThread创建线程。CreateThread(...lpStartAddress,lpParameter,...);传入的 参数一个是将要执行函数FuncA的地址,一个是FuncA参数的地址,两个地址都在当前进程的地址空间中。现在要在另一进程空间中创建执行线程执行 FuncB,需要什么?除了目标进程的句柄,[FuncB函数地址和FuncB的参数ArgB肯定都少不了]。接下去,全文就围绕这两个地址一步步展开。
step 1) 先来个简单的程序:FuncB和argB都已经在进程空间中。代码如下:
列表1):
#include <windows.h> //Cpp1.cpp char strs[]={"OutputDebugString"}; DWORD WINAPI ThreadProc(LPVOID lpParam) { char* str = (char*)lpParam; OutputDebugString(str); return 0; } int main(int argc, char* argv[]) { while(1) { Sleep(1000); } return 0; }
main函数并没有直接调用CreateThread创建线程,通过其他进程用CreateRemoteThread创建远程线程来执行ThreadProc。
遵循上面[ ]处提出的需求,需要找到FuncB和ArgB的地址。还是祭出windbg attach到进程:
图1)
?strs ?ThreadProc分别是查找全局变量strsThreadProc的地址
可以看到strs的地址时0x0042a30,ThreadProc的地址是0x00401020.(其实0x00401020一看就能知道是在代码段中)。
首先假设这些地址不变(至少我调试的时候这几个变量地址没变,再说了ThreadProc的地址也不会变,是相对main的偏移),这就可以作为CreateRemoteThread的参数了。上第一个注入程序:
列表2):
#include <windows.h> #include <stdio.h> #pragma comment(lib,"Advapi32.lib") //Cpp2.cpp int main() { FILE* fp=NULL; DWORD PID; LUID luidTmp; HANDLE hToken; TOKEN_PRIVILEGES tokenP; OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken); LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luidTmp); tokenP.PrivilegeCount = 1; tokenP.Privileges[0].Luid = luidTmp; tokenP.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; AdjustTokenPrivileges(hToken, FALSE, &tokenP, sizeof(tokenP), NULL, NULL); fp = fopen("c:\\pid.txt","r+"); fscanf(fp,"%d",&PID);//被注入的程序的pid值写在这个文件中,方便调试时获取进程号 fclose(fp); HANDLE progHd = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE,PID); DWORD ThreadId; HANDLE remoteThread = CreateRemoteThread(progHd,NULL,0,( LPTHREAD_START_ROUTINE)(0x00401020),(void*)(0x0424a30),0,&ThreadId); WaitForSingleObject(remoteThread,INFINITE); }
编译Cpp1和Cpp2,然后准备注入。首先运行Cpp1.exe,然后windbg attach Cpp1.exe(注意设置符号文件),然后在ThreadProc入口下断点,如图:
图2)
接着用windbg启动Cpp2.exe单步执行到CreateRemoteThread之后,这时会触发调试Cpp1.exe的windbg,最终看到windbg输出OutputDebugString,继续上同一张图我懒的重来了,看高亮处即为程序输出:
图3)
整个step 1)结束,无非验证了只要向CreateRemoteThread传递了正确的函数地址和函数参数,就能执行一个线程。
这 个链接 http://blog.csdn.net/lixiangminghate/article/details/41844925 前面写的dll注入,跟step 1)差不多过程,向CreateRemoteThread传递的是已经在进程地址空间中的函数地址LoadLibrary,然后运行 LoadLibrary,就像运行ThreadProc一样。
step 2) 上面的程序纯属用于测试,哪有写个函数要等别人注入进来才能得以运行?再说注入么,出于各种目的,要做的事也不同,因此还要个人定制待运行的代码。那么,问题来了,待运行的代码以及传递给代码的参数,目前不会在目标进程的地址空间中,只能注入者在注入进程内创建FuncB和ArgB的空间,然后填写这两个 空间,这就用到了VirtualAllocEx/WriteProcessMemory,并且VirtualAllocEx返回了新开辟的空间在目标进程 中的地址,这不跟step 1)中手动查找ThreadProc/strs的地址类似吗?有了这些之后就能重复step 1)的过程了!
上进化版的代码:
列表3):
// injCode.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> #include <stdio.h> #include <assert.h> #define OPENPROCESSPROITY PROCESS_CREATE_THREAD|PROCESS_QUERY_INFORMATION|PROCESS_VM_OPERATION|PROCESS_VM_WRITE|PROCESS_VM_READ typedef DWORD (*GetPid)(); typedef struct InjCode { GetPid getPid; DWORD retPid; }InjCode; DWORD _stdcall RemoteFunc(void* args) { InjCode* injCode = (InjCode*)args; injCode->retPid = (injCode->getPid)(); return 0; } InjCode dbgInjCode = {0}; int main(int argc, char* argv[]) { //RemoteFunc(NULL); DWORD pid,dwThreadId; DWORD writtenNum,readNum; InjCode injCode; FILE* fp = fopen("c:\\pid.txt","r+"); assert(fp); fscanf(fp,"%d",&pid); fclose(fp); HANDLE remoteProgHd = OpenProcess(OPENPROCESSPROITY,FALSE,pid); //0x24 sizeof RemoteFunc,calculate by disassemble HMODULE moduleBase = GetModuleHandle("kernel32.dll"); injCode.getPid = (GetPid)GetProcAddress(moduleBase,"GetCurrentProcessId"); void* remoteFunc = VirtualAllocEx(remoteProgHd,0,0x100,MEM_COMMIT, PAGE_EXECUTE_READWRITE); DWORD codeLen=0x46; WriteProcessMemory(remoteProgHd,remoteFunc,&RemoteFunc,codeLen,&writtenNum); void* remoteArgs = VirtualAllocEx(remoteProgHd,0,sizeof(InjCode),MEM_COMMIT, PAGE_EXECUTE_READWRITE); WriteProcessMemory(remoteProgHd,remoteArgs,&injCode,sizeof(InjCode),&writtenNum); HANDLE remoteThread = CreateRemoteThread(remoteProgHd,NULL,0,(LPTHREAD_START_ROUTINE)remoteFunc,remoteArgs,0,&dwThreadId); WaitForSingleObject(remoteThread, INFINITE); ReadProcessMemory(remoteProgHd,remoteArgs,&injCode,sizeof(InjCode),&readNum); return 0; }
因为FuncB和ArgB本身不在目标进程中,因此用VirtualAllocEx和WriteProcessMemory把代码拷贝到目标进程内并执行。
但是这进化版的代码,给我带来一阵噩梦:
1).计算RemoteFunc大小,这个简单 直接调试-反汇编,就能计算代码大小:从00401020到00401066所占用的空间即为codeLen所求(虽然ret指令在00401064,但ret指令占两个字节,因此函数结束位置是66);
列表4):
00401020 push ebp 00401021 mov ebp,esp 00401023 sub esp,44h 00401026 push ebx 00401027 push esi 00401028 push edi 00401029 lea edi,[ebp-44h] 0040102C mov ecx,11h 00401031 mov eax,0CCCCCCCCh 00401036 rep stos dword ptr [edi] 21: InjCode* injCode = (InjCode*)args; 00401038 mov eax,dword ptr [ebp+8] 0040103B mov dword ptr [ebp-4],eax 22: injCode->retPid = (injCode->getPid)(); 0040103E mov ecx,dword ptr [ebp-4] 00401041 mov esi,esp 00401043 call dword ptr [ecx] 00401045 cmp esi,esp 00401047 call __chkesp (004012e0) 0040104C mov edx,dword ptr [ebp-4] 0040104F mov dword ptr [edx+4],eax 23: 24: return 0; 00401052 xor eax,eax 25: } 00401054 pop edi 00401055 pop esi 00401056 pop ebx 00401057 add esp,44h 0040105A cmp ebp,esp 0040105C call __chkesp (004012e0) 00401061 mov esp,ebp 00401063 pop ebp 00401064 ret 4
2).重点来了,把RemoteFunc拷贝到目标进程后,调试输出如下:
首先来看注入进程,以及被注入的代码:
图4)
图5)
从图上显示来看,注入进程和要注入的代码似乎没错。
接着再来看目标进程:ac0000处居然是一个跳转指令,执行后诺干步,遇到异常,进入异常处理分支:
图6)
这明显出错了:WriteProcessMemory拷贝过来的指令流好像不对,RemoteFunc开始部分是push ebp 但是注入后居然代码开头是jmp!这样只能重新单步调试注入进程,查看在调用RemoteFunc时到底产生了什么代码。
反汇编注入进程,输出如下:
列表5):
RemoteFunc(NULL); 00401098 push 0 0040109A call @ILT+0(RemoteFunc) (00401005)
这是说,main函数调用RemoteFunc时(注入进程自己调用),会跳转到00401005,shit我明明是打算从00401020处开始拷贝的,大概定位错误了!再看下这个地址和内容是什么:
列表6):
@ILT+0(?RemoteFunc@@YGKPAX@Z): 00401005 jmp RemoteFunc (00401020) @ILT+5(_main): 0040100A jmp main (00401080)
顺带着看下00401005处的OPCODE:
00401005 E9 16 00 00 00 E9 71 00 .....閝.
居然这个地址是一个相对跳转jmp指令,跳转到00401020,而401020才是前面我要拷贝的目标地址,而041005处指令编码是E9160000,看到这个指令有没有想到什么?图6)中,查看为目标进程分配的堆空间中的函数内容时,看到的也是相同的相对跳转指令。到此,只能说,注入进程调用 WriteProcessMemory拷贝指令流是正确的,但是可能没有拷贝正确的数据。
那正确的数据在哪?或者应该怎么做才能拷贝正确的数据?
回到列表4)和列表6)仔细观察汇编输出:
真正的RemoteFunc在00401020处,而00401005处的跳转指令到00401020之间隔着0x15个字节的代码(后面会解释这些指令的 作用)。回想step2)-1)中用codeLen指定WriteProcessMemory需要拷贝的字节数,明显忽略了00401005处的跳转指令到00401020之间隔着0x15个字节的代码,少了这些字节,就像是120cm的长腿却穿了一条80cm的裤子,各种不舒服,因此导致执行错误。
话 说,main调用RemoteFunc,为什么要经过call-jmp这么一个中转的过程?据说这是编译器提供的增量连接的功能,出现在默认设置的debug版本中。这种编译方式产生的程序对于多个地方调用同一个RemoteFunc,所有的调用点的汇编形式都将是call 00401005 然后转向00401005处的jmp指令,最终由jmp指令跳转到RemoteFunc所在。这么做的好处是,一旦RemoteFunc函数起始地址被修 改,只要修改jmp的跳转目标,程序中对RemoteFunc调用点不需要做任何改动,加快了链接速度(以前我叫这个是imp跳转表)。
到此,只要去除增量连接,call 的跳转目标就直接冲着RemoteFunc而去,而不是往imp跳转表摆渡一下,这样能得到正确的codeLen
3).写到这,你以为没事了?扯,还有一个意外之处!
代码注入到目标进程中,运行到
injCode->retPid = (injCode->getPid)();
之后,程序会检测堆栈,cmp ecx,esp,因为不相等而异常。。。
百度堆栈检查后发现需要去除编译时的/Gx选项即可。
至此代码注入可以正常运行,远程线程退出后,通过ReadProcessMemory获得目标进程的返回