Hacktheon Sejong 2026 Quals Brain Outside Writeup
Hacktheon Sejong 2026 Quals Brain Outside WriteupHacktheon Sejong 2026 Quals Brain Outside WriteupHacktheon Sejong 2026 Quals Brain Outside 문제 풀이Hacktheon Sejong 2026 Quals Brain Outside writeup
Contents
Summary
client 바이너리는 실제 검증 코드를 품고 있지 않았다. 서버가 보내는 stage를 RWX mmap에 올려 실행하고 반환값만 다시 보내는 loader다. 그래서 stage를 전부 실행하지 않고도 protocol을 따라가며 PNG 조각을 모을 수 있었다.
Analysis
loader가 하는 일은 짧다.
read(sock, &len, 4);
buf = mmap(..., PROT_READ | PROT_WRITE | PROT_EXEC, ...);
read(sock, buf, len);
ret = ((uint64_t (*)())buf)();
send(sock, &ret, 8);stage마다 decrypt stub은 조금씩 달랐다. 8-byte XOR, cumulative add 후 XOR, NOT/pair swap 같은 변형을 패턴으로 나눠 body를 복호화했다.
처음에는 stage를 그대로 실행해 통과하려고 했다. 그런데 서버는 stage의 반환값만 받는다. 검증 코드를 실제로 돌리지 않아도 맞는 ret 값을 보내면 다음 stage를 계속 받을 수 있었다.
복호화된 stage 대부분은 flag.png의 특정 구간을 검증한다. stage마다 필요한 값은 세 개뿐이다.
file offset
length
expected bytes이 값을 빈 PNG에 계속 덮어썼다.
with open("flag_recovered.png", "r+b") as f:
f.seek(fileoff)
f.write(expected)stage를 끝까지 모으자 gap이 사라졌다.
known 9193720 of 9193720
gaps 0 []복원한 이미지는 다음과 같다.

Flag
hacktheon2026{90364e95eddf0fc1d5f54662d8e80913}
Summary
The client binary does not contain the actual verification code. It is a loader: it maps each stage sent by the server into RWX mmap memory, runs it, and sends back only the return value. So I could follow the protocol and collect the PNG fragments without executing every stage.
Analysis
The loader does very little.
read(sock, &len, 4);
buf = mmap(..., PROT_READ | PROT_WRITE | PROT_EXEC, ...);
read(sock, buf, len);
ret = ((uint64_t (*)())buf)();
send(sock, &ret, 8);The decrypt stub differed slightly from stage to stage. I grouped the variants into patterns, such as 8-byte XOR, cumulative add followed by XOR, and NOT/pair swap, and decrypted the body accordingly.
At first I tried to pass by running each stage as is. But the server only receives the stage's return value, so sending the correct ret value was enough to keep receiving the next stage, without running the verification code.
Most decrypted stages verify a specific range of flag.png. Each stage needs only three values.
file offset
length
expected bytesI kept writing these values into an empty PNG.
with open("flag_recovered.png", "r+b") as f:
f.seek(fileoff)
f.write(expected)Once all stages were collected, the gaps were gone.
known 9193720 of 9193720
gaps 0 []The recovered image:

Flag
hacktheon2026{90364e95eddf0fc1d5f54662d8e80913}