crackmes Medium

httpd

Overview

Field Value
Category Intermediate
Binary ELF 64-bit x86-64, FreeBSD 14.3, Go
Description “This file was found on an infected host. Can you figure out what it does?”
Flag CMO{fUn_w1th_m4g1c_p4ck3t5}

Recon

The binary is a Go program compiled for FreeBSD with debug info and symbols. Key dependencies:

  • net/http – serves an HTTP endpoint on :8080
  • github.com/google/gopacket/pcap – live packet capture
  • crypto/aes, crypto/cipher – AES-CBC decryption

Only two custom functions exist: main.main and main.handler.

Program Flow

main.main (0x747DC0)

  1. Prints a startup message
  2. Spawns a goroutine (net_http.init_0 at 0x748080) – the real payload
  3. Registers an HTTP handler on the default ServeMux
  4. Starts an HTTP server on :8080 as a cover

main.handler (0x747FA0)

Simple HTTP handler – responds to GET with “Nothing to see here :{” and rejects other methods. This is the innocuous-looking front.

Packet Sniffer Goroutine (0x748080)

This is the malicious payload:

  1. Opens pcap on interface re0 (FreeBSD Ethernet) with snaplen 1600
  2. Sets BPF filter to "icmp" – only captures ICMP traffic
  3. Creates a PacketSource and receives packets from a channel
  4. For each packet, calls packet.Data() to get raw Ethernet frame bytes

Packet Filtering

The goroutine checks for a very specific ICMP Echo Request (magic packet):

Offset Field Required Value
0x10 IP Total Length (big-endian) 32 (0x0020)
0x14 IP Flags/FragOff + TTL + Proto Variable (Protocol must be 1)
0x22 ICMP Type 8 (Echo Request)
0x24 ICMP Checksum Computed from message
0x26 ICMP Identifier (LE word) 0x1337
0x2A ICMP Data (LE dword) 0xE55FDEC6

The offsets are relative to the raw Ethernet frame (14-byte Ethernet header + 20-byte IP header + ICMP).

AES Key Derivation

When a matching magic packet is found, a 16-byte AES key is constructed from packet fields:

key[0]  = 0xE5 ^ checksum_lo_byte
key[1]  = 0x5F ^ checksum_hi_byte
key[2]  = IP_flags_hi_byte
key[3]  = IP_flags_lo_byte
key[4]  = TTL
key[5]  = 0x01 (protocol)
key[6]  = checksum_hi_byte
key[7]  = checksum_lo_byte
key[8]  = 0xC6 (magic byte 0)
key[9]  = 0xDE (magic byte 1)
key[10] = 0x5F (magic byte 2)
key[11] = 0xE5 (magic byte 3)
key[12] = 0x37 (identifier byte 0)
key[13] = 0x13 (identifier byte 1)
key[14] = checksum_lo_byte ^ 0xDE
key[15] = checksum_hi_byte ^ 0xC6

Where the checksum bytes are the raw bytes at packet offset 0x24 (big-endian ICMP checksum), and the XOR’d values are derived using ROL word, 8 (byte swap) on the XOR of magic cookie halves with the checksum.

AES-CBC Decryption

  • Algorithm: AES-128-CBC
  • Key: 16 bytes derived from packet fields
  • IV: Same as the key (IV = key)
  • Ciphertext (32 bytes, hardcoded at 0x7484D2):
    51 F1 A5 29 B4 DF 7E C0  2A 3B 2F 8F 24 3D 4E B3
    5A ED B0 CF 0B 9C DD 8C  CD E6 0E 9B 3E C4 64 0C
    
  • Result is converted to a string and printed to stdout

Solve

The key is mostly determined by the fixed magic values. The unknowns reduce to:

  • ICMP Sequence Number (determines checksum)
  • IP Flags/Fragment Offset (2 bytes)
  • IP TTL (1 byte)

Brute-force approach: for each combination of flags, TTL, and sequence number, compute the ICMP checksum, derive the AES key, decrypt, and check for the flag.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const crypto = require('crypto');

const ciphertext = Buffer.from([
    0x51, 0xF1, 0xA5, 0x29, 0xB4, 0xDF, 0x7E, 0xC0,
    0x2A, 0x3B, 0x2F, 0x8F, 0x24, 0x3D, 0x4E, 0xB3,
    0x5A, 0xED, 0xB0, 0xCF, 0x0B, 0x9C, 0xDD, 0x8C,
    0xCD, 0xE6, 0x0E, 0x9B, 0x3E, 0xC4, 0x64, 0x0C
]);

function computeIcmpChecksum(seq_be) {
    let sum = 0x0800 + 0x3713 + seq_be + 0xC6DE + 0x5FE5;
    while (sum > 0xFFFF) sum = (sum & 0xFFFF) + (sum >> 16);
    return (~sum) & 0xFFFF;
}

// flags=0x4000 (DF), TTL=64 (FreeBSD default), seq=1
const seq_be = 1;
const cksum_be = computeIcmpChecksum(seq_be); // 0x9A27
const B0 = (cksum_be >> 8) & 0xFF; // 0x9A
const B1 = cksum_be & 0xFF;         // 0x27

const key = Buffer.from([
    0xE5 ^ B1, 0x5F ^ B0, 0x40, 0x00, 0x40, 0x01, B0, B1,
    0xC6, 0xDE, 0x5F, 0xE5, 0x37, 0x13, B1 ^ 0xDE, B0 ^ 0xC6
]);
// key = c2 c5 40 00 40 01 9a 27 c6 de 5f e5 37 13 f9 5c

const decipher = crypto.createDecipheriv('aes-128-cbc', key, key);
decipher.setAutoPadding(false);
const pt = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
console.log(pt.slice(0, 27).toString()); // CMO{fUn_w1th_m4g1c_p4ck3t5}

The matching packet parameters:

  • IP Flags: 0x4000 (Don’t Fragment)
  • TTL: 64 (standard FreeBSD default)
  • ICMP Sequence: 1
  • ICMP Checksum: 0x9A27

Lessons Learned

  • Go binaries with debug info provide clear function names but the decompiler output is unreliable – always verify with disassembly.
  • ROL word, 8 followed by a little-endian memory store effectively swaps byte positions in the stored result – the low byte of the rotated value lands at the lower address.
  • The malware disguises itself as an HTTP server while secretly sniffing for magic ICMP packets – a classic covert channel technique.

Flag

CMO{fUn_w1th_m4g1c_p4ck3t5}