In this post I will analyze one on the ELF files captured on my honeypot. First, a dynamic analysis will be performed. Once we aknowledge it's behaviour we will move onto a more in-deep static analysis.

Let's start!


We are presented with a 32-bit ELF un-stripped executable.

$ file 05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf 
05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.2.5, not stripped, too many notes (256)  

Analyzing the network traffic

Executing the malware sample with Wireshark listening on our routing machine shows that our sample is trying to contact server 218.2.0.127 and port 48080.

IP Geolocation yields the following

And it looks like there is no domain associated to the IP address

$ dig 218.2.0.127

; <<>> DiG 9.10.3-P4-Debian <<>> 218.2.0.127
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 629
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

At the time of writting this article the server looks up but the port 48080 seems closed.

$ nmap 218.2.0.127 -p 48080

Starting Nmap 6.47 ( http://nmap.org ) at 2016-11-30 11:14 PST  
Nmap scan report for 218.2.0.127  
Host is up (0.53s latency).  
PORT      STATE  SERVICE  
48080/tcp closed unknown

Nmap done: 1 IP address (1 host up) scanned in 2.56 seconds  

Maybe it has some kind of authentication mechanism as port knocking, but I don't think so, as the server maintainers didn't try to hide open ports at all.

$ nmap 218.2.0.127
PORT     STATE    SERVICE      VERSION  
80/tcp   open     http         Microsoft IIS httpd  
135/tcp  filtered msrpc  
139/tcp  filtered netbios-ssn  
445/tcp  filtered microsoft-ds  
1026/tcp open     msrpc        Microsoft Windows RPC  
1723/tcp filtered pptp  
8899/tcp open     ospf-lite?  
Service Info: OS: Windows; CPE: cpe:/o:microsoft:windows  

This is a non-standard port, so in order to force a successful connection with our analysis machine we can use nc and iptables instead of inetsim.

Redirecting network traffic to us

The following iptables rule will redirect all outgoing network traffic to us.

# iptables -t nat -A PREROUTING -i eth0 -j REDIRECT

If we now set nc to listen on port 48080 we will get the following:

# nc -l -p 48080
VERSONEX:Linux-4.2.0-42-generic|1|3499 MHz|975MB|874MB|Hacker Tue Jun 28 21:26:26 UTC 2016$�ֻ���  
  xӻ��ػ�o����X�ẁ��������#+����
                               ��� Yh����� p��ջ�SXPF�@INFO:3%|0.00 MbpsINFO:0%|0.00 MbpsINFO:0%|0.00 MbpsINFO:0%|0.00 MbpsINFO:0%|0.00 MbpsINFO:1%|0.00 MbpsINFO:0%|0.00 MbpsINFO:0%|0.00 MbpsINFO:0%|0.00 MbpsINFO:0%|

What we can gather:

  • The sample sends the machine's kernel version
  • The sample sends out hardware specs
  • The strings VERSIONEX: and Hacker appear to be hardcoded, as they are constant through multiple executions
  • It sends the current time
  • It periodically reports CPU and network capacity
  • Some binary, non-readable data is sent

From nc we can send data back, but we don't know what to send as this is some kind of custom procol of communication. We can try running the sample against the real server, but the port appears to be really closed as there are no successful connections.

Tracing the execution

Note that for each sample execution the virtual machine has been reset to an state before the infection happened.

We can try to trace the sample with strace

$ strace -s 100 -f -i ./05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf 
[00007ff6d7551277] execve("./05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf", ["./05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf"], [/* 59 vars */]) = 0
[ Process PID=4798 runs in 32 bit mode. ]
[0807f81e] fcntl64(0, F_GETFD)          = 0
[0807f81e] fcntl64(1, F_GETFD)          = 0
[0807f81e] fcntl64(2, F_GETFD)          = 0
[0807ec0d] uname({sys="Linux", node="ubuntu", ...}) = 0
[0807ef84] geteuid32()                  = 1000
[080a4254] getuid32()                   = 1000
[080a432c] getegid32()                  = 1000
[080a42c0] getgid32()                   = 1000
[0807fa33] getrlimit(RLIMIT_STACK, {rlim_cur=8192*1024, rlim_max=RLIM_INFINITY}) = 0
[0807fafb] setrlimit(RLIMIT_STACK, {rlim_cur=2044*1024, rlim_max=RLIM_INFINITY}) = 0
[0807ef07] getpid()                     = 4798
[08065588] rt_sigaction(SIGRTMIN, {0x8061c98, [], SA_RESTORER, 0x80654f8}, NULL, 8) = 0
[08065588] rt_sigaction(SIGRT_1, {0x8061bb4, [], SA_RESTORER, 0x80654f8}, NULL, 8) = 0
[08065588] rt_sigaction(SIGRT_2, {0x8061d04, [], SA_RESTORER, 0x80654f8}, NULL, 8) = 0
[0806561d] rt_sigprocmask(SIG_BLOCK, [RTMIN], NULL, 8) = 0
[08080107] _sysctl({0x2080bdcc4, -1256928, (nil), (nil), (nil), 0}) = 0
[080a45f1] brk(0)                       = 0xa0cd000
[080a45f1] brk(0xa0cd030)               = 0xa0cd030
[080a45f1] brk(0xa0ce000)               = 0xa0ce000
[0807f8d4] readlink("/proc/self/exe", "/home/fernando/05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf", 1024) = 79
[0807ee77] fork()                       = 4799
[0807ee9d] _exit(0)                     = ?

We can see that it reads it's virtual proc identifier and forks, but for some reason strace does not follow the fork even though it was executed with the -f option. Same goes for -ff. The pid returned for the fork is 4799.

Checking running processes for the executable yields the following.

$ ps aux | grep 05fd2938
fernando   4800  5.0  0.9  12292  9972 ?        S    12:28   0:00 ./05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf  
fernando   4801  0.0  0.9  12292  9972 ?        S    12:28   0:00 ./05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf  
fernando   4802  0.0  0.9  12292  9972 ?        S    12:28   0:00 ./05fd293845e7517bcfc6e8a7fa845ef101bf716c5ec6d40c74c6f7e8aed656bf  

The sample has spawned three processes which is using to communicate to the server, amogst other stuff. The pid returned by fork is not on the list, so something pesky is going with that process.

We can, however, attach to the running processes, to try to see what they are up to.

Note: PIDs have changed because I restored the VM state

$ sudo strace -f -s 100 -i -p 2335
[0807fc4e] select(4, [3], NULL, NULL, {0, 0}) = 0 (Timeout)
... The same line is repeated over and over

According to the linux man pages this checks if the file descriptor with id 3 has blocks available for read.

$ sudo strace -f -s 100 -i -p 2336
Process 2336 attached  
[ Process PID=2336 runs in 32 bit mode. ]
[0807f908] restart_syscall(<... resuming interrupted call ...>) = 0
[0807ef27] getppid()                    = 2335
[0807f908] poll([{fd=0, events=POLLIN}], 1, 2000) = 0 (Timeout)
[0807ef27] getppid()                    = 2335
[0807f908] poll([{fd=0, events=POLLIN}], 1, 2000) = 0 (Timeout)
[0807ef27] getppid()                    = 2335
[0807f908] poll([{fd=0, events=POLLIN}], 1, 2000) = 0 (Timeout)
[0807ef27] getppid()                    = 2335
[0807f908] poll([{fd=0, events=POLLIN}], 1, 2000) = 0 (Timeout)

Also according to the man pages poll waits for some event in a file descriptor. In this case it is waiting for the event POLLIN (there is data awaiting to be read) on fd 0 (stdin). The 1 parameter means there is only one fd to watch and 2000 is the timeout in milliseconds. In short, this process is waiting for keyboard input.

[0807f58d] close(4)                     = 0
[0807fd01] munmap(0xf7773000, 4096)     = 0
[0807f534] open("/proc/cpuinfo", O_RDONLY) = 4 <-------
[0807f4a1] fstat64(4, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
[0807fcdd] old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7773000
[0807f5b4] read(4, "processor\t: 0\nvendor_id\t: GenuineIntel\ncpu family\t: 6\nmodel\t\t: 60\nmodel name\t: Intel(R) Core(TM) i7-"..., 1024) = 930
[0807f5b4] read(4, "", 1024)            = 0
[0807f58d] close(4)                     = 0
[0807fd01] munmap(0xf7773000, 4096)     = 0
[0806561d] rt_sigprocmask(SIG_BLOCK, [CHLD], [RTMIN], 8) = 0
[08065588] rt_sigaction(SIGCHLD, NULL, {SIG_IGN, [CHLD], SA_RESTORER|SA_RESTART, 0x80654f8}, 8) = 0
[0807ee61] nanosleep({1, 0}, 0xff1ff1d4) = 0
[0806561d] rt_sigprocmask(SIG_SETMASK, [RTMIN], NULL, 8) = 0
[0807f534] open("/proc/stat", O_RDONLY) = 4  <-------
[0807f4a1] fstat64(4, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0
[0807fcdd] old_mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7773000
[0807f5b4] read(4, "cpu  2025 2080 2407 482902 9734 0 92 0 0 0\ncpu0 2025 2080 2407 482902 9734 0 92 0 0 0\nintr 306354 18"..., 1024) <-------
...
[0807f58d] close(4)                     = 0
[0807fd01] munmap(0xf7773000, 4096)     = 0
[08080312] send(3, "INFO:99%|0.00 Mbps\0", 19, 0) = 19  <------- 
[0806561d] rt_sigprocmask(SIG_BLOCK, [CHLD], [RTMIN], 8) = 0
[08065588] rt_sigaction(SIGCHLD, NULL, {SIG_IGN, [CHLD], SA_RESTORER|SA_RESTART, 0x80654f8}, 8) = 0
[0807ee61] nanosleep({1, 0}, 0xff1ff944) = 0
[0806561d] rt_sigprocmask(SIG_SETMASK, [RTMIN], NULL, 8) = 0
[0807f534] open("/proc/net/dev", O_RDONLY|O_EXCL) = 4
[0807f614] lseek(4, 0, SEEK_SET)        = 0
[0807f5b4] read(4, "Inter-|   Receive                                                |  Transmit\n face |bytes    packets"..., 2047)

This last process is clearly the one sending the info to the remote server. As you can see it is reading /proc/cpuinfo and /proc/stat, and then sending that info over fd = 3.

Reboot persistence

Most malware have a persistency mechanism. For linux, startup programs are usually configured under the /etc path. We can check if our malware sets itself to be run on startup by watching for modifications on files below the /etc path with inotify.

inotifywait -r -m /etc -e create -e modify -e delete -e moved_to |  
    while read path action file; do
        echo "The file '$file' appeared in directory '$path' via '$action'"
        # do something with the file
    done

But nothing comes out of it

Setting up watches.  Beware: since -r was given, this may take a while!  
Watches established.  

So, no persistency mechanism? Looks like so, as if we reboot the infected machine it will no longer hit the running nc.

Cuckoo sandbox

Cuckoo analysis tells us what we already knew.

Seems like there is not much more to squeeze out of dynamic analysis. So, off to the static analysis!