← Back to Writeups
CUSTOMMediumReversing

Весёлый EXE (Funny EXE)

XESXOR9/2/20265 min read
#reversing#custom#medium#brute_force_per_byte#byte_transform_inversion#live_server_keystream_extracti#network_protocol_replay#post_main_crt_hijack#xref_following#windows

Весёлый EXE (Funny EXE)

Platform: hackerlab | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-06-02 | Status: Solved Techniques: brute_force_per_byte, byte_transform_inversion, live_server_keystream_extraction, network_protocol_replay, post_main_crt_hijack, xref_following

Summary

Task: a MinGW PE32+ that trolls with meme wallpapers/sounds; the real flag check runs from the CRT startup after main returns, talking a custom TCP protocol to a hardcoded C2. Solution: locate the hidden post-main function via xrefs, replay the PASS protocol to a live server to obtain the keystream + encrypted flag, then invert the self-inverting per-byte cipher by brute-forcing printable chars.

Весёлый EXE (Funny EXE) — hackerlab

Description

Смотри, что сделал мой друг. Правда некоторые картинки я не понял. Точно, он туда "вшил" что-то связанное с флагом. Не помню, что только

English translation: "Look what my friend made. True, I didn't understand some of the pictures. Right, he 'embedded' something flag-related in there. Just don't remember what."

We are given a single Windows executable good_exe.exe (PE32+ x86-64, MinGW-W64 GCC 8.1.0, 25600 bytes, MD5 c75e67da80f26dd0cb1006001cce5e7c). The goal is to recover a CODEBY{...} flag. The "pictures" mentioned in the description (desktop wallpapers the program sets, plus an embedded Unicode ASCII-art dog) are intentional decoys — they never participate in deriving the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Analysis

Initial recon

good_exe.exe is a console PE with no resources and no flag string stored anywhere in the binary. Strings of interest are obfuscated. Two distinct network code paths reference the same hardcoded endpoint:

  • IP 62.173.140.174 — ASCII string in .rdata (file offset 0x4424 / VA 0x406024), referenced by inet_pton in both code paths.
  • Port 14100 — mov ecx, 0x3714 then htons(0x3714) → TCP 14100 (constants at 0x401a7c and 0x402042).

main() is a troll branch

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

main (0x40216f) prints:

This EXE may change something on your PC. Are you sure you want to run it? (y/n)

If the user answers y, it loops 100 times calling sub_401A11 (the GET path): connect to the server, send "GET", receive two files, save them hidden (SetFileAttributesA HIDDEN), set one as the desktop wallpaper (SystemParametersInfoA SPI_SETDESKWALLPAPER) and play the other (PlaySoundA). This whole branch is pure misdirection — it spams meme wallpapers and sounds, matching the "pictures" in the task description. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The real logic runs AFTER main returns (anti-analysis)

The key insight: the flag check is not in main. After main returns, the MinGW CRT startup (around 0x4013db) checks a global and calls sub_401FD7, the PASS path. To find it in IDA:

  1. Click main → press X (xrefs) → follow the call coming from the CRT startup.
  2. Inside that "standard system function" there is a call to a function that shouldn't normally be there — sub_401FD7.

sub_401FD7 (PASS path):

  • WSAStartup, socket, connect to 62.173.140.174:14100.
  • The command string "PASS" is stored obfuscated at 0x404e20 as bytes 8f 80 92 92 and decoded at runtime by subtracting 0x3f from each byte (8f-3f=50='P', etc.).
  • Sends "PASS", waits for a reply beginning with "OK" (via sub_4015F0), then calls sub_401B84. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The check / cipher (sub_401B84)

recv(sock, buf, 100, 0);   // 100 bytes; only first 68 used
Str = buf[0x00 .. 0x22];   // encrypted flag (0x22 = 34 bytes)
key = buf[0x22 .. 0x44];   // keystream (0x22 = 34 bytes)
// remaining 32 bytes ignored
GetUserNameA(Buffer, &pcbBuffer);   // Windows username

It checks strlen(Str) == strlen(username), transforms the username in place, then memcmp against Str. On match it shows MessageBoxA("Success! This is your flag ! :)") — that success string is itself hardcoded and decoded with -0x3f; it is not the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Per-byte transform (v8 = key, v19 = previous output byte, chaining):

c
Buffer[i] ^= v8[i];
Buffer[i] += v8[i];
Buffer[i] -= v8[i];
Buffer[i] ^= v8[i];
Buffer[i] -= v8[i];
Buffer[i] ^= v8[i];
Buffer[i] ^= v19;      // chain with previous output
v19 = Buffer[i];

The flag is the username that, after this transform with the server-supplied key, equals the server-supplied Str. Equivalently, brute-force each printable char j (0x20..0x7f) through the same transform and compare to Str[i]; the matching j is the plaintext flag byte. The chaining variable propagates as last = Str[i] after each position. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Why offline / the official writeup don't suffice

The full 0x22-byte key and encrypted flag are not embedded in the EXE — they only arrive from the live server in the recv(...,100) call. The official platform writeup ships a C decryptor whose key[] array is truncated to 13 bytes and correct[] to 17 bytes (screenshots cut off at the right margin). Compiling it verbatim prints only CODEBY{n2tw0rK_h because for i>=13 it reads key[] out of bounds (UB). A reliable solve requires talking to the server. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

  1. TCP-connect to 62.173.140.174:14100.
  2. Send b"PASS".
  3. Receive b"OK".
  4. Receive the 68-byte payload:
    • s1 (encrypted flag, 0x22 bytes) = 3bb496af93d2a9258fe5a282f2b9e68ec26c2b7435597b244c6139bce383f6ba89f4
    • key (0x22 bytes) = beef1337abcdeb75ccaf9988619352ec1bc3c3c41105130a880e0ee15e231095a8a4 (note the beef1337 prefix)
  5. Invert the transform by brute-forcing the printable char at each position, carrying the chaining value last = s1[i]. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Working solver

python
import socket

HOST, PORT = '62.173.140.174', 14100

def step(j, k, last):
    t = j & 0xff
    t ^= k; t = (t + k) & 0xff; t = (t - k) & 0xff
    t ^= k; t = (t - k) & 0xff; t ^= k
    t ^= last
    return t

# 1) pull encrypted flag (s1) + key from the live server via PASS protocol
s = socket.socket(); s.settimeout(8); s.connect((HOST, PORT))
s.sendall(b'PASS')
assert s.recv(10)[:2] == b'OK'
data = b''
while len(data) < 100:
    chunk = s.recv(100 - len(data))
    if not chunk:
        break
    data += chunk
s.close()
s1, key = data[:0x22], data[0x22:0x44]
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# 2) invert the byte-wise transform: brute printable char at each position
last = 0
flag = ''
for i in range(len(s1)):
    for j in range(0x20, 0x7f):
        if step(j, key[i], last) == s1[i]:
            flag += chr(j)
            last = s1[i]
            break
print(flag)   # CODEBY{REDACTED}

Indicators of Compromise

  • C2-like host: 62.173.140.174:14100 — custom TCP, commands GET and PASS, expects reply starting with OK.
  • Server-supplied key prefix beef1337 in the 0x22-byte keystream.
  • Sets desktop wallpaper (SystemParametersInfoA SPI_SETDESKWALLPAPER) + plays sound (PlaySoundA), writes hidden files (SetFileAttributesA HIDDEN) — prank/dropper behavior.
  • Binary: good_exe.exe, MD5 c75e67da80f26dd0cb1006001cce5e7c, PE32+ x86-64, MinGW-W64 GCC 8.1.0, image base 0x400000. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