Analysis

In this Reverse Engineering challenge, we are given a remote service that prints a long hex string and asks for the value of register r0:

1
2
3
$ nc 154.57.164.72 31209
Level 1/50: 370301e3a31c02e3731c4fe3e42d0ae32b2146e3010020e0020020e0...
Register r0:

The first attempt was to decode the bytes as ARM instructions and answer with the instruction mnemonic, but the service rejected that:

1
2
Register r0:  STRCC
Value not recognized

So the challenge is not asking for the decoded instruction. It wants the final numeric value stored in r0 after executing the provided ARM code.

Looking at the first bytes:

1
37 03 01 e3

This is little-endian ARM. Reversing the byte order gives:

1
e3010337

Disassembling it with Capstone gives:

movw r0, #0x1337

That makes sense as the starting point for every level: initialize r0, then apply many arithmetic and bitwise operations.

I used Capstone to inspect one generated level:

1
2
3
4
5
6
7
from capstone import *

code = bytes.fromhex("370301e3a31c02e3731c4fe3...")

md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
for insn in md.disasm(code, 0x1000):
    print(f"{insn.address:08x}: {insn.mnemonic} {insn.op_str}")

Example output:

00001000: movw r0, #0x1337
00001004: movw r1, #0x2ca3
00001008: movt r1, #0xfc73
0000100c: movw r2, #0xade4
00001010: movt r2, #0x612b
00001014: eor r0, r0, r1
00001018: eor r0, r0, r2
...

The code is just a sequence of ARM instructions that eventually leaves the answer in r0.

Emulation

Manually reimplementing every possible ARM instruction would be slow and error-prone, especially because the generated code includes instructions like adc and sbc, which depend on the carry flag.

Instead, I used Unicorn to emulate the ARM code directly:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from unicorn import *
from unicorn.arm_const import *

BASE = 0x10000
STACK = 0x20000
MAP_SIZE = 0x10000

def emulate_r0(hex_code):
    code = bytes.fromhex(hex_code)

    uc = Uc(UC_ARCH_ARM, UC_MODE_ARM)
    uc.mem_map(BASE, MAP_SIZE)
    uc.mem_map(STACK, MAP_SIZE)
    uc.mem_write(BASE, code)

    cpsr = uc.reg_read(UC_ARM_REG_CPSR)
    uc.reg_write(UC_ARM_REG_CPSR, cpsr & ~(1 << 29))

    uc.emu_start(BASE, BASE + len(code))
    return uc.reg_read(UC_ARM_REG_R0) & 0xffffffff

The important detail is returning r0 as an unsigned 32-bit integer:

1
return uc.reg_read(UC_ARM_REG_R0) & 0xffffffff

Some levels produce values above 0x7fffffff, so sending a signed integer would be wrong.

Automation

Since the service has 50 levels and a timeout, I wrote a Python script to automate the whole interaction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/usr/bin/env python3
import re
import socket
import sys

from unicorn import Uc, UC_ARCH_ARM, UC_MODE_ARM
from unicorn.arm_const import UC_ARM_REG_CPSR, UC_ARM_REG_R0


HOST = "154.57.164.72"
PORT = 31209
BASE = 0x10000
STACK = 0x20000
MAP_SIZE = 0x10000


LEVEL_RE = re.compile(rb"Level\s+\d+/50:\s*([0-9a-fA-F]+)")


def emulate_r0(hex_code: bytes) -> int:
    code = bytes.fromhex(hex_code.decode())

    uc = Uc(UC_ARCH_ARM, UC_MODE_ARM)
    uc.mem_map(BASE, MAP_SIZE)
    uc.mem_map(STACK, MAP_SIZE)
    uc.mem_write(BASE, code)

    cpsr = uc.reg_read(UC_ARM_REG_CPSR)
    uc.reg_write(UC_ARM_REG_CPSR, cpsr & ~(1 << 29))

    uc.emu_start(BASE, BASE + len(code))
    return uc.reg_read(UC_ARM_REG_R0) & 0xFFFFFFFF


def recv_until_prompt(sock: socket.socket) -> bytes:
    data = b""
    while b"Register r0:" not in data:
        chunk = sock.recv(4096)
        if not chunk:
            break
        data += chunk
    return data


def main() -> int:
    host = sys.argv[1] if len(sys.argv) > 1 else HOST
    port = int(sys.argv[2]) if len(sys.argv) > 2 else PORT

    with socket.create_connection((host, port), timeout=10) as sock:
        sock.settimeout(10)
        while True:
            data = recv_until_prompt(sock)
            print(data.decode(errors="replace"), end="")

            match = LEVEL_RE.search(data)
            if not match:
                return 0

            value = emulate_r0(match.group(1))
            print(value)
            sock.sendall(f"{value}\n".encode())


if __name__ == "__main__":
    raise SystemExit(main())

Running it solves all 50 rounds:

1
2
3
4
5
6
7
8
9
$ python3 solve.py
Level 1/50: 370301e3...
Register r0: 3651634301
Level 2/50: 370301e3...
Register r0: 2972994780
...
Level 50/50: 370301e3...
Register r0: 2144319144
HTB{un1c0Rn_0R_C4p5T0nE_0R_qeMU_5ubPR0cE55_0r_F0r90TTen_0Ld_R45berry_4NyTH1N9_BUt_n0_M4nu4LLy}

Flag

HTB{un1c0Rn_0R_C4p5T0nE_0R_qeMU_5ubPR0cE55_0r_F0r90TTen_0Ld_R45berry_4NyTH1N9_BUt_n0_M4nu4LLy}