1.前言
注入技术中,最常用的实际上是dll注入,其好处在于方便省事,你只需要关注你dll中本身的代码逻辑即可。
一个很常见的例子是,你会看到很多破解软件的方式,都是让你将一个dll文件放在软件安装目录下完成破解。
其原理并不复杂,因为该dll一般就是该程序必加载的系统dll,一般放在系统目录下而不是在软件安装目录下。
可windows系统加载dll的机制却是先在当前目录下找,找不到才去找系统目录,找到了就直接加载,这样就轻松完成了dll注入操作。
但各种安全管家都会将这种行为视作一种病毒行为,会拦截这样的操作,现象便是你一复制进安装目录,安全管家就给你删掉了。
这种便是静态注入的方式。
而本文要介绍的动态注入就要复杂的多了,它涉及到非常多Windows机制原理,其最终能够实现的效果便是,无需理会其原程序,我们直接在自己的程序中控制目标程序的各种行为。
比如常见的外挂软件,就是一个独立的程序,但它却能在游戏启动的时候识别、并修改游戏中的各种属性数据。
2.理论知识
想要做到上面这样的效果,我们必须要了解一些底层知识。
我们在C/C++中经常使用指针,而指针实际上就是一个存储地址的变量,那么你有没有想过,如果两个exe程序中都去修改、读取同一个地址上的变量,那么它们实际操作的是同一个内存数据吗?
显然,根据我们的经验就能推测出来,这肯定是不行的,毕竟我们电脑上运行了这么多软件,软件的作者不尽相同,怎么可能知道别人用了哪些内存地址上的数据?
为了防止这样混乱的行为发生,windows系统就提供了进程空间的机制。
虽然电脑所有软件用的都是相同的电脑内存,但软件不能直接通过地址访问到实际的内存,而是要通过一个中转站。
类似下面这样:
graph TD;
A[进程1]--访问0x123456地址-->C[Windows内存管理器];
B[进程2]--访问0x123456地址-->C;
C--进程1访问0x222地址-->D[电脑真实内存];
C--进程2访问0x333地址-->D;
虽然两个进程访问的都是同一个地址,但经过了中转站后,它们访问的真实内存地址并不相同。
而中转站在这里仅仅起到了一个映射作用,比如此时上图的映射关系为:进程1 0x123456 == 0x222
,进程2 0x123456 == 0x333
。
这样做的好处就在于,不同进程之间完全隔离开了,无论怎么访问地址,都无法操作其它进程中的数据。
而我们之所以要进行代码注入,就是为了能让我们的代码运行在目标进程空间内部,这样代码操作的内存数据才是目标内存中的数据。
dll注入就是最简便的注入方式,只要目标进程加载了dll,那么dll就存在于目标进程之中,此时就可以随意修改数据信息了。
而本文要介绍的方式便是更为高级、复杂的手段,让我们可以在自己的进程中去操作目标进程中的数据,无需dll注入。
3.从零开始
经过前面的学习我们已经知道了,Windows系统提供了我们以下几个win api可以让我们控制其它进程:
- 在本进程中通过
OpenProcress
函数打开想要Hook的程序 - 使用
VirutalAllocEx
在目标进程中分配一块空间。 - 用
WriteProcessMemory
函数在分配的空间中写上我们想要让其执行的代码,以及在哪里进行跳转等等 - 一般还需要用
ReadProcessMemory
函数读取、保留其原本位置的数据,用于后面的恢复 - 可能还需要用
VirutalProtectEx
函数更改目标内存的权限,使其允许我们读写目标进程的内存。 - 可能还需要使用
CreateRemoteThread
函数在目标进程内新创建一个线程执行我们复制过去的代码
但现在问题来了,我们能复制过去的代码有哪些?怎么复制?难不成要自己手搓二进制然后复制过去不成?
这显然是不现实的,大多数时候,我们更倾向于使用vs编写C/C++函数,然后直接将编译好的整个函数二进制都复制到目标进程中去。
就像下面这样:
这里的get_kernel_addr
就是一个函数,而这个函数名实际上就是这个函数在本进程中的内存地址,直接将这个函数整个复制到目标进程中即可。
所以现在我们已经知道了如何将一个函数复制到目标进程中去,那么下一个问题便是,这个复制过去的函数里面我们能写什么东西?
因为此时你要知道,由于该函数是在本进程中进行编译运行的,所以该函数内部如果调用任何函数,比如即使是最简单的printf
函数,该函数的地址也属于本进程,一旦将其复制到目标进程,就会在相应的地址上找不到函数,一运行就会崩溃。
流程如下:
sequenceDiagram
participant A as 进程1
participant B as 进程2
A-->>A: printf函数的地址为0x123456
B-->>B: printf函数的地址为0x654321
A->>B: 复制含printf函数地址0x123456的代码
B-->>B: 由于0x123456地址上不是printf函数,程序运行该函数会崩溃
这样一来就出现了一个非常严重的问题:我们可以将代码复制过去,但这段代码中不能调用任何函数。
可一旦无法调用任何函数,那么我们将这段代码复制过去的意义是什么呢?根本什么都干不了!
这便是本节从零开始的含义,我们需要从零在目标进程中找到我们所需要的函数地址,然后就能调用它们!
这一切的源头便在于kernel32.dll
这个Windows动态库,这是windows的核心动态库,windows系统中所有的exe程序,只要运行起来,都会默认自动加载该dll。
并且最重要的是,几乎所有重要的winapi,都存放在了这个dll中,因此只要我们能够找到这个dll在目标进程中的地址,我们就能够从该dll中解析出来其中win api的地址,然后我们就能在目标进程中调用这些win api!
此时流程如下: