← Back to Writeups
CUSTOMMediumReversing

paraguas

XESXOR9/2/20266 min read
#reversing#custom#medium#compiled_stack_vm_reversing#import_stubbing#live_server_ciphertext_retriev#unicorn_emulation

paraguas

Platform: CUSTOM | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: compiled_stack_vm_reversing, import_stubbing, live_server_ciphertext_retrieval, unicorn_emulation

Summary

Walkthrough of the CUSTOM Reversing paraguas. Task: MinGW Windows PE that connects to a server, recvs an encrypted message and decrypts it inside a compiled stack-based VM; hardcoded address 8.8.8.8 is a decoy. Solution: pull ciphertext from the

Description

This app tries to connect to Google! But the correct address is 109.233.56.90

A Windows PE32+ console executable (paraguas.exe, x86-64, stripped to an external PDB, compiled with MinGW GCC). The program connects to a server, receives an encrypted message, decrypts it, and prints the flag. The hardcoded address 8.8.8.8 (Google DNS) is a decoy — the real flag server is 109.233.56.90:4343.

Analysis

Strings & imports

strings / radare2 reveal Winsock usage (socket, connect, recv, closesocket), the decoy string 8.8.8.8, and operational messages: Connect OK, Connecting to ip %s port %d..., Recieving & decrypting the message, WSAStartup failed with error: %d, socket failed with error: %ld, connect failed with error: %d. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Control flow

  • main = fcn.00401742 passes the key string "8.8.8.8", a length 0x10f7 (4343), and a data pointer (0x408088) into fcn.00401560 -> fcn.004017f4.
  • Key observation: 4343 == 0x10f7 is also the TCP port the program connects to.

The compiled VM (fcn.004017f4)

This is a compiled stack-based bytecode interpreter:

  • A 251-case jump table at 0x4080a4 dispatches opcodes read from a program pointer, operating on a stack.
  • The VM program lives at 0x405040 (set up by fcn.00402697).
  • A secondary "syscall" dispatcher (case 194 → 9-case jump table at 0x408490) maps VM operations to host operations: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
VM syscallHost operation
case 1recv(fd, buf, len=1, 0) via fcn.00401708
case 2malloc(0x81)
case 3memset(buf, 0, 0x81)
case 4 / case 7string building (fcn.004026c3, writes literal bytes)
case 6connect via fcn.0040158e (real socket/htons/inet_pton/connect)
case 8printf

Network routine (fcn.0040158e)

fcn.0040158e(ip_string, port) does WSAStartup, socket, htons(port), inet_pton(ip), connect. Program flow: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  1. connect to <IP>:4343
  2. print Connecting to ip <IP> port 4343...
  3. on success print Connect OK, then Recieving & decrypting the message
  4. recv the message byte-by-byte
  5. decrypt inside the VM and print the flag

The decryption key is embedded in the VM logic — the IP only selects which server to connect to; the flag comes from the real server's encrypted bytes decrypted by the VM.

Solution

1. Retrieve the ciphertext from the real server

The hint identifies the real server. Fetch the 40-byte encrypted message: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

bash
nc 109.233.56.90 4343 > server_msg.bin   # 40 bytes

Bytes received:

b78a3c22 d468ee23 9165e4f2 e5276861 6531cdeb
63e90bb1 5dd0e1b7 25df778d 2d29ee30 aeea81e3

2. Emulate the VM decrypt function with Unicorn

Rather than reverse the full VM bytecode, emulate fcn.004017f4 directly with Unicorn Engine. Map the PE sections at their virtual addresses, set up stack/heap, and stub the msvcrt imports (printf/memset/malloc/puts) and the WS2_32 imports — most importantly recv, which is fed sequentially from server_msg.bin. Call the function with rcx = key string, edx = 0x10f7, r8 = 0x408088. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

python
import sys, struct
import pefile
from unicorn import *
from unicorn.x86_const import *

