WINDOWS黑客基础(3):注入代码

WINDOWS黑客基础(3):注入代码

有使用过外挂的朋友应该知道,我们在玩游戏的时候,有很多辅助功能给你使用,比如吃药,使用物品等功能,这个时候我们就是使用注入代码的技术,简单的来将就是我们让另外一个进程去执行我们想让它执行的代码,这中间的关键函数是CreateRemoteThread

HANDLE WINAPI CreateRemoteThread(
  _In_   HANDLE hProcess,
  _In_   LPSECURITY_ATTRIBUTES lpThreadAttributes,
  _In_   SIZE_T dwStackSize,
  _In_   LPTHREAD_START_ROUTINE lpStartAddress,
  _In_   LPVOID lpParameter,
  _In_   DWORD dwCreationFlags,
  _Out_  LPDWORD lpThreadId
);

CreateRemoteThread的参数跟CreateThread的参数差不多,多出来的hProcess是我们要对其操作的进程HANDLE,在早期的WINDOWS版本CreateThread确实是使用CreateRemoteThread实现的,就是把hProcess传入我们自己的进程HANDLE


CreateRemoteThread的功能就是在指定的进程创建一个线程,这个线程运行我们指定的函数,看起来很简单,但是有一个问题,就是虚拟内存导致的问题

大家都知道,在WINDOWS下是使用虚拟内存来进行数据管理的,每个进程都有自己独立的地址空间,假设进程A准备向进程B注入一段代码,他要让进程B执行他进程空间的函数InjectionCode(),这个函数在进程A的地址空间地址为0X3000

现在我们开始进行代码注入,利用CreateRemoteThread我们告诉B进程,请执行虚拟内存地址为0X3000的代码,这个时候B进程该干什么呢??B进程收到这个命令后,他很听话地创建了线程,然后乖乖得CALL了0X3000的内容,请注意,现在B进程CALL的是它自己内存空间内0X3000的代码而不是A进程的,那么现在B进程的0X3000是什么内容??没人知道,运气好的话说不定真的有段代码给你执行,运气不好你自己也不知道会发生什么事情,这就跟你进错了学生公寓一样,同样号码的房间,运气好是校花的房间,运气不好就是如花的房间

那么我们怎么才能让进程去执行我们对应的代码呢??我们只要在B进程内开辟一块内存,然后把我们的代码或者数据复制进去,再执行对应的代码就可以了,我们需要用到这几个函数:

LPVOID WINAPI VirtualAllocEx(
  _In_      HANDLE hProcess,
  _In_opt_  LPVOID lpAddress,
  _In_      SIZE_T dwSize,
  _In_      DWORD flAllocationType,
  _In_      DWORD flProtect
);

BOOL WINAPI WriteProcessMemory(
  _In_   HANDLE hProcess,
  _In_   LPVOID lpBaseAddress,
  _In_   LPCVOID lpBuffer,
  _In_   SIZE_T nSize,
  _Out_  SIZE_T *lpNumberOfBytesWritten
);

这两个函数跟我们平常用的函数都差不多,只是多了个进程的选项,大概步骤如下图:
WINDOWS黑客基础(3):注入代码

现在我们将实际操作一下:

下面是我们要注入的程序,在这之前,我们最好把基地址固定掉,这样我们不会每次重新运行程序的时候函数的地址都会改变,在VS2008中,项目属性->链接器->高级,把随机基址和固定基址选择默认值

