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
2
❯ file flag_errata.exe
flag_errata.exe: PE32+ executable for MS Windows 5.02 (console), x86-64 (stripped to external PDB), 9 sections

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
2
3
4
5
6
7
8
9
sub_401560();                    // intentionally failing WinAPI call
LastError = GetLastError(); // harvest the error code
sub_40159B();
v5 = GetLastError() + LastError; // accumulate
sub_4015D6();
v6 = GetLastError() + v5;
// ... N calls total ...
if ( Buffer[i] != (int)(accumulated) % 126 )
return -247; // failure — 247CTF signature

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 before main — 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 CreateProcessA and SetConsoleDisplayMode return 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{e0fb1f6d98d7da90d7c5cef27c2f4798}