KEY = sys.argv[1] if len(sys.argv) > 1 else "109.233.56.90"

pe = pefile.PE("paraguas.exe")
image_base = pe.OPTIONAL_HEADER.ImageBase

mu = Uc(UC_ARCH_X86, UC_MODE_64)

# Map full image region and write sections at their virtual addresses
IMG_START = image_base
IMG_SIZE  = 0x100000
mu.mem_map(IMG_START, IMG_SIZE)
mu.mem_write(IMG_START, pe.header)
for s in pe.sections:
    mu.mem_write(image_base + s.VirtualAddress, s.get_data())
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Stack
STACK, STACK_SIZE = 0x200000, 0x100000
mu.mem_map(STACK, STACK_SIZE)
rsp = STACK + STACK_SIZE - 0x1000
mu.reg_write(UC_X86_REG_RSP, rsp)
mu.reg_write(UC_X86_REG_RBP, rsp)

# Heap for malloc
HEAP, HEAP_SIZE = 0x800000, 0x400000
mu.mem_map(HEAP, HEAP_SIZE)
heap_ptr = [HEAP]

# Key string area
KEYADDR = 0x600000
mu.mem_map(KEYADDR, 0x1000)
mu.mem_write(KEYADDR, KEY.encode() + b"\x00")

# msvcrt PLT stubs
PRINTF, MEMSET, MALLOC, PUTS = 0x404280, 0x404288, 0x404298, 0x404278

# Install fake stub addresses into WS2_32 IAT entries
STUB_BASE = 0x700000
mu.mem_map(STUB_BASE, 0x1000)
iat_stubs = {}
def install_iat(iat_va, name):
    stub = STUB_BASE + (len(iat_stubs) * 0x10)
    mu.mem_write(iat_va, struct.pack("<Q", stub))
    mu.mem_write(stub, b"\xc3")
    iat_stubs[stub] = name
    return stub
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

IAT = {
    "WSAStartup": 0x40c410, "closesocket": 0x40c418, "connect": 0x40c420,
    "htons": 0x40c428, "inet_pton": 0x40c430, "socket": 0x40c440,
    "WSAGetLastError": 0x40c408, "WSACleanup": 0x40c400, "recv": 0x40c438,
}
with open("server_msg.bin", "rb") as f:
    SERVER_MSG = f.read()
recv_pos = [0]
stub_name = {}
for nm, va in IAT.items():
    stub_name[install_iat(va, nm)] = nm

captured_output = bytearray()

def read_cstr(mu, addr, maxlen=4096):
    out = b""
    for i in range(maxlen):
        b = mu.mem_read(addr + i, 1)
        if b == b"\x00":
            break
        out += b
    return out
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

def do_ret(mu, retval):
    rsp = mu.reg_read(UC_X86_REG_RSP)
    ret = struct.unpack("<Q", mu.mem_read(rsp, 8))[0]
    mu.reg_write(UC_X86_REG_RSP, rsp + 8)
    mu.reg_write(UC_X86_REG_RAX, retval)
    mu.reg_write(UC_X86_REG_RIP, ret)

