Quantcast
Viewing latest article 3
Browse Latest Browse All 5

Learn How to Use Python to Assist with Network Forensics

Image may be NSFW.
Clik here to view.
share-leverage-python-590

Learn How to Use Python to Assist with Network Forensics

Leverage Python ctypes for quick development of protocol decoders

During an active incident, there typically isn’t enough time to thoroughly reverse engineer a custom protocol used to encrypt, obfuscate, or compress command and control (C2) traffic. Analysis of these algorithms can be error prone and time consuming, neither of which is acceptable when trying to quickly identify actions taken and data exfiltrated by a cyber-attacker during an incident. Python® ctypes is a great solution for these situations. It allows the malware or forensic analyst to leverage the rapid prototyping features provided by Python combined with the ability to run native code. By being able to run native code, the reverse engineer can reuse portions of an implant to assist with developing a protocol decoder.

Benefits of Python ctypes

Python ctypes is intended to provide the ability to use DLLs and shared libraries within a Python application[1]. This allows programmers to use libraries not developed for Python within their applications. An additional benefit of ctypes is that it is not limited to importing shared libraries and DLLs. It is possible to take a binary string of native instructions (e.g. shellcode) and execute it directly within Python.

A simple example: C function

Take for example the following sample C function:

int sample_function(){
	return 0x78563412;
}

When compiled, the assembly would look something like this:

B8 12 34 56 78    mov eax 0x78563412
C3                retn

This same function can be executed within Python with the following script:

import ctypes

#B8 12 34 56 78    mov eax 0x78563412
#C3                retn

#Create a Python string
sample_function_str  = "\xB8\x12\x34\x56\x78\xC3"

#Create a ctypes string buffer
sample_function_cstr = ctypes. create_string_buffer(sample_function_str)

#cast the string buffer to a function that returns an integer
#but takes no arguments
sample_function = ctypes.cast(sample_function_cstr,
                    ctypes.WINFUNCTYPE(ctypes.c_int))

#Execute
print "Return Value: %08X" % sample_function()

# Return Value: 78563412

In the above example, the sample function was cast as ctype.WINFUNCTYPE. The WINFUNCTYPE uses the stdcall calling convention where arguments are pushed onto the stack in right-to-left order[2].

A detailed example: Plug-X decryption loop

For a slightly more detailed example, take the Plug-X decryption loop. The algorithm is simple enough that it can be reversed and reproduced, but these same properties make it easy to embed within a Python script. No outside libraries are called and no sub functions are used making this a very easy candidate to run natively within Python. See Figure 1 below.

Figure 1: Plug-X Decrypt Loop
Image may be NSFW.
Clik here to view.
Figure 1: Plug-X Decrypt Loop

By looking at the start of the Plug-X decryption function in Figure 2, we can see that the function is using a fastcall calling method because the first two arguments are passed using the EAX and ECX registers:

Figure 2: Original Function Entry Point
Image may be NSFW.
Clik here to view.
Figure 2: Original Function Entry Point

Through the initial reverse engineering process, it was determined that the decrypt function takes the following four arguments:

  1. Initial key is passed in register EAX
  2. Destination buffer passed in register ECX
  3. Encrypted buffer is the first argument on the stack
  4. The length of the encrypted buffer is the second argument on the stack

Step 1: Modify the arguments

Since the example Plug-x decrypt function does not use the stdcall calling convention, a few adjustments are needed for the binary to work with Python. The two arguments passed by registers need to be modified so they are passed using the stack. Typically in this situation, it is easier to move these arguments to the end of the argument list so only a minimal amount of instructions need to be modified.

This modification would change the original function definition from:

  int PlugX_Decrypt(uint key, void *dst, void *encrypted, int len)

to:

int PlugX_Decrypt(void *encrypted, int len, uint key, void *dst)

If the argument list was kept in the original order, every reference to the original arg_0 would need to be modified to arg_8. By adjusting the argument order, the two arguments originally pushed onto the stack remain unchanged along with all references to them.

To make this modification, the following instructions need to be added before the instruction at address 0x0000000C in Figure 2:

8B 45 10  mov     eax, [ebp+arg_8]
8B 4D 14  mov     ecx, [ebp+arg_C]

The modified function start now looks like this:

Figure 3: Modified Function Start
Image may be NSFW.
Clik here to view.
Figure 3: Modified Function Start

Step 2: Modify the function return

Since the number of arguments pushed onto the stack was modified, the function return must also be modified to account for this. The original return looked like this:

Image may be NSFW.
Clik here to view.
: Modify the function return

The final instruction “retn 8” should be changed to “retn 10h”:

Image may be NSFW.
Clik here to view.
The final instruction “retn 8” should be changed to “retn 10h”:

