 
 ✉️ Challenge Overview
We got two files: a 64-bit ELF binary and a pcap capture.
First thing, I started with the pcap. It had 22 packets over loop-able 31337 port and not all of them carried data.
 
Using tcp.len > 0, they left me with 8 packets carrying actual payloads.
 
Confusing about the pcap, I moved to the binary. I ran strings on the binary and saw some fun stuff:
Type a username to send a message to, or 'EXIT' to exit:
EXIT
Exiting PlutoChat...
Send a message to %s (or 'EXIT' to select a new user):
Login successful! Welcome to PlutoChat!
%d %d
New message from %s: %s
There was an error initializing PlutoChat. Please try again.
Login to PlutoChat
Username:
Password:
Could not connect to PlutoChat servers. Try again later!
127.0.0.1Looks like a chat client.
🧠 Investigation and Discovery
I loaded the binary into Ghidra to trace how the encryption works. The function that eventually calls send() builds the message like this: it allocates a buffer, writes a random 4-byte header plus the plaintext length, prepares the cipher state, encrypts the message, and then sends [header][length][ciphertext].
Functions breakdown:
- 
FUN_00101687
 Generates a random 4-byte header using tworand()calls. This header acts as the seed for key expansion.
- 
FUN_00101510
 Expands the 4-byte header into an 80-byte key:- Repeatedly rotates the seed and collects 20 words.
- Rearranges them using the 20-byte permutation table (DAT_00104100).
- Runs the result through the 256-byte substitution table (DAT_00104120) with feedback XOR.
  
- 
FUN_00101389
 Implements RC4 Key Scheduling Algorithm (KSA). Initializes the S-box with 0–255, then scrambles it with the 80-byte key.
- 
FUN_00101285
 A helper that swaps two entries in the RC4 state array. Used by both KSA and PRGA.
- 
FUN_001012e5
 Implements RC4 PRGA. Updates indices, swaps values, and outputs one keystream byte at a time.
- 
FUN_00101452
 The encryption loop. Takes each plaintext byte, XORs it with the next keystream byte fromFUN_001012e5, and produces ciphertext.
- 
FUN_001014bf
 A keystream “warm-up” function. It discards a specified number of initial PRGA bytes (classic RC4 mitigation).
- 
FUN_001014f8
 Simple 32-bit rotate-left, used during key expansion.
- 
FUN_001016a7
 The “message sender.” It ties everything together:- Generates a random header.
- Sets up the RC4 state with FUN_00101510+FUN_00101389.
- Encrypts plaintext with FUN_00101452.
- Sends [header][length][ciphertext]viasend().
 
🔧 Decrypting the Messages
Since I already knew that there are 8 packets with messages, using my best friend ChatGPT, I generated a script to pull the payloads from the pcap:
...
def parse_pcap(path):
    with open(path,'rb') as f:
        data = f.read()
    off = 24
    while off + 16 <= len(data):
        ts_sec, ts_usec, incl_len, orig_len = struct.unpack('<IIII', data[off:off+16])
        off += 16
        pkt = data[off:off+incl_len]
        off += incl_len
        yield ts_sec + ts_usec/1e6, pkt
def parse_ipv4_tcp(pkt):
    if len(pkt) < 14 or pkt[12:14] != b'\x08\x00':
        return None
    ip = pkt[14:]
    ihl = (ip[0] & 0x0F)*4
    if len(ip) < ihl:
        return None
    if ip[9] != 6:  # not TCP
        return None
    tcp = ip[ihl:]
    if len(tcp) < 20:
        return None
    doff = ((tcp[12] >> 4) & 0x0F)*4
    if len(tcp) < doff:
        return None
    payload = tcp[doff:]
    src = '.'.join(map(str, ip[12:16]))
    dst = '.'.join(map(str, ip[16:20]))
    sport = int.from_bytes(tcp[0:2],'big')
    dport = int.from_bytes(tcp[2:4],'big')
    return (src, sport, dst, dport), payload
def reassemble_streams(pcap_path):
    streams = defaultdict(bytearray)
    for t,pkt in parse_pcap(pcap_path):
        parsed = parse_ipv4_tcp(pkt)
        if not parsed:
            continue
        key, payload = parsed
        if payload:
            streams[key] += payload
    return streams
def extract_frames_from_buffer(buf):
    frames = []
    off = 0
    while True:
        if off + 8 > len(buf):
            break
        header = buf[off:off+4]
        ln = int.from_bytes(buf[off+4:off+8], 'little')
        if ln <= 0:
            off += 1
            continue
        if off + 8 + ln > len(buf):
            break
        ct = buf[off+8:off+8+ln]
        frames.append((header, ct))
        off += 8 + ln
    return frames
...And the result is:
[0] c6237f77 7eba8d0f617bf90990dad469793c700fa16724ea028c3d4db57e0c80a077e0d5
[1] 7fc8fe7f f31cd383bb6fdd8f41a8924099f79eda967f039fda8253b668843bd32cd8e6b...
...Then I built a decryptor:
...
def rol32(x, r):
    r &= 31
    return ((x << r) & 0xffffffff) | (x >> (32 - r))
def key_from_header(header_le_bytes: bytes) -> bytes:
    seed = int.from_bytes(header_le_bytes, 'little') & 0xffffffff
    words, val = [], seed
    for _ in range(20):
        words.append(val)
        val = rol32(val, val & 0xF)
    w = words[:]
    for i in range(20):
        j = PERM20[i]
        w[i], w[j] = w[j], w[i]
    b = bytearray()
    for v in w: b += v.to_bytes(4,'little')
    out, prev = bytearray(80), 0
    for i in range(80):
        out[i] = SUB256[b[i]] ^ prev
        prev = out[i]
    return bytes(out)
def rc4_init(key: bytes):
    S = list(range(256))
    j = 0
    for i in range(256):
        j = (j + S[i] + key[i % len(key)]) & 0xFF
        S[i], S[j] = S[j], S[i]
    return S
def rc4_crypt(S_init, data: bytes) -> bytes:
    S, i, j, out = S_init[:], 0, 0, bytearray(len(data))
    for n in range(len(data)):
        i = (i + 1) & 0xFF
        j = (j + S[i]) & 0xFF
        S[i], S[j] = S[j], S[i]
        K = S[(S[i] + S[j]) & 0xFF]
        out[n] = data[n] ^ K
    return bytes(out)
def decode_message(pt: bytes) -> str:
    try:
        t = pt[0]
        nlen = pt[1]
        name = pt[2:2+nlen].decode('utf-8','replace')
        text = pt[2+nlen:].decode('utf-8','replace')
        return f"type=0x{t:02x} name={name} text={text}"
    except Exception:
        return pt.decode('utf-8','replace')
...The permutation and substitution are derived from the binary’s data sections as I mentioned earlier.
🎉 The Decrypted Messages
Once I ran it, the decrypted messages popped out.
 
sun{S3cur1ty_thr0ugh_Obscur1ty_1s_B4D}