def hook_code(mu, address, size, user_data):
    rip = address
    if rip == PRINTF:
        fmt = read_cstr(mu, mu.reg_read(UC_X86_REG_RCX))
        args = [mu.reg_read(UC_X86_REG_RDX), mu.reg_read(UC_X86_REG_R8),
                mu.reg_read(UC_X86_REG_R9)]
        out, i, ai = b"", 0, 0
        while i < len(fmt):
            c = fmt[i:i+1]
            if c == b"%" and i+1 < len(fmt):
                spec = fmt[i+1:i+2]
                if spec == b"s":   out += read_cstr(mu, args[ai]); ai += 1
                elif spec == b"c": out += bytes([args[ai] & 0xff]); ai += 1
                elif spec == b"d": out += str(args[ai]).encode(); ai += 1
                elif spec == b"%": out += b"%"
                else:              out += b"%" + spec
                i += 2
            else:
                out += c; i += 1
        captured_output.extend(out)
        sys.stdout.write(out.decode(errors='replace'))
        do_ret(mu, len(out))
    elif rip == PUTS:
        s = read_cstr(mu, mu.reg_read(UC_X86_REG_RCX))
        captured_output.extend(s + b"\n")
        do_ret(mu, 0)
    elif rip == MEMSET:
        dst = mu.reg_read(UC_X86_REG_RCX)
        val = mu.reg_read(UC_X86_REG_RDX) & 0xff
        n = mu.reg_read(UC_X86_REG_R8)
        mu.mem_write(dst, bytes([val]) * n)
        do_ret(mu, dst)
    elif rip == MALLOC:
        sz = mu.reg_read(UC_X86_REG_RCX)
        p = heap_ptr[0]
        heap_ptr[0] = (heap_ptr[0] + sz + 15) & ~15
        do_ret(mu, p)
    elif rip in stub_name:
        nm = stub_name[rip]
        if nm == "WSAStartup":      do_ret(mu, 0)
        elif nm == "socket":        do_ret(mu, 3)
        elif nm == "htons":
            v = mu.reg_read(UC_X86_REG_RCX) & 0xffff
            do_ret(mu, ((v >> 8) | (v << 8)) & 0xffff)
        elif nm == "inet_pton":
            sptr = mu.reg_read(UC_X86_REG_RDX)
            buf = mu.reg_read(UC_X86_REG_R8)
            ip = read_cstr(mu, sptr).decode()
            try:
                mu.mem_write(buf, bytes(int(x) for x in ip.split(".")))
                do_ret(mu, 1)
            except Exception:
                do_ret(mu, 0)
        elif nm == "recv":
            buf = mu.reg_read(UC_X86_REG_RDX)
            ln = mu.reg_read(UC_X86_REG_R8)
            data = SERVER_MSG[recv_pos[0]:recv_pos[0] + ln]
            if data:
                mu.mem_write(buf, data)
                recv_pos[0] += len(data)
                do_ret(mu, len(data))
            else:
                do_ret(mu, 0)
        elif nm in ("connect", "closesocket", "WSAGetLastError", "WSACleanup"):
            do_ret(mu, 0)
        else:
            do_ret(mu, 0)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

mu.hook_add(UC_HOOK_CODE, hook_code)

# Args: rcx=key ptr, edx=0x10f7 (port 4343), r8=0x408088
mu.reg_write(UC_X86_REG_RCX, KEYADDR)
mu.reg_write(UC_X86_REG_RDX, 0x10f7)
mu.reg_write(UC_X86_REG_R8, image_base + 0x8088)

# Fake return address so emu_start can detect completion
FAKE_RET = 0x1000
mu.mem_map(FAKE_RET, 0x1000)
rsp = mu.reg_read(UC_X86_REG_RSP) - 8
mu.mem_write(rsp, struct.pack("<Q", FAKE_RET))
mu.reg_write(UC_X86_REG_RSP, rsp)

FUNC = image_base + 0x17f4
mu.emu_start(FUNC, FAKE_RET, timeout=60 * 1000000, count=50_000_000)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

rax = mu.reg_read(UC_X86_REG_RAX)
print("[*] result string at RAX:", read_cstr(mu, rax))
print("[*] captured printf output:", bytes(captured_output))

Running it:

python3 emu.py 109.233.56.90

produces:

Connecting to ip 109.233.56.90 port 4343...Connect OK
Recieving & decrypting the message
[*] result string at RAX: b'spbctf{REDACTED}'

The IP argument only changes the printed connection string; the flag is the VM decryption of the server's bytes (running with 8.8.8.8 and the same bytes yields the identical flag). ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering compiled_stack_vm_reversing, import_stubbing, live_server_ciphertext_retrieval, unicorn_emulation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-06-24 • CUSTOM • paraguas • Difficulty: Medium