Introduction

During the WIZ Cloud Security Championship, I encountered a fascinating malware analysis challenge called “Malware Busters.” The scenario presented us with terminal access to a compromised environment containing a suspicious binary named “buu.” This writeup details the complete analysis workflow—from safe extraction and manual unpacking through reverse engineering, configuration decryption, and ultimately intercepting C2 communications to capture the flag.

This challenge showcased several realistic malware techniques including modified UPX packing, configuration obfuscation, and encrypted command-and-control (C2) communications. Let’s dive into the technical details.

Before touching any malware sample, proper isolation is critical. For this analysis, I set up a dedicated malware analysis lab consisting of:

  • REMnux VM: Primary analysis workstation for static and dynamic analysis
  • INetSim VM: Network simulation for capturing malicious traffic
  • Isolated network: Both VMs connected via internal network with no external connectivity
  • Docker containers: For executing the malware in controlled environments with specific GLIBC versions

This multi-layered approach ensures the malware cannot escape or cause damage while allowing us to observe its behavior comprehensively.

Stage 1: Safe Binary Extraction

The first challenge was extracting the binary from the compromised environment for offline analysis. The target file “buu” was a 2MB ELF 64-bit executable:

$ file buu
buu: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header

$ sha256sum buu
128aa33a7d605a05f7fb2671dc12a7f4a44d46b6ec95eed8f55f7c464e673288  buu

The environment had several restrictions that made extraction challenging:

  • HISTSIZE limited to 10,000 lines
  • No network connectivity (no ip or netstat commands)
  • No direct file transfer capabilities

The Extraction Method

I devised a safe extraction strategy using base64 encoding to neutralize the malware during transfer:

# Encode the binary to base64
$ base64 buu > buu.enc64

# Split into manageable chunks of 5000 lines each
$ split -l 5000 buu.enc64 buu_part_

$ ls -la buu_part_*
-rw-r--r-- 1 user user  385000 Dec 12 23:17 buu_part_aa
-rw-r--r-- 1 user user  385000 Dec 12 23:17 buu_part_ab
-rw-r--r-- 1 user user  385000 Dec 12 23:17 buu_part_ac
-rw-r--r-- 1 user user  385000 Dec 12 23:17 buu_part_ad
-rw-r--r-- 1 user user  385000 Dec 12 23:17 buu_part_ae
-rw-r--r-- 1 user user  385000 Dec 12 23:17 buu_part_af
-rw-r--r-- 1 user user  385000 Dec 12 23:17 buu_part_ag
-rw-r--r-- 1 user user   28462 Dec 12 23:17 buu_part_ah

Each file could be displayed with cat and manually copied from the terminal. After transferring all parts to my analysis machine, I reconstructed and decoded the binary:

# Reassemble the parts
$ cat buu_part_* > buu.enc64

# Decode from base64
$ base64 -d buu.enc64 > buu

# Verify integrity
$ sha256sum buu
128aa33a7d605a05f7fb2671dc12a7f4a44d46b6ec95eed8f55f7c464e673288  buu

Perfect match! The binary transferred successfully. A critical step here was clearing my clipboard afterward to prevent any accidental leaks of the malware sample.

Stage 2: Initial Static Analysis

With the sample safely in my analysis environment, I began basic static analysis using the strings utility:

$ strings buu -n 7 | tail -20
...
PROT_EXEC|PROT_WRITE failed.
$Info: g
 executable packer http://upx.sf.net $
$Id: UPX 3.96 Copyright (C) 1996-2020 the UPX Team. All Rights Reserved. $
j"AZR^j
/proc/self/exe

UPX Packer Detection

The strings output immediately revealed the presence of UPX (Ultimate Packer for eXecutables) version 3.96. UPX is a legitimate executable packer commonly abused by malware authors to:

  • Reduce file size
  • Obfuscate code and strings
  • Hinder static analysis
  • Evade signature-based detection

The natural next step would be to unpack it using the UPX utility:

$ upx -d buu
                       Ultimate Packer for eXecutables
                          Copyright (C) 1996 - 2020
