最近我在准备OSCE exam, 所以一直想找一些有趣的漏洞和POC代码来练习一下,顺便在漏洞方面学习一些新的东西。
在寻觅了一会后,发现了 QuickZip v4.60 Buffer Overflow exploit, 是 corelanc0d3r在这篇博客里记录的漏洞。
因为这个漏洞是2010年的,是为了32 位的window-xp 设计的。我试下能否在64位的windows 7 操作系统上重现它,这会是一个有趣的挑战!
PoC
起初,我先获取了 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
字节,换行回车符)。
这个假设可能会使问题变难(因为我没有使用那些本来应该没有问题的字符),或者会导致更多后续的问题,如果我的假设的字符范围是不对的话。
我不喜欢做出这样的假设,但是出于练习的原因,这次姑且做个假设吧。
我只需要记住要小心,如果有什么问题的话,再次检查下坏的字符。有点冒险,但没关系,来吧!
POP-POP-RET
让我们用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链后面都被切断了,所以我们只有很少的空间来完成任务了。
shellcode 哪里去了?!
好的,我们来分析下目前处于哪一阶段。
程序崩溃,然后控制了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
所以现在我们知道我们应该能够使用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,但是,不使用坏的字符的话,没有办法直接写出来。
要解决这个问题,只需这样做:
- 找出我们想要写的指令的操作码
- 使用简单的数学指令(即
ADD
和SUB
) 我们将使用仅允许的字符将来自上述步骤的操作码的值放入我们选择的寄存器(例如EAX
) - 我们将这个寄存器的值写入栈里,有效地将我们想要的指令写入
ESP
指向的区域
听起来很复杂?实际上并没有,一旦你开始尝试它会变得很有意思。
首先,我们需要调整堆栈才能写入我们控制的内存区域。看下 ESP
寄存器的值和我们在程序里的位置(上面的截图),我们需要将 ESP
偏移量0x62C
(0x0018FB58
( EIP
的值) 减去 0x0018F528
(value of ESP
) 减去 0x4
(用于填充的空字节))。
用下面的指令可以实现:
以上指令的相应操作码如下:
但是,我们有一个问题 - ;\x05\x2c\x06\x00\x00; 有两个NULL
字节,这将破坏我们的漏洞利用。
然而,我们可以通过使用有效字符执行ADD
和 SUB
指令的数量来设置我们想要的值,例如,
瞧!我们可以使用有效的字符来实现同样的事情。我们来更新下漏洞,看看会发生什么。
太好了,我们的payload 完全符合预期的堆栈状态,准备开始写编码器。
注意:由于pop esp
指令 (\x5c
),我们的ZIP文件的内容看起来有点不同。\x5c
表示一个反斜杠,被QuickZip解释为一个文件夹; 这可能在后面有影响,但现在问题不大。
现在需要做的最后一件事就是写一套指令把egghunter 的起始地址放入EAX寄存器,然后跳转过去。
为了避免出现坏字符,我们将在EAX
寄存器中设置我们需要的操作码的值,并将其压入调整的堆栈上。这样,所需的指令会被写入可控区域。
用一个例子来解释最好不过了。
让我们从我们真正想要写的指令开始吧?以下将恰恰是我们要做的:
相当简单 ESP
入栈, EAX 出栈,把调整好的值传到egghunter中 (我们不知道确切的值,因此现在的占位符为 0xDEADBEEF
) 并从EAX
寄存器跳转到调整的地址。
来生成我们所需的字节:
每组写入4个:
因为我们一次写入了4个字节,所以需要在末尾填充3个nops(\x90
) (把要写入的字节的总长度设为12)。
现在,我们从右下角开始写字节(因为 字节顺序) - 这就提示了我们要把需要的入栈操作。
记住我们只能使用ASCII值,这意味着我们可以使用任何01
to 7f
字节的组合来进行计算。
让我们想出一个利用友好的指令,将第一组字节写入eax寄存器:
我们来更新漏洞利用代码并运行它。
太棒了,我们已经在EAX寄存器中成功设好了所需的值,并把它放到堆栈上,这实际上写好了我们需要的指令!
接下来的字节也是相同的操作。
在所有数学操作后,更新完的POC代码如下:
在执行后是这样的:
太棒了,我们已经成功地只使用有效字符编写了代码!现在只需要跳回到该区域来执行程序。我们还需要将我们写入的临时0xDEADBEEF
地址更改为实际的偏移量,一旦我们知道它是什么; 但是这是最后要做的。
跳跃
不幸的是,我们没有太多的空间来跳过。在我们的编码器代码之后只有5 个字节,编码器代码之前是 4 个字节。我们想出来几条指令来得到我们刚刚写的代码。
事实证明,由于字符限制,实际上我们几乎什么都做不了。任何近的向后跳转都包含无效的字符,并不能让我们到想去的地址。另外,如果我们重新使用之前的跳转; 就是之前使用的跳转。
看下目前的payload。
我们需要有所创新。让我们重新使用JNO
指令跳回到SEH处,再次回到我们控制的区域。在当前编码器的有效载荷的起始处,添加一些NOP指令,然后用自定义的编码器覆盖到另一个跳转指令,使我们位于刚刚写的代码前面的位置。
呼,我希望这是有用的?我来解释下。
我们需要使用的跳转指令只是JMP $-16
(\xeb\xee
), 不幸的是它包含无效的字符,它这样不会有效的,任何包含有效字符都跳转都会使我们离得更远。
但是,我们可以使用自制的编码器编写它们,就像我们将egghunter的地址放置到EAX
-寄存器一样,我们只需要调整偏移量并且稍微修改下代码就行。
首先,添加下JMP
指令,而非那些用编码器编写的NOP指令。其次,我们需要调整初始堆栈,使SEH跳跃准确到达我们的初始位置。最后,我们将添加一些NOP指令,覆盖编码器的开头。
在使用编码器之前,先从NOP指令开始。由于我们需要使用有效的字符集,所以可以使用 \x41\x41
(INC ECX
)作为NOP指令。
接下来,调整堆栈。看目前的状态,看来我们需要再偏移6个字节,开始写入我们要覆盖的区域。让我们做同样的改变。
最后,我们需要用编码器编写 JNZ $-16
(\x75\xee
) 指令。让我们用新的指令来替换最后两个 \x90
(记住小端字节序,以相反顺序书写).
把它们放在一起,看起来应该是这样的:
- 引发崩溃
- POP-POP-RET 指令调用
- 向后跳转
JNO $-92
执行 - 自定义编码器启动
- 该代码最终将从步骤3到达
JNO
指令 JNO
指令执行,但这次,我们的第一个指令是新写的16字节的跳转。- 跳转执行
- 使用自定义编码器编写的指令将被执行
让我们看看实际上是什么样子
真棒,和预期的完全一样!现在我们只需要弄清楚用什么来替代0xDEADBEEF
我们就完工了!
让我们来计算一下 - ESP
的当前值是 0x0018FB4E
我们的egghunter代码从 0x0018FA90
,这意味着我们需要偏移 0xBE
个字节来让EAX
指向我们需要的地方。
修改下我们的漏洞,而不是从 EAX
中减去0xDEADBEEF
个字节 只需要去掉 0xBE
。 PoC应该改成下面这样:
跑下程序看看怎么样。
真棒!落到了egghunter处。现在应该很容易插入一段shellcode代码,并让egghunter找到它。
我们来运行 !mona findmsp
以防止我们的有效载荷仍然在内存中。
什么?!它消失了!去哪儿了发生了什么?所有的心血都白费了???
快进几个小时
唉,我知道怎么回事了。在自定义编码器前添加的指令破坏了payload,使shellcode代码不起作用了。指令故障是POP ESP
(\x5c
) - 和之前一样的字节使我们的文件名被解释为一个目录!
我花了很多时间思考,调试并试图找到一个不会破坏payload的办法,但运气不好。在这种情况下,使用任何有效的字符集根本什么也做不了。
但是,还是有办法的!也许不是最合适的,但也有。看看我们的漏洞中的这行:
如果在header_3之后再次添加payload 会怎么样?它会在ZIP文件的末尾附加一些垃圾,但它仍然可以工作。让我们来试下!
修改成下面这个样子,用QuickZip打开。
有一个警告显示在文件末尾有一些垃圾,但没关系,看来我们仍然可以成功打开该文件。
让我们触发崩溃,再次看到,这一次,我们可以在内存中找到这个模式。
对对!!!在那!!!现在可以了。
Shellcode
现在我们只需要按照通常的shellcode设置payload的过程,我们需要弄清楚坏字符,然后在shellcode之前插入一个“egg”(w00tw00t
)并对齐堆栈。
我不会详细介绍找到坏字符的细节,因为我已经在这里详细介绍了这些细节。幸运的是这部分payload中的唯一的坏字符是 \x00
, \x0a
and \x0d
.
我们还需要在shellcode的开头插入w00tw00t
字符,以确保egghunter可以定位它,并将执行程序重定向到“egg”之后的第一个指令。
最后,我们需要对齐堆栈,以确保ESP
指针指向的是16个字节倍数的地址。因为有一些 SIMD (单指令,多数据) 指令可以对存储器中的多个字执行并行操作,但是要求这些多个字是从16字节倍数的地址开始的块。
如果我们没有正确对齐堆栈,shellcode根本不起作用。我们可以很轻松地用单个指令AND esp,0xFFFFFFF0
,这需要加到 w00tw00t
之后,实际的shellcode前面。
对于PoC,我们将使用msfvenom
生成一个简单的,calc
弹出的shellcode。总结一下,shellcode代码将如下所示:
而涵盖迄今所讨论的所有PoC代码应该如下所示:
当我们启动生成的cst.zip
文件时,我们的漏洞将运行几秒钟(因为egghunter通过应用程序的内存找到“egg”),我们应该看到计算器二进制打开。
成功了!!
总结
差不多就是这样- 我们已经成功地在64位的window7上重新创建了QuickZip漏洞利用程序
总而言之,我们通过使用非常有限的允许的字符集(大部分是可以打印的ASCII字符)创建一个egghunter漏洞,写了我们自己的编码器,在内存之间跳转以获得egghunter代码,最后是shellcode。
需要记住几点:
- 在发生错误时,找出您被允许使用的字符,并记住这些字符
- 如果缓冲区大小不够,不要气馁 - 换个思路!
- 确保您使用正确的egghunter代码(32位或者64位),具体取决于您正在开发漏洞的平台
- 编写自己的编码器不是那么难,但需要大量的练习和耐心
- 确保在执行shellcode之前对齐堆栈
无论如何,希望您发现它是有用的!一如既往,如果您有任何问题/想法/建议或只是想和我聊一下相关的内容,请随时给我发表评论,或者在 @TheKnapsy 或是 IRC (主要是freenode上的 #vulnhub )上联系我。