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}