FLRSCRNSVR.SCR
The binary is a Windows screensaver that displays bouncing frog.
1. Analyze entry point
At function sub_140002BF0 lies the WinMain function prototype that contain the message handler.
sub_140001AE0((wchar_t *)L"Crackmes.one");
// ...
qword_1400089A8 = CreateFontW(..., L"Impact");
// ...
SetTimer(Window, 1u, 0x32u, 0); // 50ms timer
sub_140001AE0is called withL"Crackmes.one"as the argument to display the text- A timer fires every 50 ms for the ainmation loop
dword_1400089A4 = rand() % 51 + 10which is random number of bouncing frog objects (10–60)- The frog positions are stored as [x, y, dx, dy] quads in
unk_140008A80` ho= loaded bitmap (the bouncing logo),hdc= offscreen DC for double-buffering
At the function sub_140001AE0, it contains a global variable of byte_140008898. This byte is referred in the flag decryption logic.
2. Analyze Window Procedure
At function sub_140003500, it contain WndProc that handles all windows messages.
LRESULT __fastcall sub_140003500(HWND a1, UINT a2, WPARAM a3, LPARAM a4)
{
// WM_TIMER (0x113): bouncing animation + collision detection
// WM_PAINT (0x0F): flag decryption and display
// WM_COMMAND (0x111): menu/dialog handlers
// WM_DESTROY (0x2): cleanup + PostQuitMessage
}
At this function, it contains anti-debug and some junk code that serve no purpose
IsDebuggerPresent()check with a dummy counter loop- Repeated pointless calls:
GetDesktopWindow()+IsWindow()with discarded results RegOpenKeyExW(... "Control Panel\\Desktop" ...) + RegCloseKey()— opened and immediately closedFindFirstFileW(... "*.dll" ...) + FindClose()— opened and immediately discardedQueryPerformanceCounter()into locals never usedSetLastError(GetTickCount() % 100 + 5)is pure noise
WM_TIMER Handler logic:
case 0x113: // WM_TIMER
++dword_140008970; // tick counter
// Move all frog, bounce off edges
for each sprite [x,y,dx,dy]:
x += dx; y += dy
if x out of bounds: dx = -dx
if y out of bounds: dy = -dy
// After 600 ticks AND flag not yet set:
if (dword_140008970 > 600 && !byte_140008899):
// Check ALL frog pairs for collision via IntersectRect()
if collision: ++dword_140008984; swap velocities
if dword_140008984 >= 3000:
byte_140008899 = 1 // UNLOCK flag display path
WM_PAINT Handler, the flag logic:
case 0x0F: // WM_PAINT
if (byte_140008899) { // only after 3000 collisions
// RC4-decrypt SrcBuf (18 bytes) using FindFileData as S-box state
// ...
if (SrcBuf[0] == byte_140008898 + 69 &&
SrcBuf[1] == byte_140008898 + 75 &&
SrcBuf[2] == 83 - byte_140008898) {
// SUCCESS: display SrcBuf[3..17] as wide string
v44 = &DstBuf[3];
} else {
v44 = L"Crackmes.one"; // wrong key
}
} else {
v44 = L"Keep Calm and FLARE On"; // not yet unlocked
}
DrawTextW(hdc, v44, -1, &Rect, 0x25u);
Encrypted Flag Found:
xmmword_140008280 = 0B3018B4DF559D6DE60A341962628F9FBh
word_140008290 = 0x346D
In memory (little-endian), SrcBuf[0..17]:
FB F9 28 26 96 41 A3 60 DE D6 59 F5 4D 8B 01 B3 6D 34
The decryption uses FindFileData as the RC4 S-Box, initialized by sub_1400010D0. The magic check requires byte_140008898 is written. It is set only inside sub_140001AE0, the function called from WinMain with L"Crackmes.one".
3. Analyze function sub_140001AE0
Traced where byte_140008898 is written. It is set only inside sub_140001AE0, the function called from WinMain with L"Crackmes.one".
LSTATUS sub_140001AE0(wchar_t *Source)
{
// Check registry: HKCU\Software\FLRSCRNSVR
if (RegOpenKeyExW(..., L"Software\\FLRSCRNSVR", ...) FAILS) {
wcscpy_s(Source, 0x100u, L"Crackmes.one"); // no key = default
return;
}
// Key exists: read "Text" value into Source
RegQueryValueExW(hKey, L"Text", ..., Source, ...);
// Must be exactly 25 wide characters
if (wcsnlen(Source, 0x100u) == 25) {
sub_140001300(Destination); // transform Source in-place
sub_140001890(v35); // generate expected value
// Compare transformed input vs expected
if (Destination == v35) {
byte_140008898 = 1; // PASSWORD CORRECT
}
}
}
- User create registry key
HKCU\Software\FLRSCRNSVRwith a string valueText - The value must be exactly 25 wide characters
- It is passed through a transform pipeline (
sub_140001300) and compared to a stored expected value (sub_140001890) - If they match:
byte_140008898 = 1
With byte_140008898 = 1, the magic check becomes:
dec[0] == 1 + 69 == 70 == 'F'
dec[1] == 1 + 75 == 76 == 'L'
dec[2] == 83 - 1 == 82 == 'R'
The magic header is “FLR” as in FLARE. The RC4 key is the 25 characters password itself.
4. RC4 KSA
Decompiled the S-box initializer to confirm the cipher type and key derivation.
BOOL sub_1400010D0(unsigned __int8 *a1, __int64 a2, int a3)
{
// Standard RC4 Key Scheduling Algorithm (KSA)
// a1 = S-box (FindFileData struct, 256+ bytes)
// a2 = key pointer (Dst = ASCII "Crackmes.one" or the actual password)
// a3 = key length
// Step 1: Initialize S[0..255] = [0..255]
for (i = 0; i < 256; i++) *v7++ = i;
// Step 2: KSA
do {
v9 = (S[i] + key[i % a3] + v9) % 256;
swap(S[i], S[v9]);
} while (++v6 < 256);
a1[256] = 0; // i = 0
a1[257] = 0; // j = 0
}
- Pure standard RC4 KSA. All the
RegOpenKeyExW,FindFirstFileW,GetStockObjectcalls inside are junk. sub_1400010D0()always return0- Key length =
wcstombs_s output (26 bytes for 25 chars + null) - 0 - 1 = 25The RC4 key is the 25-character password as 25 ASCII bytes
5. Analyze transform pipeline function
At function sub_140001300, reversed the password transformation to understand how to invert it.
Stage 1: Substitution cipher
wcscpy_s(Destination, L"abcdefghijklmnopqrstuvwxyz"
L"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
L"0123456789}_{=-"); // source alphabet
wcscpy_s(v43, L"-={_}9876543210ZYXWVUTSRQPONMLKJIHGF"
L"EDCBAzyxwvutsrqponmlkjihgfedcba"); // dest alphabet
for each char in input:
idx = find(char, source_alphabet)
if found: input[i] = dest_alphabet[idx]
Stage 2: XOR with “FLAREALF” Key
// v32 as WORDs: 0x0046, 0x004C, 0x0041, 0x0052, 0x0045,
// 0x0052, 0x0041, 0x004C, 0x0046 = "FLARERALF"
for (j = 0; j < 25; j++):
input[j] ^= (FLARERALF[j % 9] + j) & 0xFF
The XOR key FLARERALF is a 9-character palindrome of “FLARE” embedded as integer constants 4980806, 5374017, 5374021, 4980801, 70.
Stage 3: String reversal
for i in range(25 // 2):
swap(input[i], input[24 - i])
6. Expected value at sub_140001890 + aQj
Analyzed sub_140001890 to find the comparison target, then located the hardcoded fallback string aQj in IDA’s .rdata section.
aQj Raw Bytes (from .rdata:00000001400064D0)
UTF-16LE: '<', 'Q', 'j', 0x09, 0x02, 0x07, '%', 0x03, '0',
0x08, 0x04, ')', 'h', '$', 0x01, '$', 0x18,
'k', 'w', 0x0F, 'p', '6', 0x02, 0x0E, 0x0B
This is the expected output of sub_140001300(correct_password). Inverting the transform on aQj gives us the password directly.
Here is the solve script:
#!/usr/bin/env python3
"""
Crackme Solve Script
Recovers password and decrypts flag from hardcoded constants.
"""
# ── Constants from binary ────────────────────────────────────────────────────
# Encrypted flag: xmmword_140008280 (16 bytes LE) + word_140008290 (2 bytes LE)
SRCBUF = bytes([
0xFB, 0xF9, 0x28, 0x26, 0x96, 0x41, 0xA3, 0x60,
0xDE, 0xD6, 0x59, 0xF5, 0x4D, 0x8B, 0x01, 0xB3,
0x6D, 0x34
])
# aQj from .rdata:00000001400064D0 (25 UTF-16LE wchars, null-terminated)
AQJ = [
0x3C, 0x51, 0x6A, 0x09, 0x02, 0x07, 0x25, 0x03,
0x30, 0x08, 0x04, 0x29, 0x68, 0x24, 0x01, 0x24,
0x18, 0x6B, 0x77, 0x0F, 0x70, 0x36, 0x02, 0x0E, 0x0B
]
# Substitution alphabets from sub_140001300
SRC_ALPHA = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789}_{=-"
DST_ALPHA = "-={_}9876543210ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba"
# XOR key: v32 integers decoded as WORDs = "FLARERALF" (9 chars)
XOR_KEY = [0x46, 0x4C, 0x41, 0x52, 0x45, 0x52, 0x41, 0x4C, 0x46]
# byte_140008898 = 1 (set when password is correct)
MAGIC_BASE = 1
# ── Step 1: Invert sub_140001300 on aQj to recover password ─────────────────
def invert_transform(arr):
"""
Inverse of sub_140001300:
forward: substitute -> XOR with FLARERALF+pos -> reverse
inverse: un-reverse -> un-XOR -> un-substitute
"""
arr = list(arr)
n = len(arr)
# Un-reverse (Stage 3 inverse)
for i in range(n // 2):
arr[i], arr[n - 1 - i] = arr[n - 1 - i], arr[i]
# Un-XOR (Stage 2 inverse — XOR is self-inverse)
for j in range(n):
xor_val = (XOR_KEY[j % 9] + j) & 0xFF
arr[j] = arr[j] ^ xor_val
# Un-substitute (Stage 1 inverse)
for i in range(n):
ch = chr(arr[i])
idx = DST_ALPHA.find(ch)
if idx != -1:
arr[i] = ord(SRC_ALPHA[idx])
return arr
password_ints = invert_transform(AQJ)
password_str = ''.join(chr(c) for c in password_ints)
password_bytes = bytes(c & 0xFF for c in password_ints)
print(f"[+] Recovered password ({len(password_str)} chars): {password_str!r}")
# ── Step 2: RC4 decrypt SrcBuf using password as key ────────────────────────
def rc4_decrypt(key: bytes, data: bytes) -> bytes:
# KSA
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
# PRGA
i = j = 0
out = []
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
out.append(byte ^ S[(S[i] + S[j]) % 256])
return bytes(out)
decrypted = rc4_decrypt(password_bytes, SRCBUF)
print(f"[+] Decrypted bytes: {decrypted.hex()}")
# ── Step 3: Verify magic header and extract flag ─────────────────────────────
d0, d1, d2 = decrypted[0], decrypted[1], decrypted[2]
magic_ok = (
d0 == MAGIC_BASE + 69 and # 'F'
d1 == MAGIC_BASE + 75 and # 'L'
d2 == 83 - MAGIC_BASE # 'R'
)
print(f"[+] Magic header: '{chr(d0)}{chr(d1)}{chr(d2)}' — {'PASS' if magic_ok else 'FAIL'}")
flag = decrypted[3:].decode('ascii', errors='replace')
print(f"\n{'='*50}")
print(f" FLAG: {flag}")
print(f"{'='*50}")
FLAG: CMO{frogt4s7ic_r3vers1ng}