翻译:shan66
【资料图】
预估稿费:300RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
本文将为读者详细介绍QuickZip v4.60缓冲区溢出漏洞方面的知识。由于漏洞在2010年就出现了,所以它的设计仅适用于32位Windows XP。所以,我决定尝试在64位Windows 7上重现该漏洞,这将是一个(有趣的)挑战!
PoC
为此,我从exploit-db中抓取了QuickZip v4.60 Windows XP漏洞,并将用它创建了一个简单的PoC来触发崩溃。
上述代码创建了一个压缩文件,其中只包含一个名为4064A的文件,它的扩展名为“.txt”。 Header_1,header_2和header_3是ZIP文件结构所需的标题。 我不会详细介绍,但您可以在这里阅读更多。
如果您在QuickZip中打开刚创建的ZIP文件,并尝试提取其内容(或只需双击文件名),那么QuickZip就会崩溃。
了解崩溃详情
好的,我们来运行PoC,看看到底发生了什么。
使用上面的Python脚本创建ZIP文件,使用QuickZip打开它,启动ImmunityDebugger,附加到QuickZip进程,并在QuickZip中双击文件名以触发崩溃。 注意:我们将不断重复这个过程!
很好,崩溃如期而至。 另外,这里出现了一个异常,屏幕底部可以看到“Access violation when writing to [00190000]”。 这意味着我们试图写入一个无效的内存地址,从而触发了一个异常。
下面,我们来研究一下SEH链。
很好,看来我们能够控制nSEH指针!下面,我们尝试算出偏移量。
偏移量
一如既往,我要借助mona(
https://github.com/corelan/mona )来完成许多工作。
首先,我们生成一个4064个独特字符的模版,并将其放在PoC漏洞利用代码的有效载荷中:
再次触发崩溃,看看会发生什么情况。
呃,崩溃看起来有点不同。 这里的问题是LEAVE指令尝试从堆栈跳回到0EEDFADE地址,不过这里是该程序的无效内存地址。
此外,似乎我们无法控制SEH了。
但是,请注意,我们实际上是在内核模块中(请看Immunity窗口的名称:“CPU – main thread, module KERNELBA”)。 使用SHIFT + F9将执行权传回给程序,看看是否触发另一个异常,但是是在QuickZip模块中。
真棒,看起来成功了!
使用以下命令让mona计算所有偏移量:
在这里,我们最感兴趣的偏移是nSEH field: offset 292。
让我们用偏移信息更新PoC,并尝试再次触发崩溃。
太好了,我们控制了SEH!让我们将异常传给程序(SHIFT + F9),并进一步调查发生了什么。
当然,另外一个异常也被触发,因为43434343是这个程序的无效内存地址,但是让我们看看堆栈上到底发生了什么——通常是SEH溢出,我们需要调用一组POP-POP-RET指令来返回到缓冲区。
找到这样的指令是很容易的,但首先,我们必须知道允许使用哪些字符。这就是我们需要关注的下一个问题。
坏字符
总的来说,大部分是这样的。为什么?因为我们的溢出是针对filename参数的,而文件名用到的字符类型是相当有限的: 通常只有ASCII可打印的字符。
如果使用手动方式的话,那么使用mona通过遍历方法找到所有坏的字符将需要太长的时间,所以这里简单假设除了0x00、0x0a和0x0d(分别代表NULL、换行和回车)之外,我可以使用ASCII表中所有的字符(最高值为0x7F的字符)。
这个假设可能会比事情比实际情况要更困难(因为我需要避免使用实际可以使用的字符)一些,或者可能会导致更多的问题,如果我的假设范围内的某些字符其实是错误的话。
我不喜欢这样做假设,但为了进行这个练习,这里例外一次。
我只需要记住,要格外小心,如果有情况,则需要再次检查坏的字符。这有点冒险,但很好玩,继续!
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,这非常好! 问题是我们的有效载荷受制于一个非常有限的字符集,并且因为我们必须使用NULL字节的地址来调用POP-POP-RET指令,我们的有效载荷被切断了,并且留给shellcode的空间也不是很大。
那么它究竟有多大呢? 别忘了,为了获得SEH,我们还在有效负载开始部分进行了填充:
那么我们有多少空间呢? 共计292个字节。 不幸的是,这些是不够的。
不过,这个问题好像可以用egghunter来解决!
Egghunter只是一堆指令,在程序的内存空间中查找一个特定的、已知的字节序列(“egg”),一旦找到,将重定向到该区域。
这样我们就不用担心我们的shellcode在哪里结束了,我们可以调用eghtunter例程,它会为我们找到它们!
听起来不错,但下一个问题是,有效载荷的“截止”部分真的位于在内存中吗? 我们来看看吧。
让我们生成3764个单字符的模版(在NULL字节之后填写我们的有效负载),并用它替换现有的A。
我们触发崩溃,当我们得到我们的第一个异常时,不要将异常传递给程序,而是调用以下命令来在内存中搜索以前生成的模版:
太棒了! 有效载荷的整个“截断”部分仍然在内存中,所以我们应该能够成功地使用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/ ):
注意:我已经使用bufferedregister = eax选项,这是因为编码器需要找到它在内存中的位置,以便能够对有效载荷进行解码。 最初,负责该项工作的例程不在ASCII可打印的字符集中,因此会破坏我们的有效载荷。
指定bufferregister选项基本上就是告诉编码器不用担心如何在内存中找到自己的位置,我们会事先做好这件事情,我们将其地址放在EAX寄存器中。 这样,我们的编码后的egghunter就是纯ASCII字符(更多关于生成字母数字shellcode的信息可以在这里找到)。
我们更新我们的PoC漏洞利用代码,以反映我们迄今为止所做的工作的成效。
让我们触发崩溃,将控制权传递给该程序并执行POP-POP-RET指令。 之后,在CPU窗口中向上滚动,寻找egghunter有效载荷和一组EC ECX指令(代表字符A)的结束位置。
好的,看起来像是在那里,它似乎也是正确的:没有使用不符合要求的字符!
跳转回来
现在我们还有更多的事情需要考虑——这里最重要的一点是,我们需要把egghunter的地址放在EAX中,然后跳转到那里。
我们如何在空间有限的情况下做到这一点? 首先,我们有多少空间? 简单计算一下就知道是146字节(nseh偏移减去egghunter的大小)。
146字节可以做什么? 我们只需要写几个指令,但是它们必须属于允许使用的有限的字符集。 在这种情况下,我们不能使用已经用于egghunter的通用编码器,因为我们根本没有足够的空间来满足它。
所以,我们需要创建自己的编码器! 这听起来很让人头疼,但实际上比看起来要简单得多。
首先,我们来看看目前在程序中的位置。
我们只有4个字节,可由我们支配用来跳回有效载荷并开始写定制的编码器。同时,这4个字节最好是字母数字。 幸运的是,有多个指令可供使用,特别是在那些情况下!
在这方面,可以参考TheColonial分享的相关技巧:
http://buffered.io/posts/jumping-with-bad-chars/。
简而言之,我们可以简单地使用JO和JNO指令来调用近转移指令到我们的有效载荷。 但我们能跳多远? 通过用一些允许的字符的包裹后,我发现一些坏的字符会被转换为A2,它转换成十进制就是92,这应该能给我们提供足够的空间,以创建我们的自定义编码器。
令人惊奇的是,这次跳转到了我们的有效载荷! 现在,创建自定义编码器,编写指令,跳转到egg hunting例程。
定制编码器
为了跳到eghunter,我们需要写许多条指令,因为不使用“坏”字符的话,就没有直接的方法。
要解决这个问题,我们需要执行以下操作:
找出我们想要写的指令的操作代码
使用简单的数学指令(即ADD和SUB),通过允许的字符将来自上述步骤的操作码的值放入我们选择的寄存器(例如EAX)中
我们将这个寄存器的值写入堆栈,从而将我们想要的指令写入ESP指向的内存区域
听起来很复杂? 但实际上并不是那么糟糕。
首先,我们需要调整堆栈才能写入我们控制的内存区域。 通过观察ESP的值和我们目前的位置(上面的截图),可以发现,我们需要将ESP偏移0x62C(0x0018FB58(EIP的值)减去0x0018F528(ESP的值)再减去0x4(用于填充的空字节))。
太棒了,我们的有效载荷完全与堆栈可以完美搭配了,下面开始编写我们的编码器。
注意:由于pop esp指令( x5c)的缘故,ZIP文件的内容看起来会有点不同。 x5c表示一个反斜杠,由QuickZip解释为一个文件夹…这可能在以后有一些影响,但现在没什么。
现在,我们需要做的最后一件事是写一组指令,将egghunter的起始地址放入EAX并跳转到它。
为了避免“坏”字符,我们将在EAX寄存器中设置我们需要的操作码的值,并将其压入我们调整的堆栈上。这样,我们需要的指令将写到我们控制的内存区域中。
下面用一个例子来解释。
我们来更新漏洞利用代码并运行它。
太棒了,我们已经在EAX中成功设定了我们需要的值,并把它压入堆栈上,实际上写的是我们需要的指令!
让我们对所有剩余的字节做同样的处理。
完成上述处理后,新的PoC应该如下所示:
执行之后:
太棒了,我们已经成功地利用有效字符编写出了想要的代码! 现在只需跳回到该区域来执行就好了。 我们还需要将我们写入的临时0xDEADBEEF地址更改为实际的偏移量,前提是我们知道它是什么…但现在为时过早。
跳转
不幸的是,我们没有太多的空间可用于跳转:在我们的编码器代码之后只有5个字节,编码器代码之前是4个字节。所以,我们需要找到相应的指令,让我们跳转到刚写的代码。
事实证明,由于字符限制,实际上我们无法做太多的事情。 任何短的向后跳转指令都包含无效的字符,无法跳转至恰当的地方。所以,应该考虑是否重用之前用过的跳转。
下面来看看我们目前拥有的有效载荷。
我们需要发挥创造性。让我们重用SEH中的JNO跳转,以便再次回到我们控制的内存区域。我们可以在当前编码器有效载荷的开头部分添加一些NOP,然后通过自定义编码器用其他跳转指令将其覆盖,以将我们跳转到刚编写的代码之前。
哎,这样行得通吗?让我解释一下。
我们需要使用的跳转指令本来可以是简单的JMP $ -16( xeb xee),不幸的是它包含了无效的字符,因此不适用于我们…。但是,任何带有有效的字符的跳转指令都会让我们离的太远。
然而!我们可以使用自定义的编码器来处理它们,就像我们将egghunter的地址放置到EAX一样,只需要调整偏移量并修改代码即可。
首先,添加我们的JMP指令。然后,修改我们的原始堆栈,使SEH跳转能够准确到达我们的初始位置。最后,在编码器的开头部分添加一些NOP,它们之后将被所覆盖。下面我们具体介绍其工作原理。
这里,让我们先从自定义的编码器前面的NOP开始。 由于我们要求使用有效的字符集,因此可以使用 x41 x41(INC ECX)作为NOP。
接下来,进行堆栈调整。 从目前的状态来看,我们需要进一步偏移6个字节,以便写入到要覆盖的区域。为此,我们可以进行相应的调整。
最后,我们需要用编码器写入JNZ $ -16( x75 xee)指令。 让我们用新的指令来替换最后两个 x90(记住这里使用的是little – endianness,所以我们需要反过来写入)。
最后,代码将变成这样:
一旦执行,会发生以下情况:
崩溃被触发
POP-POP-RET指令被调用
获得JNO $ -92的跳转地址
从头开始执行自定义编码器
代码最终将到达第3步中跳转的JNO指令
再次取得JNO的跳转地址,但这次,我们登陆的第一条指令是刚刚写入的16个字节的跳转指令
获取跳转指令的跳转地址
使用自定义编码器写入要执行的指令
我们来看看到底发生了什么。
执行自定义的编码器后:
取得JMP的跳转地址
在写入指令之前登陆,准备执行
真棒,正是我们期待的! 现在我们只需要弄清楚用什么值替代0xDEADBEEF就可以了!
让我们来计算一下——ESP的当前值是0x0018FB4E,而egghunter代码从0x0018FA90开始,这意味着我们需要将EAX减去0xBE,让EAX指向我们的目的地。
我们开始修改漏洞利用代码,这里不是从EAX中减去0xDEADBEEF,而是减去0xBE。 PoC应进行以下修改:
有一个警告指出在文件末尾有一些垃圾,但没关系,因为仍然可以成功打开该文件。
让我们触发崩溃,看看这一次我们是否可以在内存中找到这个模版。
我的天哪,它就在那里!!!
Shellcode
现在,我们只需安装常规流程来处理一下shellcode就行了——我们需要找出坏字符,然后在shellcode之前插入一个“egg”(w00tw00t)并对齐堆栈。
我不会详细介绍寻找坏字符的细枝末节,因为我已经在这里详细介绍过了。 幸运的是,对于我们来说,这部分有效负载中仅有的坏字符是 x00, x0a和 x0d。
我们还需要在shellcode的开头插入w00tw00t字符,以确保egghunter可以定位它,并将执行权重定向到“egg”之后的第一个指令。
最后,我们需要对齐堆栈,以确保ESP指向一个16字节倍数的地址。 这样做的原因是有一些“SIMD”(单指令,多数据)指令可以并行处理内存中的多个字,所以要求这些字的起始地址是16字节的倍数。
如果我们没有正确对齐堆栈,那么shellcode根本不起作用。 我们可以轻松地利用单个指令AND esp,0xFFFFFFF0来对齐堆栈,也就是让它正好在w00tw00t“蛋”之后,在实际shellcode之前。
对于这个概念验证来说,我们将使用msfvenom生成一个简单的、弹出计算器的shellcode,具体如下所示:
当我们打开生成的cst.zip文件时,我们的漏洞利用代码就会运行,几秒钟(因为egghunter通过应用程序的内存找到“蛋”)后,我们应该看到计算器被打开。
成功了!!
小结
在本文中,我们已经成功地重新创建了QuickZip漏洞利用代码的64位版本,它已经可以在Windows 7上运行了!
总而言之,我们通过使用非常有限的、被允许的字符集(几乎可以ASCII打印)创建了一个egghunter漏洞利用代码,编写了我们自己的编码器,并通过在内存中的跳转,到达egghunter代码,最终到达shellcode。
需要注意的是:
找出允许使用的字符,并在发生错误时记住这些字符
如果缓冲区大小不够,不要气馁——发挥你的创造性!
确保您使用正确的egghunter代码(32位与64位),具体取决于您正在开发漏洞的平台
编写自己的编码器不是那么难,但需要大量的练习和耐心
确保在执行shellcode之前对齐堆栈