上回写到用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获得目标进程的返回