247CTF - Flag Errata
We stored the flag within this binary, but made a few errors when trying to print it. Can you make use of these errors to recover the flag?
Overview
1 | ❯ file flag_errata.exe |
A PE64 binary (GCC 7.3 / MinGW-w64) with a monstrous 53KB
main function. It prompts for a 40-character password,
validates each character through error-code accumulation, then returns
-247 on any mismatch.
The core pattern repeats 40 times:
1 | sub_401560(); // intentionally failing WinAPI call |
The password is never stored anywhere. It’s implicitly encoded by the count and sequence of WinAPI calls whose error codes reconstruct each character.
The Error Code Cycle
13 functions form a fixed cycle, each designed to fail deterministically:
| # | Function | Address | API Call | Expected Error | Code |
|---|---|---|---|---|---|
| 0 | errgen_open_247ctf_dll_system32 |
0x401560 |
CreateFileA("C:\Windows\System32\247CTF.dll", ...) |
ERROR_FILE_NOT_FOUND |
2 |
| 1 | errgen_open_247ctf_dll_nested |
0x40159B |
CreateFileA("C:\247CTF\247CTF\247CTF.dll", ...) |
ERROR_PATH_NOT_FOUND |
3 |
| 2 | errgen_open_user32_dll_readwrite |
0x4015D6 |
CreateFileA("user32.dll", GENERIC_RW, exclusive) |
ERROR_SHARING_VIOLATION |
32 |
| 3 | errgen_getthreadtimes_null |
0x401611 |
GetThreadTimes(0, 0, 0, 0, 0) |
ERROR_INVALID_HANDLE |
6 |
| 4 | errgen_findfiles_exhaust |
0x401634 |
FindFirstFile("C:\*") + loop
FindNextFile |
ERROR_NO_MORE_FILES |
18 |
| 5 | errgen_virtualqueryex_null |
0x401674 |
VirtualQueryEx(proc, NULL, NULL, 8) |
ERROR_BAD_LENGTH |
24 |
| 6 | errgen_open_cmdexe_twice |
0x401697 |
CreateFileA("cmd.exe") x2 (exclusive) |
ERROR_SHARING_VIOLATION |
32 |
| 7 | errgen_open_user32_invalid_create |
0x401704 |
CreateFileA("user32.dll", ..., dwCreation=0) |
ERROR_INVALID_PARAMETER |
87 |
| 8 | errgen_setconsoledisplaymode |
0x40173F |
SetConsoleDisplayMode(FULLSCREEN) |
Varies by OS | ~87 |
| 9 | errgen_open_247ctf_colon |
0x401764 |
CreateFileA("247CTF:", ...) |
ERROR_INVALID_NAME |
123 |
| 10 | errgen_loadlibrary_fake_dll |
0x4017A2 |
LoadLibraryA("C:\247CTF.DLL") |
ERROR_MOD_NOT_FOUND |
126 |
| 11 | errgen_createmutex_twice |
0x4017B0 |
CreateMutexA("247CTF") x2 |
ERROR_ALREADY_EXISTS |
183 |
| 12 | errgen_createprocess_user32 |
0x4017DF |
CreateProcessA("user32.dll" as exe) |
ERROR_BAD_EXE_FORMAT |
193 |
Each character block calls N consecutive functions from this
cycle, accumulates the error codes, then checks sum % 126
against the input.
Algebraic Solution
The key insight: since the error codes cycle deterministically, we can bypass the Windows environment entirely and solve this as pure modular arithmetic.
I use Arch BTW.
Let:
- $S = \sum_{k=0}^{12} e_k$, the sum of one full cycle (all 13 error codes)
- $P(r) = \sum_{k=0}^{r-1} e_k$, the partial sum of the first r codes
For character i with ni total API calls:
$$q_i = \left\lfloor \frac{n_i}{13} \right\rfloor, \qquad r_i = n_i \bmod 13$$
password[i] ≡ qi ⋅ S + P(ri) (mod 126)
The flag format 247CTF{...} gives us 8 known characters
for free. Three of them — blocks 2, 3, 6 — share the same remainder
r = 9: same position in the
error-code cycle, different lap counts. Shared P(9) cancels on subtraction.
Blocks 2, 3, 6 correspond to 7 (ASCII 55, q = 16), C (67, q = 10), { (123, q = 24):
$$\begin{aligned} 16S + P(9) &\equiv 55 \pmod{126} && \quad (1) \\ 10S + P(9) &\equiv 67 \pmod{126} && \quad (2) \\ 24S + P(9) &\equiv 123 \pmod{126} && \quad (3) \end{aligned}$$
Subtracting (2) from (1) eliminates P(9):
$$\begin{aligned} (16 - 10)\,S &\equiv 55 - 67 \pmod{126} \\ 6S &\equiv -12 \equiv 114 \pmod{126} \end{aligned}$$
Since gcd (6, 126) = 6 divides 114, we may divide the entire congruence by 6 (reducing the modulus to 126/6 = 21):
S ≡ 19 (mod 21) ⇒ S ∈ {19, 40, 61, 82, 103, 124}
Similarly, (3) − (1) yields a second constraint:
$$\begin{aligned} 8S &\equiv 68 \pmod{126} \\ 4S &\equiv 34 \pmod{63} && \quad (\text{dividing by } \gcd(2, 126) = 2) \end{aligned}$$
To isolate S, multiply both sides by the modular inverse 4−1 ≡ 16 (mod 63), since 4 × 16 = 64 ≡ 1:
S ≡ 16 × 34 = 544 ≡ 40 (mod 63)
Intersecting via CRT: 40 mod 21 = 19 ✓, so both congruences agree.
$$\boxed{\,S = 40\,}$$
Back-substituting into (1):
$$\begin{aligned} 16 \times 40 + P(9) &\equiv 55 \pmod{126} \\ 640 + P(9) &\equiv 55 \pmod{126} \\ 10 + P(9) &= 55 && \quad (640 \bmod 126 = 10) \\ P(9) &= 45 \end{aligned}$$
Same process with the remaining known characters fills all partial sums:
| P(r) | Value | Source |
|---|---|---|
| P(3) | 10 | '2' (ASCII 50) |
| P(4) | 16 | e3 = 6 |
| P(5) | 34 | e4 = 18 |
| P(6) | 58 | '4' (ASCII 52) |
| P(7) | 90 | 'T' (ASCII 84) |
| P(9) | 45 | '7' (ASCII 55) |
| P(10) | 42 | 'F' (ASCII 70) |
| P(12) | 99 | e10, e11 |
With S and all P(r) determined, every character computes directly:
| Block | Calls | q | r | Calculation | Char |
|---|---|---|---|---|---|
| 0 | 16 | 1 | 3 | (40 + 10) mod 126 | 2 |
| 7 | 191 | 14 | 9 | (560 + 45) mod 126 | e |
| 9 | 109 | 8 | 5 | (320 + 34) mod 126 | f |
| 15 | 285 | 21 | 12 | (840 + 99) mod 126 | 9 |
| 20 | 298 | 22 | 12 | (880 + 99) mod 126 | a |
| 39 | 35 | 2 | 9 | (80 + 45) mod 126 | } |
All 40 characters land in the valid charset — 247CTF{
prefix, [0-9a-f] hex body, } suffix —
confirming the solution without ever running the binary.
Character-by-Character Breakdown
| Block | Calls | q | r | Formula | Char |
|---|---|---|---|---|---|
| 0 | 16 | 1 | 3 | (1*40+10)%126=50 | 2 |
| 1 | 45 | 3 | 6 | (3*40+58)%126=52 | 4 |
| 2 | 217 | 16 | 9 | (16*40+45)%126=55 | 7 |
| 3 | 139 | 10 | 9 | (10*40+45)%126=67 | C |
| 4 | 46 | 3 | 7 | (3*40+90)%126=84 | T |
| 5 | 101 | 7 | 10 | (7*40+42)%126=70 | F |
| 6 | 321 | 24 | 9 | (24*40+45)%126=123 | { |
| 7 | 191 | 14 | 9 | (14*40+45)%126=101 | e |
| 8 | 280 | 21 | 7 | (21*40+90)%126=48 | 0 |
| 9 | 109 | 8 | 5 | (8*40+34)%126=102 | f |
| 10 | 19 | 1 | 6 | (1*40+58)%126=98 | b |
| 11 | 256 | 19 | 9 | (19*40+45)%126=49 | 1 |
| 12 | 109 | 8 | 5 | (8*40+34)%126=102 | f |
| 13 | 241 | 18 | 7 | (18*40+90)%126=54 | 6 |
| 14 | 215 | 16 | 7 | (16*40+90)%126=100 | d |
| 15 | 285 | 21 | 12 | (21*40+99)%126=57 | 9 |
| 16 | 17 | 1 | 4 | (1*40+16)%126=56 | 8 |
| 17 | 215 | 16 | 7 | (16*40+90)%126=100 | d |
| 18 | 217 | 16 | 9 | (16*40+45)%126=55 | 7 |
| 19 | 215 | 16 | 7 | (16*40+90)%126=100 | d |
| 20 | 298 | 22 | 12 | (22*40+99)%126=97 | a |
| 21 | 285 | 21 | 12 | (21*40+99)%126=57 | 9 |
| 22 | 280 | 21 | 7 | (21*40+90)%126=48 | 0 |
| 23 | 215 | 16 | 7 | (16*40+90)%126=100 | d |
| 24 | 217 | 16 | 9 | (16*40+45)%126=55 | 7 |
| 25 | 12 | 0 | 12 | (0*40+99)%126=99 | c |
| 26 | 38 | 2 | 12 | (2*40+99)%126=53 | 5 |
| 27 | 12 | 0 | 12 | (0*40+99)%126=99 | c |
| 28 | 191 | 14 | 9 | (14*40+45)%126=101 | e |
| 29 | 109 | 8 | 5 | (8*40+34)%126=102 | f |
| 30 | 16 | 1 | 3 | (1*40+10)%126=50 | 2 |
| 31 | 217 | 16 | 9 | (16*40+45)%126=55 | 7 |
| 32 | 12 | 0 | 12 | (0*40+99)%126=99 | c |
| 33 | 16 | 1 | 3 | (1*40+10)%126=50 | 2 |
| 34 | 109 | 8 | 5 | (8*40+34)%126=102 | f |
| 35 | 45 | 3 | 6 | (3*40+58)%126=52 | 4 |
| 36 | 217 | 16 | 9 | (16*40+45)%126=55 | 7 |
| 37 | 285 | 21 | 12 | (21*40+99)%126=57 | 9 |
| 38 | 17 | 1 | 4 | (1*40+16)%126=56 | 8 |
| 39 | 35 | 2 | 9 | (2*40+45)%126=125 | } |
Anti-Analysis
- TLS Callbacks: Two callbacks
(
0x401C50,0x401C20) execute beforemain— a common anti-debug vector. - 53KB of bloat: Up to 321 API calls per character, making manual static analysis impractical without scripting.
- Environment sensitivity: Functions like
CreateProcessAandSetConsoleDisplayModereturn different error codes under a debugger, making dynamic analysis a trap. The algebraic approach sidesteps this entirely by reasoning at the mathematical level, independent of the Windows runtime.