re-core-principle-2
逆向工程核心原理笔记-2
0x00 windows消息钩取
钩子:偷看或截取信息时所用的手段或工具。有一个例子非常形象:假设有一个非常重要的军事设施,其外围设置了3层岗哨以进行保护。外部人员若想进入,需要经过3层岗哨复杂的检查程序。若间谍在通往该军事设施的道路上私设一个岗哨,经过该岗哨的人员未起疑心,通过时履行同样的检查程序,那么间谍就可以坐享其成,轻松获取来往该岗哨的所有信息。
消息钩子。Windows操作系统向用户提供GUI(Graphic User Interface,图形用户界面),它以事件驱动(Event Driven)方式工作。发生此类事件时,OS会把事先定义好的消息发送给相应的应用程序,应用程序分析收到的信息后执行相应动作。也就是说,敲击键盘时,消息会从OS移动到应用程序。消息钩子就在此间偷看这些信息。
Windows消息处理流如下所示:
1 | 1. 发生键盘输入事件时,WM_KEYDOWN消息被添加到[OS message queue] |
消息钩子示意图如下:
如上所示,钩链中的钩子会比应用程序先看到相应消息。在钩子函数内部,除了查看消息之外,还可以修改消息。钩链指的是:可以设置多个相同的消息钩子,并按照设置顺序依次调用这些钩子。相应的工具是SPY++
(以前用过),它可以查看操作系统中来往的所有消息。
使用SetWindowsHookEx()
API可以轻松实现消息钩子,其定义如下所示:
钩子过程(hook procedure)是由操作系统调用的回调函数。安装消息钩子时,钩子过程需要存在于某个DLL内部,且该DLL的示例句柄(instance handle)即是hMod。若dwThreadID被设置为0,则安装的钩子为全局钩子(Global Hook),它会影响到所有进程。
什么是回调函数?回调函数是指以参数的形式传递给其它代码的可执行代码,通常用于在某个事件发生时或某个任务完成后执行特定的操作。回调函数可以提高代码的灵活性和解耦性。
下面给一个键盘钩子的例子,不理解的直接可以看例子:
KeyHook.dll
文件是一个含有钩子过程(KeyboardProc
)的DLL文件。HookMain.exe
是最先加载KeyHook.dll
并安装键盘钩子的程序。HookMain.exe
加载KeyHook.dll
文件后使用SetWindowsHookEx()
安装键盘钩子(KeyboardProc
)。若其他进程中发生键盘输入事件,OS就会强制将KeyHook.dll
加载到相应进程的内存,然后调用KeyboardProc
函数。消息钩取技术常被用作DLL注入技术。
下面是HookMain.exe
与KeyHook.dll
的源代码:
1 | // HookMain.cpp -> HookMain.exe |
1 | // KeyHook.cpp -> KeyHook.dll |
程序运行的时候,有点问题:当运行HookMain.exe
时,当按下某个键后,程序会卡死,经过分析是CallNextHookEx(g_hHook, nCode, wParam, lParam);
没有正常的将消息传递给应用程序。
最后终于弄明白了,原来是WH_KEYBOARD
与WH_KEYBOARD_LL
的问题,改成WH_KEYBOARD_LL
就好了。他俩的区别是:WH_KEYBOARD_LL
在消息发送之前就已经处理了, 而WH_KEYBOARD
是发送到注入线程以后。这样的话,如果是WH_KEYBOARD
,那么所有程序都会首先注入DLL,然后才执行KeyboardProc
函数,造成卡顿。
程序运行机理如下:
1 | 1. 安装好键盘钩子后,无论哪个进程,只要发生键盘输人事件,OS就会强制将KeyHook.dll注入相应进程。 |
其中,KeyboardProc
函数的定义如下:
上面3个参数中,wParam
指用户按下的键盘按键的虚拟键值(virtual-key code
)。对键盘而言,英文字母”A”与”a”具有完全相同的虚拟键值。lParam
根据不同的位具有多种不同的含义。
0x02 恶意键盘记录器
一些无关紧要的知识,略。
0x03 DLL注入
DLL注入指的是向运行中的其他进程强制插入特定的DLL文件。从技术细节来说,DLL注入要求其他进程自动调用LoadLibrary() API
,并加载用户指定的DLL文件。如下图所示:
当DLL被插入到进程后,进程后会自动运行DLLMain()
函数,用户可以把想执行的代码放到DLLMain()
函数,每当加载 DLL时,添加的代码就会自然而然得到执行。DLLMain()
函数如下所示:
1 | BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved) |
DLL注入可以改善功能与修复Bug。没有程序对应的源码,就可以使用此技术为程序添加新功能(类似于插件),或者修改有问题的代码、数据。
向某个进程注入DLL时主要使用以下方法:
1 | 1. 创建远程线程(CreateRemoteThread() API) |
CreateRemoteThread进行DLL注入
例子P200,不再赘述,跟着做。
工具:DebugView
(可用于监视本地系统上的调试输出)。
这个例子有一个InjectDll.exe
,通过InjectDll.exe process_pid C:\Work\myhack.dll
,用来向notepad.exe
进程注入myhack.dll
,myhack.dll
执行的操作是输出一个字符串(在DebugView
中查看),然后将一个网页下载到本地。但是调试的时候DebugView
没显示调试字符串,其他倒是正常。
之后解决了上述问题,解决方法如下:以管理员身份运行debugview
、在debugview
中勾选Capture Win32
和Capture Global Win32
选项、在注册表中设置Debug Print Filter
键值,以启用DbgPrint
输出、把debugview
放到和运行文件同文件夹下。
分析例子源代码:
1 | // myhack.cpp -> myhack.dll |
DLL首先会自动运行DLLMain
函数中可以看到,该DLL被加载(DLL_PROCESS_ATTACH
)时先输出一个调试字符串(myhack.dll Injection!!
),然后创建线程调用函数(ThreadProc
)。在ThreadProc
函数中通过调用urlmon!URLDownloadToFile
API来下载指定网站的index.html
文件。所以当myhack.dll
注入到notepad.exe
进程后,这些流程就会被触发。
1 | // InjectDll.cpp -> InjectDll.exe |
上述代码的解释:
1 | #1. 主要是使用OpenProcess()获得notepad.exe的句柄,句柄有PROCESS_ALL_ACCESS权限。 |
我们的目标明明是获取notepad.exe
进程的kernel32.dll
的LoadLibraryW
地址,但上面的代码获取InjectDll.exe
进程的kernel32.dll
的LoadLibraryW
地址。如果notepad.exe
中的kernel32.dIl
的地址与InjectDll.exe
进程中的kernel32.dll
的地址相同,那么代码就不会有什么问题。但是如果不同,那么上面的代码就错了,执行时会发生内存引用错误。其实在windows系统中,kernel32.dll在每个进程中的加载地址都是相同的。
注:调试API。
1 | Windows 操作系统提供了调试 API,借助它们可以访问其他进程的内存空间。其中具有代表性的有VirtualAllocEx、VirtualFreeEx、WriteProcessMemory、ReadProcessMemory等。 |
CreateRemoteThread()原型如下:
使用注册表进行DLL注入(AppInit_DLLs)
Windows操作系统的注册表中默认提供了AppInt_DLLs
与LoadAppInit_DLLs
两个注册表项。在注册表编辑器中,将要注入的DLL的路径字符串写入AppInit_DLLs
项目,然后把LoadAppInit_DLLs
的项目值设置为1。重启后,指定DLL会注入所有运行进程。
上述方法的工作原理是:User32.dll
被加载到进程时,会读取AppInit_DLLs
注册表项。所以,相应DLL并不会被加载到所有进程,而只是加载至加载user32.dll
的进程。
下面分析myhack2.dll
的源码:
1 | // myhack2.cpp -> myhack2.dll |
可以看到,上述代码就是:如果打开了notepad.exe
,就运行浏览器并访问http://www.naver.com
,但是书中说的是以隐藏模式运行浏览器,我并没有找到相关参数。
0x04 DLL卸载
DLL卸载(DLL Ejection)是强制弹出进程中DLL的一种技术,其基本工作原理与使用CreateRemoteThread
进行DLL注入的原理类似。前面使用CreateRemoteThread0API
进行DLL注入的工作原理:驱使目标进程调用LoadLibrary
。同样,DLL卸载工作原理:驱使目标进程调用FreeLibrary
。
注:每个Windows内核对象(Kernel Object
)都拥有一个引用计数(Reference Count
),代表对象被使用的次数。每调用一次LoadLibrary
,引用计数会加1;每调用一次Freelibrary
,引用计数会减1。
下面分析EjectDll.exe
程序,其用来从目标进程notepad.exe
中卸载myhack.dll
。源代码如下所示:
1 | // EjectDll.cpp -> EjectDll.exe |
注:使用FreeLibrary
的方法仅适用于卸载自己强制注入的DLL文件。PE文件直接导入的DLL文件是无法在进程运行过程中卸载的。
0x05 通过修改PE加载DLL
除了前面所讲的DLL动态注入技术外,还可以采用手工修改可执行文件
的方式加载用户指定的DLL文件。之后给出了一个例子,通过修改TextView.exe
文件(主要是修改其IDT),在其运行时自动加载myhack3.dll
文件,具体过程在P224中。这个例子与前面的PE头知识联动了,可以趁势复习一波。
下面是myhack3.dll
的源代码,注释写的很明白了。
1 | // myhack3.cpp -> myhack3.dll |
上述dummy
函数是myhack3.dll
的导出函数,它没有任何功能,其存在是为了保持形式上的完整性,使myhack3.dll
能够顺利添加到TextView.exe
文件的导出表。在PE文件中导入某个DLL,实质就是在文件代码内调用该DLL提供的导出函数。PE文件头中记录着DLL名称、函数名称等信息。因此,myhack3.dll
至少要向外提供1个以上的导出函数才能保持形式上的完整性。
之后的修改环节跟着书做就行,里面有很多注意事项,到时候修改的时候可以对照着看。
注:更改IDT时,需要保证映射到内存后,程序不会使用这段区域。PE文件尾部有些部分填充着 NULL,但这不意味着这些部分就是Null-Padding区域(空白可用区域),这些区域有可能是程序使用的区域。且并非所有Null-Padding区域都会加载到内存,只有分析节区头信息后才能判断。
其中有一个细节值得注意:就是要更改.rdata
节区头的属性,添加可写属性。但是,TextView.exe
文件的IAT原来位于.rdata
节区,且.rdata
节区原本就没有可写属性,但程序仍能正常运行。可是对源程序进行修改之后,就要添加可写属性,这是为什么?因为原文件中就存在原始IAT,运行TextView.exe
时根本不用对.rdata
节区进行修改。
比较a.exe
与b.exe
是否相同:fc a.exe b.exe /b
0x06 PE Tools
功能强大的PE文件编辑工具,具有进程内存转储、PE文件头编辑、PE重建等功能,并且支持插件,带有插件编写示例,用户可以自己开发需要的插件。简单教程见P245。
进程内存转储(dump)
转储(Dump),意为将内存中的内容转存到文件。这种转储技术主要用来查看正在运行的进程内存中的内容。
文件是运行时解压缩文件时,其只有在内存中才以解压缩形态存在,此时借助转储技术可以轻松查看与源文件类似的代码与数据。然而,使用PE保护器时,文件在内存中仍处于压缩与加密状态,即便应用内存转储技术也无法准确把握文件内容。并且常因为使用Anti-Dump(反转储)技术而给转储带来困难。
PE Tools 在进程转储操作时能有效绕开反转储技术。
0x07 代码注入(Code Injection)
代码注入是一种向目标进程插入代码并使之运行的技术,它一般调用CreateRemoteThread
(也可用于DLL注入)。此技术以远程线程形式运行插入代码,所以也被称为线程注入。下图展示了实现原理:
上图中,代码以线程过程(Thread Procedure)插入,而代码中使用的数据则以线程参数的形式传入。
DLL注入与代码注入的比较
采用DLL注入技术时,整个DLL会被插入目标进程,代码与数据共存于内存,所以代码能够正常运行。而代码注入不仅向进程注入必要的代码,还必须将代码中使用的数据(参数,函数地址等)一同注入。因此,代码注入技术时要考虑的事项比DLL注入技术要多。
代码注入的优点:(1)占用内存少。(2)难以查找痕迹。
DLL注入技术主要用在代码量大且复杂的时候,而代码注入技术则适用于代码量小且简单的情况。
分析如下代码注入的程序:
1 | // CodeInjection.cpp -> CodeInjection.exe |
上述示例可以不传递LoadLibraryA
与GetProcAddress
的地址,直接传递MessageBoxA
的地址使用即可。上述示例的好处在于可以把相关库准确加载到指定进程。若将Windows套接字(Socket)中的ws2 32!connect
地址传递给notepad.exe
进程之后再使用,就会发生运行错误,因为notepad.exe
默认不加载ws2_32.dll
,但是默认加载user32.dll
。
上述示例也基于某条件:加载到所有进程的kerel32.dll
的地址都相同,所以CodeInjection.exe
进程中的LoadLibraryA
与GetProcAddress
地址与目标进程是一样的。
之后跟着书上进行调试,具体见书P258。
0x08 使用汇编语言编写注入代码
为什么要用汇编?避免因编译器不同,而在调试时出现我们自己看不懂汇编代码的情况。
跟着书做就行。
首先写出ThreadProc
函数的汇编(后面再解释为什么这样写):
上述汇编转为16进制为:
1 | 0x55,0x8b,0xec,0x8b,0x75,0x08,0x68,0x6c,0x6c,0x00,0x00,0x68,0x33,0x32,0x2e,0x64,0x68,0x75,0x73,0x65,0x72,0x54,0xff,0x16,0x68,0x6f,0x78,0x41,0x00,0x68,0x61,0x67,0x65,0x42,0x68,0x4d,0x65,0x73,0x73,0x54,0x50,0xff,0x56,0x04,0x6a,0x00,0xe8,0x0c,0x00,0x00,0x00,0x52,0x65,0x76,0x65,0x72,0x73,0x65,0x43,0x6f,0x72,0x65,0x00,0xe8,0x14,0x00,0x00,0x00,0x77,0x77,0x77,0x2e,0x72,0x65,0x76,0x65,0x72,0x73,0x65,0x63,0x6f,0x72,0x65,0x2e,0x63,0x6f,0x6d,0x00,0x6a,0x00,0xff,0xd0,0x33,0xc0,0x8b,0xe5,0x5d,0xc3 |
再来看代码注入程序CodeInjection2.cpp
:
1 |
|
下面详细分析这段二进制代码ThreadProc
:
1 | 004010ED 55 PUSH EBP |
上述代码中,地址0040111B
使用CALL指令将包含在代码间的字符串数据地址压入栈。其中00401120-0040112B
为"ReverseCore", 0
所在区域,使用call指令之后,返回地址00401120
被压入栈中,并且接着调用call 00401145
,之后00401131
也被压入栈中,这样,就完成了"ReverseCore"
与"www.reversecore.com", 0
的入栈。
留言
- 文章链接: https://wd-2711.tech/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!