← Back to Writeups
CUSTOMMediumReversing

Всё связано (Everything Is Connected)

XESXOR9/2/20267 min read
#reversing#custom#medium#keygen_reimplementation#loop_semantics_correction#unicorn_based_unpacking#upx_stub_emulation#windows

Всё связано (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: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • UPX0
  • UPX1
  • UPX2

However, normal unpacking fails. The file header area is intentionally damaged: at file offset 0x200 the bytes are:

text
34 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, and VirtualAlloc are 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:

  1. section names still look like UPX,
  2. the header is slightly corrupted to break automated unpackers, and
  3. the stub itself is still executable.

Main validation logic

After unpacking, the interesting functions are:

  • 0x401a5e — key format validator
  • 0x4017e1 — first username hash
  • 0x40191e — second username hash
  • 0x401550 — 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
0xB42DD243
0x304BD9D9
0x177934A3
0x17DAA0BA
0x77713B7C
0x29C33CDF

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

There is also one extra constant:

text
0x434F4445

which is ASCII "CODE".

Reimplementing the keygen

The two username hash functions are straightforward to port. The last key block is especially convenient:

text
part3 = 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 cnt down to 1
  • but: from cnt - 1 down to 0

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
cnt = (u % 57) - 11

Choosing u = 12 gives:

text
cnt = 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
#!/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 = 12 so cnt = 1,
  • derive the three displayed key blocks.

Step 3: Generate a valid key

Using the corrected model, a canonical working credential pair is:

text
Username: admin
Key:      57D37122-80FECFE6-D83E0849

Step 4: Send it to the service

Connecting to 62.173.140.174:14000 with that pair returns:

text
Good!
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
#!/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
admin
57D37122-80FECFE6-D83E0849

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