picoCTF 2025 WriteUp

技能競賽前稍微練下reverse
差一題爛題rev破台

其實有加入隊伍(CFW),但直到比賽結束隊友好像都沒通過我的加入申請, 這場算是單打
rk. 1715
image

Rev

Flag Hunters

lyric-reader.py

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
import re
import time


# Read in flag from file
flag = open('flag.txt', 'r').read()

secret_intro = \
'''Pico warriors rising, puzzles laid bare,
Solving each challenge with precision and flair.
With unity and skill, flags we deliver,
The ether’s ours to conquer, '''\
+ flag + '\n'


song_flag_hunters = secret_intro +\
'''

[REFRAIN]
We’re flag hunters in the ether, lighting up the grid,
No puzzle too dark, no challenge too hid.
With every exploit we trigger, every byte we decrypt,
We’re chasing that victory, and we’ll never quit.
CROWD (Singalong here!);
RETURN

[VERSE1]
Command line wizards, we’re starting it right,
Spawning shells in the terminal, hacking all night.
Scripts and searches, grep through the void,
Every keystroke, we're a cypher's envoy.
Brute force the lock or craft that regex,
Flag on the horizon, what challenge is next?

REFRAIN;

Echoes in memory, packets in trace,
Digging through the remnants to uncover with haste.
Hex and headers, carving out clues,
Resurrect the hidden, it's forensics we choose.
Disk dumps and packet dumps, follow the trail,
Buried deep in the noise, but we will prevail.

REFRAIN;

Binary sorcerers, let’s tear it apart,
Disassemble the code to reveal the dark heart.
From opcode to logic, tracing each line,
Emulate and break it, this key will be mine.
Debugging the maze, and I see through the deceit,
Patch it up right, and watch the lock release.

REFRAIN;

Ciphertext tumbling, breaking the spin,
Feistel or AES, we’re destined to win.
Frequency, padding, primes on the run,
Vigenère, RSA, cracking them for fun.
Shift the letters, matrices fall,
Decrypt that flag and hear the ether call.

REFRAIN;

SQL injection, XSS flow,
Map the backend out, let the database show.
Inspecting each cookie, fiddler in the fight,
Capturing requests, push the payload just right.
HTML's secrets, backdoors unlocked,
In the world wide labyrinth, we’re never lost.

REFRAIN;

Stack's overflowing, breaking the chain,
ROP gadget wizardry, ride it to fame.
Heap spray in silence, memory's plight,
Race the condition, crash it just right.
Shellcode ready, smashing the frame,
Control the instruction, flags call my name.

REFRAIN;

END;
'''

MAX_LINES = 100

def reader(song, startLabel):
lip = 0
start = 0
refrain = 0
refrain_return = 0
finished = False

# Get list of lyric lines
song_lines = song.splitlines()

# Find startLabel, refrain and refrain return
for i in range(0, len(song_lines)):
if song_lines[i] == startLabel:
start = i + 1
elif song_lines[i] == '[REFRAIN]':
refrain = i + 1
elif song_lines[i] == 'RETURN':
refrain_return = i

# Print lyrics
line_count = 0
lip = start # RIP
while not finished and line_count < MAX_LINES:
line_count += 1
for line in song_lines[lip].split(';'):
if line == '' and song_lines[lip] != '':
continue
if line == 'REFRAIN':
song_lines[refrain_return] = 'RETURN ' + str(lip + 1) # RETURN; -> RETURN int 副歌結束後回REFRAIN;的下一行
lip = refrain
elif re.match(r"CROWD.*", line):
crowd = input('Crowd: ')
song_lines[lip] = 'Crowd: ' + crowd
lip += 1
elif re.match(r"RETURN [0-9]+", line):
lip = int(line.split()[1])
elif line == 'END':
finished = True
else:
print(line, flush=True)
time.sleep(0.5)
lip += 1



reader(song_flag_hunters, '[VERSE1]')

輸入 ;RETURN 0;
image

Quantum Scrambler

image

quantum_scrambler.py

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
import sys

def exit():
sys.exit(0)

def scramble(L):
A = L
i = 2
while (i < len(A)):
A[i-2] += A.pop(i-1)
A[i-1].append(A[:i-2])
i += 1

return L

