1. Abstract
My colleague sended me a latest LSDMiner sample(MD5: 114d76b774185b826830cb6b015cb56f) in mid-October. I noticed a DNS TXT and DoH(DNS over HTTPS) module with AES decryption by simple reverse engineering analysis. Then I moved on to something else without deep analysis. I got start to deal with this sample in the last few days, and found that Anomali has published a blog post about this case by Googling a function name used in the sample as NewAesCipher128() :
Anomali’s blog post: Illicit Cryptomining Threat Actor Rocke Changes Tactics, Now More Difficult to Detect
But Anomali Labs didn’t detail the process of DNS TXT record data decryption, that’s why I write this blog post.
This sample is still written in Go language as same as it’s old version. But the coding architecture and functianlity internally has changed a lot, significant differences sumerized as below:
- Moves it’s malicious shell script from Pastebin to it’s own CC servers(
*.systemten.org
) - Integrates multiple exploits to speed and broaden it’s propagation
- Transports multiple kinds of malicious encrypted data via DNS TXT record:
- Latest malicious shell script downloading URL for Cron job
- Lastet version info
- Latest malicious shell script
- A group of downloading URLs of malicious binary files
Analysis of main aspects of this threat actor could be done by general approaches of threat analysis, and was covered by Anomali’s blog post. I will take 114d76b774185b826830cb6b015cb56f as example to decribe details of decryption of DNS TXT record data received by this threat actor in this post.
2. Overall execution flow
The overall execution flow could be sumeraized as 3 steps:
- Lookups DNS TXT record via DNS request or DoH, and decrypts the data with AES128bit to extract malicious URL for Cron job.
- Scan current /16 IP Block, and compromise alive hosts via 4 methods:
- SSH brute
- Redis Unauthorized Access
- Jekins RCE CVE-2019-1003000 Exploit
- ActiveMQ RCE CVE-2016-3088 Exploit
- Release cryptomining program and achieve persistence on the exploited machines
By the way, the sample will also lookup the DNS TXT records to get malicious shell script and URL to download binary file at the 3rd step.
Firstly, let’s look at the overall process of lookuping DNS TXT record and tampering cron job:
We can see that the sample lookups DNS TXT record from cron.iap5u1rbety6vifaxsi9vovnc9jjay2l.com
and then decrypts the record data with AES-128bit. I lookuped this DNS TXT record with command dig as below:
The response of DNS TXT request is a Base64 encoded string A7PZtADnYAEMEArGhmA9xQihPq9TRz4QigssjeOmUnQ , the codes in function github_com_hippies_LSD_LSDC__AesCipher128_Decrypt() is responsible for decoding this string:
Taking all the factors above, we could come to conclusion that there’re 3 steps to accomplish the decryption task:
- Decodes the raw DNS TXT response string with Base64
- Initialize the AES-128bit decryption handle
- Decrypts the binary bytes generated by Base64 decoding with AES-128bit
3. Base64 Decoding
Now I use command base64 within Linux to decode the raw DNS TXT record response:
It’s a bit odd that decoding failed. So I guess it’s not encoded by the standard Base64 encoding. Here are 2 points of background knowledge of Base64 encoding:
- There’re 2 encoding types for Base64: Standard Encoding and URL Encoding. The alphabet for Standard Encoding is ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ and ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ for URL Encoding. (refer: RFC4648)
- The 2 types of Base64 encoding share the same default padding character, that is =. However they all could padd nothing.
These 2 points has been shown at Go doc for Base64:
So, 2 kinds of padding style and 2 types of encoding, then there’re 4 subdivision types of Base64 encoding:
Then which kind of Base64 encoding dose LSDMiner use? We should check how the Base64 handler is initialized in this sample. As the screenshot picture shows below, the sample implements the decoding operation with Base64 encoding handler b64EncodingObj in function github_com_hippies_LSD_LSDC__AesCipher128_Decrypt().
And the Base64 encoding handler is initialized in function encoding_base64_init() as IDAPro xrefs demonstrates. Here is the detail of function encoding_base64_init() :
Two key points:
- The sample passes URLEncodings alphabet to function base64.NewEncoding(), thus it uses URLEncoding style base64 encoding;
- The samples passes -1 to function base64.URLEncoding.WithPadding(), aka base64.NoPadding, thus base64.RawURLEncoding.
Now I can decoding the DNS TXT record response string with these testing codes:
4. AES Decrytion
We know that the sample will decrypt those binary data with AES-128bit decoded by Base64 encoding as metioned above. Then we should make 4 points clear at first to leverage AES algorithm correctly:
- AES Key
- AES Initialization Vector(IV)
- Encryption mode(CBC/ECB etc.)
- Padding method
According to analysis above, we can notice that the sample calls a function related to AES decryption named crypto_cipher_NewCBCDecrypter(), so we can confirm that the AES encryption mode is CBC).
Now we confirm other 3 points next by analyzing function NewAesCipher128() which initializes the AES decryption handler and AesCipher128_Decrypt() which takes the decryption operation.
4.1 function NewAesCipher128()
Firstly, this function is passed to an argument, which is the domain name string that will be lookuped DNS TXT record. The domain name string for malicious cron job is cron.iap5u1rbety6vifaxsi9vovnc9jjay2l.com
:
Then this function initializes a crypto/md5 digest handler, we can confirm this by comparing the standard Go library function crypto_md5_New() at the right side of the screenshot below:
And the domain name string will be converted to byte slice and written to MD5 digest handler, then the sample executes the 1rst round of MD5 hash calculation with md5.degets.Sum(nil):
When the 1rst round MD5 hash value is calculated, it will be converted to 32-Bytes string by hex.EncodeToString(), that’s the normal MD5 hash string. And this MD5 value string is cut to two halves, the first half(16-Bytes) will be saved to variable which I name it as r1HashStr_16bytes:
The next step is the 2nd round of MD5 hash calculation also by calling md5.degets.Sum(nil) without writing any bytes to the MD5 digest. This 2nd MD5 hash value will also be cut to two halves, but this time the 2nd half will be save to anather variable which I name it as r2HashStr_16bytes.
At last, we will see that the r1HashStr_16bytes is passed to function aes.NewCipher() as AES Key, to initialize AES decryption handler:
And the r2HashStr_16bytes will be return by this function, and passed to later function AesCipher128_Decrypt(). AesCipher128_Decrypt() will call crypto_cipher_NewCBCDecrypter() , with this r2HashStr_16bytes as AES IV.
4.2 AES Padding method
After analyzing the AES encryption mode, AES Key and AES IV, we have the last key point left to decrypt the DNS TXT record correctly, that’s the Padding method).
And we can confirm that this sample uses simple ZeroPadding from function AesCipher128_Decrypt() , because it calls byte.Trim() after data decryption:
4.3 Additional specification– 2 rounds MD5 hash calculation
As metioned above, the sample takes 2 Rounds of MD5 Calculation operation to generate AES Key and AES IV separately. Anomali’s blog post metioned this process too. But there’s a unexpected problem due to Go programming language’s standard MD5 Sum() function: the two MD5 hash values of two rounds of calculation are equal.
I show this problem by this piece of codes:
I’m not sure about this but whether by design or just due to the malware author’s misunderstanding about Go crypto/md5 libary. But this acctually could confuse malware researcher to consider two different MD5 hash values by two rounds of calculation.
5. Complete decryption
Based on the above analysis, we could write some code to complete the decryption work. My Go program has been uploaded to Github:
https://github.com/0xjiayu/LSDMiner_DNS_TXT_Decrypt
And that’s the show case:
The current decrypted text is a domain name string: lsd.systemten.org
, which is also the default value if the sample failed to lookup the DNS TXT record or failed to decrypt these data:
6. Conclusion
The screenshot in the head of this blog post shows that the sample will jump to another code branch to get DNS TXT record from CloudFlare’s DNS Server via DoH(DNS over HTTPS), when net.LookupTXT() failed.
We can confirm this method also works correctly:
It will make this botnet more robust and flexible with the assistance of encrypted DNS TXT record and DoH. In view of the facts that the botnet has existed for a long time and updates continuesly, I think it should cause community’s attention.
I’ve metioned above that the sample transports other kinds of malicious encrypted data via DNS TXT record except for malicious cron URL. The process of lookuping DNS TXT record, decoding data with Base64 and decrypting data with AES-128bit are all the same. There’re more domain names and they all have been covered by my Go program uploaded to Github before:
|
|
At last, I would add more details aboud the Packer of this LSDMiner family’s malicious binary files. Almost all of it’s binary files are packed by malformed UPX Packer, and the packer’s feature is not so distinctive to wirte detection rules easily. Besides, it’s packer’s Magic Number has always been changed. For example, the sample I analyze in this blog post(MD5: 114d76b774185b826830cb6b015cb56f) gets packed with Magic Number 0x2124922A, while another sample (MD5: 78e3582c42824f17aba17feefb87ea5f) gets packed with Magic Number 0x215E77F2.