模糊测试与漏洞利用实战:MikroTik无需认证的远程代码执行漏洞(CVE-2018–7445)(下)
导语:希望这篇文章能为正在进行的MikroTik RouterOS漏洞研究做出一点贡献……
在上篇文章中,我们已经详细讲述了目标选择和漏洞发现的过程。本文将侧重于深入分析漏洞,并详细介绍漏洞利用的探索过程。
六、深入理解崩溃
首先,我们要确保可以重现崩溃。可以复制Mutiny发送的最后一个数据包,也可以从WireShark中提取消息。我们对NetBIOS下面的层不感兴趣,需要创建一个小脚本,以通过TCP发送数据包。
从导出的文件中提取原始字节。
>>> open(“req”).read() ‘\x81\x00\x00 \x00\x00 \x00\x00 \x00\x00 \x00\x00 \x00\x00 \x00\x00 \x00\x00 \x00\x00 \x00\x00 \x00\x00 \x00\x00’
创建一个简单的Python脚本,将Payload发送到远程服务。为了清晰起见,我已经使用等效的十六进制表示替换了其中的空格。
在产生新的SMB进程(pkill smb && /nova/bin/smb)后运行该脚本几次,并查看会发生什么。我们现在,已经拥有一种可靠的方法,来通过单个请求重现崩溃。
在这种情况下,我们需要处理WireShark具有解析器的协议,因此可以借助该信息从协议级别来了解崩溃的具体原因。显然,发送NetBIOS会话请求(消息类型0x81)将会在SMB二进制文件中执行易受攻击的代码路径。
我们从路由器中提取二进制文件,这样就可以在反汇编程序中打开该文件。将/nova/bin/smb复制到/flash/rw/pckg,以便可以通过FTP访问并下载。
在这里,可以使用GDB对进程进行调试,我喜欢使用PEDA来增强GDB的显示,并添加一些有用的命令。
我们打开两个到目标路由器的连接。在其中的一个连接中,我们使用pkill smb && /nova/bin/smb获得实时输出。在另一个连接中,我们启动附加到新生成进程的gdbserver。
最后,我们在测试主机上打开GDB,并使用目标远程IP:PORT连接到调试服务器。
通过执行文件smb来指示GDB我们所附加的二进制文件也很有帮助。在下一次连接到调试服务器时,将会尝试解析已加载库的符号。
在调试对话中按C,可以继续执行。
运行PoC将导致服务因SIGSEGV而停止。在这里,我们看到在执行复制操作时,解引用了NULL指针。
现在,我必须承认,我做静态分析非常糟糕,特别是在涉及C++程序的情况下。因此,我将尽可能多地依赖于动态分析,在实际场景中,我将依赖于Wireshark解析器提供的信息,将会给我们更多关于协议字段的信息。
可以看出,我们发送的NetBIOS会话服务数据包的第一个字节将消息类型设置为会话请求(0x81)。
第二个字节中包含标志,在我们的概念验证中,将所有位设置为零。
接下来的两个字节表示消息长度,设置为32。
最后,剩余的32个字节,为非法的NetBIOS名称。
我们可以假设在某个时刻会读取数据包的大小,并且其大小可能与漏洞相关。为了测试这个假设,我们将在read和recv之类的常用函数上放置断点,以识别应用程序从套接字读取数据包的位置。
运行脚本后,程序在read()中断。
我们使用ni导航至下一个命令,并在执行读取系统调用后立即停止。
Read的定义如下:
ssize_t read(int fd, void *buf, size_t count);
EAX将保留读取的字节数,在这里似乎是0x24(36)。与我们之前所分析的标头(消息类型1字节、标志1字节、消息长度2字节、NetBIOS名称32字节)一致。
ECX包含存储数据读取的缓冲区地址。我们可以使用vmmap $ecx或vmmap 0x8075068来验证是否符合实际堆区域。
最后,EDX声明调用读操作,从套接字中读取最多0x10000字节。
从这里开始,我们可以继续步进,并添加观察点,从而查看数据的变化。
由于WireShark中没有与NetBIOS名称中识别和分析协议相关的任何内容,因此我们将更改Payload,以包含更多可以区分的字符,例如“A”,以便在我们的调试会话中更容易地识别出该Payload。
其中,有2个字节可以重放不同的大小,可以尝试发送不同长度的消息,来查看崩溃是否发生。之前,已经尝试了32个字节,接下来我们依次尝试64、250、1000、4000、16000和65005。
64 (payload = “\x81\x00\x00\x40”+”A”*0x40)
与原始的PoC相同,寄存器看起来与之一致。
250 (payload = “\x81\x00\x00\xfa”+”A”*0xfa)
这是一个非常有趣的变异,我们看到大多数寄存器都设置为0x41414141,这是我们的输入,我们看到栈中充满了大量的“A”,并且EIP甚至已经被破坏。
1000 (payload = “\x81\x00\x03\xe8”+”A”*0x3e8)
与之前的Payload相同。
4000 (payload = “\x81\x00\x0f\xa0”+”A”*0xfa0)
与之前的Payload相同。
16000 (payload = “\x81\x00\x3e\x80”+”A”*0x3e80)
我们发现,栈也会损坏,但这一次是在不同的指令处发生的崩溃。
65500 (payload = “\x81\x00\xff\xdc”+”A”*0xffdc)
与之前的Payload相同。
所以我们看到,程序在执行不同的指令时发生了崩溃。但是,我们可以观察到,当从单个NetBIOS会话请求消息解析NetBIOS名称时,栈在某些时刻就已经损坏,并且当我们发送250字节消息时,大多数寄存器都包含我们Payload的一部分。这使得分析过程非常有趣,因为我们已经拥有直接的EIP覆盖和栈控制。
需要注意的是,我们无法确保所有的崩溃都是因为相同的问题。也许发送更大的缓冲区,会产生截然不同的结果,最终会更加容易被利用,所以我们必须自己来回答这个问题。
在其中,还有一些看上去像随机数的“.”(0x2e)字符。
在崩溃之前,程序将会打印一条“New connection:”的消息,这对于了解当前情况非常有帮助,也无需再向缓冲区中添加观察点,并跟踪数十个读取操作了。
我们在Binary Ninja中打开/nova/bin/smb二进制文件,并搜索字符串。
0x80709fb只出现了一次。通过检查交叉引用,可以显示出单一的用法,这可能是我们所需要的。
如果我们转到sub_806b11c的开头,会注意到需要满足几个条件,才能让我们到达打印字符串的块。
第一个条件是与0x81的字节比较,这是我们发送的消息类型。
将断点放在0x806b12e并执行后,我们可以检查寄存器的值,从而更好地了解所发生的情况。我们可以观察到,请求中发送的大小要大于0x43,这样才能进入到我们所关注的块中。
根据之前的测试,我们知道从这个块中调用的函数之一必须是能够破坏栈的函数。我们继续使用n来替代s,遍历GDB中的每条指令。每次运行函数之后,都要检查栈的情况。
遇到的第一个函数是0x805015e。
运行后,我们看到栈似乎没有出现问题,因此该函数可能不是导致溢出的函数。
在几个指令之后,我们得到了下一个备选函数,位于0x8054607。我们再一次运行该函数,并观察栈的情况,同时记录上下文。
终于,我们找到了罪魁祸首。查看EBP,发现栈框架已经损坏。继续调试,直到该函数即将返回。在这里,会从包含我们的数据到的栈中弹出各种寄存器。
其实,我们不需要了解这个函数在漏洞利用时所完成的一切。目前,我们已经掌握了EIP控制,并且大多数栈看起来或多或少都是未经损坏的输入数据。
我们花费一些时间,来具体看看0x5054607位置的函数,借助GDB的帮助,产生了如下伪代码:
int parse_names(char *dst, char *src) { int len; int i; int offset; // take the length of the first string len = *src; offset = 0; while (len) { // copy the bytes of the string into the destination buffer for (i = offset; (i - offset) < len; ++i) { dst[i] = src[i+1]; } // take the length of the next string len = src[i+1]; // if it exists, then add a separator if (len) { dst[i] = "."; } // start over with the next string offset = i + 1; } // nul-terminate the string dst[offset] = 0; return offset; }
实际上,该函数会接收两个栈分配的缓冲区,其中源缓冲区的预期格式为:SIZE1-BUF1、SIZE2-BUF2、SIZE3-BUF3,以此类推。其中,“.”用作条目的分隔符。
读取源缓冲区的第一个字节,并将其用作复制操作的大小。然后,该函数将这一字节数复制到目标缓冲区中。在完成后,读取源缓冲区的下一个字节,并将其用作新的大小。当要复制的大小等于零时,循环结束。在这里,没有对数据进行验证,因此无法保证数据大小在目标缓冲区的范围之内,从而导致栈溢出的问题。
七、编写漏洞利用
如何处理漏洞利用,要取决于目标设备和架构的具体细节。在这里,我们只对Cloud Hosted Router x86二进制文件感兴趣。
值得一提的是,可能有几种不同的方法来实现对该漏洞的可靠利用,因此我们提出的方法可能不是最简洁或最有效的。
Tobias Klein的checksec脚本是一个很好的资源,可以检查我们需要采取哪些缓解措施。我们可以从PEDA调用此脚本。
由于缺少栈金丝雀(Stack Canaries)保护,所以使得基于栈的缓冲区溢出很容易被利用。如果程序使用了栈金丝雀保护机制编译,那么我们前面的测试将会产生截然不同的结果。栈金丝雀保护会将随机值放在分配缓冲区的每个函数框架的重要数据之前,并在函数返回之前检查这些值。如果发生溢出,那么就会终止执行,并且不再进一步利用。
PIE被禁用意味着我们可以依赖程序代码的固定位置,RELRO被禁用意味着我们可以覆盖全局偏移表(Global Offset Table)中的条目。
总而言之,我们只需要处理NX,该机制限制从可写区域(例如栈或者堆)执行。
系统实施的另一项缓解措施是ASLR。由于这是一个32位系统,因此部分覆盖或者暴力破解的方法,可以被视为是绕过ASLR的一种可行方案。在我们的实际情况中,没有必要实现这一点。
通过检查RouterOS中任意程序的内存映射,表明栈确实是随机的,但堆不是。可以运行/proc/self/maps几次并比较结果,从而得出这一结论。
创建漏洞利用的第一步,是获得精确的偏移,从而获得对EIP的控制。为了实现这一点,我们可以使用PEDA生成一个独特的模式,命令模式创建256并将其插入到我们的漏洞利用框架中。请注意,标头之后的第一个字节是易受攻击的函数解析的大小,因此我们指定0xFF始终读取256个字节,并避免将“.”字符放置在Payload的中间。
发生崩溃时,可以使用pattern offset VALUE命令来确定覆盖EIP的确切位置。
修改Payload,并验证EIP是否可以设置为任意值。
我们没有观察到烦人的“.”字符,这很好。
既然我们已经控制了EIP和栈的其他部分,那么就可以使用借用的代码块技术,这种技术被称为面向返回编程(ROP)。
其主要思想是,我们将连接以RET指令结尾的各种代码片段,来执行或多或少的任意代码。如果有足够的这类小工具,我们应该能够运行我们想要的任何东西。但在特定的场景中,我们只希望将堆区域标记为可执行文件。我们的最终目标是在堆中存储特定内容(已经包含从客户端读取的消息),并利用静态基址实现跳转。
在这里,相关的函数是mprotect,如下所示:
int mprotect(void *addr, size_t len, int prot);
地址是0x8072000,这是堆的基址,需要页对齐(Page-Aligned)才可以正常工作。
Len可以使我们想要的任何内容,我们可以改变整个0x14000字节的保护。
最后,prot代表执行“按位或”的保护措施。7代表PROT_READ | PROT_WRITE | PROT_EXEC,本质上也就是我们所需要的RWX。
有各种工具可以用来自动创建链,例如ROPGadget和Ropper。我们将使用Ropper来手动构建ROP链,以展示它是如何完成工作的。
根据Linux系统的调用约定,EAX将包含系统调用编号,针对mprotect该值为0x7d。EBX将包含地址参数、ECX(大小)和EDX(所需的保护)。
我们最初将EBX设置为0x8072000。我们寻找包含POP EBX指令的小工具,并尽量减少副作用带来的风险。
我们选择较小的小工具来开始构建链,具体如下。执行流将被重定向至0x804c39d,它将首先执行POP EBX指令,将EBX设置为所需的值0x8072000。接下来,将会执行POP EBP,因此我们需要提供一些虚拟值,以便从栈中弹出一些东西。最后,执行RET指令,弹出栈中的下一个,并跳转到相应位置。这一过程需要成为我们链中的下一步。
所有值都将以低字节序(Little-Endian)无符号整型表示。
我们使用相同的过程,在ECX中设置所需的大小。理解其顺序至关重要,因为我们一旦不小心,就会在不知不觉中覆盖已经设置好的寄存器。此外,有时小工具看起来不如POP DESIRED_REG好用,RET和我们将不得不处理需要额外调整的潜在副作用。
在这里,我们选择更加良性的0x080664f5。这个小工具改变了EAX的值,但我们目前不依赖于EAX中的任何特定设置,因此它非常有用。我们将它添加到ROP链中。
重复这一构成,这次将EDX设置为7,也就是RWX保护级别。
这一次,在0x8066f24处选择小工具,这不会破坏我们之前设置的寄存器。
最后,我们需要将EAX设置为系统调用编号0x7d。我们搜索包含POP EAX的小工具,但找不到任何不会改变当前设置的内容。
我们可以尝试以不同的方式来重新排序小工具,但我们仅仅是搜索另一个会执行XCHG EAX, EBP的小工具,并使用无处不在的POP EBP; RET将其连接。
在这里,我们取0x804f94a和0x804c39e并附加。
现在可以根据需要,配置寄存器,并执行mprotect系统调用。为此,我们需要调用INT 0x80,它将通知内核我们要执行系统调用。
但是,当我们查找包含此指令的小工具时,我们找不到任何内容。这可能会使事情变得更加困难。
幸运的是,还有另一个地方可以找到这种小工具。在所有的用户空间应用程序中,都有一个小型共享库,由内核映射到其地址空间,称为vDSO(虚拟动态共享对象)。这是由于性能原因而存在的,并且是内核将某些函数导出到用户空间,以避免对经常调用的函数进行上下文切换的方法。如果我们查看用户手册,可以看到一些有趣的内容:
这意味着,vDSO中有一个可能可以执行系统调用的函数。我们可以检查这个函数在GDB中的作用。
从上面的屏幕截图中可以看出,__kernel_vsyscall中包含一个有用的小工具。我们执行该进程几次,随后意识到它不受ASLR的影响,这将允许我们使用这个小工具。EBX、ECX和EBP的值现在并不重要,因为它们将在执行系统调用后被设置。
我们更新漏洞利用代码,以发送我们构建的链,并将GDB附加到正在运行的SMB二进制文件中。
EIP将会执行重定向到我们的第一个小工具,因此最好将断点放在0x804c39d,这是链的起点。
使用stepi可以观察寄存器如何设置为所需的值。在INT 0x80之后,我们可以列出映射区域,如果一切正常,堆将被标记为RWX。
剩下的部分包括,将堆中的任意代码存储在已知位置,这样我们就可以跳转到该位置,并获得Shell。但是,怎样实现呢?
当我们在read()中放置一个断点时,我们发现请求数据存储在堆中的某个地方。此外,我们有各种协商协议请求的请求示例,因此可以确定,如果消息类型字节为0x00,那么我们将到达程序中的某个路径,其中Payload将会被处理并存储在堆中。
为了测试这一假设,我们再次在read()中放置一个断点,并更改PoC Payload,发送一个带有512个“A”内容的良性协商协议请求消息。提醒一下,格式应该为:
消息类型(1字节)+标志(1字节)+消息长度(2字节)+消息
此时,消息类型将设置为NETBIOS_SESSION_MESSAGE(0x00)。我们没有使用另一个会话请求消息(0x81)来避免意外触发漏洞,并且必须处理易受攻击函数的“.”字符。
逐步执行读取功能,直至从网络读取0x204字节(512个“A”+4字节标头)。如前所述,ECX中包含缓冲区的地址。
检查指定地址处的内存内容,将显示我们的Payload。
按c键允许执行继续,并发送新请求,以检查前一个请求是否被覆盖,或者是否留在堆中。
当再次到达断点时,我们尝试打印读缓冲区的内容。但不幸的是,我们发现它已经被清零。
但是,如果应用程序具有未归零的副本,那么之前的请求仍然可能在其他位置,并没有消失。可以使用find或searchmem命令,借助PEDA搜索当前地址空间。我们的消息由512个“A”组成,因此我们试图找到一个连续的“A”块。这些命令支持由空格分隔的可选参数,可以将搜索限制在特定区域。我们只对堆中可能存在的结果感兴趣。
这意味着,正在复制请求的内容,并将其保留在某个未清除的缓冲区中。我们需要进行一些测试,以便能够信任该位置,并存储我们的Payload。特别是,如果能将脚本更改为512个“B”而不是“A”,我们就将看到0x8085074将在处理请求后最终包含“B”。我们需要数据也在其他请求中持续存在,因此这个方案不够好。
但是,如果我们首先发送512个“A”,然后发送256个“B”,那么明显前半部分会被覆盖,但后半部分仍然包含前一个请求的字节。奇怪的0x00000e89是来自堆控制结构的块元数据,与我们的场景无关。
知道数据将至少在两个请求中保持不变这件事之后,我们可以制定如下计划:
1. 使用我们要执行的代码,发送协商协议请求。第一部分将是几百个NOP指令,因为当我们发出第二个请求时,这些字节将被覆盖,并带有触发漏洞的相应会话请求消息。
2. 发送破坏栈的会话请求消息,将ROP保护为mprotect,将堆标记为可执行文件,并跳转到存储#1 Payload的硬编码位置,从而滥用“堆基址不会被随机化”的这一事实。
我们决定为第二个请求预留512个字节,因此我们将跳转到0x8085074+512=0x8085270的硬编码位置。该地址需要附加到我们的ROP链中。之前的小工具将执行最终的RET指令,从栈中弹出0x8085270,然后执行程序。
ShellCode的第一个版本将只包含INT3指令,因此调试器在执行时会中断。INT3的操作码为CC。
该脚本也被修改为打开两个连接,每个连接对应着一个请求。
附加到新的SMB进程中,并运行漏洞利用程序。
现在,我们正在执行任意代码。让我们使用msfvenom生成反向Shell的Payload。
我们修改第一阶段,存储Payload,并再次运行漏洞利用。
这次,我们在指定的端口打开一个netcat监听器,以便我们可以接收连接。
终于,得到了Shell,漏洞利用大获成功。
八、总结
使用基于变异的方法对网络服务进行模糊测试,其实可以通过事半功倍的方式进行。
如果各位读者正在寻找那些使用特有协议的应用程序中的漏洞,或者不希望构建一个全面的模板,那么就可以使用Dumb Fuzzing的方式。我们可以让模糊器持续工作,并且运用一些逆向工程的知识,从而了解协议,并构建出更好的漏洞利用方式。
如今,使用RouterOS系统的设备无处不在。因此,缺乏现代漏洞利用的缓解方式这一现实,让我们非常担忧这一系统的整体安全性。如果启用了完整的ASLR,将会使漏洞利用变得更加困难。同时,如果二进制文件使用了栈金丝雀(Stack Canaries)支持编译,在没有信息泄露漏洞的前提下,大多数栈溢出都会变得无法利用。
值得一提的是,MikroTik的漏洞提交响应和补丁发布速度都很快。起初,补丁更改日志中没有暗示其存在安全漏洞:
6.41.3(2018年3月8日 11:55)版本更新:
Smb – 改进NetBIOS名称处理和稳定性。
但是,显然看起来他们更加重视了。他们在有关安全漏洞的更改日志中添加了更加详细的评论,并且似乎开设了一个博客,专门发布这些类型漏洞的官方公告。
对于读者来说,如果你们完整复现了这篇文章所详细描述的操作步骤,甚至你们可能已经在RouterOS SMB服务中发现了另外的一些0 day漏洞。
九、参考资源
发表评论