def get_flag():
flag = open('flag.txt', 'r').read()
flag = flag.strip()
hex_flag = []
for c in flag:
hex_flag.append([str(hex(ord(c)))])

return hex_flag

def main():
flag = get_flag()
cypher = scramble(flag)
print(cypher)

if __name__ == '__main__':
main()

flag.txt 用 abcdefghijkl 測試
輸出整理後發現規律

1
2
3
4
5
6
7
8
9
[
['0x61', '0x62'],
['0x63', [], '0x64'],
['0x65', [['0x61', '0x62']], '0x66'],
['0x67', [['0x61', '0x62'], ['0x63', [], '0x64']], '0x68'],
['0x69', [['0x61', '0x62'], ['0x63', [], '0x64'], ['0x65', [['0x61', '0x62']], '0x66']], '0x6a'],
['0x6b', [['0x61', '0x62'], ['0x63', [], '0x64'], ['0x65', [['0x61', '0x62']], '0x66'], ['0x67', [['0x61', '0x62'], ['0x63', [], '0x64']], '0x68']]],
['0x6c']
]

solve.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enc = [ ...(略)... ]
plain = ""

def descramble(L):
plain = ""
for l in L[:-2]:
print(l)
plain += chr(int(l[0],16))
plain += chr(int(l[-1],16))
print(plain)

plain += chr(int(L[-2][0],16))
plain += chr(int(L[-1][0],16))

print(plain)


descramble(enc)

image

Tap into Hash

block_chain.py

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
import time
import base64
import hashlib
import sys
import secrets


class Block:
def __init__(self, index, previous_hash, timestamp, encoded_transactions, nonce):
self.index = index
self.previous_hash = previous_hash
self.timestamp = timestamp
self.encoded_transactions = encoded_transactions
self.nonce = nonce

def calculate_hash(self):
block_string = f"{self.index}{self.previous_hash}{self.timestamp}{self.encoded_transactions}{self.nonce}"
return hashlib.sha256(block_string.encode()).hexdigest()


def proof_of_work(previous_block, encoded_transactions):
index = previous_block.index + 1
timestamp = int(time.time())
nonce = 0

block = Block(index, previous_block.calculate_hash(),
timestamp, encoded_transactions, nonce)

while not is_valid_proof(block):
nonce += 1
block.nonce = nonce

return block


def is_valid_proof(block):
guess_hash = block.calculate_hash()
return guess_hash[:2] == "00"


def decode_transactions(encoded_transactions):
return base64.b64decode(encoded_transactions).decode('utf-8')


def get_all_blocks(blockchain):
return blockchain


def blockchain_to_string(blockchain):
block_strings = [f"{block.calculate_hash()}" for block in blockchain]
return '-'.join(block_strings)


def encrypt(plaintext, inner_txt, key):
midpoint = len(plaintext) // 2

first_part = plaintext[:midpoint]
second_part = plaintext[midpoint:]
modified_plaintext = first_part + inner_txt + second_part
block_size = 16
plaintext = pad(modified_plaintext, block_size)
key_hash = hashlib.sha256(key).digest()

ciphertext = b''

for i in range(0, len(plaintext), block_size):
block = plaintext[i:i + block_size]
cipher_block = xor_bytes(block, key_hash)
ciphertext += cipher_block

return ciphertext


def pad(data, block_size):
padding_length = block_size - len(data) % block_size
padding = bytes([padding_length] * padding_length)
return data.encode() + padding


def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))


