信息安全公益宣传,信息安全知识启蒙。
加微信群回复公众号:微信群;QQ群:16004488
加微信群或QQ群可免费索取:学习教程
教程列表见微信公众号底部菜单
在这篇推文中,我们从理论和实践的角度来解释两个主题:
1、 如何在有源代码的情况下进行 windows中二进制代码的模糊测试(面向开发人员)。
2、 如何处理大文件(又名热图分析)以及崩溃分析(用于更具技术性的安全顾问)。
为什么我要选择Mimikatz作为Fuzzing的目标
在开发过程中,Fuzzing是一个非常重要的概念。
为了演示fuzzing,我们必须找到一个可提供源代码的目标。与此同时,通过阅读源代码来了解应用程序的内部情况。
因此,我们决定使用mimikatz,它对信息安全专家来说是一个非常有用的工具。
mimikatz包含超过261 000行代码。Mimikatz必须解析许多不同的数据结构,因此它很可能受到漏洞本身的影响。
接下来我们需要一个好的攻击场景。
Mimikatz可用于从LSASS进程转储当前登录用户的明文凭据和散列。
如果在mimikatz的解析代码中存在漏洞,我们该怎么利用?
事实证明,安全顾问和黑客不会直接在系统上调用mimikatz。相反,他们的做法是在系统上创建LSASS进程的minidump,下载并在攻击系统上调用mimikatz。
现在我们有一个很好的攻击场景!
首先创建一个蜜罐,将一些恶意代码注入到我们自己的LSASS进程中。等到黑客入侵了这台机器,转储LSASS进程,并在自己的系统上调用mimikatz,读取我们操作的内存转储文件,我们就可以获得一个反弹shell!
但是,由于mimikatz是采用ASLR和DEP等保护措施进行编译,而想要利用这种客户端的漏洞是比较困难的。
令我们惊讶的是,mimikatz存在一个高危漏洞,允许我们绕过ASLR(和DEP)。
理论:Windows二进制文件模糊测试
我们要的不是一个基于mimikatz的特定fuzzer,而是一个基于覆盖引导性的模糊测试 。这意味着我们要在模糊过程中提取代码覆盖率信息。
当一个突变的输入文件(内存转储文件)在mimikatz中生成更多的代码覆盖,我们就可以将其添加到模糊测试队列中,同时也可以对该输入进行模糊处理。
这意味着从一个转储文件开始,fuzzer就可以为我们识别不同的代码路径,进而“自主地学习”所有的内部解析逻辑!
因此我们只需要编写一个fuzzer,就可以将它用于绝大多数类型的应用程序!
目前fuzzer(American Fuzzy lop,即AFL)已经完全实现了这个想法。
AFL的四大特点让它在识别漏洞方面效果显著:
1、高速
2、 它提取边缘覆盖(具有命中计数),而不仅仅是代码覆盖。这意味着我们执行的是“路径”而不是“代码”(例如,覆盖代码将会在把在if语句中执行的输入文件添加到队列中区,边缘覆盖将会在队列中添加一个含if判断代码的入口点和另一个不含if判断代码的入口点。)
3、 它可以实现确定性的突变(对输入的每个字节/位进行操作)
4、操作简单
现在最大的问题是:AFL如何提取代码/覆盖边缘信息。答案取决于AFL的配置模式。
默认选项是hack gcc中生成的对象文件,并在所有可能的位置插入检测代码。
还有一个LLVM模式,其中使用LLVM编译器传递来插入指令。然后,如果源代码不可用,那么会有一个qemu模式,模拟二进制文件并通过qemu添加指令。
还有一扩展AFL的其他方法。
例如,利用硬件功能提取覆盖代码(WinAFL-IntelPT,kAFL)。或者是使用类似PIN或DynamoRio(例如WinAFL)的动态指令框架。
这些框架可以动态的(在运行期间)指导,这意味着调度程序要执行接下来的指令,并在它们前后添加额外的指令。
所有这些都需要动态重新定位指令,这个过程对目标应用程序是完全透明的,做到这点事非常复杂的,因为框架隐藏了用户的所有逻辑。显然,这种方法不是很快(但是在Windows上有效)。
当源代码可用时,我们认为在windows上也可以和在Linux一样使用GCC。
我们的第一个尝试是在Windows上使用“libfuzzer”。
Libfuzzer不仅是像AFL那样的模糊器,它还起到了函数级的模糊作用,因此更适用于开发人员。
AFL默认模糊测试完整的二进制文件,也可以在函数级模糊的持久模式下启动。
Libfuzzer使用LLVM clang编译器中的“源代码覆盖”功能,正是我想在Windows上要的。
现在我可以在Windows上使用LLVM编译mimikatz。
当添加“基于源代码覆盖”的标志时,我再次发现一些错误,这些错误可以通过将在包含路径的链接器中添加所需的库来修复。
但是,该标志为所有目标文件添加了相同的功能,导致链接错误。
解决这个问题的唯一方法是将所有.c文件合并成一个文件。
值得一提的是,在Windows上使用LLVM进行应用程序分析这一方法,以后会是很好的分析路径。
实践:模糊窗口
我们首先要做一个添加代码的头文件:
添加了代码的头文件
对于我们想要模糊的项目,用/ PROFILE linker标记(Visual Studio - >项目属性- > linker -> Advanced - >配置文件)来编译一个32位的应用程序。
对于mimikatz,我从wmain函数(mimikatz.c中)中删除了命令提示符代码,并且刚刚调用了kuhl_m_sekurlsa_all(argc,argc),因为我想直接从minidump转储哈希值/密码(在程序中发出sekurlsa :: logonpasswords命令)调用)。
由于mimikatz会从LSASS进程中提取每个默认的信息,所以我在kuhl_m_sekurlsa_all()中添加了一行以加载转储。
此外,我们在此函数中还添加了persistent循环。
下面是我们的新kuhl_m_sekurlsa_all()函数的外观:
新kuhl-m-sekurlsa-all函数
提示:模糊测试时不要使用上述代码。
我添加了一个微妙的缺陷,编译后,我们可以启动二进制文件并查看一些其他消息:
afl-fuzz.exe文件
下一步是对代码进行调试。
为此,我们必须注册“msdia140.dll”。
这可以通过以下命令完成:
注册msdia140.all文件
然后我们可以使用生成的mimikatz.exe和mimikatz.pdb文件调用下载的instrument.exe二进制文件来添加指令:
instrument.exe二进制文件
我们可以启动指令化的二进制文件并查看已修改的状态消息:
mimikatz.inetr.exe文件
现在我们可以为mimikatz生成一个minidump文件(在Windows 7 x86测试系统上,我使用任务管理器来转储LSASS进程),将其放入输入目录并开始fuzzing:
minidump文件
我们可以看到,我们有一个文件大小问题!
我们可以看到,AFL将输入文件限制为最大为1 MB。这是什么原因呢?
我们知道,AFL在为每个队列输入切换到随机模糊之前,都要进行确定性的模糊处理,这意味着它要为输入中的每个()字节/位进行特定的位和字节翻转/添加/删除等。
这包括几个操作,如bitflip 1/1,bitflip 2/1,...,arith 8/8,arith 16/8等等。所有这些操作的共同之处在于,所需的应用程序执行次数取决于文件大小!
我们假设AFL只是()做bitflip1/1确定性策略。这意味着我们执行目标应用程序的次数和相应输入文件大小的比特数相同。
AFL执行更多的是确定性策略。对于Linux上的AFL来说,这不是很大,每个核心的执行速度为每秒1000 - 8000次。这种快速的执行速度以及确定性的模糊(和边缘覆盖)使得AFL很成功。
现在我们假设输入文件的大小为1 MB(AFL限制),这意味着对于bitflip 1/1而言,需要执行8 388 608次。
我们的目标应用程序有点慢,因为它在Windows上运行较大(200个exec /秒),我们只有一个核心可以用于模糊,但我们需要用115个小时才能完成一个输入条目的bitflip 1/1!
在我们的情况下,输入(内存转储)的大小为27 MB,这将是216 036 224必需的执行只针对bitflip 1/1。AFL检测到并直接中止,这需要很长时间(AFL永远不会发现漏洞,因为它被卡在确定性的模糊中)。
为缩小文件,我们第一次去尝试读取mimikatz的源代码,并了解它如何在内存转储中找到重要的内存区域。在fuzzing期间,我们不想理解应用程序来编写一个特定的fuzzer,而是希望fuzzer自己学习一切。
在阅读代码时,我们发现了一些有趣的东西:Mimikatz先通过kernel32!MapViewOfFile()加载内存转储,然后读取必需的信息。
如果我们可以将所有内存访问记录到这个内存区域,那么我们就可以减少所需的执行次数!
这是一个基于27 MB输入文件(通过Python脚本生成)中的mimikatz的内存读取操作生成的热图:
mimikatz内存读取操作热图
黑色区域从未被mimikatz读取。该区域越亮,访问的内存读取操作也就越多。
该文件的开头位于图片的左下方。我们先打印1000字节,然后向下一行,再打印1000字节,依此类推。
在这个缩放级别,我们没有看到对内存转储起始位置的访问尝试。但是,因为文件大小为27 MB,所以较小的红/黄/白点不可见。但是我们可以将图片放大:
放大后的内存读取操作热图
上述图片中最重要的信息:我们不必从黑色区域模糊字节!
对于mimikatz,我们之前使用了一个简单的PoC调试器脚本,现在,我们正在开发一个更复杂的脚本,这个脚本使用动态的动态指令框架,可以从任何应用程序中提取信息。
调试器脚本的使用非常慢,并且对于每个应用程序都不起作用。然而,对于mimikatz,却能工作正常,因为大部分时间都花在了mimikatz读取MapViewOfFile()映射到的区域上。
我们的WinAppDbg脚本非常简单,只需使用WinAppDbg的Api-Hooking机制来挂接MapViewOfFile的后续例程。
返回值包含加载输入内存转储的内存地址。我们在它上面放置一个内存断点,在每个断点命中我们只记录相对偏移量,并增加命中计数器。
作为参考,这里是脚本:
参考脚本
我们通过调试器启动mimikatz,一旦mimikatz完成,我们需要只根据命中次数来对偏移量进行排序。
偏移量排序
这是挂接MapViewOfFile获取输入内存中的基址的代码。
该代码还添加了内存断点,每次在我们的输入读取指令时调用memory_breakpoint_callback。
剩下的就是回调函数:
回调参数
我用capstone反汇编触发指令,以获得正确更新偏移量的读取操作的大小。
脚本很简单(至少这个只适用于mimikatz)。
调试器内存断点方法的缺点是非常慢,这个脚本执行大约30分钟,但是,我们只需要这样做一次。另一个缺点是它不适用于所有应用程序,因为通常应用程序将输入字节复制到其他缓冲区并在那里访问它们。
我们还可以通过从输入文件中删除(将它们归零)来验证黑色字节无关紧要:
输入文件归零验证
上图显示两个文件的输出完全一样,因此黑色字节真的没关系。
因为要修改WinAFL代码才能使所识别的字节模糊,所以我们要先启动一个简单的fuzzer。
我们的第一个fuzzer是一个翻转字节的50行代码的 python脚本,接下来我们在WinDbg上调用mimikatz,解析其输出并检测崩溃。
我们在12个家庭核心系统上开发了8个并行模糊测试工作(4个核心用于私人软件),3天后WinDbg发现了28次独特的崩溃。
这8个工作中,每个工作的执行速度为每秒2exec ,因此所有内核的总执行速度为每秒16exec 。
我们用了一些方法来修改WinAFL,用以模糊只含有热图的字节。
一种方法是使用用于测试例后处理的“post_handler”程序。由于这个程序没有被干运行(AFL的第一次执行),因此不起作用。相反,它可以修改write_to_testcase()。
在主函数内部,我们将完整的内存转储复制到堆缓冲区。在输入目录中,我们存储的文件只包含散热图字节,因此文件大小比91KB或3KB小得多。
接下来的实验中,我们将代码添加到write_to_testcase中,并且将输入文件中的所有字节复制到正确位置的堆缓冲区。
因此,AFL只看到小文件,而正确的内存转储则会被传递给mimikatz。如果启发式检测到这个有效,我们将调用每个队列条目的热图计算。
请注意:AFL和WinAFL会将每个testcase写入磁盘!也就是说,我们必须在每次执行时编写一个27 MB的文件!
从性能的角度来看,如果能在内存中的目标应用程序中修改testcase,那将更易操作。
WinAFL输出截图
一些彩色框相同的屏幕截图
好消息是:我们已经确定了16个独特的崩溃和137条独特的路径。
坏消息是:Fuzzing速度只有每秒13 exec,这对于AFL而言是非常缓慢的。
最糟糕的是:我们运行了14.5个小时,它还没有完成第一个输入文件的1 / 1阶段(还有136个在队列中)。
由此我们意识到,持续的工作效率并不高。
为了更好地演示和过滤,我们修改了WinAppDbg脚本,并保留了fuzzer的运行。现在我们重新进行fuzzing工作。
新的WinAppDbg脚本将“热字节”的数量减少到了3KB(从最开始的27 MB到后来的91KB再到现在只有3KB到fuzz)。
这个WinAppDbg脚本大约是800行代码。
55小时后(带有91KB输入文件)的截图
我们看到,fuzzer完成了bitflip 1/ 1和2 / 1,现在在4 / 1(98%完成)。
另外,bitflip 1/ 1策略需要执行737k大小的程序,而这些737k的程序中有159个程序导致了新的覆盖(或崩溃)。
在bitflip 2 / 1的执行进度为29 / 737k的地方,我们发现:两天零7小时内总共发生了22个独特的崩溃。
3KB文件在2小时后的fuzzing
2小时后,我们得到了30个独特的崩溃!(而用了55小时的91KB文件,却只发生了22次崩溃!)
我们发现,因为输入的文件变小了,所以bitflip 1/ 1阶段只需要执行179k大小的程序。
此外,我们在103K的执行程序中发现了245条独特的路径。现在我们需要考虑:要花多长时间才能对27MB的文件进行模糊测试,以及在几天的fuzzing之后能看到什么样的结果。
这里还有一个不同大小输入文件的演示。
可视化的27 MB原始minidump文件
在这个27 MB的转储文件中,所有不重要的(未读取的)字节都被0x00替换了,因此我们只看到了用WinAFL模糊处理的3KB字节。
三个坐标轴的交点
然而,即使是3KB的fuzzing,工作效率还是很低。
为了能够在多个核心中工作,并使该工作成功运行一到两周,我们需要进入到更深的层次。
由于输入触发错误条件导致了mimikatz不会执行完整的代码,因此后面的队列条目将会导致更快的执行速度。
但在Linux上,我们需要每秒处理数千次的执行。
一个主要的问题是,我们的每一个执行都必须搜索和加载一个27MB的文件,所以减少这个文件的大小势在必行。
接下来,我们比较了不同设置的执行速度:
在syzygy模式下执行以WinAFL执行速度:每秒 13 exec
本机执行速度无WinAFL,无仪表(内存):每秒335 exec
无WinAFL,但使用了二进制:每秒50 exec
执行本机二进制文件:每秒163 exec
有趣的是,在syzygy instrument上的执行速度有接近6倍的缩小,而这个缩小在syzygy + WinAFL上是近似25倍的。因为针对进程开关和文件读写,我们并没有做全内存的fuzzing!
DynamoRio工具比syzygy(163 exec/sec vs. 50 exec/ sec)更快,所以我们用DynamoRio模式启动WinAFL。这样,就可以用WinAFL得到的一个每秒0 - 2 exec的执行速度。
基于这些,我们认识到了一个问题:DynamoRio模式更快!
我们在开始时添加了两行代码:
kuhl_m_sekurlsa_reset()
pMinidumpName = argv[1]
这正是“sekurlsa::minidump”命令所做的事情。
我们想要实现的是立即执行“sekurlsa:minidump”命令,然后执行“sekurlsa:logonpassword”。
然而这是一个大问题,因为我们退出了函数(DynamoRio模式)或__afl_persistent_loop(syzygy模式)处于输入文件打开的状态。
在这个状态下,输入的文件仍然是打开的,因为我们是在函数开始时调用了kuhl_m_sekurlsa_reset()函数,而不是在最后!也就是说我们只执行一个“执行”内存,然后WinAFL尝试修改输入文件,检测到文件仍然是打开的,不能写入,然后终止正在运行的进程。
从afl - fuzz.c:2195开始,我们就用syzygy或nudges在DynamoRio模式下写入文件。因此,我们不对实际的内存进行fuzzing。
只要将kuhl_m_sekurlsa_reset()重新排序到函数的末尾,我们就可以解决这个问题了
但是如果我们执行这个函数,还是会面临同样的问题:kuhl_m_sekurlsa_reset()函数中的Mimikatz有一个错误,它通过三个调用打开输入文件:
CreateFile
CreateFileMapping
MapViewOfFile
因此,我们必须关闭这三个句柄/映射。然而,Mimikatz未能关闭其中的CreateFile句柄。
kuhl_m_sekurlsa_reset()代码
Kull_m_memory_close()正确关闭了文件映射,但最后一个关闭处理(toClose)调用应该关闭从CreateFile接收的句柄。
toClose存储来自(kull_m_minidump_open())的堆地址
这意味着代码调用了堆地址上的关闭Handle,并且不在原始文件上调用关闭Handle。
解决这个问题后,WinAFL的速度变成了每秒30 - 50exec !
然而,这些执行并不一致,有些会变成每秒只执行一次。基于这一点,我们得到了更好的使用边缘覆盖的fuzzing性能和syzygy模式。
WinAFL DynamoRio模式的屏幕截图
WinAFL syzygy模式的截图
通过以上实践我们发现:如果源代码可用,使用syzygy模式是更有效的!
然而我们在代码中出现了一个错误,该错误使模糊测试的时间至少缩短了两倍,但我们却没有在混合模糊测试中观察到它!
最后,我们用syzygy模式、一个主分支以及7个从分支来展开一项运行三天的模糊测试工作。
最终我们识别了130个独特的AFL崩溃签名,它们最终可以减少到42个独特的WinDbg崩溃签名。这些签名中的大多数是不安全的,然而其中有两次崩溃是很重要的。
可识别崩溃分析
漏洞1:任意部分相对堆栈覆盖
这一部分,我们只想描述两个关键的崩溃。
在fuzzing之后,有几个独特的崩溃,我们用了一个简单的自写启发式对它们进行排序。从这一系列的崩溃中,我选了第一个并进行分析。
以下是具体步骤:
首先是漏洞的代码
变量“myDir”完全在我们的控制之中,myDir指向内存转储,因此我们可以控制该结构中的所有内容。
在继续之前你可能想自己找到这些问题,这里有一些提示:
目标参数缓冲区的长度是0x40(64)
参数源在我们的控制之下
显然,我们希望通过一些恶意参数从第29行到达RtlCopyMemory()。
如何利用
提示:恶意字节从偏移0xF0开始,这个偏移量和在minidump中的文件有所不同。
如果我们检查0x0C(= 0x20)中的字节,我们会发现这是“streams”目录的偏移量。
因此,上述minidump有一个从偏移0x20开始的流目录。每个条目由3个DWORDS组成,第一个是类型,最后一个是偏移量。
我们使用类型0x09(= Memory64ListStream)来搜索条目。如果我们从这里取第三个DWORD,就会发现这完全是0xF0——恶意字节开始的偏移量。
如果这个偏移量在minidump文件中是不同的,那么我们需要先修复它。
可控制返回地址的证明
漏洞2:堆溢出
引发第二个漏洞的因素可以在kull_m_process_getUnicodeString()中找到。
第一个参数(string)是一个带有字段缓冲区(数据指针)的结构,这个参数的最大长度是字符串可以容纳的长度。
因为内容是从minidump中解析的,所以它完全在攻击者的控制之下。
此外,源也指向minidump。Mimikatz总是从minidump中提取字符串结构,然后调用kull_m_process_getUnicodeString()来填充字符串- >缓冲区,从minidump中获取真正的字符串。
从minidump获取的真正字符串
你找出问题所在了吗?
第9行字符串- >被分配了最大字节的空间,该行字符串从minidump中复制到堆的字节数与第11行的字节数完全相同。
但是,代码从不检查字符串-长度,因此字符串->长度可以大于字符串-最大长度,但如果以后的代码使用字符串- >长度,就会发生堆溢出。
最后我们对模糊测试的工作流程进行总结:
尽可能多地下载输入文件
计算一个输入文件的minset
过滤器快速检测不同覆盖
对于生成的minset,计算代码覆盖范围
在fuzzing期间,提取边缘覆盖
来源:漏洞银行
下面阅读原文,有啥 ?