64位Linux下的栈溢出
今天在看《捉虫日记》,其中讲解的Linux下的栈缓冲区溢出是32位机器的,和我的64位机器有些不同之处。下面是我在64位Linux (Ubuntu14) 下的栈缓冲区溢出实验的记录。
首先是有溢出漏洞的程序,这个程序来自于《捉虫日记》。
#include<string.h>
void overflow(char *arg){
char buf[12];
strcpy(buf, arg);
}
int main(int argc, char *argv[]){
if(argc==2)
overflow(argv[1]);
return 0;
}
禁用堆栈保护编译
gcc -m64 stackoverflow.c -o stackoverflow -z execstack -fno-stack-protector
生成汇编文件
gcc -m64 stackoverflow.c -S -masm=intel -o stackoverflow.s -z execstack -fno-stack-protector
生成的.stackoverflow.s文件内容如下所示
.file "stackoverflow.c"
.intel_syntax noprefix
.text
.globl overflow
.type overflow, @function
overflow:
.LFB0:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 32
mov QWORD PTR [rbp-24], rdi
mov rdx, QWORD PTR [rbp-24]
lea rax, [rbp-16]
mov rsi, rdx
mov rdi, rax
call strcpy
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE0:
.size overflow, .-overflow
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsp
.cfi_def_cfa_register 6
sub rsp, 16
mov DWORD PTR [rbp-4], edi
mov QWORD PTR [rbp-16], rsi
cmp DWORD PTR [rbp-4], 2
jne .L3
mov rax, QWORD PTR [rbp-16]
add rax, 8
mov rax, QWORD PTR [rax]
mov rdi, rax
call overflow
.L3:
mov eax, 0
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
.section .note.GNU-stack,"",@progbits
在主函数中,38、39行将命令行参数放入栈中,第40行比较第一个参数(argc)的值是否等于2,若不等于则跳转(第41行)到L3,随后退出,若等于2,则取第二个参数加8后的地址单元中的值存放到寄存器rdi中。(第二个参数argv是char **型的,加8是因为我们传给函数overflow的参数是argv数组中的第二个值argv[1],它是一个字符串,也就是指向字符的指针。)在64位机器中,参数的传递会使用寄存器而不是堆栈。处理好参数之后便调用函数:call overflow。
在 函数overflow中,先执行“push rbp,mov rbp, rsp”(9-12行)称之为序幕(prolog)工作,对应的有第21行的leave指令完成收尾(epilog)工作。第14行的“sub rsp, 32”为局部变量留出空间。15-19行的指令为调用函数strcpy准备好了参数,参数依旧存在寄存器中(rsi是arg,rdi是buf,都是字符指针)。由于内存只能以字为单位寻址,64位机器中一个字是8个字节,buf[12]占12个字节,所以需要两个字的存储空间,也就是16字节,所以17行“lea rax, [rbp–16]”中减去了16 (最接近rbp的空间是留给buf的,因为它在函数中最先定义)。运行到第17行时,堆栈如下图所示:
函数strcpy会将rdi的值当做指针,将其指向的字符串复制到buf中,从上图可以看出,buf的大小很有限,若超出其长度,则会覆盖掉老rbp,老rip,使函数无法正常返回。尝试运行程序:
当参数字符串的长度小于等于15时运行不会出错
./stackoverflow 0123456789abcde
当参数字符串的长度大于15时运行才会报错
./stackoverflow 0123456789abcdef
Segmentation fault (core dumped)
为何不是16?C语言中的字符串是以\0作为结束标志的,所以实际的存储空间会比可见的长度多一个字节。
由于64位机器中的rip被设计成前47位有效,当指定一个大于0x00007fffffffffff的地址时会抛出异常,所以经典的0x4141414141414141无法实现。那就让我们把rip变成0x0000414141414141吧。
用gdb调试程序(-q使得gdb不输出gdb程序的版本等信息):
gdb -q ./stackoverflow
然后在gdb中输入命令
run $(python -c "print 'A'*30')
以30个大写字母A作为输入参数,其中16个用于填满buf,8个用于覆盖“老rbp”,最后6个用于覆盖rip。一运行就会看到这样的信息
Program received signal SIGSEGV, Segmentation fault.
0x0000414141414141 in ?? ()
可见rip的确被覆盖为了0x0000414141414141。若是想将rip覆盖为0x00007fffffffffff,则需要修改参数为
run $(python -c "print 'A'*24+'\xff'*5+'\x7f'")
运行结果为
Program received signal SIGSEGV, Segmentation fault.
0x00007fffffffffff in ?? ()