CreateRemoteThread 直接注入代码执行

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值