HTB: Bare Metal Challenge

Bare Metal - HackTheBox Challenge Writeup

Challenge Information

FieldValue
Challenge NameBare Metal
CategoryHardware / Misc
DifficultyMedium
Authord3vn0mi

Description

Management ordered a review of a remote fabrication plant after concerns were raised about the integrity of devices produced there. During the review, a technician found a suspicious embedded device tapped into one of the production line’s serial networks. While attempting to remove it, a hardware failsafe was accidentally triggered, bricking the device — but not before its firmware had already been extracted. The microcontroller was identified as an ATmega328P, and the goal was to reverse-engineer the firmware to figure out what the device was doing to the “slave” device it was tapped into.

Solution Overview

The challenge boils down to classic AVR firmware reverse engineering: convert the provided Intel HEX firmware into raw binary, disassemble/read the AVR machine code, and recognize a manual bit-banged protocol being driven on two GPIO pins of PORTD. Once the clock and data lines are identified, the firmware’s instruction stream can be “simulated” statically — walking each SBI/CBI (set/clear bit in I/O register) pair to recover the bitstream that would have been shifted out to the tapped slave device, which decodes directly to the flag.

Key Steps

Step 1: Recover the challenge artifact

The provided extracted_firmware.hex file was initially 0 bytes on disk. Since the file was missing, the challenge had to be re-fetched from HackTheBox directly rather than relying on the corrupted local copy.

from lib.htb_api import HTBClient
c = HTBClient()
# Confirm the challenge ID and category before pulling the archive
info = c.challenge_info(194)
print(info) # -> confirms category "Hardware", name "Bare Metal"
# Download the real challenge zip (previous local copy was 0 bytes)
name, blob = c.download_challenge(194)
with open("bm.zip", "wb") as f:
f.write(blob)

The archive was password-protected with the standard HTB challenge password:

Terminal window
unzip -P hackthebox bm.zip -d bm/
# -> yields a real 6444-byte Intel HEX file: extracted_firmware.hex

Step 2: Convert Intel HEX to a raw binary image

The firmware ships as an Intel HEX (.hex) file, which needs to be flattened into a raw binary before it can be read as AVR machine code.

import binascii
def ihex_to_bin(path):
data = bytearray()
with open(path) as f:
for line in f:
line = line.strip()
if not line.startswith(":"):
continue
raw = binascii.unhexlify(line[1:])
count, addr, rectype = raw[0], (raw[1] << 8) | raw[2], raw[3]
if rectype == 0x00: # data record
payload = raw[4:4 + count]
if addr + count > len(data):
data.extend(b"\x00" * (addr + count - len(data)))
data[addr:addr + count] = payload
return bytes(data)
fw = ihex_to_bin("extracted_firmware.hex")
open("firmware.bin", "wb").write(fw)
print(len(fw)) # -> 2286 bytes of AVR/ATmega328P code

Step 3: Identify the bit-banged protocol

With avr-objdump/avr-gcc/simavr unavailable in the environment, the binary was walked manually for AVR opcodes. The firmware was almost entirely composed of SBI (Set Bit in I/O register) and CBI (Clear Bit in I/O register) instructions targeting I/O address 0x0B, which corresponds to PORTD on the ATmega328P — i.e., the firmware was manually toggling individual GPIO pins rather than using a hardware USART/SPI/I2C peripheral.

# AVR SBI/CBI opcode layout: 1001 1010/1000 AAAA Abbb (16-bit, little-endian)
def decode_sbi_cbi(word):
op = word & 0xFF00
if (word & 0xFF00) in (0x9A00, 0x9800): # SBI=0x9A00, CBI=0x9800
set_bit = (word & 0x0200) != 0
io_addr = (word >> 3) & 0x1F
bit = word & 0x07
return set_bit, io_addr, bit
return None

Scanning the ~1060 SBI/CBI pairs against I/O address 0x0B (PORTD) revealed a repeating pattern on exactly two bits:

  • Bit 6 — toggled in a strict, evenly-spaced pattern → the clock line (352 pulses total).
  • Bit 5 — toggled/held around each clock transition → the data line.

This is a textbook software (bit-banged) synchronous serial shift-out, similar to how firmware without a UART/SPI peripheral available would manually clock data out to a tapped slave device on the line.

Step 4: Reconstruct the bitstream

Walking the decoded instruction stream in order, the state of the data pin (bit 5) was sampled on every rising edge of the clock pin (bit 6), reconstructing the transmitted bitstream MSB-first.

bits = []
clock_state = 0
data_state = 0
for set_bit, io_addr, bit in sbi_cbi_stream:
if io_addr != 0x0B:
continue
if bit == 5:
data_state = 1 if set_bit else 0
elif bit == 6:
new_clock = 1 if set_bit else 0
if new_clock == 1 and clock_state == 0: # rising edge
bits.append(data_state)
clock_state = new_clock
# 352 bits sampled -> 44 bytes, MSB-first
bytestream = bytearray()
for i in range(0, len(bits), 8):
byte = 0
for b in bits[i:i+8]:
byte = (byte << 1) | b
bytestream.append(byte)
print(bytestream.decode())

Step 5: Decode and submit

Decoding the reconstructed byte stream as ASCII yielded a command string followed directly by the flag:

set_flag:HTB{REDACTED}

The flag was verified against the HTB API:

from lib.htb_api import HTBClient
c = HTBClient()
r = c.submit_challenge(194, "HTB{REDACTED}")
print(r) # -> "Congratulations! ..."

Tools Used

ToolPurpose
Python 3 (binascii)Intel HEX parsing and manual AVR opcode decoding
HTB API client (lib/htb_api.py)Re-fetching the challenge archive and flag submission
Manual AVR ISA referenceDecoding SBI/CBI opcode fields (I/O address + bit + set/clear) since no avr-objdump/avr-gcc/simavr toolchain was available

Key Learnings

  • Firmware is a bitstream, not just a program. When a microcontroller drives GPIO pins with no dedicated peripheral (UART/SPI/I2C) involved, the “protocol” only exists as a sequence of SBI/CBI toggles in the disassembly — recovering it means statically walking the instruction stream as if it were a hardware simulator.
  • I/O address ↔ port mapping matters. Recognizing that AVR I/O address 0x0B maps to PORTD on the ATmega328P was the key pivot that turned “a pile of set/clear-bit instructions” into “two identifiable clock/data lines.”
  • Sample on the right edge. Bit-banged serial protocols only make sense once you pick the correct edge (rising vs. falling) to sample data against clock — sampling on the wrong edge produces garbage instead of readable ASCII.
  • 0-byte artifacts are a recovery step, not a dead end. When a downloaded challenge file was corrupted/empty, re-deriving the correct challenge ID and re-pulling the archive from the HTB API directly resolved it rather than treating the challenge as unsolvable.
  • Leetspeak hint tied it together: “bit banging is everywhere” — a direct nod to the manual GPIO bit-banging technique used by the firmware.

Flag

HTB{REDACTED}