PHDays AICTF 2025 Writeup
I got 8th place in PHDays AICTF 2025. This is a writeup for it.
Floor Check
We are given a single parquet file. Simply observe the table, the rows and columns visually form characters corresponding to the flag.
D .mode box D select * from read_parquet('miccheck_a8749ce.parquet'); ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │ 0 │ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │ 7 │ 8 │ 9 │ ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ │ - │ - │ - │ - │ - │ 8 │ - │ - │ - │ - │ │ - │ - │ - │ - │ - │ 8 │ - │ - │ - │ - │ │ - │ - │ - │ - │ 8 │ - │ 8 │ - │ - │ - │ │ - │ - │ - │ - │ 8 │ - │ 8 │ - │ - │ - │ │ - │ - │ - │ 8 │ - │ - │ - │ 8 │ - │ - │ │ - │ - │ - │ 8 │ - │ - │ - │ 8 │ - │ - │ │ - │ - │ 8 │ 8 │ 8 │ 8 │ 8 │ 8 │ 8 │ - │ │ - │ - │ 8 │ - │ - │ - │ - │ - │ 8 │ - │ │ - │ 8 │ - │ - │ - │ - │ - │ - │ - │ 8 │ │ - │ 8 │ - │ - │ - │ - │ - │ - │ - │ 8 │ │ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ - │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ 8 │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ 8 │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ 8 │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ 8 │ - │ - │ - │ - │ - │ - │ - │ │ - │ - │ 8 │ - │ - │ - │ - │ - │ - │ - │ ... etc.
VoiceGuard
We are given a reference sample of someone's voice and asked to clone it to speak new text. The premise is security verification. I ran the IndexTTS model locally and passed this easily.
Athrad Edhellen
This was a frustrating one. You can see the final script in action on YouTube. You have to solve 100 captchas by decoding the 6 Tengwar number symbols shown. The code I wrote to automate this is shit, basically a bunch of heuristics and heuristics to override heuristics. The challenge included 6 fonts and for each I gathered examples of the 12 symbols. Then, I just "slide" the representative examples across the new captcha and look for matches (the image is thresholded to remove noise). Basic selenium code completely automates the process of solving captchas.
Ham Filter
We are allowed to poison a spam-detection model with 100 training samples. Our goal is to get the test score of the model below 60%. We are also given access to the entire training data. My idea was to take the spam messages, generate my own data based on them, and wrongly label these messages as no spam. Firstly, we can get the spam messages like so:
D .mode json D select message from read_parquet('train_data_chocobo.parquet') where label=1; [{"message":"-PLS STOP bootydelious (32/F) is inviting you to be her friend. Reply YES-434 or NO-434 See her: www.SMS.ac/u/bootydelious STOP? Send STOP FRND to 62468"}, {"message":"network operator. The service is free. For T & C's visit 80488.biz"}, {"message":"XMAS Prize draws! We are trying to contact U. Todays draw shows that you have won a å£2000 prize GUARANTEED. Call 09058094565 from land line. Valid 12hrs only"}, ... etc.
Then we can paste this JSON into the Python script. The script takes the messages and samples new words according to the frequency of the words present. I initially set the length l
to between 20 and 30, but I was only getting a drop from 87% to 83%. I noticed that although only 100 rows were allowed, 1MB of data was allowed. So I extended the length by a factor of 10 which managed to drop the test score sufficiently to show the flag.
import random
from collections import Counter
import duckdb
data = [...]
acc = []
for x in data:
acc += x['message'].split()
c = Counter(acc)
words = list(c.keys())
ws = list(c.values())
con = duckdb.connect()
con.execute('CREATE TABLE foo (label BIGINT, message VARCHAR);')
for _ in range(100):
l = random.randint(200, 300)
con.execute(
'INSERT INTO foo (label, message) VALUES (0, ?);',
[' '.join(random.choices(words, weights=ws, k=l))],
)
print(con.execute('SELECT * FROM foo;').fetchall())
con.execute("COPY foo TO 'my_custom_data.parquet' (FORMAT 'parquet');")
Rate My Car
We upload an image onto the website and the judge (a multimodal LLM) returns a score between 0 and 100 based on the car present in the image. Our goal is to get a score of 1337. I used the following low resolution image to break the LLM, which given previous challenges, I suspected to be GPT-4o.

Flag Leak
We are given a webpage with 1000 YouTube videos. We are told one of them contains footage of a person submitting the flag. I noticed that a lot of the videos were popular, that is had a lot of views. Hence, I scraped the view counts of every video like so:
import os
for i, line in enumerate(
"""
[... 1000 youtube video urls here]
""".strip().splitlines()
):
os.system(f'yt-dlp --print "%(upload_date)s.%(view_count)s" {line} >{i}; echo "{line}" >>{i}')
Then I sorted the results by views, and wouldn't you know it the first video (9 views) was the correct one. It showed some Russian guy developing a ping pong app with ChatGPT and at the end, he submits the flag.
9 20250519 https://www.youtube.com/watch?v=7TE0p3kx5p0 110 20240809 https://www.youtube.com/watch?v=YK5Db_HOFC0 121 20250305 https://www.youtube.com/watch?v=fWu18blUXDo 191 20191019 https://www.youtube.com/watch?v=XCviY4qNQ_Q 1015 20221212 https://www.youtube.com/watch?v=OGmvoj5Dj_A ... etc.
Brauni
We are given an executable and need it to output "valid." IDA gives this code:
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v4; // [rsp+10h] [rbp-90h] BYREF
unsigned int v5; // [rsp+14h] [rbp-8Ch]
signed int i; // [rsp+18h] [rbp-88h]
int DeviceCount; // [rsp+1Ch] [rbp-84h]
unsigned int *v8; // [rsp+20h] [rbp-80h] BYREF
__int64 v9; // [rsp+28h] [rbp-78h] BYREF
unsigned int v10; // [rsp+30h] [rbp-70h]
__int64 v11; // [rsp+34h] [rbp-6Ch] BYREF
unsigned int v12; // [rsp+3Ch] [rbp-64h]
_DWORD v13[18]; // [rsp+40h] [rbp-60h] BYREF
unsigned __int64 v14; // [rsp+88h] [rbp-18h]
v14 = __readfsqword(0x28u);
if ( argc == 2 )
{
DeviceCount = cudaGetDeviceCount(&v4, argv, envp);
if ( !DeviceCount && v4 )
{
v5 = 16;
if ( strlen(argv[1]) < 0x10 )
v5 = strlen(argv[1]);
for ( i = 0; i < (int)v5; ++i )
v13[i] = argv[1][i];
cudaMalloc<unsigned int>(&v8, 4LL * (int)v5);
cudaMemcpy(v8, v13, 4LL * (int)v5, 1LL);
dim3::dim3((dim3 *)&v11, v5, 1, 1);
dim3::dim3((dim3 *)&v9, 1, 1, 1);
if ( !(unsigned int)_cudaPushCallConfiguration(v9, v10, v11, v12, 0LL, 0LL) )
kernel(v8);
cudaMemcpy(v13, v8, 4LL * (int)v5, 2LL);
if ( v13[0] == 1 )
puts("Valid");
else
puts("Fail");
cudaFree(v8);
return 0;
}
else
{
puts("Fail: No CUDA-capable GPU found.");
return 1;
}
}
else
{
puts("Usage: program <key>");
return 0;
}
}
So clearly all the important logic is in the kernel. We can output the PTX code.
0 kjc@kjc:~/Downloads/brauni$ /opt/cuda/bin/cuobjdump -ptx ./brauni_linux.elf Fatbin elf code: ================ arch = sm_52 code version = [1,7] host = linux compile_size = 64bit Fatbin elf code: ================ arch = sm_52 code version = [1,7] host = linux compile_size = 64bit Fatbin ptx code: ================ arch = sm_52 code version = [8,0] host = linux compile_size = 64bit compressed .version 8.0 .target sm_52 .address_size 64 .visible .entry _Z6kernelPj( .param .u64 _Z6kernelPj_param_0 ) { .reg .pred %p<17>; .reg .b32 %r<126>; .reg .b64 %rd<5>; .shared .align 4 .b8 _ZZ6kernelPjE5match[64]; ld.param.u64 %rd2, [_Z6kernelPj_param_0]; cvta.to.global.u64 %rd1, %rd2; mov.u32 %r1, %tid.x; setp.lt.s32 %p1, %r1, -7; mov.u32 %r125, 269671431; @%p1 bra $L__BB0_7; add.s32 %r22, %r1, 7; max.s32 %r2, %r22, 0; add.s32 %r23, %r2, 1; and.b32 %r124, %r23, 3; setp.lt.u32 %p2, %r2, 3; mov.u32 %r125, 269671431; @%p2 bra $L__BB0_4; sub.s32 %r119, %r2, %r124; $L__BB0_3: and.b32 %r25, %r125, 1; setp.eq.b32 %p3, %r25, 1; shr.u32 %r26, %r125, 1; xor.b32 %r27, %r26, 180; selp.b32 %r28, %r27, %r26, %p3; shr.u32 %r29, %r28, 1; and.b32 %r30, %r28, 1; setp.eq.b32 %p4, %r30, 1; xor.b32 %r31, %r29, 180; selp.b32 %r32, %r31, %r29, %p4; shr.u32 %r33, %r32, 1; and.b32 %r34, %r32, 1; setp.eq.b32 %p5, %r34, 1; xor.b32 %r35, %r33, 180; selp.b32 %r36, %r35, %r33, %p5; shr.u32 %r37, %r36, 1; and.b32 %r38, %r36, 1; setp.eq.b32 %p6, %r38, 1; xor.b32 %r39, %r37, 180; selp.b32 %r125, %r39, %r37, %p6; add.s32 %r119, %r119, -4; setp.ne.s32 %p7, %r119, -1; @%p7 bra $L__BB0_3; $L__BB0_4: setp.eq.s32 %p8, %r124, 0; @%p8 bra $L__BB0_7; $L__BB0_6: .pragma "nounroll"; and.b32 %r40, %r125, 1; setp.eq.b32 %p9, %r40, 1; shr.u32 %r41, %r125, 1; xor.b32 %r42, %r41, 180; selp.b32 %r125, %r42, %r41, %p9; add.s32 %r124, %r124, -1; setp.ne.s32 %p10, %r124, 0; @%p10 bra $L__BB0_6; $L__BB0_7: mul.wide.s32 %rd3, %r1, 4; add.s64 %rd4, %rd1, %rd3; ld.global.u32 %r43, [%rd4]; xor.b32 %r16, %r43, %r125; add.s32 %r44, %r1, 3; setp.lt.u32 %p11, %r44, 7; shl.b32 %r45, %r1, 2; mov.u32 %r46, _ZZ6kernelPjE5match; add.s32 %r17, %r46, %r45; @%p11 bra $L__BB0_14; bra.uni $L__BB0_8; $L__BB0_14: shr.s32 %r77, %r1, 31; shr.u32 %r78, %r77, 30; add.s32 %r79, %r1, %r78; and.b32 %r80, %r79, 536870908; sub.s32 %r81, %r1, %r80; shl.b32 %r82, %r81, 3; mov.u32 %r83, 1502581021; shr.u32 %r84, %r83, %r82; xor.b32 %r85, %r16, %r84; and.b32 %r86, %r85, 255; st.shared.u32 [%r17], %r86; bra.uni $L__BB0_15; $L__BB0_8: and.b32 %r18, %r1, -4; setp.eq.s32 %p12, %r18, 4; @%p12 bra $L__BB0_13; bra.uni $L__BB0_9; $L__BB0_13: shr.s32 %r67, %r1, 31; shr.u32 %r68, %r67, 30; add.s32 %r69, %r1, %r68; and.b32 %r70, %r69, 536870908; sub.s32 %r71, %r1, %r70; shl.b32 %r72, %r71, 3; mov.u32 %r73, -1065423868; shr.u32 %r74, %r73, %r72; xor.b32 %r75, %r16, %r74; and.b32 %r76, %r75, 255; st.shared.u32 [%r17], %r76; bra.uni $L__BB0_15; $L__BB0_9: setp.eq.s32 %p13, %r18, 8; @%p13 bra $L__BB0_12; bra.uni $L__BB0_10; $L__BB0_12: shr.s32 %r57, %r1, 31; shr.u32 %r58, %r57, 30; add.s32 %r59, %r1, %r58; and.b32 %r60, %r59, 536870908; sub.s32 %r61, %r1, %r60; shl.b32 %r62, %r61, 3; mov.u32 %r63, 1987891068; shr.u32 %r64, %r63, %r62; xor.b32 %r65, %r16, %r64; and.b32 %r66, %r65, 255; st.shared.u32 [%r17], %r66; bra.uni $L__BB0_15; $L__BB0_10: setp.ne.s32 %p14, %r18, 12; @%p14 bra $L__BB0_15; shr.s32 %r47, %r1, 31; shr.u32 %r48, %r47, 30; add.s32 %r49, %r1, %r48; and.b32 %r50, %r49, 536870908; sub.s32 %r51, %r1, %r50; shl.b32 %r52, %r51, 3; mov.u32 %r53, 1336265249; shr.u32 %r54, %r53, %r52; xor.b32 %r55, %r16, %r54; and.b32 %r56, %r55, 255; st.shared.u32 [%r17], %r56; $L__BB0_15: bar.sync 0; setp.ne.s32 %p15, %r1, 0; @%p15 bra $L__BB0_17; ld.shared.u32 %r87, [_ZZ6kernelPjE5match]; ld.shared.u32 %r88, [_ZZ6kernelPjE5match+4]; or.b32 %r89, %r87, %r88; ld.shared.u32 %r90, [_ZZ6kernelPjE5match+8]; or.b32 %r91, %r89, %r90; ld.shared.u32 %r92, [_ZZ6kernelPjE5match+12]; or.b32 %r93, %r91, %r92; ld.shared.u32 %r94, [_ZZ6kernelPjE5match+16]; or.b32 %r95, %r93, %r94; ld.shared.u32 %r96, [_ZZ6kernelPjE5match+20]; or.b32 %r97, %r95, %r96; ld.shared.u32 %r98, [_ZZ6kernelPjE5match+24]; or.b32 %r99, %r97, %r98; ld.shared.u32 %r100, [_ZZ6kernelPjE5match+28]; or.b32 %r101, %r99, %r100; ld.shared.u32 %r102, [_ZZ6kernelPjE5match+32]; or.b32 %r103, %r101, %r102; ld.shared.u32 %r104, [_ZZ6kernelPjE5match+36]; or.b32 %r105, %r103, %r104; ld.shared.u32 %r106, [_ZZ6kernelPjE5match+40]; or.b32 %r107, %r105, %r106; ld.shared.u32 %r108, [_ZZ6kernelPjE5match+44]; or.b32 %r109, %r107, %r108; ld.shared.u32 %r110, [_ZZ6kernelPjE5match+48]; or.b32 %r111, %r109, %r110; ld.shared.u32 %r112, [_ZZ6kernelPjE5match+52]; or.b32 %r113, %r111, %r112; ld.shared.u32 %r114, [_ZZ6kernelPjE5match+56]; or.b32 %r115, %r113, %r114; ld.shared.u32 %r116, [_ZZ6kernelPjE5match+60]; or.b32 %r117, %r115, %r116; setp.eq.s32 %p16, %r117, 0; selp.u32 %r118, 1, 0, %p16; st.global.u32 [%rd1], %r118; $L__BB0_17: ret; }
To put it shortly, since I forget where I put the code, you can use Z3 to solve for the input and hence the flag.
Conclusion
I definitely could've completed a few more tasks, but I was tired. For one of them, you had to exploit a torch.load()
vuln (pickle RCE) but I kept getting an internal server error when trying to download the files to exfiltrate the flag. Another involved a SQL injection in the semantic search feature to leak all user notes; but, the notes are encrypted client-side. So, I tried using vec2text
to invert the embedding and it led to very promising results—unfortunately, not good enough becuase the IP address was garbage (I don't have an OpenAI key to improve with num_steps
).