crackmes Medium

Connected

Overview

Field Value
Category Intermediate
Author toasterbirb
Binary ELF 64-bit x86-64 PIE, C++, not stripped
Description “No matter where you go, everybody’s connected.”
Flag CMO{secret_code_v9hcdkd2}

Recon

The binary is a 142KB C++ program simulating a network of PCs, switches, and routers. Key imports: rand, srand, time, nanosleep, sscanf, getline, toupper, memcmp, strtol, std::to_chars, plus C++ STL (strings, maps/rb-trees, iostream, deques).

Custom functions include str_to_ip, ip_to_str, adler_32, fletcher_16, is_same_subnet, and 9 lambda callbacks for each PC.

Network Topology

Switch 1 (38.15.199.0/24)           Switch 2 (100.25.26.0/24)
  PC 38.15.199.42  [#1: printer]      PC 100.25.26.10  [#4: orchestrator]
  PC 38.15.199.41  [#2: flag builder]  PC 100.25.26.11  [#5: spammer]
  PC 38.15.199.40  [#3: OK responder]  PC 100.25.26.15  [#6: validator]
        |                                    |
   Router 1 (38.15.199.1)              Router 3 (100.25.26.1)
   Router 1 (42.20.102.1)              Router 3 (85.32.15.2)
        |                                    |
   Router 2 (42.20.102.2) -------- Router 2 (85.32.15.1)
   Router 2 (185.23.59.2)          Router 3 (64.16.15.1)
        |                                    |
   Router 4 (185.23.59.1)          Switch 3 (64.14.3.0/24)
   Router 4 (83.48.92.1)             PC 64.14.3.25  [#7: length counter]
        |                             PC 64.14.3.29  [#8: checksum calc]
   Switch 4 (83.48.92.0/24)
     PC 83.48.92.5  [no handler]
     PC 83.48.92.8  [#9: XOR service]

Program Flow

main (0x696e)

  1. Seeds RNG with time(0)
  2. Constructs the entire network: 4 switches, 3 routers, 10 PCs with routing tables
  3. Assigns a lambda callback to each PC (except 83.48.92.5 which has none)
  4. Prompts user for what (message) and where (destination IP)
  5. Sends a packet from PC 38.15.199.42 with: {flags=0, src=38.15.199.42, dst=user_ip, ttl=32, payload=user_msg}

Lambda Callbacks

# PC IP Address Behavior
#1 38.15.199.42 0x35c0 Prints received: <payload> – the output display
#2 38.15.199.41 0x632d Constructs flag from payload. Rejects packets from 38.15.199.42
#3 38.15.199.40 0x3b35 Responds with “OK”
#4 100.25.26.10 0x55a2 Orchestrator – multi-step validation pipeline
#5 100.25.26.11 0x5261 Sends 4096 spam packets then forwards
#6 100.25.26.15 0x4e8c Proxies to 83.48.92.8, validates byte properties
#7 64.14.3.25 0x4798 Proxies to 83.48.92.8, returns payload length as string
#8 64.14.3.29 0x3f24 Proxies to 83.48.92.8, returns checksum as string
#9 83.48.92.8 0x3b95 XOR service: XORs each byte with 0x42. Returns “…” if any byte is ‘B’

Orchestrator Pipeline (#4 at 0x55a2)

The orchestrator on PC 100.25.26.10 is the key. When it receives a packet from 38.15.199.42:

  1. Prefix check: Payload must start with msg_. Strips the prefix -> message M.
  2. Empty check: M must not be empty.
  3. Step 1: Forwards M to 83.48.92.8 -> #9 XORs with 0x42 -> stores result as xored
  4. Step 2: Forwards xored to 64.14.3.25 -> #7 forwards to #9 (double XOR = original M) -> #7 counts length -> returns length string -> stored as len_str
  5. Step 3: Forwards xored to 64.14.3.29 -> #8 forwards to #9 (double XOR = M) -> #8 computes checksums -> returns checksum string -> stored as cksum_str
  6. Step 4: Forwards xored to 100.25.26.15 -> #6 forwards to #9 (double XOR = M) -> #6 validates byte properties -> returns “1” or “0” -> stored as valid_str

Final check (verified from disassembly at 0x5E96):

1
2
3
cmp     r12d, 8           ; stoi(len_str) == 8
cmp     r13b, 31h         ; valid_str[0] == '1'
cmp     ebp, 6022E46h     ; stoi(cksum_str) == 100806214

If all three pass, M is forwarded to 38.15.199.41 (the flag builder). Otherwise, “I don’t want to talk to you” is sent back.

XOR Service (#9 at 0x3b95)

Iterates through each byte of the payload:

  • If byte == 0x42 (‘B’): stops and responds with “…” (three dots)
  • Otherwise: XORs byte with 0x42

Returns the XOR’d payload to the sender.

Byte Validator (#6 at 0x4e8c)

After receiving the double-XOR’d (= original) response from #9:

  • Check 1: All bytes must have bit 0 clear (all even)
  • Check 2: All bytes must be in range [33, 126] (printable)
  • Returns “1” if both pass, “0” otherwise

Checksum Calculator (#8 at 0x3f24)

After receiving the double-XOR’d (= original) response from #9, computes:

adler = adler_32(M, len)
fletch = fletcher_16(M, len)
custom = sum(M[i] << i for i = 0..len-1)
is_pal = (M is a palindrome)

if is_pal:
    result = custom ^ fletch ^ adler
else:
    result = custom

Returns result as a decimal string.

Flag Builder (#2 at 0x632d)

When receiving from any IP except 38.15.199.42:

alphabet = "abcdefghijklmnopqrstuvwxyz0123456789_"
flag = "CMO{secret_code_"
for i in range(len(M)):
    flag += alphabet[(i + M[i]) % 37]
flag += "}"

Sends the flag back to 38.15.199.42, where #1 prints it.

Constraints Summary

The message M (8 bytes) must satisfy:

  1. Length: exactly 8
  2. No ‘B’: M cannot contain byte 0x42 (would break XOR service)
  3. Even bytes: all bytes must have bit 0 clear
  4. Printable: all bytes in [34, 126]
  5. Palindrome: M must read the same forwards and backwards (otherwise the checksum formula doesn’t include adler/fletcher and can’t reach the target)
  6. Checksum: custom_hash ^ fletcher_16 ^ adler_32 = 0x6022E46

Solve

Since M is an 8-byte palindrome with 4 independent bytes, each from a set of 46 valid values (even, [34,126], not 66), the search space is 46^4 ~ 4.5M – trivially brute-forceable.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
const TARGET = 0x6022E46; // 100806214
const VALID = [];
for (let b = 34; b <= 126; b += 2) { if (b !== 66) VALID.push(b); }

function adler32(buf) { let s1=1,s2=0; for(let i=0;i<buf.length;i++){s1=(s1+buf[i])%65521;s2=(s2+s1)%65521;} return ((s2<<16)+s1)>>>0; }
function fletcher16(buf) { let s1=0,s2=0; for(let i=0;i<buf.length;i++){s1=(s1+buf[i])%255;s2=(s2+s1)%255;} return (s1|(s2<<8))&0xFFFF; }
function customHash(buf) { let h=0; for(let i=0;i<buf.length;i++){h+=(buf[i]<<i);} return h|0; }

for(const a of VALID) for(const b of VALID) for(const c of VALID) for(const d of VALID) {
    const M = Buffer.from([a, b, c, d, d, c, b, a]);
    const ch = customHash(M), fl = fletcher16(M), ad = adler32(M);
    if (((ch ^ fl ^ ad) >>> 0) === TARGET) {
        const alph = 'abcdefghijklmnopqrstuvwxyz0123456789_';
        let flag = 'CMO{secret_code_';
        for (let i = 0; i < 8; i++) flag += alph[(i + M[i]) % 37];
        console.log(flag + '}');
    }
}

Unique solution: M = [58, 34, 42, 36, 36, 42, 34, 58] = :"*$$*":

  • custom_hash = 12102
  • fletcher_16 = 85 (0x55)
  • adler_32 = 0x06020155
  • 12102 ^ 85 ^ 0x06020155 = 0x06022E46

To run the program: input msg_:"*$$*": as “what” and 100.25.26.10 as “where”.

Lessons Learned

  • Complex multi-stage network simulations can be reverse engineered by mapping each node’s callback behavior individually, then tracing the full message flow.
  • The palindrome requirement was critical: without it, the custom hash alone (max ~32K) could never reach the target value (~100M). The XOR with adler_32 (a 32-bit value) was needed to reach that range.
  • Callback offset pattern: each net_pc stores its function pointer at offset +0x268 from the structure start on the stack.

Flag

CMO{secret_code_v9hcdkd2}