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
❯ file flag_errata.exeflag_errata.exe: PE32+ executable for MS Windows 5.02 (console), x86-64 (stripped to external PDB), 9 sectionsA 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:
sub_401560(); // intentionally failing WinAPI callLastError = GetLastError(); // harvest the error codesub_40159B();v5 = GetLastError() + LastError; // accumulatesub_4015D6();v6 = GetLastError() + v5;// ... N calls total ...if ( Buffer[i] != (int)(accumulated) % 126 ) return -247; // failure — 247CTF signatureThe 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 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:
- , the sum of one full cycle (all 13 error codes)
- , the partial sum of the first codes
For character with total API calls:
The flag format 247CTF{<flag-redacted>} gives us 8 known characters for free. Three of them — blocks 2, 3, 6 — share the same remainder : same position in the error-code cycle, different lap counts. Shared cancels on subtraction.
Blocks 2, 3, 6 correspond to 7 (ASCII , ), C (, ), { (, ):
Subtracting from eliminates :
Since divides , we may divide the entire congruence by (reducing the modulus to ):
Similarly, yields a second constraint:
To isolate , multiply both sides by the modular inverse , since :
Intersecting via CRT: , so both congruences agree.
Back-substituting into :
Same process with the remaining known characters fills all partial sums:
| Value | Source | |
|---|---|---|
| 10 | '2' (ASCII 50) | |
| 16 | ||
| 34 | ||
| 58 | '4' (ASCII 52) | |
| 90 | 'T' (ASCII 84) | |
| 45 | '7' (ASCII 55) | |
| 42 | 'F' (ASCII 70) | |
| 99 |
With and all determined, every character computes directly:
| Block | Calls | Calculation | Char | ||
|---|---|---|---|---|---|
| 0 | 16 | 1 | 3 | 2 | |
| 7 | 191 | 14 | 9 | e | |
| 9 | 109 | 8 | 5 | f | |
| 15 | 285 | 21 | 12 | 9 | |
| 20 | 298 | 22 | 12 | a | |
| 39 | 35 | 2 | 9 | } |
All 40 characters land in the valid charset — 247CTF{<flag-redacted>} suffix — confirming the solution without ever running the binary.
Character-by-Character Breakdown
| Block | Calls | q | r | Formula | Char |
|---|---|---|---|---|---|
| 0 | 16 | 1 | 3 | 2 | |
| 1 | 45 | 3 | 6 | 4 | |
| 2 | 217 | 16 | 9 | 7 | |
| 3 | 139 | 10 | 9 | C | |
| 4 | 46 | 3 | 7 | T | |
| 5 | 101 | 7 | 10 | F | |
| 6 | 321 | 24 | 9 | { | |
| 7 | 191 | 14 | 9 | e | |
| 8 | 280 | 21 | 7 | 0 | |
| 9 | 109 | 8 | 5 | f | |
| 10 | 19 | 1 | 6 | b | |
| 11 | 256 | 19 | 9 | 1 | |
| 12 | 109 | 8 | 5 | f | |
| 13 | 241 | 18 | 7 | 6 | |
| 14 | 215 | 16 | 7 | d | |
| 15 | 285 | 21 | 12 | 9 | |
| 16 | 17 | 1 | 4 | 8 | |
| 17 | 215 | 16 | 7 | d | |
| 18 | 217 | 16 | 9 | 7 | |
| 19 | 215 | 16 | 7 | d | |
| 20 | 298 | 22 | 12 | a | |
| 21 | 285 | 21 | 12 | 9 | |
| 22 | 280 | 21 | 7 | 0 | |
| 23 | 215 | 16 | 7 | d | |
| 24 | 217 | 16 | 9 | 7 | |
| 25 | 12 | 0 | 12 | c | |
| 26 | 38 | 2 | 12 | 5 | |
| 27 | 12 | 0 | 12 | c | |
| 28 | 191 | 14 | 9 | e | |
| 29 | 109 | 8 | 5 | f | |
| 30 | 16 | 1 | 3 | 2 | |
| 31 | 217 | 16 | 9 | 7 | |
| 32 | 12 | 0 | 12 | c | |
| 33 | 16 | 1 | 3 | 2 | |
| 34 | 109 | 8 | 5 | f | |
| 35 | 45 | 3 | 6 | 4 | |
| 36 | 217 | 16 | 9 | 7 | |
| 37 | 285 | 21 | 12 | 9 | |
| 38 | 17 | 1 | 4 | 8 | |
| 39 | 35 | 2 | 9 | } |
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.
247CTF{[flag redacted]}