Step 3: Determine proper cast for the function

Now that the binary has been modified to work with the stdcall calling convention, the last step is to determine the proper cast for the function. Using the function definition:

int PlugX_Decrypt(void * encrypted, int len, uint key, void *dst,)

The proper Python ctypes cast would be:

     ctypes.WINFUNCTYPE(
            ctypes.c_int,    #return value
            ctypes.c_char_p, #encrypted buffer
            ctypes.c_int,    #encrypted buffer length
            ctypes.c_uint,   #initial key
            ctypes.c_char_p  #destination buffer
            )

The pointers to character buffers are cast as c_char_p. Python strings are converted to c_char_p using the create_string_buffer function (specific details regarding the use of ctypes can be found in the Python documentation[1]).

import binascii
import re
import ctypes
import struct

plugx_decrypt_func_hex = re.sub("[ \r\n]", "", '''
55 8B EC 83 EC 08 83 7D 0C 00 56 57 8B 45 10 8B
4D 14 8B F8 8B F1 8B CF 8B D7 89 7D FC 7E 6B 8B
7D 08 2B FE 89 7D F8 8B 7D 0C 53 89 7D 0C EB 09
8D 9B 00 00 00 00 8B 55 08 8B F8 C1 EF 03 8D 84
38 EF EE EE EE 8B F9 C1 EF 05 8D 8C 39 DE DD DD
DD 8B FA C1 E7 07 BB 33 33 33 33 2B DF 8B 7D FC
03 D3 C1 E7 09 BB 44 44 44 44 2B DF 01 5D FC 8D
1C 01 02 DA 02 5D FC 89 55 08 8B 55 F8 32 1C 32
46 FF 4D 0C 88 5E FF 75 AD 5B 5F 33 C0 5E 8B E5
5D C2 10 00''')

#Convert the ASCII-Hex representation to a binary format
plugx_decrypt_func_hex = binascii.unhexlify(plugx_decrypt_func_hex)
plugx_decrypt_func_bin = ctypes.create_string_buffer(plugx_decrypt_func_hex)

#cast the string buffer to a stdcall/WINFUNCTYPE function
plugx_decrypt_native = ctypes.cast(
            plugx_decrypt_func_bin, #function string buffer
            ctypes.WINFUNCTYPE(
                            ctypes.c_int,    #return value
                            ctypes.c_char_p, #encrypted buffer
                            ctypes.c_int,    #encrypted buffer length
                            ctypes.c_uint,   #initial key
                            ctypes.c_char_p  #destination buffer
                            ))

def plugx_decrypt(encrypted, key):
    """Wrapper for native Plug-X decryption function"""
    N = struct.unpack("<I", key)[0]
    encrypted_ptr = ctypes.create_string_buffer(encrypted)
    decrypted_ptr = ctypes.create_string_buffer(len(encrypted))
    decrypted = plugx_decrypt_native(encrypted_ptr, len(encrypted), N, decrypted_ptr)
    return decrypted_ptr.raw

def plugx_parse_hdr(hdr):
    """Parse fields from Plug-X message header"""
    hdr_vars = {}
    key, opts, deflate_len, msg_len, pad = struct.unpack("<IIHHI", hdr)
    hdr_vars['options'] = opts
    hdr_vars['deflate_len'] = deflate_len
    hdr_vars['inflate_len'] = msg_len
    hdr_vars['pad'] = pad
    return hdr_vars

sample_plugx_header = "77 eb b6 a5 1b cd 8e 6b  23 b2 69 94 2d 35 e8 e4"
sample_plugx_header = re.sub("[ \r\n]", "", sample_plugx_header)
sample_plugx_header = binascii.unhexlify(sample_plugx_header)

#For plugx, the initial encryption key is the first 4 bytes of the message
decrypted = plugx_decrypt(sample_plugx_header, sample_plugx_header[:4])

print plugx_parse_hdr(decrypted)

Final thoughts

In certain situations, it is faster to use native code when developing a command and control decoder. I’ve demonstrated that by adding two instructions and modifying a third, it is possible to skip the sometimes tedious process of reverse engineering a decryption function. Also, by using native code, the resulting decoder can run substantially faster than a pure Python implementation.

I would also suggest taking a look at Chopshop developed by Mitre[3]. It is a python framework that uses pynids to assist with the development of command and control decoders.

References

[1] http://docs.python.org/2/library/ctypes.html
[2] http://en.wikipedia.org/wiki/X86_calling_conventions#stdcall
[3] https://github.com/MITRECND/chopshop

Image: Fotolia.com, Andrzej Wilusz
Python is a registered trademark of Python Software Foundation

Image may be NSFW.
Clik here to view.

Viewing latest article 3
Browse Latest Browse All 5

Trending Articles