UPX 3.96        Markus Oberhumer, Laszlo Molnar & John Reiser   Jan 23rd 2020

        File size         Ratio      Format      Name
   --------------------   ------   -----------   -----------
upx: buu: NotPackedException: not packed by UPX

Unpacked 0 files.

Interesting! The UPX tool refused to unpack the binary. This suggests the malware author modified the UPX header to prevent automated unpacking—a common anti-analysis technique.

Modified UPX Confirmation

To confirm this theory, I used Detect it Easy (DiE), a powerful signature-based file analyzer:

ELF64
    Operation system: Unix(0)[AMD64, 64-bit, EXEC]
    Packer: UPX(3.96)[NRV2E_LE32,brute,Modified(54525457)]

DiE confirmed our suspicions: the binary is UPX-packed with modifications to prevent standard unpacking. We’ll need to manually unpack it.

Stage 3: Manual Unpacking with GDB

Manual unpacking requires understanding how UPX operates. The packer decompresses the original executable in memory at runtime, then jumps to the Original Entry Point (OEP). Our strategy is to:

  1. Let UPX decompress the executable in memory
  2. Capture the unpacked code after decompression but before execution
  3. Dump the memory to a new file

Resolving GLIBC Dependency Issues

First attempt to run the binary under strace revealed GLIBC version issues:

$ strace ./buu
./buu: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.32' not found (required by ./buu)
./buu: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.34' not found (required by ./buu)

Solution: Use a Docker container with Ubuntu 22.04 which includes the required GLIBC versions:

$ docker run -it --rm -v $(pwd):/workspace -w /workspace ubuntu:22.04 bash
/workspace $ apt install gdb gdbserver libc6 strace

Tracing the Unpacking Process

Now we can trace the system calls to understand the unpacking routine:

