This is a part of a series of writeups for a malware challenge I made for DamCTF 2020. Please see here for the overview.

Phase 3

Great work extracting the config from that malware! Based on your analysis, we’ve identified many other samples for another variant that appears to be using a slightly different config encryption function. Can you automate the config extraction for these new samples?

Please connect to our sample server to receive samples for analysis: nc chals.damctf.xyz 32153

Now that players knew where the malware config was located and generally how to extract the flag, they would need to automate that extraction process to provide a specified config item from 10 different binaries within 60 seconds. The “sample server” had 3000 different binaries that had randomly generated configs. However, we threw a curveball at players by modifying the config encryption code for the phase 3 binaries, so players would have to save the first binary for manual analysis to build their solving script.

When players connect to the service, they are given a Base64 blob of the ELF to analyze, and then are prompted to provide a config value:

What's the value of the 'xvee' config item?

Assuming they provide the correct config value, they will repeat the process 10 times and will then be given the flag.

Let’s take a look at the new config decryption code from one of the binaries:

void FUN_00101a19(uchar *param_1,long param_2)

{
  char *__src;
  uchar *outdata;
  long in_FS_OFFSET;
  byte local_455;
  int local_454;
  int local_450;
  RC4_KEY local_438;
  byte local_28 [24];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  __src = (char *)(param_2 + 1);
  RC4_set_key(&local_438,0x20,param_1);
  outdata = (uchar *)calloc(0x42,1);
  RC4(&local_438,0x10,param_1,outdata);
  strncpy((char *)(outdata + 0x10),__src,0x31);
  local_454 = 0;
  while (local_454 < 0x31) {
    RC4(&local_438,0x10,outdata + local_454,local_28);
    local_455 = 0;
    local_450 = 0;
    while (local_450 < 0x10) {
      local_455 = local_455 ^ local_28[local_450];
      local_450 = local_450 + 1;
    }
    __src[local_454] = __src[local_454] ^ local_455;
    local_454 = local_454 + 1;
  }
  free(outdata);
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

FUN_00101a19 in Phase 3 uses RC4-CFB8, which for some reason is a supported crypto scheme in OpenSSL but I’ve been told should never be used and is effectively a meme (dear crypto people: I am not a crypto person, and apologize in advance). One of OSUSEC’s resident crypto experts helped with the specific crypto implementations for phases 2 and 3, otherwise it would’ve just been random XOR if I had implemented it (thanks Athos!).

Guide to solving beta version of Phase 2/3

Using the above decompilation, we can write a new function in Python to decrypt a config chunk, along with the other code necessary to interact with the service:

from pwn import *
import sys
from Crypto.Cipher import ARC4
import base64
from typing import Dict

def get_config(elf: bytes) -> Dict[str, str]:
    def decrypt_chunk(key, chunk):
        cipher = ARC4.new(key)
        outbytes = list(chunk[1:])

        ret = cipher.decrypt(key[:16])
        ptr = ret + chunk[1:] 

        for i in range(49):
            ret = cipher.decrypt(ptr[i:i+16])
            local_455 = 0
            for j in range(16):
                local_455 ^= ret[j]
            outbytes[i] ^= local_455

        return bytes(outbytes)

    config_bytes = elf[0x51a0:0x5394]
    config_key = elf[0x4010:0x4010+32]

    config = {}

    for i in range(10):
        plaintext = decrypt_chunk(config_key, config_bytes[i*50:(i+1)*50])
        key = plaintext[:4].decode()
        val = plaintext[4:].decode().split("\x00")[0]
        config[key] = val
    
    return config

p = remote(sys.argv[1], int(sys.argv[2]))

p.sendlineafter("continue)", "")

for _ in range(10):
    p.recvline()
    a = p.recvline()
    elf = base64.b64decode(p.recvline())

    config = get_config(elf)
    line = p.recvline()
    key = line.decode().split("'")[2][:4]
    p.sendline(config[key])

    p.recvline()

p.recvline()
print(p.recvline().decode())

Running that script will grant us the Phase 3 flag after ~15 seconds:

$ python3 interact.py chals.damctf.xyz 32153
[+] Opening connection to chals.damctf.xyz on port 32153: Done
dam{w0w_c0nf1gs_ar3_sup3r_import4nt_f0r_analys1s}