这次实验是在WIN7 X86系统上进程,使用的编译器是VS2017。
所谓的DLL注入,其实就是在其他的进程中把我们编写的DLL加载进去。如下图所示
而加载Dll的API就是LoadLibrary,它的参数是保存要加载的DLL的路径的地址。所以DLL注入的核心就是把要注入的DLL的路径写到目标进程中,然后在目标进程中调用LoadLibrary函数,并且指定参数为保存了DLL路径的地址。
要实现DLL注入,首先就要创建一个用来注入的DLL。在VS2017中要生成一个DLL项目,只需要向下图这样创建一个DLL工程就好
在生成的文件中,有个dllmain.cpp,打开以后内容如下
当DLL的状态发生变化的时候,就会调用DllMain函数。而传递的ul_reason_for_call这个参数代表了4种不同的状态变化的情况,我们就可以根据这四种不同的状态根据需要来写出相应的代码,就会让注入的DLL执行我们需要的功能
DLL_PROCESS_DETACH
不过在实现DLL注入的时候用的DLL几乎都是在Dll刚刚映射到进程空间的时候就执行相关的代码。比如像下面这样,创建一个新线程来执行代码,这里在桌面打开一个文件来并写入加载这个DLL的进程的完成路径名。由于是独占方式打开,此时如果多个线程同时打开这个文件,CreateFile就会出错,错误码就会是32,根据这个来对线程进行休眠,等其他线程使用完了,再次打开文件进行操作。
点击生成解决方案以后就可以在项目目录下找到相应的DLL文件,如下图。这个文件就是用来注入到其他进程的DLL。
由于要编写的代码中,只有注入功能不同,但是其他的辅助功能。比如,提权,获取进程PID等等是一样的,为了避免重复就先在这给出代码的框架。后面的不同注入技术只需根据需要加进去就好。注意,如果想要提权成功,需要用管理员权限运行代码
这种注入方式可以说是最常用的注入方式了,它的核心就是调用Windows提供的CreateRemoteThread函数。该函数可以在其他的进程空间中创建一个新的线程进行执行,该函数在文档中的定义如下
其中的关键三个参数分别是
hProcess用来指定在哪个进程中创建新线程
lpStartAddress用来指定将进程中的哪个地址开始作为新线程运行的起始地址
lpParameter保存的也是一个地址,这个地址中保存的就是新线程要用到的参数
那也就是说只要我们指定了一个地址给lpStartAddress,那么我们就可以在其他进程中创建一个线程来执行程序。而再看加载DLL的LoadLibrary函数在文档中的定义如下
可以看到,这个函数同样也只需要一个参数,这个参数是一个地址,而这个地址中保存的是我们要加载的DLL的名称的字符串。
根据这些,不难想到,只要我们可以获取新进程中的LoadLibrary函数的地址以及包含有要加载的DLL的字符串的地址就可以通过CreateRemoteThread函数来成功开起一个线程执行LoadLibrary函数来加载我们的DLL。
那么现在的问题就是如何获得LoadLibrary函数的地址以及保存有要加载的DLL路径的字符串的地址。
对于LoadLibrary函数,由于它是在常用的系统DLL,也就是KERNEL32.dll中,所以这个DLL是可以按照它的ImageBase成功装载到每个进程的空间中。这样的话Kernel32.dll在每个进程中的起始地址是一样的,那么LoadLibrary函数的地址也就会一样。那么我们就可以在本进程中查找LoadLibrary函数的地址,并且完全可以相信,在要注入DLL的进程中LoadLibrary的地址也是这个。
至于DLL名称的字符串,我们可以通过在进程中申请一块可以将DLL完整路径写入的内存,并在这个内存中将DLL的完整路径写入,将写入到注入进程DLL完整路径的内存地址作为参数就可以实现进程的注入。
具体代码如下
上面的方法虽然可以方便的注入DLL。但是在WIN7,WIN10系统上,会由于SESSION 0隔离机制从而导致只能成功注入普通的用户进程,如果注入系统进程就会导致失败。而经过逆向分析发现,使用Kernel32.dll中的CreateRemoteThread进行注入的时候,程序会走到ntdll.dll中的ZwCreateThreadEx函数进行执行。这是一个未导出的函数,所以需要手动获取函数地址来进行调用,相比于CreateRemoteThread更加底层。这个函数在64位和32位系统中的函数声明也不相同,在32位中的声明如下
而在64位中的声明如下
根据逆向分析的结果,在内核6.0(WIN7, WIN10)等系统上调用CreateRemoteThread的时候,当程序走到ZwCreateThreaEx的时候它第7个参数,也就是CreateThreadFlags会被设置为1,如下图
它会导致线程创建的时候就被挂起,随后查看要运行的进程所在的会话层之后再决定是否要恢复线程的运行。所以要破解这种情况只需要将第7个参数设为0就可以,相应代码如下
在Windows系统中,每个线程都会维护一个自己的APC队列,这个APC队列中保存了要求线程执行的一些APC函数。对于用户模式的APC队列,当线程处在可警告状态时,就会执行这些APC函数。而要往APC队列中增加APC函数,需要通过QueueUserAPC函数来实现,这个函数在文档中的定义如下
可以看到pfnAPC和dwData这两个参数和CreateRemoteThread中的lpStartAddress和lpParameter的作用是一样的。不过这里是对线程进行操作,一个进程有多个线程。所以为了确保程序正确运行,所以需要遍历所有线程,查看是否是要注入的进程的线程,依次获得句柄插入APC函数。具体代码如下
这种注入方式主要是通过修改注册表中HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Windows中的AppInit_DLLs和LoadAppInit_Dlls,如下图
只要将AppInit_DLLs设置为要注入的DLL的路径并且将LoadAppInit_DLLs的值改成1。那么,当程序重启的时候,所有加载user32.dll的进程都会根据AppInit_Dlls中的DLL路径加载指定的DLL。
所以这种DLL注入的实现代码如下
运行程序以后,会发现相应的键值已经被设置
Windows系统中的大多数应用都是基于消息机制的,也就是说它们都有一个消息过程函数,可以根据收到的不同消息来执行不同的代码。基于这种消息机制,Windows维护了一个OS message queue以及为每个程序维护着一个application message queue。当发生各种事件的时候,比如敲击键盘,点击鼠标等等,操作系统会从OS message queue将消息取出给到相应的程序的application message queue。
而OS message queue和application message queue的中间有一个称为钩链的结果如下
在这个钩链中保存的就是设置的各种钩子函数,而这些钩子函数会比应用程序还早接收到消息并对消息进行处理。所以程序员可以通过在钩子中设置钩子函数,而要设置钩子函数就需要使用SetWindowHookEx来将钩子函数安装到钩链中,函数在文档中的定义如下
如果函数成功,则返回钩子过程的句柄,否则为NULL。
根据上面的介绍可以得知,想要创建一个全局钩子,就必须在DLL文件中创建。这是因为进程的地址空间是独立的,发生对应事件的进程不能调用其他进程地址空间的钩子函数。如果钩子函数的实现代码在DLL中,则在对应事件发生时,系统会把这个DLL加载到发生事件的进程地址空间中,使它可以调用钩子函数进行处理。
所以只要在系统中安装了全局钩子,那么只要进程接收到可以发出钩子的消息,全局钩子的DLL就会被系统自动或者强行加载到进程空间中,这就可以实现DLL注入。
而这里之所以设置为WH_GETMESSAGE,是因为这种类型的钩子会监视消息队列,又因为Windows系统是基于消息驱动的,所以所有的进程都会有自己的一个消息队列,都会加载WH_GETMESSAGE类型的全局钩子。
当idHook设置为WH_GETMESSAGE的时候,回调函数lpfn的定义如下
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2022-3-18 10:37
被1900编辑
,原因: