简单缓冲区溢出原理

背景

什么是缓冲区溢出?
这里我借某个栈溢出靶机里面的第一道题目来解释缓冲区溢出的原理。

可以看到靶机里面有两份权限不同的文件,而我目前拿到的shell是level0

审计levelOne.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
uid_t uid = geteuid();
setresuid(uid, uid, uid);
long key = 0x12345678;
char buf[32];
strcpy(buf, argv[1]);
printf("Buf is: %s\n", buf);
printf("Key is: 0x%08x\n", key);
if(key == 0x42424242) {
execve("/bin/sh", 0, 0);
}
else {
printf("%s\n", "Sorry try again...");
}
return 0;
}

1、审计:
可以看到,程序一开始声明并初始化了key=0x12345678,
并声明了32字节的buf,
然后通过strcpy函数将第一个参数拷贝到buf中,
接着判断key是否为0x42424242,如是则获得shell。(levelOne的权限为level1)
key已经初始化为0x12345678,如何才能将其修改为0x42424242?

2、溢出:
如何将key修改为0x42424242?–通过缓冲区溢出覆盖key值为0x42424242。
什么是缓冲区溢出?
在样例程序中声明了一段32字节的buf,此为缓冲区,
当通过strcpy函数将输入的函数第一个参数拷贝到缓冲区的时候,由于strcpy函数为危险函数,其不会对操作对象进行任何检查,故在输入数据不超过32个字节时不会发生任何情况,
但当输入的数据超过32个字节的时候,就会发生溢出,也即所谓的缓冲区溢出。

3、覆盖:
根据前面我们学习到的内容,一个程序在载入内存后,其栈区会存在函数的各种变量、参数、栈针、返回地址等,并且各种数据是相邻分布的。
这就意味着,一个存在缓冲区溢出的程序,在精准控制溢出范围的情况下,可以精准覆盖内存栈区中某些特殊位置的数据。
这就为利用构造了条件,也即在本样例程序中的覆盖key值为0x42424242。

4、危险函数:
显而易见,在缓冲区溢出的过程中,最关键的就是strcpy函数。
那么还有哪些类似strcpy的危险函数呢?


可以看到这些危险函数集中为IO函数。

反汇编levelOne

1、命令

1
objdump -d levelOne -M intel

2、反汇编
下面为样例程序的反汇编情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
000011e9 <main>:
11e9: 8d 4c 24 04 lea ecx,[esp+0x4]
11ed: 83 e4 f0 and esp,0xfffffff0
11f0: ff 71 fc push DWORD PTR [ecx-0x4]
11f3: 55 push ebp
11f4: 89 e5 mov ebp,esp
11f6: 56 push esi
11f7: 53 push ebx
11f8: 51 push ecx
11f9: 83 ec 3c sub esp,0x3c
11fc: e8 ef fe ff ff call 10f0 <__x86.get_pc_thunk.bx>
1201: 81 c3 ff 2d 00 00 add ebx,0x2dff
1207: 89 ce mov esi,ecx
1209: e8 42 fe ff ff call 1050 <geteuid@plt>
120e: 89 45 e4 mov DWORD PTR [ebp-0x1c],eax
1211: 83 ec 04 sub esp,0x4
1214: ff 75 e4 push DWORD PTR [ebp-0x1c]
1217: ff 75 e4 push DWORD PTR [ebp-0x1c]
121a: ff 75 e4 push DWORD PTR [ebp-0x1c]
121d: e8 0e fe ff ff call 1030 <setresuid@plt>
1222: 83 c4 10 add esp,0x10 //调整栈帧
1225: c7 45 e0 78 56 34 12 mov DWORD PTR [ebp-0x20],0x12345678 //声明并初始化了一个变量,地址为ebp-0x20,数据为0x12345678
122c: 8b 46 04 mov eax,DWORD PTR [esi+0x4]
122f: 83 c0 04 add eax,0x4
1232: 8b 00 mov eax,DWORD PTR [eax]
1234: 83 ec 08 sub esp,0x8
1237: 50 push eax
1238: 8d 45 c0 lea eax,[ebp-0x40]
123b: 50 push eax
123c: e8 1f fe ff ff call 1060 <strcpy@plt> //将输入点数据拷贝到ebp-0x40
1241: 83 c4 10 add esp,0x10 //调整栈帧
1244: 83 ec 08 sub esp,0x8
1247: 8d 45 c0 lea eax,[ebp-0x40]
124a: 50 push eax
124b: 8d 83 08 e0 ff ff lea eax,[ebx-0x1ff8]
1251: 50 push eax
1252: e8 e9 fd ff ff call 1040 <printf@plt>
1257: 83 c4 10 add esp,0x10
125a: 83 ec 08 sub esp,0x8
125d: ff 75 e0 push DWORD PTR [ebp-0x20]
1260: 8d 83 14 e0 ff ff lea eax,[ebx-0x1fec]
1266: 50 push eax
1267: e8 d4 fd ff ff call 1040 <printf@plt>
126c: 83 c4 10 add esp,0x10
126f: 81 7d e0 42 42 42 42 cmp DWORD PTR [ebp-0x20],0x42424242 //判断ebp-0x20处的变量是否为0x42424242
1276: 75 18 jne 1290 <main+0xa7> //如果相同则跳转到后门函数
1278: 83 ec 04 sub esp,0x4
127b: 6a 00 push 0x0
127d: 6a 00 push 0x0
127f: 8d 83 24 e0 ff ff lea eax,[ebx-0x1fdc] //将’/bin/sh’压入栈中,作为函数参数
1285: 50 push eax //将0压入栈中作为函数参数
1286: e8 05 fe ff ff call 1090 <execve@plt> //后门
128b: 83 c4 10 add esp,0x10
128e: eb 12 jmp 12a2 <main+0xb9>
1290: 83 ec 0c sub esp,0xc
1293: 8d 83 2c e0 ff ff lea eax,[ebx-0x1fd4]
1299: 50 push eax
129a: e8 d1 fd ff ff call 1070 <puts@plt>
129f: 83 c4 10 add esp,0x10
12a2: b8 00 00 00 00 mov eax,0x0
12a7: 8d 65 f4 lea esp,[ebp-0xc]
12aa: 59 pop ecx
12ab: 5b pop ebx
12ac: 5e pop esi
12ad: 5d pop ebp
12ae: 8d 61 fc lea esp,[ecx-0x4]
12b1: c3 ret
12b2: 66 90 xchg ax,ax
12b4: 66 90 xchg ax,ax
12b6: 66 90 xchg ax,ax
12b8: 66 90 xchg ax,ax
12ba: 66 90 xchg ax,ax
12bc: 66 90 xchg ax,ax
12be: 66 90 xchg ax,ax

栈中的情况如下图:

pwn

如此当填充满32个字节的数据后,再填充4个B,即达成利用条件,进入后门函数。