Всё связано (Everything Is Connected)
Всё связано (Everything Is Connected)
Platform: hackerlab | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Windows | Author: XESXOR | Date: 2026-04-06 | Status: Solved Techniques: keygen_reimplementation, loop_semantics_correction, unicorn_based_unpacking, upx_stub_emulation
Summary
Task: a tiny PE64 console binary packed with a deliberately broken UPX header asks for a username and a hex license key. Solution: emulate the UPX stub with Unicorn, recover the unpacked OEP, reimplement the mixer correctly, and generate a valid server-side key such as the one for admin.
Всё связано (Everything Is Connected) — Hackerlab
Description
Всё связано
English summary: we are given links.exe inside task.zip, a small Windows x64 console binary that asks for a username and a license key. The goal is to understand the packed checker, generate a valid key for some username, and use the real service to obtain the final CODEBY{...} flag.
Analysis
Initial recon
links.exe is a PE32+ Windows x86-64 console program of about 9.7 KB. The section table immediately suggests UPX-style packing:
UPX0UPX1UPX2
However, normal unpacking fails. The file header area is intentionally damaged: at file offset 0x200 the bytes are:
text read-only • copy allowed34 2e 30 32 00 55 50 58 31
That is 4.02\0UPX1, not the metadata layout that standard upx -d expects.
Strings are still useful and reveal the basic program shape:
- banner:
Everything is connected - input prompts for username and key
- fake local placeholder:
C0DEBY{Your_flag}!
This matched a known pattern from other Hackerlab reverse tasks: a small PE packed with a deliberately broken UPX header. Instead of repairing the header by hand, the cleaner approach was to run the original UPX stub under emulation and dump the unpacked image after it finished.
Unpacking with Unicorn
The workspace already contains unpack_links.py, which emulates the packed entry stub using Unicorn.
Important facts:
- packed stub entry is around
0x40dde0 - the script maps the PE image into memory
- imported functions such as
LoadLibraryA,GetProcAddress,VirtualProtect, andVirtualAllocare stubbed with fixed emulated handlers - execution stops when control reaches the unpacked original entry point at about
0x4014e0
After emulation, the script dumps the unpacked code region to links_unpacked.bin.
This is a good trick whenever:
- section names still look like UPX,
- the header is slightly corrupted to break automated unpackers, and
- the stub itself is still executable.
Main validation logic
After unpacking, the interesting functions are:
0x401a5e— key format validator0x4017e1— first username hash0x40191e— second username hash0x401550— main mixer/validator
The key validator at 0x401a5e enforces:
- exact length
26 - format
XXXXXXXX-XXXXXXXX-XXXXXXXX - only hexadecimal characters in each block
So the program is not checking an arbitrary string; it expects three 32-bit values written in uppercase-style hex blocks.
The main path also requires strlen(username) > 4. If validation succeeds locally, the program prints Good!. On the real service the same logic is used, but a valid pair also returns the real flag.
Constants used by the mixer
The main function builds a constant table on the stack and passes it into the validator:
text read-only • copy allowed0xB42DD243 0x304BD9D9 0x177934A3 0x17DAA0BA 0x77713B7C 0x29C33CDF
There is also one extra constant:
text read-only • copy allowed0x434F4445
which is ASCII "CODE".
Reimplementing the keygen
The two username hash functions are straightforward to port. The last key block is especially convenient:
text read-only • copy allowedpart3 = u ^ hash2(name)
The harder part is the mixer at 0x401550, which transforms two internal 32-bit states through a small loop. My first reimplementation was close, but not exact: generated keys looked plausible and passed the local model, yet the server rejected them.
The critical reverse-engineering detail was the loop direction.
The effective counter semantics are:
- not: from
cntdown to1 - but: from
cnt - 1down to0
That off-by-one interpretation changes the state transition enough to produce different keys. Once the loop was corrected, generated keys matched the real service.
Another useful simplification comes from the control value:
text read-only • copy allowedcnt = (u % 57) - 11
Choosing u = 12 gives:
text read-only • copy allowedcnt = 1
So the mixer runs only one effective round. That makes inversion trivial for any username longer than four bytes. In practice, many usernames work as long as the matching key is generated from the corrected model.
Solution
Step 1: Unpack the binary
Run the Unicorn-based unpacker from the workspace:
python read-only • copy allowed#!/usr/bin/env python3 # unpack_links.py (already present in the workspace) import pefile from unicorn import Uc, UC_ARCH_X86, UC_MODE_64 # The real script maps the PE, emulates the UPX stub, # hooks imported APIs, stops at 0x4014e0, and dumps # the unpacked region to links_unpacked.bin.
This avoids manual PE header patching and directly recovers the post-stub memory image.
Step 2: Recover the validation algorithm
From the unpacked image:
- port the two name hash functions,
- port the mixer,
- correct the loop semantics,
- select
u = 12socnt = 1, - derive the three displayed key blocks.
Step 3: Generate a valid key
Using the corrected model, a canonical working credential pair is:
text read-only • copy allowedUsername: admin Key: 57D37122-80FECFE6-D83E0849
Step 4: Send it to the service
Connecting to 62.173.140.174:14000 with that pair returns:
text read-only • copy allowedGood! CODEBY{REDACTED}
Compact solver example
The following compact script captures the final keygen logic for the known-good admin example. It mirrors the corrected mixer behavior and prints the valid key:
python read-only • copy allowed#!/usr/bin/env python3 MASK = 0xFFFFFFFF KEY = [0xB42DD243, 0x304BD9D9, 0x177934A3, 0x17DAA0BA, 0x77713B7C, 0x29C33CDF] def u32(x): return x & MASK def h_name_a(s: bytes) -> int: acc = 0 prev = 0 for b in s: t = u32(b * 0x3488) t = u32((prev << 5) * (t * 8) + (t << 8) + (t << 3) + (prev << 20)) t = u32((t << 5) + (t << 4) + (prev << 3) + (prev << 5) + (t << 20)) prev = t acc = u32(acc + t) return acc def h_name_b(s: bytes) -> int: acc = 0 for b in s: t = u32(b * 0x123312) t = u32((t << 5) * (t << 4) + (t << 7) + (t << 12) + (t << 24)) t = u32((t << 10) + (t << 20) + (t << 3) + (t << 3) + (t << 10)) acc = u32(acc + t) return acc def reverse_state(h1: int, h2: int, x: int): cnt = u32((x % 57) - 11) if cnt == 0: raise ValueError("bad cnt") a1 = u32(h2 ^ 7 ^ ((((cnt + 0x539) >> 3) - 9) & MASK) ^ 0x5277) a0 = u32(h1 ^ u32(cnt - 0x5F) ^ 0x1E ^ 4) z = u32((x << (x & 31)) - (cnt * x)) c = 0 while c != cnt: c = u32(c + 1) t4 = u32(c // 9 + 3) sh1 = u32((t4 + 1) ^ 8) t2 = u32(a1 << sh1) sh2 = u32(((c >> 1) - t4 + 0x3A) ^ 0x64) t3 = u32(a1 >> (sh2 & 31)) if c == u32((c >> 1) + (t4 ^ 0x51)): mix2 = u32((t2 ^ t3) + (a1 ^ c)) else: mix2 = u32((t2 ^ t3) + a1) a0_prev = u32(a0 + ((((z + KEY[z & 3]) ^ mix2) - 0x123) ^ 0x17BEE2)) z_old = u32(z + x) t1 = c // 10 m1 = u32(a0_prev << (c // 6)) m2 = u32(a0_prev >> ((t1 + 6) & 31)) idx = u32(z_old >> ((c // 9 + 3) & 31)) & 3 mix = u32((((m1 ^ m2) + a0_prev) ^ u32(KEY[idx] + z_old)) - c) mix = u32(mix ^ 0xFFFFCDF3) if c == cnt: a1 = u32(a1 ^ c) a1_prev = u32(a1 + mix + c) a0, a1, z = a0_prev, a1_prev, z_old return a0, a1 def make_key(name: str) -> str: if len(name) <= 4: raise ValueError("username must be longer than 4") x = 12 # gives cnt = 1 h1 = h_name_a(name.encode()) h2 = h_name_b(name.encode()) p1, p2 = reverse_state(h1, h2, x) p3 = u32(x ^ h2) return f"{p1:08X}-{p2:08X}-{p3:08X}" if __name__ == "__main__": name = "admin" print(name) print(make_key(name))
Expected output:
text read-only • copy allowedadmin 57D37122-80FECFE6-D83E0849