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 \(n_i\) total API calls:
\[q_i = \left\lfloor \frac{n_i}{13} \right\rfloor, \qquad r_i = n_i \bmod 13\]
\[\text{password}[i] \equiv q_i \cdot S + P(r_i) \pmod{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 \equiv 19 \pmod{21} \qquad \Longrightarrow \qquad S \in \{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} \equiv 16 \pmod{63}\), since \(4 \times 16 = 64 \equiv 1\):
\[S \equiv 16 \times 34 = 544 \equiv 40 \pmod{63}\]
Intersecting via CRT: \(40 \bmod 21 = 19\;\checkmark\), 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 | \(e_3 = 6\) |
| \(P(5)\) | 34 | \(e_4 = 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 | \(e_{10}, e_{11}\) |
With \(S\) and all \(P(r)\) determined, every character computes directly:
| Block | Calls | \(q\) | \(r\) | Calculation | Char |
|---|---|---|---|---|---|
| 0 | 16 | 1 | 3 | \((40+10) \bmod 126\) | 2 |
| 7 | 191 | 14 | 9 | \((560+45) \bmod 126\) | e |
| 9 | 109 | 8 | 5 | \((320+34) \bmod 126\) | f |
| 15 | 285 | 21 | 12 | \((840+99) \bmod 126\) | 9 |
| 20 | 298 | 22 | 12 | \((880+99) \bmod 126\) | a |
| 39 | 35 | 2 | 9 | \((80+45) \bmod 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.