An attacker has gained access to the supply chain of StoreD and has successfully uploaded malicious firmware to an environmental sensor which has then attempted to connect to many hosts on the network. Your task is to reverse engineer the firmware to discover what the device tries to do.
A supply-chain compromise put trojanised firmware on a StoreD environmental sensor. The image is a 4 MB flash backup of an ESP32-class device running FreeRTOS, and it keeps its Wi-Fi settings in the NVS configuration partition and again as strings in the application. Alongside the legitimate sensor loop that ships temperature and pressure readings to a collector, a reporting task sweeps the local subnet and fires a canned FTP exploit at the hosts it finds. The exploit is a classic stack overflow: a long run of filler, a hard-coded return address, and a shikata_ga_nai encoded shellcode that connects back to an attacker-controlled address on the HTTPS port. All of it was recovered statically with strings, a hex viewer and a short Python decoder.
evidence/fw-backup-4M.bin → 4 MB SPI flash dump. Triage with strings and xxd, then carve regions by offsetsta.ssid and sta.apinfo0x12d00 to 0x13300 in the app imageTip: No Xtensa disassembler was available, so everything below comes from strings, offsets and byte patterns. Where an answer rests on that alone, the walkthrough says so.
Run strings over the flash dump and look at the application region first. Wi-Fi settings are also persisted in the NVS partition, so search for the station keys there. sta.ssid sits at offset 0x90e4 and holds the corporate network name, which matches the StoreD strings in the app.
strings -n 6 evidence/fw-backup-4M.bin > work/strings.txt
grep -abo StoreD-Corporate evidence/fw-backup-4M.bin
xxd -s 37092 -l 128 evidence/fw-backup-4M.bin
The password lives next to the SSID in NVS as sta.apinfo at 0x9524, and the same string appears again in the application rodata at 0x13274. Two independent copies agreeing rules out a stray string.
xxd -s 38148 -l 128 evidence/fw-backup-4M.bin
xxd -s 0x13160 -l 0x400 evidence/fw-backup-4M.bin
Dump the app rodata around 0x12fd8. The telemetry code’s format strings sit there, including Message sent to, with the destination IPv4 address stored as a plain string right beside it.
xxd -s 0x12a00 -l 0x600 evidence/fw-backup-4M.bin
strings -n 4 -t x evidence/fw-backup-4M.bin | awk '$1 >= "12d00" && $1 <= "12e80"'
Without a disassembler, count the task-name strings the app passes to task creation: udp_send_task, report_task and sensor_task. That gives three.
sed -n 525,566p work/strings.txt
Note: This was reasoned from the three task-name strings, not from counting
xTaskCreatecalls in disassembly. HTB accepted it, but verify in Ghidra with an Xtensa processor module if you want certainty.
The same task-name strings show which task does what. report_task is paired with a report log TAG whose messages are about scanning the subnet, so it is the task driving the attack traffic, while sensor_task handles the measurements and udp_send_task ships them.
sed -n 525,566p work/strings.txt
Note: Inferred from task names and the log TAG, not from reading the task body in disassembly. The scanning behaviour and the payload both sit next to the
reportstrings.
The payload blob in rodata starts with USER BBBB..., an FTP login command with an oversized username. That is a buffer overflow against an FTP server’s USER handler.
grep -a -n -E 'USER|AAAA|\x90\x90' evidence/fw-backup-4M.bin | head
xxd -s 198184 -l 256 evidence/fw-backup-4M.bin
Find where the run of B filler (2000 bytes) ends. The next four bytes are 37 50 88 7c, the overwrite for the saved return address. EIP is x86 little-endian, so read them reversed to get the address.
python3 -I - <<'EOF'
d=open('evidence/fw-backup-4M.bin','rb').read()
i=d.find(b'USER '+b'B'*2000)+5+2000
print(d[i:i+4].hex(), hex(int.from_bytes(d[i:i+4],'little')))
EOF
Gotcha: The bytes in the file read
37 50 88 7cleft to right; submitting them without reversing gives the wrong value.
The shellcode after the return address is not readable as-is: it opens with a shikata_ga_nai decoder stub. Recover the XOR key from the stub (0x427c96fc), decode the body in Python, and the plaintext contains push 0x3fb2a8c0 (the address, little-endian) followed by a port word of 0x01bb, which is 443. The pushed dword c0 a8 b2 3f is 192.168.178.63.
python3 -I - <<'EOF'
# XOR-decode the shikata_ga_nai body with key 0x427c96fc, then disassemble the push imm32 values
EOF
Tip: Common reverse-shell stagers push the IP and port as immediates, so look for
push imm32followed by apushof a port word (sockaddr_insetup) in the decoded bytes.
| Phase | Action |
|---|---|
| Initial Access | Trojanised firmware delivered to the sensor through the vendor supply chain |
| Credential Access | Corporate Wi-Fi SSID and password stored in NVS and in the app image |
| Command and Control | Legitimate sensor loop sends temperature and pressure readings to a collector |
| Discovery | report_task scans the local subnet for hosts |
| Lateral Movement | Hosts are hit with an FTP USER overflow |
| Execution | Return address overwritten with a hard-coded value, shikata_ga_nai shellcode runs |
| Command and Control | Shellcode connects back to an internal address on port 443 |
| Type | Value |
|---|---|
| File | fw-backup-4M.bin |
| SSID | StoreD-Corporate |
| Wi-Fi password | StoreD@2024 |
| IP (telemetry sink) | 10.137.244[.]155 |
| IP (reverse shell) | 192.168.178[.]63 |
| Port (reverse shell) | 443 |
| Return address | 0x7C885037 |
| FreeRTOS tasks | udp_send_task, report_task, sensor_task |
| Technique | ID | Description |
|---|---|---|
| Supply Chain Compromise: Compromise Software Supply Chain | T1195.002 | Malicious firmware pushed to the sensor via the vendor pipeline |
| Unsecured Credentials: Credentials In Files | T1552.001 | Wi-Fi password stored in clear in NVS and rodata |
| Network Service Discovery | T1046 | report_task sweeps the subnet |
| Exploitation of Remote Services | T1210 | FTP USER stack overflow against hosts found |
| Obfuscated Files or Information | T1027 | Shellcode encoded with shikata_ga_nai |
| Non-Application Layer Protocol | T1095 | Raw socket reverse shell to port 443 |
Firmware images are executables and need provenance checks. The device accepted an image from a compromised supply chain. Signed firmware with secure boot and flash encryption on the ESP32 would have rejected the trojanised build.
Credentials in NVS and rodata are readable to anyone with a flash dump. The Wi-Fi password was recoverable in two places with strings. Use a dedicated IoT SSID with a rotated, per-device key, and enable NVS encryption so a lost or backed-up image does not leak the corporate network key.
IoT devices that scan the subnet are visible on the wire. A sensor that normally talks to one collector suddenly probing many hosts and opening FTP sessions with 2000-byte usernames is easy to catch. Put sensors in a segment that only allows traffic to the collector and alert on any other egress.
Legacy FTP is still the target. The payload exploits a stack overflow in an FTP server’s USER handling. Retire or patch internal FTP services, and use IDS signatures for oversized FTP commands.
A reverse shell on 443 still stands out by destination. The callback goes to an internal address on the HTTPS port. Egress allow-lists keyed on destination, not just port, catch it.
The sensor was turned into an internal scanner with a ready-made FTP exploit and a reverse shell to a fixed address, all hidden in one firmware image. The decisive evidence sat in plain strings and one encoded blob, so signed firmware and segmented IoT networks are what would have stopped it.