crackmes Medium

httpd

Overview

FieldValue
CategoryIntermediate
BinaryELF 64-bit x86-64, FreeBSD 14.3, Go
Description“This file was found on an infected host. Can you figure out what it does?”
FlagCMO{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):

OffsetFieldRequired Value
0x10IP Total Length (big-endian)32 (0x0020)
0x14IP Flags/FragOff + TTL + ProtoVariable (Protocol must be 1)
0x22ICMP Type8 (Echo Request)
0x24ICMP ChecksumComputed from message
0x26ICMP Identifier (LE word)0x1337
0x2AICMP 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}