New Threat: A Deep Dive Into the Zergeca Botnet

New Threat: A Deep Dive Into the Zergeca Botnet

Background

On May 20, 2024, while everyone was happily celebrating the holiday, the tireless XLab CTIA(Cyber Threat Insight Analysis) system captured a suspicious ELF file around 2 PM, located at /usr/bin/geomi. This file was packed with a modified UPX, had a magic number of 0x30219101, and was uploaded from Russia to VirusTotal, where it was not detected as malicious by any antivirus engine.

Later that evening at 10 PM, another geomi file using the same UPX magic was uploaded to VT from Germany. The suspicious file path, modified UPX, and multi-country uploads caught our attention. After analysis, we confirmed that this is a botnet implemented in Golang. Given that its C2 used the string "ootheca," reminiscent of the swarming Zerg in StarCraft, we named it Zergeca.

Functionally, Zergeca is not just a typical DDoS botnet; besides supporting six different attack methods, it also has capabilities for proxying, scanning, self-upgrading, persistence, file transfer, reverse shell, and collecting sensitive device information. From a network communication perspective, Zergeca also has the following unique features:

  • Supports multiple DNS resolution methods, prioritizing DOH for C2 resolution.
  • Uses the uncommon Smux library for C2 communication protocol, encrypted via XOR.

During the investigation of Zergeca's infrastructure, we found that its C2 IP address, 84.54.51.82, has been serving at least two Mirai botnets since September 2023. We speculate that the author behind Zergeca accumulated experience operating the Mirai botnets before creating Zergeca.

On June 10, XLab command tracking system captured a vector 7 DDoS command that the current samples did not support, indicating that Zergeca's author is actively developing and updating, with new samples yet to be discovered. Our persistence paid off when we captured a new sample on the 19th that supports the vector 7. Currently, the detection rates for Zergeca samples and C2 are very low. Considering Zergeca's potential threat in DDoS attacks, we have decided to release this article to share our findings with the community.

Sample & C2 Detection

From the sample perspective, we captured a total of 5 Zergeca samples. While their functions are nearly identical, there is a significant discrepancy in their detection rates. How can this anomaly be explained? Most antivirus vendors have categorized the sample 23ca4ab1518ff76f5037ea12f367a469 as Generic Malware. We speculate that the detection of Zergeca by antivirus software is based on file hash. Therefore, as long as the hash changes, the detection effectiveness diminishes.

MD5 Detection First Seen Telemetry
23ca4ab1518ff76f5037ea12f367a469 28/64 2024.05.20 Russian
9d96646d4fa35b6f7c19a3b5d3846777 0/67 2024.05.20 Germany
d78d1c57fb6e818eb1b52417e262ce59 1/67 2024.05.22 China
604397198f291fa5eb2c363f7c93c9bf 1/66 2024.06.11 France
60f23acebf0ddb51a3176d0750055cf8 0/67 2024.06.18 France

To verify our hypothesis, we appended the 4-byte string "Xlab" to the end of the file 23ca4ab1518ff76f5037ea12f367a469 and re-uploaded it to VirusTotal. The detection rate changed to 9/67, partially confirming our speculation.

geomi_patch.png

Additionally, the current detection is based on the packed samples, after unpacking, the detection rate drops to 0.

geomi_unpack.png

From the Domain Perspective, the four samples share two C2 domains that were created on the same day. The samples prioritize using DOH (DNS over HTTPS) for C2 resolution, which obscures the relationship between the samples and the C2 domains to some extent. Because of this, VirusTotal couldn't even associate the C2 domains with the samples, resulting in a naturally low detection rate.

Domain Detection Create date
ootheca.pw 1/93 2024.04.28
ootheca.top 1/93 2024.04.28

Profile of 84.54.51.82

The two C2 servers of Zergeca point to the same IP address, 84.54.51.82. According to our data, this IP has been in use since September 2023, serving a variety of roles. During this period, it has acted as a Scanner, Downloader, Mirai botnet C2, and Zergeca botnet C2.

Scanner