void PrintMsg(const char *msg)
{
    printf("ThreadI D:%d Msg:%s
",GetCurrentThreadId(),msg);
}
int main(void)
{
    printf("%d
",GetCurrentThreadId());
    printf("Print Msg Function Address:%X
",PrintMsg);
    system("pause");
}

假设我们的PrintMsg的地址是0x401000,现在我们需要往这个进程里面注入一段代码,让她可以自动调用PrintMsg这个函数

 1 static const char *msg = "INJECTION CODE SUCESS
";
 2 static const unsigned int PARAM_SIZE = 100;
 3 static const unsigned int EXE_SIZE = 500;
 4 
 5 
 6 void InjectionCode(const char *msg)
 7 {
 8     __asm
 9     {
10         push eax
11         push msg
12         mov eax,0x401000
13         call eax            //因为在我们要注入的进程中,PrintMsg位于0x401000这个位置
14         pop eax
15         pop eax
16     }
17 }
18 int main(void)
19 {
20     HANDLE hProcess = OpenProcessByProcessNmae("main.exe"); //这个函数在上一章
21     
22     if (hProcess == INVALID_HANDLE_VALUE)
23     {
24         printf("error open process %d
",GetLastError());
25         return 1;
26     }
27     //一定要把函数的代码和msg写入要注入的进程,否则会发生位置错误(一般是崩溃)
28     LPVOID RemoteExe = VirtualAllocEx(hProcess,NULL,EXE_SIZE,MEM_COMMIT,PAGE_EXECUTE);
29     LPVOID RemoteParam = VirtualAllocEx(hProcess,NULL,PARAM_SIZE,MEM_COMMIT,PAGE_READWRITE);
30 
31     SIZE_T WriteCount = 0;
32     int ret = 0;
33     ret = WriteProcessMemory(hProcess,RemoteParam,msg,PARAM_SIZE,&WriteCount);
34     ret = WriteProcessMemory(hProcess,RemoteExe,InjectionCode,0x13,&WriteCount);
35 
36     HANDLE hThread = CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)RemoteExe,RemoteParam,0,NULL);
37     WaitForSingleObject(hThread,INFINITE);
38 }

运行上面的程序,我们就可以在另外一个进程中创建一个线程,并且这个线程将会输出该线程的ID以及我们要输出的消息

上面的程序还有几个要注意的:

1.资源竞争

由于是创建线程执行相应代码所以肯定会有资源竞争的问题,以后要写代码一定要注意,在本例中我忽略了这个问题

2. 关于代码的长度问题

在本例中,我们的代码长度是0X13,但是要知道,汇编代码的长度随便懂一下就可能更改,可能因为一个指令,也可能因为一个参数,所以我们需要时刻注意这点,关于代码长度怎么测量,我是看了反汇编的代码后计算的,这个方法比较准确,也可以大概估计下,只要能把代码复制完整就可以,超出也没关系,只要不超出申请的内存大小就可以

3.记得备份我们使用的寄存器

这个十分重要,一旦你更改了寄存器,如果没有后面没有恢复,可能会导致一系列错误,特别是ESP,EBP等重要的寄存器

3.注入代码多次调用系统DLL中的函数

<<WINDOWS核心编程>>里面说,系统的DLL都会加载到一个固定的地址,比如VirtualAllocEx,一般我们在A进程和B进程的时候,call或者jmp的地址都是一样的,所以一般我们如果调用的是系统函数,一般我们不需要担心,但是,昨天我想到了一个问题,比如我们进程A要命令进程B调用CreateToolhelp32Snapshot这个系统API,现在我们假设CreateToolhelp32Snapshot这个API在单独的TLHELP32.DLL里面(实际上这个在KERNEL32.DLL里面,所有进程都会加载这个DLL,所以不需要担心下面的问题,这个只是举例),操作系统在加载DLL的时候,会统一把这个API的地址映射到虚拟内存的0XFF40100的地址,按照我们原来的想法进程B会自己跑去call 0XFF40100这个地址。但问题在于,如果我们的进程根本就没有加载TLHELP32.DLL这个DLL,那么进程call 0XFF40100会怎么样??这个就要看你这个地方是什么代码了,有人说操作系统会帮你加载这个DLL,但我觉得是错的,因为操作系统要帮你加载的DLL都在PE头里面的导入表里面,要嘛就是我们要显示地去加载,否则操作系统不会知道我们的API在哪个DLL里面

4.注入代码多次调用我们自己编写的函数

比如我们有IntejectionCode,里面调用了IntejectionCode1,这个时候我们需要把IntejectionCode1也写入对方进程里面,不能只写入IntejectionCode,并且,我们需要更改IntejectionCode里面call IntejectionCode1跳转指令,让其跳转到正确的位置。总之,别人的地盘别人做主,对方进程想把代码放哪里就放哪里,我们无法管理(实际上virtualAllocEx是可以指定位置的,但是一般我们都尽量让操作系统去指定),我们只能入乡随俗,人家让我们去哪里调用我们就要去哪里调用,不然很容易导致进程崩溃

5.关于代码的基地址

在本例中PrintMsg的基地址是固定的,是我们人为去固定基地址的,我们在开发的时候很少人会去把基地址固定掉,所以在进程运行的时候,PrintMsg这个函数的地址是会改变的,当然我们也可以算出来这段代码在运行的时候会放在哪里,因为PrintMsg这段代码以二进制放在EXE文件的时候,也有一个文件偏移量,当操作系统把EXE文件加载进内存后,会根据基地址和文件偏移量,来算出PrintMsg在虚拟内存中的位置,所以我们只要能拿到进程运行时候的基地址,并且把EXE反汇编查看这段代码的文件偏移量,也能算出每次运行的时候PrintMsg的地址,虽然很麻烦,特别是反汇编找代码的那部分,但也没办法,这个在后面我会讲