最近我在准备OSCE exam, 所以一直想找一些有趣的漏洞和POC代码来练习一下,顺便在漏洞方面学习一些新的东西。
在寻觅了一会后,发现了 QuickZip v4.60 Buffer Overflow exploit, 是 corelanc0d3r在这篇博客里记录的漏洞。
因为这个漏洞是2010年的,是为了32 位的window-xp 设计的。我试下能否在64位的windows 7 操作系统上重现它,这会是一个有趣的挑战!
起初,我先获取了 QuickZip v4.60 Windows XP exploit from exploit-db, 然后分成几部分来写一个简单的POC,从而触发崩溃。
上面的代码创建了一个名为4064A;的压缩文件,后缀名为 txt; Header_1
, header_2
和 header_3
是zip 文件结构要求的文件头,我不会深入探讨这个问题。你想了解更多的话,看 这里。
如果你在QuickZip 中打开创建的ZIP文件,尝试提取其内容(只是双击文件名),QuickZip就会崩溃。
好的,让我们来运行一下POC,看看究竟会发生什么。
用上面的python代码创建zip文件,用QuickZip打开它,打开 ImmunityDebugger
, 附加到QuickZip 进程上,在QuickZip 中,双击文件名来引发崩溃。 注意: 我们会反复不厌其烦地重复这个过程,所以习惯它吧!
非常好,我们像预期那样引发了崩溃。同时,我们得到了一个异常 - 看屏幕的底部 ; 写入[00190000];时访问冲突。这说明我们向无效的内存地址写入时引发了一个异常。
让我们来看看SHE(结构化异常处理)链。
非常棒,上图表明我们能够控制nSHE 指针! 看起来很有希望,让我们来试试找出偏移量。
和往常一样,我还是使用mona
(https://github.com/corelan/mona) 来解决这里的问题。
首先,先生成一个有4064 个特殊字符的样例,把它放在POC 利用的payload上:
再次引发崩溃,看看这次发生了什么。
这次与上次不太一样了。 LEAVE
指令尝试从堆栈跳转回一段无效的内存地址0EEDFADE
。
但,我们确不再能够控制SHE了。
然而,注意到我们实际是在内核模块(看下Immunity 窗口的名称-CPU 主线程,KERNELBA 模块)。通过 SHIFT + F9
返回程序执行的上一步,看看在QuickZip 模块中能否引发异常情况。
很好,看来我们又回来了!
用下面的命令让mona计算偏移量:
到这以后,一个很有意思的事情是 nSEH字段:偏移量 292
。
让我们用偏移信息更新下POC,再次尝试触发崩溃。
太好了,我们控制了SEH!让我们跳过异常程序(SHIFT + F9
) 并进一步看下发生了什么。
当然了,另一个异常程序也触发了,因为43434343
对于这个程序来说是一段无效的内存地址,但是我们来看看在栈上发生了什么--通常为SEH溢出,我们需要调用一组POP-POP-RET指令来返回到我们的缓冲区。
很容易用mona
找到这样的指令,但是首先我们必须知道哪些指令是被允许使用的。而这才是问题的关键。
大体上来说,大部分是这样的。为什么?因为我们在文件名参数和文件名上面的溢出是被严格限定的-------通常只有可打印的ASCII字符。
因为实际上如果手工通过mona 找到所有坏的字符需要很长时间,我只是假设使用了整张ASCII 表(字符最多为127个)除了0x00
, 0x0a
和 0x0d
(NULL
字节,换行回车符)。
这个假设可能会使问题变难(因为我没有使用那些本来应该没有问题的字符),或者会导致更多后续的问题,如果我的假设的字符范围是不对的话。
我不喜欢做出这样的假设,但是出于练习的原因,这次姑且做个假设吧。
我只需要记住要小心,如果有什么问题的话,再次检查下坏的字符。有点冒险,但没关系,来吧!
让我们用mona 找到一个易于使用的POP-POP-RET 指令地址:
发现了很多东西(7909个!),但是突出显示的看起来更让人兴奋------全部由数字字母这样的字符组成,出现在QuickZip.exe
二进制文件中,希望这能够使它更加地跨平台,而我们就不必依赖于特定操作系统的DLL库了。
这里只有一个问题,就是 0x00
字节。又因为程序地址全部以0x00
开始 我们来试试看这是否会破坏我们的漏洞。
更新下PoC漏洞,用 \x33\x28\x42\x00
替换目前代表SEH的CCCC
再次触发崩溃,调查下SEH链。
很好,看来我们的地址没有乱码,正如我们所预料的那样。设置断点 (F2
) ,按 SHIFT + F9
将控件传给程序。
如你所见,我们被重定向到了POP-POP-RET 指令,通过F8
单步步过, 然后在 RETN 4
指令后停下。
太好了,我们又重新地找到了我们的有效数据。但是有个问题,由于存在NULL
字节,每个SEH链后面都被切断了,所以我们只有很少的空间来完成任务了。
好的,我们来分析下目前处于哪一阶段。
程序崩溃,然后控制了SEH,太棒了!问题是我们被限制在一个非常有限的字符集里,当使用payload时,因为必须使用NULL
字节的地址来调用POP-POP-RET指令 ,我们payload 中的重要部分被截断了,我们的shellcode剩余的空间一点也不大。
但是到底有多大呢?记住我们还有payload开始时使用的填充为了获得SEH:
所以我们有多少空间呢?正好是292个字节。不幸的是,对于 任何有用的shellcode--只包含可打印的 ASCII码字符并且需要加密,都是 不够的。
这听起来可以用egghunter来解决!
Egghunter 是一堆在程序内存空间中查找一个特定的,已知的字节序列的指令,一旦找到的话,将重定向到该区域。这样的话,我们不用担心shellcode在哪里结束了,只需要调用egghunter例程,它会为我们找到的!
听起来不错,接下来的问题是,payload 中被截断的部分在内存的哪里呢?我们来找找看。
让我们生成3764个唯一字符的模式(在NULL
字节后填充payload),并用它替换现有的A。
触发崩溃,当第一次出现 异常时,不要将异常传给程序,而是调用下面的命令在内存中搜索以前生成的模式:
太好了!payload中被截断的部分仍然在内存中,所以我们能够成功地用egghunter来获取到shellcode。
所以现在我们知道我们应该能够使用egghunter来获取我们的shellcode,但是我们只有292个字节可供我们使用。实际上,292字节做很多事儿了,但是,我们得记住只能使用非常有限的字符集。
我们试着用metasploit的x86/alpha_mixed
编码器对egghunter进行编码,看看在这之后剩下多少空间。
首先,让我们生成egghunter 的有效载荷。牢记现在是64位的操作系统,所以我们需要使用正确的egghunter 例程 (更多细节可以在https://www.corelan.be/index.php/2011/11/18/wow64-egghunter/)找到:
把生成的字节拷贝到一个文本文件中,用xxd
把它转换为二进制文件:
现在,我们需要用编码器来确保只用了可打印的ASCII码 字符。
注意: 我已经用了 bufferedregister=eax
选项。原因在于需要知道编码器在内存中的位置,以便于能对有效载荷解码。最初,负责这个的例程不在 可打印的ASCII码字符集中,因此这会毁掉我们的payload。
指定 bufferregister
选项只是告诉编码器不用担心找不到它在内存中的位置,因为已经提前把它的地址放在了EAX 寄存器中。这样的话,我们的编码器egghunter就只含有ASCII码字符了(更多关于生成只包含字母和数字的shellcode可以在 这里找到)。
更新下POC漏洞,看看目前为止做到哪了。
让我们触发崩溃,通过执行程序执行POP-POP-RET 指令。在这之后,在CPU窗口中向上滚动,尝试找到egghunter 的有效载荷和长指令集INC ECX指令的结束(代表A字符)。
非常好,看起来就是这里,都是和预期想的一样,没有错误-也没有坏字符出现!
跳回
现在,我们有更多的事情要考虑---最重要的是要把egghunter的起始地址放入EAX寄存器中,然后跳转到它。
我们能用有限的空间做什么呢?首先是---我们有多少空间?Quick math 算出有146个字节(nseh 偏移量减去egghunter 的大小)。
那么146个字节可以干什么呢?我们只要写几条指令,但是所使用的字符要在所要求的字符集中。在这种情况下,我们不能使用已经egghunter 的通用编码器,因为根本没有足够的空间容纳它。
这样只有一个选择了-自己来写编码器! 听起来很可怕并且复杂,但事实比想象的要简单的多。
但是我们先来看看我们目前在程序的哪里。
所以我们只有4个字节跳转回有效载荷,开始编写定制的编码器。而且,这4个字节最好只含字母数字。幸运的是,在那些情况下,我们有很少的指令可以使用。
信用额度到期了,特别感谢TheColonial 分享的这个技巧: http://buffered.io/posts/jumping-with-bad-chars/。
简而言之,我们可以只使用JO 和JNO
指令跳回我们的有效载荷。但是能跳多远呢?在处理了一些允许的字符后,发现一些坏字符被转换成了 A2
,就是十进制的92…这使得有足够的空间来写自制的编码器。
让我们用metasm
生成所需的操作码,并添加到我们的有效载荷中替代nSEH。
注意: \x9b
(-99), 因为它是一个坏字符,实际上将转化为 \xa2
(-92)。
我们的PoC部分应该像下面这样:
让我们触发崩溃,将执行过程传递给程序,步过POP-POP-RET 指令,观察当我们步过 JNO
/JO
指令时会发生什么。
太棒了,跳转到了payload处。让我们来向自己的编码器里写入指令跳转到egghunting 例程处。
我们需要写几条指令才能跳到我们的egghunter,但是,不使用坏的字符的话,没有办法直接写出来。
要解决这个问题,只需这样做:
听起来很复杂?实际上并没有,一旦你开始尝试它会变得很有意思。
首先,我们需要调整堆栈才能写入我们控制的内存区域。看下 ESP
寄存器的值和我们在程序里的位置(上面的截图),我们需要将 ESP
偏移量0x62C
(0x0018FB58
( EIP
的值) 减去 0x0018F528
(value of ESP
) 减去 0x4
(用于填充的空字节))。
用下面的指令可以实现:
以上指令的相应操作码如下:
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!