Starting from September 18, 2023, scanning activities commenced, primarily targeting protocols such as Telnet, HTTP, and socks4. The main ports scanned include23, 8080, 3128, 80, and 8888.

geomi_scan.png

Mirai Downloader&C2

From September and October 2023 to April 2024, 84.54.51.82 was primarily used as the Loader IP and Downloader IP for the Mirai botnet.

  • 2023.09 - 2023.10, it was used as the Loader and Downloader IP to implant the following related samples.

     #Downloader
    
    http://84.54[.51.82/jaws
    http://84.54[.51.82/bin
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.x86
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.spc
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.sh4
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.ppc
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.mpsl
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.mips
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.m68k
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.i686
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.arm7
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.arm6
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.arm5
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.arm
    http://84.54[.51.82/596a96cc7bf9108cd896f33c44aedc8a/db0fa4b8db0333367e9bda3ab68b8042.arc
    
    #CC
    
    mirai://bot.hamsterrace.space:59666
    
  • 2024.04, it was used as the Loader IP to implant the following related samples.

    #Downloader
    http://145.239[.108.150/Fantazy.sh
    http://145.239[.108.150/Fantazy/Fantazy.arm5
    http://145.239[.108.150/Fantazy/Fantazy.arm6
    http://145.239[.108.150/Fantazy/Fantazy.mpsl
    http://145.239[.108.150/Fantazy/Fantazy.sh4
    http://145.239[.108.150/Please-Subscribe-To-My-YT-Channel-VegaSec/1isequal9.x86
    http://145.239[.108.150/cache
    
    # CC
    
    mirai://145.239.108.150:63645
    

Zergeca C2

Starting from April 29, 2024, 84.54.51.82 began being used as the C2 server for Zergeca. The relevant C2 domains and their resolution records are as follows:

Zergeca.rdata.png

Exploits

In our observation, the primary methods used by 84.54.51.82 to propagate samples are Telnet weak passwords and certain known vulnerabilities. The relevant vulnerability identifiers are as follows:

Telnet Weak Password
CVE-2022-35733
CVE-2018-10562
CVE-2018-10561
CVE-2017-17215
CVE-2016-20016

DDoS Statistics

From early to mid-June 2024, the Zergeca botnet primarily targeted regions such as Canada, the United States, and Germany. The main type of attack was ackFlood (atk_4), with victims distributed across multiple countries and different ASNs.

Zergeca.ddos.png

Reverse Analysis

The four Zergeca samples in our observation are all designed for the x86-64 CPU architecture and target the Linux platform. The presence of strings like "android," "darwin," and "windows" in the samples, along with Golang's inherent cross-platform capabilities, suggests that the author may eventually aim for full platform support.

This article focuses on the earliest captured sample for detailed analysis. The sample is packed with UPX and has a magic number of 0x30219101. For this type of modified UPX packer, simply changing the magic back to the standard "UPX!" allows for unpacking with the command upx -d.

MD5:23ca4ab1518ff76f5037ea12f367a469
Mgaic:ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, corrupted section header size
Packer: UPX
Version:0.0.01c

After unpacking, it becomes evident that Zergeca is a botnet implemented in Go language. The symbols are not obfuscated, making reverse analysis relatively straightforward.

geomi_main.png

The figure above shows a code snippet of the main_main function. Functionally, it can be broken down into four distinct modules. The persistence and proxy modules are self-explanatory, with the former ensuring persistence and the latter handling proxying. The silivaccine module is used to remove competing malware, ensuring exclusive control over the device. The most crucial module is zombie, which implements the full botnet functionality. It reports sensitive information from the compromised device to the C2 and awaits commands from the C2, supporting six types of DDoS attacks, scanning, reverse shell, and other functions.

0x00: String Decryption

Zergeca uses XOR encryption for many sensitive strings. Using IDA, we found that the XOR key is referenced 240 times across various functions. Each decryption involves two uses of the XOR key: one for initialization and one for decryption. So there are 120 decryption operations needed.
geomi_xorref.png

The XOR key is initially set to EC 22 2B A9 F3 DD DF 1C CD 46 AC 1E, but only the first six bytes (EC 22 2B A9 F3 DD) are used.

geomi_xor.png

Manually decrypting 120 times is impractical. Although the decryption process isn't confined to a single function, CFG analysis revealed a specific pattern in most decryption-related code blocks:

  1. The XOR block has one predecessor and one successor.
  2. The predecessor block's first instruction is mov, with the first operand being an address pointing to the original length of the XOR key.
  3. The successor block's first instruction is cmp, with the first operand being a number indicating the ciphertext's length.
  4. The predecessor block's predecessor's first instruction is lea, with the first operand being an address pointing to the ciphertext's starting address.

geomi_blocks.png

By identifying these patterns, we can automate the decryption process and restore all encrypted strings efficiently.We implemented IdaPython decryption script in the Appendix with the following results: 111 successful decryptions and 9 mismatches.

geomi_example.png

The 9 mismatched codes are distributed across six functions. Among them, the packets__Cursor Read/WriteString functions handle network packet encryption/decryption and can be ignored.

gomi_bot_zombie__Zombie_Connect
geomi_common_utils_init_0_func1, 
geomi_bot_discovery_Run, 
geomi_common_packets__Cursor_WriteString,
geomi_common_packets__Cursor_ReadString, 
geomi_common_utils_RandomUserAgent

For the remaining four functions, the issue was that the ciphertexts were arrays rather than single entries, causing the pattern match to fail. For example, in the RandomUserAgent function, the user_agent_list contains 1000 encrypted user agents.

geomi_agent.png

For such cases, we can use the manual_decode function, where the first parameter is the starting address of the ciphertext array and the second parameter is the number of array elements.

ey=b"\xEC\x22\x2B\xA9\xF3\xDD"

def manual_decode(base,cnt):
    for i in range(cnt):
        start=idc.get_qword(base)
        addr=idc.get_qword(start+i*16)
        size=idc.get_qword(start+8+i*16)
        buff=idc.get_bytes(addr,size)
        out=bytearray()
        for k,v in enumerate(buff):
            out.append(v ^ key[k%6])
        print(out.decode())

manual_decode(0x000000000C56FA0,1000)  #user agent
manual_decode(0x0000000000C56F80,0xc)  #opennic dns
manual_decode(0x000000000C56C40,2) # c2

Decrypted examples include various user agents, OpenNIC DNS server, and C2s.

geomi_manual.png

With all strings successfully decrypted, we can now begin reverse-engineering Zergeca's various functionalities.

0x01: Persistence Module

Zergeca achieves persistence on compromised devices by adding a system service geomi.service. This service ensures that the Zergeca sample automatically generates a new geomi process if the device restarts or the process is terminated.

[Unit]
Description=
Requires=network.target
After=network.target
[Service]
PIDFile=/run/geomi.pid
ExecStartPre=/bin/rm -f /run/geomi.pid
ExecStart=/usr/bin/geomi
Restart=always
[Install]
WantedBy=multi-user.target

Experiment A

When running the Zergeca sample on a virtual machine and restarting the device, geomi.service automatically launches the Zergeca sample. The resulting process named geomi had a PID of 897. Terminating this process with kill -9 897 immediately spawned a new geomi process with PID 8460.

geomi_kill.png

When network administrators discover a geomi process and suspicious traffic on a device, they can attempt the following cleanup steps:

  1. Delete /etc/systemd/system/geomi.service
  2. Delete the sample file referenced by the ExecStart parameter
  3. Terminate the geomi process

0x2: Silivaccine Module

To monopolize the device, Zergeca includes a list of competitor threats, covering miners, backdoor trojans, botnets, and more. Some familiar names on the list include mozi, kinsing, and various mining pools. Zergeca continuously monitors the system and terminates any process whose name or runtime parameters match those on the list, deleting the corresponding binary files.

Mozi.a com.ufo.miner kinsing kthreaddi
kaiten srv00 meminitsrv .javae
solr.sh monerohash minexmr c3pool
crypto-pool.fr f2pool.com xmrpool.eu .........

Experiment B

We renamed the system program /bin/sleep to Mozi.a and ran it. The Mozi.a process was killed, and the corresponding binary file was deleted.

geomi_remove.png

0x3: Zombie Module

Zergeca resolves the C2 IP address using the geomi_common_utils_Resolve function, which supports four resolvers: Public DNS, Local DNS, DoH (DNS over HTTPS), and OpenNIC.

geomi_resolve.png

Zergeca prioritizes two DoH resolvers, masking C2 domain resolution in DNS traffic.

https://cloudflare-dns.com/dns-query
https://dns.google/resolve

geomi_dns.png

After obtaining the C2 IP, the bot reports device sensitive information encapsulated in a DeviceInfo structure, including details like "country, public IP, OS, user groups, runtime directory, and reachability".

struct DeviceInfo
{
Country string
PlucAddress byte[]
MAC string
OS string
ARCH string
Name string
MachineId string
Numcpu uint32
CPUMODEL string
username string
uid string
gid string
Users []string
Uptime time.Duration
PID	uitn32
Path string
checksum []uint8
version string
Reachable bool
}

The bot then awaits commands from the C2, processing them with different handlers.

geomi_func.png

The supported functions are as follows:

ID Task
0x01 Proxy
0x02 Reverse Shell
0x03 FileTransfer
0x05 Self-update
0xa0 DDoS
0xb0 Stop Discovery
0xb1 Start Discovery

The DDoS functionality supports the following seven attack vectors:

Sub-ID Attack Vector
1 minecraft
2 httpPPS
3 synFlood
4 ackFlood
5 pushFlood
6 rstFlood
7 pushOVHFlood

Communication Protocol

Zergeca uses smux for Bot-C2 communication. Smux(Simple MUltipleXing) is a Golang multiplexing library that relies on underlying connections like TCP or KCP for reliability and ordering, providing stream-oriented multiplexing. Smux packets feature an 8-byte header: VERSION(1B) | CMD(1B) | LENGTH(2B) | STREAMID(4B) | DATA(LENGTH).

From an analysis perspective, only the LENGTH and DATA fields are of primary concern. The captured traffic includes various messages such as online status, device information reporting, command 0xb0, and heartbeat messages.

geomi_packet.png

Online Message:

  • Length: 0x04 bytes
  • Content: Hardcoded 13 3a 12 79

Device Info Report:

  • Length: 0xd5 bytes (varies by device)
  • Content (excluding IP): XOR encrypted with key EC 22 2B A9 F3 DD
  • Decrypted DeviceInfo as follows
    pos: 0x4 len: 0x2 <----> b'JP'
    pos 0x7 len: 4 <----> 45.14.XX.XX
    pos: 0xc len: 0x11 <----> b'72:ba:29:e9:b8:08'
    pos: 0x1f len: 0x5 <----> b'linux'
    pos: 0x26 len: 0x5 <----> b'amd64'
    pos: 0x2d len: 0x6 <----> b's22262'
    pos: 0x35 len: 0x20 <----> b'b19642a3c672d4f20cbdb5b1569bf98f'
    pos: 0x5b len: 0x29 <----> b'Intel(R) Xeon(R) CPU E5-2678 v3 @ 2.50GHz'
    pos: 0x86 len: 0x4 <----> b'root'
    pos: 0x86 len: 0x4 <----> b'root'
    pos: 0xa2 len: 0x2 <----> b'\x92\xf1'
    pos: 0xa6 len: 0xe <----> b'/usr/bin/geomi'
    pos: 0xb6 len: 0x14 <----> b'r\xbd>\xcfY\x15[\xd9]\xa4\xe7m\x86\x9f\xbf\x895\xaa\x19\xe8'
    pos: 0xcc len: 0x7 <----> b'0.0.01c'
    

Command 0xb0 Message:

  • Length: 0x08 bytes
  • Function: Stop scanning

Heartbeat Message:

  • Length: 0x03 bytes
  • Content: ff 00 00

Let's take a look at the DDoS-related packets. The format is cmd (1 byte) + length (2 bytes) + sub_cmd (1 byte) + target_info (length-1), where cmd is 0xa0, indicating a DDoS command, and sub_cmd is 0x4, indicating an ACK flood attack. The target_info field focuses on the first 4 bytes, which represent the target IP. For example, 1f 06 10 21 corresponds to the IP address 31.6.16.33.

geomi_ddos.png

When the Bot receives the aforementioned command, the resulting attack traffic aligns perfectly with our analysis.

geomi_ack.png

Experiment C

Based on our network protocol analysis, we implemented a fake C2 to control the Bot and observe its behavior upon receiving different commands. In this experiment, we sent the Bot a 0xb1 command, which is to "start scanning."

geomi_fakec2.png

Upon receiving this command, the Bot immediately began scanning 16 ports on randomly generated IP addresses.

geomi_syn.png

Summary

Through reverse analysis, we gained initial insights into Zergeca's author. The built-in competitor list shows familiarity with common Linux threats. Techniques like modified UPX packing, XOR encryption for sensitive strings, and using DoH to hide C2 resolution demonstrate a strong understanding of evasion tactics. Implementing the network protocol with Smux showcases their development skills. Given this combination of operational knowledge, evasion tactics, and development expertise, encountering more of their work in the future would not be surprising.

This is our basic intelligence of Zergeca. We welcome unique insights from other companies, such as Init Access. And readers can contact us on Twitter for more details.

IOC

Sample

23ca4ab1518ff76f5037ea12f367a469
9d96646d4fa35b6f7c19a3b5d3846777
d78d1c57fb6e818eb1b52417e262ce59
604397198f291fa5eb2c363f7c93c9bf

f68139904e127b95249ffd40dfeedd21
d7b5d45628aa22726fd09d452a9e5717
6ac8958d3f542274596bd5206ae8fa96

pathced with "xlab" at the end of file
980cad4be8bf20fea5c34c5195013200

sample captured on 2024.06.19, support ddos vector 7
60f23acebf0ddb51a3176d0750055cf8

Domain

ootheca.pw
ootheca.top
bot.hamsterrace.space

IP

84.54.51.82	The Netherlands|None|None	AS202685|Aggros Operations Ltd.

Appendix

IdaPython Script

# Test script, only for 23ca4ab1518ff76f5037ea12f367a469
# Modidy keyaddr,sizeaddr in your case

def decode(buf):
    key=b"\xEC\x22\x2B\xA9\xF3\xDD"
    out=bytearray()
    for i in range(len(buf)):
        out.append(buf[i]^key[i%6])
    return out
    
count=0
notcount=0
failedfunc=[]
successedfunc=[]

keyaddr=0x0000000000C56FC0
sizeaddr=0x0000000000C56FC8

refs=XrefsTo(keyaddr, flags=0)
for ref in refs:
    f_blocks = idaapi.FlowChart(idaapi.get_func(ref.frm), flags=idaapi.FC_PREDS)
    for blk in f_blocks:
        if blk.start_ea!=ref.frm:
            continue
        if len(list(blk.preds()))!=1 and len(list(blk.succs()))!=1:
            continue
        predblk=list(blk.preds())[0]
        succsblk=list(blk.succs())[0]
        
        if idc.get_operand_value(predblk.start_ea,1)!=sizeaddr:
            
            continue
        if idc.get_operand_type(succsblk.start_ea,1)!=0x5:
            print(idc.get_func_name(ref.frm),hex(ref.frm),"not matched")
            notcount+=1
            failedfunc.append(idc.get_func_name(ref.frm))
            continue
        ppredblk=list(predblk.preds())
        if len(ppredblk)!=1:
            continue
        addr=idc.get_operand_value(ppredblk[0].start_ea,1)
        size=idc.get_operand_value(succsblk.start_ea,1)
        buf=idc.get_bytes(addr,size)
        out=decode(buf)
        count+=1
        print(idc.get_func_name(ref.frm),hex(ppredblk[0].start_ea),"matched, ciphertext at", hex(addr), "<---->",bytes(out))
        successedfunc.append(idc.get_func_name(ref.frm))

print("\n--------------------Statistic--------------------")
print(f'Success:{count},Failed:{notcount}\n')
print("---------Success Function---------")
print(set(successedfunc),'\n')
print("---------Failed Function---------")
print(set(failedfunc),'\n')