Windows内存基础
- 每个Windows进程独占4GB虚拟地址空间,其他进程无法访问。
- 页大小为4KB。
- 内存保护属性的最小粒度是页。
- PAGE_GUARD内存保护修饰器可被用作一次性的页访问警告。
典型的Windows内存布局
内存模糊测试示例
目标应用程序:
- 循环等待客户端连接。
- 收到连接后创建新线程,新线程使用recv()接收客户端的请求。
- 调用unmarshal()来解压或者是解密。
- 传参到parse(),由parse()调用其他函数进行处理后返回。
内存模糊测试的思路不再是通过网络传送模糊测试数据,而是跳过这一环,直接在最有可能出现漏洞的parse()函数上进行模糊测试,这样可以大大提高效率。
大体思路:hook目标应用中位于parse()函数前方的位置,修改其输入后让其运行。
方法一:变异循环插入
首先通过逆向,人工定位parse()函数的开始和结束位置,然后用变异循环插入的工具向目标的内存空间中插入一个变异函数和两条无条件跳转指令。变异函数即mutate()函数,它负责修改parse()函数拿到的数据,无条件跳转指令的作用如图所示,负责mutate()与parse()的首尾衔接,以循环地向parse()函数传递参数。
方法二:快照恢复变异
首先通过逆向,人工定位parse()函数的开始与结束,快照恢复变异工具在到达parse()的开始位置时,为目标进程建立快照,在parse()结束后,恢复进程快照,并对原来的数据进行变异,用新数据重新执行parse()。
快照恢复变异(SRM)工具开发
如何在特定点放置钩子
可以通过调试器断点在进程中放置钩子,此处我们选用软件断点。
假设需要在如下图所示的0xDEADBEEF
处下断点:
-
首先使用
ReadProcessMemory()
读取目标地址的原始字节值8B
并保存。 -
然后使用WriteProcessMemory()将INT3指令写入目标地址。由于原指令
8B FF
的第一个字节被修改为了CC
,因此FF
被与后面的字节一起反汇编为call [ebp-75]
。 -
此时,当执行到断点时,INT3指令会触发一个带
EXCEPTION_BREAK_POINT
异常码的EXCEPTION_DEBUG_EVENT
调试事件,调试器可对其进行捕捉。 -
但此时有两个问题:第一,原指令
8B FF
并不能得到执行;第二,执行完INT3指令后,EIP指向0xDEADBEF0
。
-
解决第一个问题:使用
WriteProcessMemory()
将8B
写回原位置即可。 -
解决第二个问题:通过调用
GetThreadContext()
能获得当前线程的CONTEXT
结构,里面包括了指令指针EIP,对其进行修改后,再使用SetThreadContext()
写回。
如何生成进程快照
在进程运行时,很多东西都会发生改变,进程运行时会创建新线程、终止老线程、打开关闭文件…等等,此处我们只考虑进程中线程上下文和内存的变化。
生成快照的步骤:
- 保存目标进程中每个线程的上下文,基于
GetThreadContext()
实现。 - 保存所有可能发生变化的内存的内容。在进程的
0x00000000 ~ 0x7FFFFFFF
中,有的页会发生改变,有的不会,具体来说,以下属性的页不会,另外,可执行代码所在的页也不大可能发生改变,因此我们对这些页不予考虑。使用VirtualQueryEx()
可逐一访问所有可用的内存页,如果发现了我们想要包含到快照中的页,就使用ReadProcessMemory
读取该页的内容、页的信息,存入快照列表中。
存在的问题:如果在进行快照时,某一页的属性是PAGE_READONLY
,那根据此方法对其不予保存,但可能紧接着,该页的属性被修改为可写,那么它可能就会发生快照中不存在的改变。对这个问题,作者予以忽略,但是给出了一个思路:对能修改页属性的API进行挂钩,在钩子中,根据API的参数对之前的快照进行相应的修改。
在何处放置钩子
第一个是快照点:在执行到何处时保存目标进程的状态?
第二个是恢复点:在执行到何处时回退进程状态,变异我们的输入,继续插桩的执行循环?