def generate_random_string(length):
return secrets.token_hex(length // 2)


random_string = generate_random_string(64)


def main(token):
key = bytes.fromhex(random_string)

print("Key:", key)

genesis_block = Block(0, "0", int(time.time()), "EncodedGenesisBlock", 0)
blockchain = [genesis_block]

for i in range(1, 5):
encoded_transactions = base64.b64encode(
f"Transaction_{i}".encode()).decode('utf-8')
new_block = proof_of_work(blockchain[-1], encoded_transactions)
blockchain.append(new_block)

all_blocks = get_all_blocks(blockchain)
print(all_blocks)

blockchain_string = blockchain_to_string(all_blocks)
print(blockchain_string)
encrypted_blockchain = encrypt(blockchain_string, token, key)

print("Encrypted Blockchain:", encrypted_blockchain)


if __name__ == "__main__":
text = sys.argv[1]
main(text)

沒hash沒加密,XOR而已

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import base64
import hashlib


key = b'\xa9\xcco`\xfa\xf9\xb5\xc0\xda\xf6*\xb3\xbe\xa9t\x0fi\xae\x13\x01q-\xae\x9ap\xb7\xa45\x1e{\xaa\xb4'
enc = b'\xf7Y\x8db\x8bS\xb2\x80q\xf2\xa0\x87\xd6(\xfc\xe6\xf2\\\x82`\x8c\\\xb4\xd4v\xf0\xf2\xd1\xde/\xfa\xb0\xfb]\xdfg\x8bV\xe2\xd1$\xa5\xa6\xd9\x8c+\xa8\xe7\xa6X\x82d\xda\x01\xb1\x85u\xa4\xa3\xd3\xda}\xff\xbc\xeeZ\x8am\x8d\x01\xb1\x84$\xa1\xf4\x85\x8c,\xfa\xe7\xf0S\x8f4\x8f\x02\xb1\x82w\xf1\xf6\x85\xd7/\xff\xb3\xa6]\xdf`\x8b\x00\xe3\xd1"\xf2\xf6\xd8\xda|\xfd\xb7\xf3Z\xd83\xdc\\\xbe\xd6!\xa2\xae\xd8\x8c{\xfa\xb0\xf2G\x8ae\x8a\x01\xe3\xd7q\xf4\xa3\xd9\x8aq\xfd\xbd\xfb\x08\x8ag\x8dQ\xe3\xd0"\xa4\xf2\x84\xdeq\xac\xb5\xf4\x0e\xca<\xdd\x0b\xc5\xe6U\xbc\xf5\x8d\x81*\xa6\xdb\xf09\xe8=\xe8\r\xd4\xd0G\xf6\xe6\x82\xb6\x16\x95\xd1\xa9\'\x8a\'\x8a]\xe5\xfaL\xb6\xd4\x9b\x83\x03\x97\xfe\x81!\xe5a\x87\\\xbf\xd4*\xa2\xf6\x9c\xdex\xf4\xe5\xf6R\x8em\x87W\xb1\x80 \xf5\xf4\xd6\x8a{\xfd\xe7\xf3\x08\x89d\xdfP\xb2\x82u\xf4\xa0\x80\xc3y\xfd\xb2\xa5[\x83m\x8dQ\xe5\x84+\xfe\xa5\x84\xd7p\xf4\xb6\xf1S\x89m\xddQ\xb0\xd7&\xf2\xf2\xd3\x8b,\xf9\xb5\xfa\x08\xd8f\x8c\\\xe5\xd7"\xf3\xf6\xd4\x8c|\xac\xe5\xa7\x08\x8ag\x8d\x02\xbe\xd1$\xa2\xa3\x80\x8ad\xfd\xb4\xa6]\xdc0\xd8W\xb1\x85q\xa5\xf6\x80\xdbq\xa9\xb5\xf0_\xdee\x8e\x00\xbe\xd3r\xf4\xa2\xd4\x88y\xf4\xb0\xa5_\xdc4\xda\x05\xb7\x80r\xf1\xa3\x84\x8ap\xfb\xb2\xfa\x08\x8c`\x8eR\xe3\x81q\xfe\xae\x84\x88x\xcf\x86'

def xor_bytes(a, b):
return bytes(x ^ y for x, y in zip(a, b))

key_hash = hashlib.sha256(key).digest()
print(key_hash)

plain = b''

for i in range(0, len(enc), 16):
block = enc[i:i + 16]
plain_block = xor_bytes(block, key_hash)
plain += plain_block
print(plain)

image

perplexed

ida

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
__int64 __fastcall check(const char *a1)
{
__int64 v2; // rbx
__int64 v3; // [rsp+10h] [rbp-50h]
__int64 v4[3]; // [rsp+18h] [rbp-48h]
int y; // [rsp+34h] [rbp-2Ch]
int x; // [rsp+38h] [rbp-28h]
int v7; // [rsp+3Ch] [rbp-24h]
int j; // [rsp+40h] [rbp-20h]
unsigned int i; // [rsp+44h] [rbp-1Ch]
int k; // [rsp+48h] [rbp-18h]
int idx; // [rsp+4Ch] [rbp-14h]

if ( strlen(a1) != 27 )
return 1LL;
v3 = 0x617B2375F81EA7E1LL;
v4[0] = 0xD269DF5B5AFC9DB9LL;
*(__int64 *)((char *)v4 + 7) = 0xF467EDF4ED1BFED2LL;
idx = 0;
k = 0;
v7 = 0;
for ( i = 0; i <= 22; ++i )
{
for ( j = 0; j <= 7; ++j )
{
if ( !k )
k = 1;
x = 1 << (7 - j); // x = 10000000 01000000 00100000 ... 00000001
y = 1 << (7 - k); // y = 01000000 00100000 ... 00000001
// k = 1~7 1~7...
if ( (x & *((char *)&v4[-1] + (int)i)) > 0 != (y & a1[idx]) > 0 )
return 1LL;
if ( ++k == 8 )
{
k = 0;
++idx;
}
v2 = idx;
if ( v2 == strlen(a1) )
return 0LL;
}
}
return 0LL;
}
  • v3 是 v4[-1]
  • v4[-1] 最後 1 byte 會被覆蓋,由於小端序實際
  • 所以總共 23 bytes => for ( i = 0; i <= 22; ++i )
  • byte & 1 << (7 - j) > 0 的方式提取byte的 8 個 bit => for ( j = 0; j <= 7; ++j )
  • 23 bytes 依序跟 a1 的指定 byte 指定 bit 比較,全部一樣就是 flag
    solve.py
    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
    import struct

    v = struct.pack('<Q',0x617B2375F81EA7E1) + b'\xB9\x9D\xFC\x5A\x5B\xDF\x69' + struct.pack('<Q',0xF467EDF4ED1BFED2)
    print(v)

    a1 = ["\x00"] *27
    print(a1)

    idx = 0
    k = 0

    for i in range(23): # v 23 byte
    for j in range(8): # 8 bit

    if k == 0:
    k = 1

    x = 1 << (7 - j)
    # y = 1 << (7 - k)

    v_bit = x & v[i] > 0
    # print(v_bit)
    a1[idx] = chr(ord(a1[idx]) | v_bit << (7 - k))

    k += 1
    if k == 8:
    k = 0
    idx += 1

    print(''.join(a1))
    image

Binary Instrumentation 1

bininst1.exe 直接執行
image
ida

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
__int64 start()
{
struct _PEB *v0; // rbx
HANDLE ProcessHeap; // rax
void *v2; // rdi
HANDLE v3; // rax
__int64 v4; // rdi
int *ImageBaseAddress; // rbp
__int64 v6; // rsi
char *i; // rbx
__int64 v9; // rdi
__int64 v10; // rbx
char *v11; // rdi
__int64 v12; // [rsp+50h] [rbp+8h] BYREF
__int64 v13; // [rsp+58h] [rbp+10h] BYREF

v0 = NtCurrentPeb();
ProcessHeap = GetProcessHeap();
v2 = HeapAlloc(ProcessHeap, 8u, 0x400ui64);
if ( GetLastError() == 13852 )
{
ReleaseSRWLockExclusive(0i64);
ReleaseSRWLockShared(0i64);
SetCriticalSectionSpinCount(0i64, 0);
TryAcquireSRWLockExclusive(0i64);
WakeAllConditionVariable(0i64);
SetUnhandledExceptionFilter(0i64);
UnhandledExceptionFilter(0i64);
CheckMenuItem(0i64, 0, 0);
GetMenu(0i64);
GetSystemMenu(0i64, 0);
GetMenuItemID(0i64, 0);
EnableMenuItem(0i64, 0, 0);
MessageBeep(0);
GetLastError();
MessageBoxW(0i64, 0i64, 0i64, 0);
MessageBoxA(0i64, 0i64, 0i64, 0);
UpdateWindow(0i64);
GetWindowContextHelpId(0i64);
}
else
{
v3 = GetProcessHeap();
HeapFree(v3, 0, v2);
}
if ( !v0 || v0->OSMajorVersion != 10 )
return 0xFFFFFFFFi64;
v4 = 0i64;
v13 = 0i64;
v12 = 0i64;
ImageBaseAddress = (int *)v0->ImageBaseAddress;
v6 = ImageBaseAddress[15];
for ( i = (char *)ImageBaseAddress + v6 + 264; (unsigned int)sub_1400014B0(i) != 0x9F520B2D; i += 40 )
{
if ( ++v4 > (unsigned __int64)*(unsigned __int16 *)((char *)ImageBaseAddress + v6 + 6) )
return 0xFFFFFFFFi64;
}
v9 = *((unsigned int *)i + 3);
v10 = *((unsigned int *)i + 4);
v11 = (char *)ImageBaseAddress + v9;
if ( !v11
|| !v10
|| !(unsigned int)sub_1400018B0()
|| (unsigned int)sub_140001300(1, (_DWORD)v11, v10, (unsigned int)&v13, (__int64)&v12) )
{
return 0xFFFFFFFFi64;
}
sub_140001DC0(v13, v12, 1i64);
return 0i64;
}

一堆 windows api,看不出跟cmd輸出有甚麼關聯
由於似乎輸出後就停住,推測有用 windows api Sleep(),用 frida-trace 檢測

1
frida-trace .\bininst1.exe -i "*Sleep*"

image
把參數都改成 0
frida.js

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
var Sleep = Module.getExportByName("KERNEL32.DLL", "Sleep");

Interceptor.attach(Sleep, {
onEnter: function(args)
{
console.log("Sleep 傳入參數 " + args[0]);
args[0] = ptr(0)
console.log("改成 " + args[0])
},
onLeave: function(retval)
{
console.log('SleepEx 返回');
}
});

var SleepEx = Module.getExportByName("KERNEL32.DLL", "SleepEx");

Interceptor.attach(SleepEx, {
onEnter: function(args)
{
console.log("SleepEx 傳入參數 " + args[0] + " " + args[1]);
args[0] = ptr(0)
args[1] = ptr(1)
console.log("改成 " + args[0])
},
onLeave: function(retval)
{
console.log('SleepEx 返回值 ' + retval);
}
});
1
frida .\bininst1.exe -l .\frida.js

image
image
image

Binary Instrumentation 2

bininst2.exe 執行後沒有任何輸出就結束
題目敘述包含 create a file and write the flag directly to the file

1
frida-trace .\bininst2.exe -i "*Create*"

image
"*Write*" 沒找到
frida.js

1
2
3
4
5
6
7
8
9
10
11
12
var func = Module.getExportByName("KERNEL32.DLL", "CreateFileA");

Interceptor.attach(func, {
onEnter: function(args)
{
console.log("CreateFileA 傳入參數 \n" + hexdump(args[0]));
},
onLeave: function(retval)
{
console.log('CreateFileA 返回' + retval);
}
});
1
frida .\bininst2.exe -l .\frida.js

image
image

Pwn

PIE TIME

image
vuln.c

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

void segfault_handler() {
printf("Segfault Occurred, incorrect address.\n");
exit(0);
}

int win() {
FILE *fptr;
char c;

printf("You won!\n");
// Open file
fptr = fopen("flag.txt", "r");
if (fptr == NULL)
{
printf("Cannot open file.\n");
exit(0);
}

// Read contents from file
c = fgetc(fptr);
while (c != EOF)
{
printf ("%c", c);
c = fgetc(fptr);
}

printf("\n");
fclose(fptr);
}

int main() {
signal(SIGSEGV, segfault_handler);
setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

printf("Address of main: %p\n", &main);

unsigned long val;
printf("Enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
printf("Your input: %lx\n", val);

void (*foo)(void) = (void (*)())val;
foo();
}
1
2
3
└─$ r2 vuln
[0x000011a0]> aaa
[0x000011a0]> afl

image

1
2
3
4
5
└─$ python3
>>> 0x133d-0x12a7
150
>>> hex(0x5f5e07df533d-150)
'0x5f5e07df52a7'

image

hash-only-1

1
└─$ ssh ctf-player@shape-facility.picoctf.net -p 63629

image
有 SUID

1
ctf-player@pico-chall$ strings flaghasher

image
由於md5sum會用root執行,嘗試建立假的 md5sum 並修改 $PATH
image
image