← Back to Writeups
HTBN/AReversing

PacMan

XESXOR8/23/20266 min read
#reversing#htb#n/a

PacMan

Platform: D3C2026 | Category: Reversing | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-07-25 | Status: Solved Techniques: static_analysis, unicorn_emulation, import_hooking, synchronous_transport_substitution, rc4_decryption

Summary

Task: A minimal iOS Pac-Man IPA hides a flag behind gameplay telemetry and a Mach-port-backed authenticated VM. Solution: Emulate the original ARM64 verifier and workers under Unicorn, substitute synchronous transport, and verify the final RC4 decryption.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: d3c2026 | ID: 20260725_d3c2026_pacman
  • Tags: ios, rc4, custom_vm, arm64, macho, mach_ports, anti_instrumentation
  • Indicators: score >= 10000 unlocks VM verification, 72 records at file offset 0xa800, Mach-port worker dispatcher at 0x100006d2c, 40-byte RC4 ciphertext at file offset 0xb3a0
  • Source: 20260725_d3c2026_pacman.md

Foothold

Vulnerability / Misconfiguration

  1. Static_analysis
  2. Unicorn_emulation
  3. Import_hooking
  4. Synchronous_transport_substitution
  5. Rc4_decryption
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • static_analysis
  • unicorn_emulation
  • import_hooking
  • synchronous_transport_substitution
  • rc4_decryption
  • Tags: ios, rc4, custom_vm, arm64, macho, mach_ports, anti_instrumentation

Original Writeup

<details><summary>Click to expand original content</summary>

Description

let us play Pac-Man.

The supplied pacman.ipa is a minimal iOS application. The goal is to reverse the game and recover the value displayed only after its hidden verification path succeeds.

Initial Triage

Extract the IPA and identify its only native executable:

unzip -q pacman.ipa -d extracted
file extracted/Payload/MachActorVM.app/MachActorVM
shasum -a 256 extracted/Payload/MachActorVM.app/MachActorVM

The bundle contains only Info.plist and an ARM64 Mach-O executable. The executable's SHA-256 is:

06478236457c0f1a6f4fc4cefcf834d0c8c43a75ba08bd661e8c75d05c75221a

There are no images, audio files, or other resources in which to hide the result, and ordinary string extraction does not reveal it. The solution therefore requires reversing the native code.

Analysis

Gameplay is only the first gate

-[ViewController stepGame:] at 0x100005148 delegates game updates to 0x100007a68. That routine moves Pac-Man through a 25-by-13 maze, updates score and telemetry, relocates ten beans, and sets an unlock byte when:

(score >> 4) >= 0x271

This is equivalent to score >= 10000. The renderer then overlays VM UNLOCKED, but this is not the secret. -[ViewController updateWithFrame:] at 0x1000051b0 obtains a 40-byte telemetry object and invokes the verifier at 0x100005c7c.

Authenticated Mach-port VM

The verifier first calls a telemetry validator at 0x100006668, initializes four Mach-port workers, and performs 288 authenticated state transitions. Its important components are:

ComponentAddress / offsetPurpose
Verifier0x100005c7cDrives all transitions and accepts the terminal state
Request packer0x100006384Serializes VM state and a record into a Mach message
Worker dispatcher0x100006d2cSelects and executes the original worker formulas
RC4 wrapper0x100006a1cDecrypts the embedded 40-byte ciphertext
Record tablefile 0xa80072 records, each encoded as <HBBIQQ>
Ciphertextfile 0xb3a040 encrypted bytes

The records use several magic values and reference tables near 0xaec0. SplitMix64-style mixing repeatedly uses the constants 0x9e3779b97f4a7c15, 0xbf58476d1ce4e5b9, and 0x94d049bb133111eb. The final state after all authenticated transitions becomes the eight-byte RC4 key.

Directly treating record fields, table entries, or their SplitMix/endian variants as keys fails. Those values are only inputs to the 288-step state evolution.

Solution

Reimplementing every undocumented Mach request field and worker formula would be error-prone. A more faithful approach is to map the original Mach-O under Unicorn ARM64 and let the challenge binary execute its own packer, dispatcher, transition checks, and verifier.

1. Map the original executable

emulate_vm.py maps the Mach-O at its preferred base and creates isolated stack and heap regions:

def fresh():
    u = Uc(UC_ARCH_ARM64, UC_MODE_ARM)
    u.mem_map(BASE, 0x20000)
    u.mem_write(BASE, RAW)
    u.mem_map(STACK, 0x200000)
    u.mem_map(HEAP, 0x200000)
    u.mem_map(STOP, 0x1000)
    # Imported data pointers and four nonzero fake receive rights are installed.
    return u

The harness stubs imported functions such as mutex operations, port allocation, and mach_absolute_time. A fixed time value makes execution reproducible.

2. Replace only the transport

When the original request packer calls mach_msg to send, the hook captures the serialized message. On its receive call, the harness starts a fresh emulator at the original worker dispatcher, supplies that request, captures the worker response, and copies it into the client's receive buffer:

if opt == 1:                         # client send
    transport["request"] = bytes(uc.mem_read(buf, send_size))
    ret(uc, 0)
elif opt == 2:                       # client receive
    response = dispatch_worker(
        transport["request"], worker_index(magic)
    )
    uc.mem_write(buf, response + bytes(max(0, recv_size - len(response))))
    ret(uc, 0)

This substitution preserves the binary's exact request layout and all worker arithmetic while avoiding unavailable iOS Mach-port infrastructure.

3. Run the complete verifier

The harness executes 0x100005c7c with deterministic telemetry bytes. It bypasses only the external pre-VM telemetry validator by forcing its return value to one. Calls to the request packer are redirected through the synchronous transport described above; all 288 transitions and their authentication checks still execute in the original ARM64 code.

At the RC4 wrapper, the verifier supplies these final key bytes:

a7 40 41 a0 71 5b ad 5e

Interpreted as a little-endian integer, the state is 0x5ead5b71a04140a7.

4. Independently verify RC4

The harness also performs an independent Python RC4 decryption over exactly 40 bytes from file offset 0xb3a0:

def rc4(key, data):
    s = list(range(256))
    j = 0
    for i in range(256):
        j = (j + s[i] + key[i & 7]) & 0xff
        s[i], s[j] = s[j], s[i]
    out = bytearray()
    i = j = 0
    for c in data:
        i = (i + 1) & 0xff
        j = (j + s[i]) & 0xff
        s[i], s[j] = s[j], s[i]
        out.append(c ^ s[(s[i] + s[j]) & 0xff])
    return bytes(out)

Run the authoritative harness from the task directory:

python3 emulate_vm.py

The verifier returns ok=1, reports the expected eight-byte key, and prints plaintext=b'd3ctf{REDACTED}'. This confirms both the original verifier path and the independent RC4 implementation.

Failed Approaches

  • Strings and resource inspection: the IPA has no useful resources, and the executable contains no plaintext result.
  • Direct key extraction: record qwords, table qwords, terminal-record relations, SplitMix variants, and endian swaps do not produce meaningful plaintext because the key evolves through 288 authenticated transitions.
  • Partial manual worker translation: translating one worker was insufficient because the exact Mach message payload layout remained ambiguous. Executing the original packer and dispatcher removed that uncertainty.

Artifacts

  • notes.md: complete analysis chronology and addresses.
  • solve.py: record parser, SplitMix64 helpers, RC4 implementation, and rejected direct-key experiments.
  • emulate_vm.py: complete reproducible Unicorn harness and final verifier.
</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR