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\n",GetCurrentThreadId(),msg);

}

int main(void)

{

    printf("%d\n",GetCurrentThreadId());

    printf("Print Msg Function Address:%X\n",PrintMsg);

    system("pause");

}

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

 1 static const char *msg = "INJECTION CODE SUCESS\n";

 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\n",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的地址,虽然很麻烦,特别是反汇编找代码的那部分,但也没办法,这个在后面我会讲

你可能感兴趣的:(windows)