第27章 代码注入
本文最后更新于:2022年5月19日 晚上
第27章代码注入
本章将讲解代码注入(CodeInjection)相关技术,并借助一个练习示例向各位展示代码注入的实施原理与方法。通过比较分析,了解代码注入与DLL注入的不同点。
27.1 代码注入
代码注入是一种向目标进程插入独立运行代码并使之运行的技术,它一般调用CreateRemoteThread()API以远程线程形式运行插入的代码,所以也被称为线程注入。图27-1描述了代码注入技术的实现原理。
首先向目标进程target.exe插入代码与数据,在此过程中,代码以线程过程(Thread Procedure )形式插入,而代码中使用的数据则以线程参数的形式传入。也就是说,代码与数据是分別注入的。如上所言,代码注入的原理非常简单,但具体实现过程中有一些内容必须注意。下面就通过与DLL注入比较来讲解实现代码注入的注意事项。
27.2 DLL注入与代码注入
请看下面这段简单的代码,它用来弹出Windows消息框。
1 |
|
若使用DLL注入技术,则需要先把上述代码放入某个DLL文件,然后再将整个DLL文件注入目标进程。采用该技术完成注入后,运行OllyDbg调试器,查看上述ThreadProc()代码区域,如图27-2所示。
请注意图27-2代码中使用的地址。首先,10001002地址处有一条PUSH 10009290指令,紧接其下的是PUSH 1000929C指令。在OllyDbg的内存Dump窗口中查看地址10009290与1000929C,如 图27-3所示。
从图27-3中可以看到,这2个地址(10009290与1000929C)分别指向DLL数据节区中的字符串(“ReverseCore”、“www.reversecore.com”)。上面2条PUSH指令将MessageBoxA() API中要使用
的字符串(“ReverseCore”、“www.reversecore.com”)的地址存储到栈。继续看图27-2,1000100E地址处有一条CALL DWORD PTR DS:[100080F0]指令,该CALL指令即是调用user32!MessageBoxA() API的命令,转到100080F0地址处查看,如图27-4所示。
从图27-4可知,100080F0地址就是DLL的IAT区域(在其上方可以看到其他API的地址)。像这样,DLL代码中使用的所有数据均位于DLL的数据区域。釆用DLL注入技术时,整个DLL会被插入目标进程,代码与数据共存于内存,所以代码能够正常运行。与此不同,代码注入仅向目标进程注入必要的代码(图27-2 ),要想使注入的代码正常运行,还必须将代码中使用的数据(图 27-3、图27-4) —同注入(并且要通过编程将已注入数据的地址明确告知代码)。基于这种原因,使用代码注入技术时要考虑的事项比使用DLL注入技术要多得多。通过分析后面示例的代码,大家可以更准确地把握。
使用代码注入的原因
其实,代码注入要实现的功能与DLL注入类似,但具体实施时要考虑的事项更多,使用起来更加不便。那它的优点究竟是什么呢?
占用内存少
如果要注入的代码与数据较少,那么就不需要将它们做成DLL的形式再注入。此时直接采用
代码注入的方式同样能够获得与DLL注入相同的效果,且占用的内存会更少。难以查找痕迹
采用DLL注入方式会在目标进程的内存中留下相关痕迹,很容易让入判断出目标进程是否被执行过注入操作。但采用代码注入方式几乎不会留下任何痕迹(当然也有一些方法可以检测),因此恶意代码中大量使用代码注入技术。其他
不需要另外的DLL文件,只要有代码注入程序即可。大家刚开始会觉得代码注入技术生疏,熟悉之后就会觉得简单好用。
简单归纳一下:DLL注入技术主要用在代码量大且复杂的时候,而代码注入技术则适用于代码量小且简单的情况。
27.3 练习示例
本节我们学习一个代码注入示例(CodeInjection.exe),用它向notepad.exe进程注入简单的代
码,注入后会弹出消息框。
27.3.1 运行 notepad.exe
首先运行notepad.exe,然后使用Process Explorer查看notepad.exe进程的PID,如图27-5所示。
我的测试环境中,notepad.exe的PID为2036。
27.3.2 运行 CodeInjection.exe
在命令行窗口中输入命令与参数(notepad.exe的PID ),运行CodeInjection.exe文件,如图27-6所示。
27.3.3 弹出消息框
notepad.exe进程中弹岀一个消息框,如图27-7所示。
弹出的消息框可能位于notepad.exe窗口的下方,查看时请注意。
接下来看示例的源代码,仔细分析代码注入是如何实现的。
27.4 Codelnjection.cpp
为便于说明,下面即将介绍的源代码略去了异常处理部分,完整代码请参考本书源代码中的Codelnjection.cpp文件。
Codelnjection.cpp 使用 VC-H- 2010 Express Edition 工具编写而成,在 Windows XP/7 32位系统中通过测试。
27.4.1 main()函数
首先看一下main()函数。
1 |
|
main()函数用来调用InjectCode()函数,传入的函数参数为目标进程的PID。
27.4.2 ThreadProc()函数
分析InjectCode()函数之前,先看一下要注入目标进程的代码(线程函数)。
1 |
|
上述代码中实际被注入的部分是ThreadProc()函数(前面的typedef语句是针对C语言语法的,不需要注入)。ThreadProc()函数代码中使用了很多函数指针,乍一看比较复杂,但稍微整理就会发现其实很简单。
1 |
|
同时参考代码27-2中的注释,相信大家能够很容易地理解ThreadProc()函数的代码。
其实,重要的是ThreadProc()代码这一概念。代码注入技术的核心内容是注入可独立运行的代码,为此,需要同时注入代码与(代码中引用的)数据,并且要保证代码能够准确引用注入的数据。从上述代码中的ThreadProc()函数可以看到,函数中并未直接调用相关API,也未直接定义使用字符串,它们都通过THREAD_PARAM结构体以线程参数的形式传递使用。 若ThreadProc()函数在一个普通程序中,其函数代码将非常简单,代码如下:
1 |
|
编译代码27-3后,使用调试器调试生成的文件,如图27-8所示。
若将图27-8中的代码(10001000~10001018区域)注入其他进程,则代码将无法正常运行。原因在于,代码中引用地址(10009290、1000929C、100080F0 )的内容并不存在于目标进程。要使代码能够正常工作,必须向相应地址同时注入相关字符串以及API地址。并且通过编程方式使图27-8中的代码也能够准确引用被注入数据的地址。
为满足这样的条件,在代码27-2的ThreadProc()函数中使用THREAD_PARAM结构体来接收2 个API地址与4个字符串数据。其中2个API分别为LoadLibraryA()与GetProcAddress(),只要有了这2个API,就能够调用所有库函数。
上述示例可以不传递LoadLibraryA()与GetProcAddress()的地址,直接传递MessageBoxA()的地址使用即可。但原则上要先传递LoadLibraryA()与GetProcAddress(),然后使用它们加载需要的DLL,再直接获取要用的函数地址。这种方式的好处在于可以把相关库准确加载到指定进程3若将Windows套接字(Socket) API中的ws2_32!connect()地址传递给notepad.exe进程之后再使用,就会发生运行错误(notepad.exe默认不加载ws2 32.dll)。
大部分用户模式进程都会加载kernel32.dll,所以直接传递LoadLibraryA()与GetProcAddress()的地址不会有什么问题。但是,有些系统进程(如:smss.exe )是不会加载kernel32.dll的,事前务必确认。
像kernel32.dll这样的系统库,在OS启动的状态下,所有进程都会将其加载到相同地址。但是若OS版本不同(Vista、7等),或系统重启后,即使是相同模块,加载地址也会变化。
使用调试器调试代码27-2中的ThreadProc()函数代码,如图27-9所示。
这里一调试就崩了,还没找到原因,先用书上的图。
从图27-9中的代码可以看到,所有重要数据都是从线程参数lParam[EBP+8]接收使用的。也就是说,图27-9中的ThreadProc()函数是可以独立运行的代码(不直接引用被硬编码的地址数据)。若将上图27-9与前面介绍过的图27-8比较,可以明显看到它们的不同之处。
Visual C++ 2010 Express Edition 集成开发环境中,根据所用模式“Release/Debug” 及“优化”选项的不同,Codeinjection.cpp文件经过编译生成的代码可能与图27-9不同。
27.4.3 lnjectCode()函数
InjectCode()是代码注入技术的核心部分,以下是其代码。
1 |
|
代码27.4与DLL注入代码非常相似。InjectCode()函数的set THREAD_PARAM部分用来设置THREAD_PARAM结构体变量,它们会注入目标进程,并且以参数形式传递给ThreadProc()线程函数。
Windows OS中,加载到所有进程的kernel32.dll的地址都相同,所以CodeInjection.exe进程中获取的 API(“LoadLibraryA”、“GetProcAddress”)地址与 notepad.exe 进程中获取的 API (“LoadLibraryA”、“GetProcAddress”)地址是一样的,请记住这一点。
设置好THREAD_PARAM结构体后,接着调用了一系列API函数,其核心API函数归纳整理如下:
1 |
|
上述代码主要用来在目标进程中分别为data与code分配内存,并将它们注入目标进程。最后 调用CreateRemoteThread() API,执行远程线程。至此,使用代码注入技术的示例源码讲解完毕。
为便于说明,我选的示例非常基础、简单,但这丝毫不会影响我们对代码注入技术的学习与理解。恰恰相反,它能帮助我们更快速、更轻松地理解代码注入技术的原理。之后,大家可以多做些相关练习、多思考,形成自己特有的代码注入技术。
我实现代码注入技术时,通常会用汇编语言编写要注入的代码,编写时可以使用复杂些的MASM,也可以使用简单的OllyDbg “汇编”命令(快捷键Space)。编好之后,再使用InjectCode()函数将Hex代码的缓冲区注入目标进程。这种方法更有利于创建更为直观的注入代码。
27.5 代码注入调试练习
本节将调试代码注入技术,了解代码注入的动态过程。
27.5.1 调试 notepad.exe
用OllyDbg开始调试notepad.exe文件。如图27-10所示,按F9运行键,使notepad.exe处于“Runing” (运行)状态。
27.5.2 设置OllyDbg 选项
代码注入是一种向目标进程创建新线程的技术,如图27-11所示,设置好OllyDbg的选项后,
即可从注入的线程代码开始调试。
从现在开始,每当notepad.exe进程中生成新线程,调试器就暂停在线程函数开始的代码位置。
27.5.3 运行 Codelnjection.exe
借助Process Explorer工具查看notepad.exe进程的PID,如图27-12所示
在命令行窗口输入PID作为参数,运行CodeInjection.exe,如图27-13所示
27.5.4 线程开始代码
运行CodeInjection.exe进程,代码注入成功后,调试器就会暂停在被注入的线程代码的开始位置,如图27-14所示。调试器准确暂停在ThreadProc()函数开始的位置,由此开始调试即可。
有时候只是调试器暂停到此处,但是EIP并未设置于此。这时,可以先在ThreadProc()函数开始的地址(8F0000)处设置断点,再按F9运行键,这样运行控制流就会准确到达设有断点的地址(8F0000)处
8F0004地址处的MOV ESI,DWORD PTR SS:[EBP+8]
指令中,[EBP+8]地址就是ThreadProc()函数的IParam参数,而参数IParam则指向被一同注入的THREAD_PARAM结构体(参考图27-15)。
根据不同用户的运行环境,上述地址可能标识有异。
27.6 小结
本章我们借助OllyDbg的强大功能学习了调试注入代码的方法,下一章将学习使用汇编语言(非C语言)创建注入代码。
Q.我不太理解lnjectCode()代码中计算ThreadProc()函数大小的方法。
dwSize=(DWORD)lnjectCode-(DWORD)ThreadProc;
像上面这样计算,不是只把地址值相减了吗?
A.在MS Visual C++中使用Release模式编译程序代码后,源代码中函数顺序与二进制代码中的顺序是一致的。比如,在源代码中按照Funcl()、Func2()的顺序编写,编译生成二进制代码后,二进制文件中这2个函数的顺序也是如此查看InjectCode.cpp源代码可以注意到,我有意按照ThreadProc()、InjectCode()的顺序编写程序,所以在编译生成的InjectCode.exe文件中,这2个函数也按相同顺序排列出现。又因为函数名称就是函数地址,所以
InjectCode-ThreadProc做减法运算后,所得结果就是ThreadProc()函数的大小。
参考
《逆向工程核心原理》 第27章