/workspace $ strace ./buu
execve("./buu", ["./buu"], 0x7ffdbe568000 /* 8 vars */) = 0
open("/proc/self/exe", O_RDONLY)        = 3
mmap(NULL, 2016342, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f09a43e8000
...
munmap(0x7f09a43e8000, 2016342)         = 0
...
openat(AT_FDCWD, "/tmp/.X11/cnf", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
write(1, "I don't belong here...\n", 23) = 23
exit_group(0)                           = ?

Key observations:

  1. The binary reads itself via /proc/self/exe
  2. It allocates memory and performs unpacking operations
  3. After unpacking (visible via the munmap call), it jumps to the OEP
  4. It then tries to open /tmp/.X11/cnf and exits if not found

Dumping the Unpacked Binary

Using GDB, we’ll set a breakpoint at the first munmap syscall, which signals the end of unpacking:

(gdb) catch syscall munmap
Catchpoint 1 (syscall 'munmap' [11])

(gdb) run
Starting program: /workspace/buu 
Catchpoint 1 (call to syscall munmap), 0x000000000063b6e2 in ?? ()

(gdb) ni
0x00007f448e11a290 in ?? ()

(gdb) info proc mappings
process 3460
Mapped address spaces:
          Start Addr           End Addr       Size     Offset  Perms  objfile
            0x400000           0x63c000   0x23c000        0x0  r-xp   
            0x63c000           0x850000   0x214000        0x0  r--p   
            0x850000           0x8c6000    0x76000        0x0  rw-p   
      0x7f448e0f9000     0x7f448e0fa000     0x1000        0x0  r--p   /workspace/buu
      ...

The memory mappings show the unpacked code from 0x400000 to 0x8c6000. Let’s dump it:

(gdb) dump memory upx_full.raw 0x400000 0x8c6000

Verify the unpacked binary:

/workspace # file upx_full.raw 
upx_full.raw: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, stripped

/workspace # chmod +x upx_full.raw 

/workspace # ./upx_full.raw 
I don't belong here...

Success! We now have a functional unpacked binary ready for deeper analysis.

Alternative Unpacking Method: Magic Byte Restoration

Note: After completing this challenge, I discovered a simpler alternative approach. The UPX modification that prevented automated unpacking was simply a change to the magic bytes. The standard UPX header uses the magic bytes UPX! (0x55505821), but this binary had been modified to use TRTW (0x54525457) instead.

By restoring the original magic bytes, the standard upx -d command would have successfully unpacked the binary:

# Find and replace the modified magic bytes
$ hexeditor buu
# Locate: 54 52 54 57 (TRTW)
# Replace with: 55 50 58 21 (UPX!)

# Now UPX unpacks successfully
$ upx -d buu -o buu_unpacked

This technique demonstrates that while manual unpacking with GDB is valuable for understanding the unpacking process, sometimes the simplest solution is to identify and revert the anti-analysis modifications. Both approaches are valid—manual unpacking teaches fundamentals, while magic byte restoration is more efficient.

Stage 4: Deep Reverse Engineering

With the unpacked binary, I loaded it into IDA Pro for comprehensive static analysis. Several critical indicators emerged immediately:

Go Binary Identification

The binary is written in Go (Golang), identifiable by:

  • Large binary size (typical for statically compiled Go binaries)
  • Go runtime functions (runtime_mapassign_faststr, runtime_intstring, etc.)
  • Go standard library references

Key Functionality Discovery

The strings and function analysis revealed the malware’s core capabilities:

Cryptographic Operations:

crypto_aes_NewCipher
crypto_cipher_NewCBCEncrypter
crypto_cipher_NewCBCDecrypter
ciphertext too short
ciphertext is not a multiple of the block size
plaintext is empty
invalid padding

Network Communications:

server
enc_key
application/octet-stream
BAD URL
Bad Response
failed to unmarshal json: %w

Configuration File:

  • The binary attempts to read /tmp/.X11/cnf during startup
  • If this file doesn’t exist, it prints “I don’t belong here…” and exits
  • This file likely contains C2 configuration

Control Flow Analysis

The main execution flow follows this pattern:

  1. Initialization: Read and parse /tmp/.X11/cnf
  2. Configuration Decryption: Apply XOR decryption to the config file
  3. JSON Parsing: Deserialize configuration to extract server and enc_key values
  4. C2 Loop: Continuously poll C2 server for commands
  5. Command Execution: Execute received commands and exfiltrate results

Stage 5: Configuration File Analysis

The configuration file /tmp/.X11/cnf was available on the compromised machine. I copied it to my analysis environment for examination.

Initial Attempts: The Hard Way

Initially, I took a complex approach using dynamic analysis. I set breakpoints after the deserialize_to_json function to inspect the Go JSON struct in memory:

(gdb) b *0x63b2cb
(gdb) run
(gdb) info reg rax
rax            0xc000072c60        824634190944

(gdb) x/8gx 0xc000096360
0xc000096360:    0x000000000000086c    0x000000c000018338
0xc000096370:    0x0000000000000006    0x000000c000018350

(gdb) x/s 0x000000c000018338
0xc000018338:    "server"

(gdb) x/s 0x000000c000018350
0xc000018350:    "enc_key"

This confirmed the JSON structure contains server and enc_key fields, but didn’t help decrypt the file.

The Simple Solution: Keep It Simple, Stupid (KISS)

Upon closer inspection of the disassembly, I discovered a function called immediately after os_ReadFile that I had initially overlooked. This function performs a simple 4-byte rolling XOR operation on the configuration data.

The XOR implementation uses two hardcoded keys:

  • KEY1: 861204156 (0x3355CCCC)
  • KEY2: -289655980 (0xEEBBDD44)

The function takes a boolean parameter to select which key to use, allowing for easy key rotation during compilation.

I wrote a Python script to decrypt the configuration file:

KEY1 = 861204156
KEY2 = -289655980

with open("conf", "rb") as f:
    data = f.read()

def decrypt(data, key=False):
    decrypted = bytearray()
    
    if key == False:
        key_bytes = KEY2.to_bytes(4, byteorder='little', signed=True)
    else:
        key_bytes = KEY1.to_bytes(4, byteorder='little', signed=True)

    for i in range(0, len(data), 4):
        decrypted.append(data[i + 2] ^ key_bytes[0])
        decrypted.append(data[i + 3] ^ key_bytes[1])
        decrypted.append(data[i] ^ key_bytes[2])
        decrypted.append(data[i + 1] ^ key_bytes[3])

    return decrypted

if __name__ == "__main__":
    decrypted_data = decrypt(data)
    with open("decrypted_conf", "wb") as f:
        f.write(decrypted_data)

Running this script revealed the decrypted configuration:

{
    "server": "https://wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws/command",
    "enc_key": "73eeac3fa1a0ce48f381ca1e6d71f077"
}

Excellent! We now have:

  • C2 Server URL: AWS Lambda function endpoint
  • Encryption Key: Hex-encoded AES key

Stage 6: Understanding C2 Communication

With the configuration decrypted, I analyzed how the malware communicates with its C2 server.

URL Parameter Analysis

The disassembly revealed the malware constructs URLs with two parameters:

Parameter n (hostname):

v66[0] = "-n";
v66[1] = 2;
hostname = exec_commands("uname", 5, v66, 1, 1);
v7 = (__int64 *)runtime_mapassign_faststr(&RTYPE_url_Values, &v69, "n", 1);

The malware executes uname -n to get the hostname and passes it as the n parameter.

Parameter s (sequence number):

size_as_string = runtime_intstring(0, len, v12, v14);
v56 = size_as_string._r0;
v48 = size_as_string._r1;
v16 = (__int64 *)runtime_mapassign_faststr(&RTYPE_url_Values, &v69, "s", 1);

The s parameter is a sequence number that:

  • Starts at 48
  • Increments by 1 after each request
  • Likely used for command ordering or replay prevention
v6 = 48;
while ( 1 )
{
    v14 = v6;
    v20 = recv_data(r0, v6);
    // ... command execution ...
    v6 = v14 + 1;
}

Communication Encryption

All data exchanged with the C2 is encrypted using AES-256-CBC:

  • Key: The enc_key from the configuration (hex-decoded)
  • IV: First 16 bytes of each message
  • Format: [16-byte IV][encrypted_data]

Stage 7: C2 Impersonation and Flag Capture

First, I attempted to connect from my analysis machine:

$ curl -X GET 'https://wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws/command?n=remnux&s=48' -v

< HTTP/1.1 401 Unauthorized
< Content-Type: text/plain
< Content-Length: 13

What are you?

The C2 rejected my connection with a 401 Unauthorized response. This indicates hostname-based authentication—the C2 only accepts connections from known infected machines.

Successful Authentication

Using the hostname from the compromised environment (monthly-challenge):

$ curl -X GET 'https://wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws/command?n=monthly-challenge&s=48' -v --output enc_data

< HTTP/1.1 200 OK
< Content-Type: application/octet-stream
< Content-Length: 32

{ [32 bytes data]

Success! The server responded with 32 bytes of encrypted data.

Decryption Implementation

I implemented an AES-CBC decryption function in Python:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import binascii

KEY_HEX = "73eeac3fa1a0ce48f381ca1e6d71f077"

def get_key() -> bytes:
    return binascii.unhexlify(KEY_HEX)

def decrypt_aes_cbc(data: bytes) -> bytes:
    """
    Decrypt AES-CBC data.
    Input format: [16-byte IV][ciphertext...]
    """
    key = get_key()
    iv = data[:16]
    ciphertext = data[16:]

    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)

    return plaintext

Decrypting the initial response:

$ python3 decrypt.py enc_data
whoami

The C2 sent a simple whoami command. Let’s automate the process and enumerate multiple sequence numbers.

Automated C2 Enumeration

I wrote a script to iterate through different s values and decrypt each response:

import requests
from aes_cbc_crypto import decrypt_aes_cbc

BASE_URL = "https://wehiy6oj3hpaud3yske7nrt5xu0lcovj.lambda-url.us-east-2.on.aws/command"
HOSTNAME = "monthly-challenge"

def fetch_and_decrypt(s_value: int):
    url = f"{BASE_URL}?n={HOSTNAME}&s={s_value}"
    r = requests.get(url)
    
    if r.status_code != 200:
        print(f"s={s_value}: HTTP error {r.status_code}")
        return
    
    encrypted = r.content
    try:
        plaintext = decrypt_aes_cbc(encrypted)
        print(f"s={s_value}: {plaintext.decode()}")
    except Exception as e:
        print(f"s={s_value}: Decryption failed ({e})")

def main():
    for s in range(-10, 101):
        fetch_and_decrypt(s)

if __name__ == "__main__":
    main()

Flag Discovery

Running the enumeration script:

$ python3 fetch_and_decrypt.py 
s=-10: whoami
s=-9: whoami
s=-8: whoami
...
s=0: whoami
s=1: id
s=2: cat /etc/passwd
s=3: DONE WIZ_CTF{\*\*\*\*\*\*\*\*\*}
s=4: whoami
s=5: whoami
...

Flag captured! At sequence number 3, the C2 sent a special “DONE” message containing the flag: WIZ_CTF{*********}

Technical Summary

This challenge demonstrated a realistic malware analysis workflow involving multiple sophisticated techniques:

Malware Techniques Observed

  1. Modified UPX Packing: Custom header modifications to prevent automated unpacking
  2. Multi-layer Obfuscation: Configuration XOR encryption + AES-CBC communication encryption
  3. Hostname-based Authentication: C2 whitelist to prevent unauthorized analysis
  4. AWS Lambda C2: Using serverless infrastructure for command and control
  5. Rolling XOR Configuration: Simple but effective configuration obfuscation with switchable keys

Analysis Techniques Applied

  1. Safe Sample Extraction: Base64 encoding for inert transfer
  2. Manual Unpacking: GDB-based memory dumping at OEP
  3. Static Analysis: IDA Pro reverse engineering of Go binaries
  4. Dynamic Analysis: Controlled detonation with GDB/strace
  5. Cryptanalysis: Identifying and implementing XOR and AES decryption
  6. Network Analysis: C2 protocol reverse engineering and impersonation

Key Architectural Insights

The malware follows this control flow:

sequenceDiagram participant Startup participant Config participant C2 participant Exec Startup->>Config: Read /tmp/.X11/cnf Config->>Config: XOR Decrypt Config Config->>Config: Parse JSON Config-->>Startup: Extract server + enc_key loop Main Loop Startup->>C2: HTTP GET (hostname + sequence) C2-->>Startup: Encrypted command Startup->>Startup: Decrypt AES-CBC command Startup->>Exec: Execute command Exec-->>Startup: Results Startup->>Startup: Encrypt results Startup->>C2: HTTP POST results Startup->>Startup: Increment sequence end

The configuration encryption uses a 4-byte rolling XOR with compile-time selectable keys, while network communications use AES-256-CBC with a shared symmetric key. The C2 implements hostname-based access control, likely checking against a whitelist of infected machines.

Defensive Recommendations

Based on this analysis, defenders should:

  1. Monitor for UPX-packed binaries with header anomalies
  2. Detect suspicious file locations like /tmp/.X11/cnf
  3. Analyze AWS Lambda traffic patterns for C2 communication
  4. Implement network-based AES-CBC traffic detection
  5. Look for Go binary artifacts in unknown executables
  6. Monitor command execution patterns (whoami, id, cat /etc/passwd sequences)

Conclusion

This challenge provided excellent hands-on experience with modern malware analysis techniques. The combination of modified packing, multi-layer encryption, and cloud-based C2 infrastructure represents real-world malware sophistication.

The key lessons learned:

  • Always KISS: Don’t overcomplicate analysis (I initially over-engineered the config decryption)
  • Layer your security: The malware used multiple defensive techniques that each required different approaches
  • Understand the environment: Proper lab setup is crucial for safe malware analysis
  • Document everything: Maintaining clear notes helps when you hit roadblocks

Thanks to WIZ for creating this engaging challenge that mirrors real-world malware analysis scenarios!

References