paraguas
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.00401742passes the key string"8.8.8.8", a length0x10f7(4343), and a data pointer (0x408088) intofcn.00401560 -> fcn.004017f4.- Key observation:
4343 == 0x10f7is 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
0x4080a4dispatches opcodes read from a program pointer, operating on a stack. - The VM program lives at
0x405040(set up byfcn.00402697). - A secondary "syscall" dispatcher (case 194 → 9-case jump table at
0x408490) maps VM operations to host operations:
| VM syscall | Host operation |
|---|---|
| case 1 | recv(fd, buf, len=1, 0) via fcn.00401708 |
| case 2 | malloc(0x81) |
| case 3 | memset(buf, 0, 0x81) |
| case 4 / case 7 | string building (fcn.004026c3, writes literal bytes) |
| case 6 | connect via fcn.0040158e (real socket/htons/inet_pton/connect) |
| case 8 | printf |
Network routine (fcn.0040158e)
fcn.0040158e(ip_string, port) does WSAStartup, socket, htons(port),
inet_pton(ip), connect. Program flow:
- connect to
<IP>:4343 - print
Connecting to ip <IP> port 4343... - on success print
Connect OK, thenRecieving & decrypting the message recvthe message byte-by-byte- 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 read-only • copy allowednc 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 read-only • copy allowedimport 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