The Inner Workings Of Railgun

Recently, Railgun functionality was added to Metasploit’s Python Meterpreter. This blog describes details of the implementation and how it provides the functionality to make arbitrary calls to native API functions through Metasploit. This is a technical companion piece to the Metasploit Blog post outlining some of the new features to the Python Meterpreter and their uses.

One of the first challenges to bringing Railgun functionality to the Python Meterpreter was getting the information on the native process architecture of a compromised system. The sysinfo information includes the system’s native architecture and. Although Metasploit would be aware that the session was a Python Meterpreter, there was no way to determine if the instance of Python was 32-bit or 64-bit. This piece of information is critical for determining the size of a few basic types used by Railgun including LPVOID and HANDLE.

To expose this information a new core_native_arch method was added. This allows Metasploit to get all relevant details of a 32-bit version of Python running on a 64-bit version of Windows. Using this information, Metasploit can properly construct the parameters and arguments necessary to pass to the methods provided by Railgun.

There are currently four methods exposed by the Railgun API in Meterpreter. These functions are:

  • railgun_memread
  • railgun_memwrite
  • railgun_api
  • railgun_api_multi

With all four, Metasploit has the necessary interface to make native API function calls. The first two, as their respective names imply, are for basic memory reading and writing operations. The last two call one (or more in the case of multi) API functions.

When a request is made to Meterpreter from Metasploit to issue an API call using Railgun, a number of parameters describing the arguments to the function are provided in the form of Type-Length-Values (TLVs). Parameters are passed to Meterpreter from Metasploit in the form of binary blobs that are processed in the TLV format. These TLVs include the amount of space that should be allocated for parameters which write data from pointer arguments, and two additional binary blobs for parameters which read and both read and write data from pointer arguments. Railgun deals with Pointer arguments differently depending on if they are in the in, out, or inout directions.

All three of these blobs for pointer data are then joined by a fourth which describes each of the arguments to be passed to the function. Each argument is either treated as a literal value or an offset to one of the three previously mentioned buffers.

Each one of these arguments are then used to call the specified function in the desired library. Once the function finishes, the out and inout parameter buffers are then returned back to Metasploit where the values are extracted.

The Python Meterpreter implementation accomplishes all of this by heavily leveraging the ctypes library. The ctypes library is already responsible for much of the functionality provided by the Python Meterpreter and is the primary reason that Python 2.5 is the oldest version that can be supported. When a request is dispatched to railgun_api, Python uses the ctypes library to retrieve the function. The function’s argument types, return type, and calling convention are then all defined at runtime using function prototypes. This approach does have a couple of limitations, namely the cdecl calling convention is the only convention available on non-Windows platforms (which support both stdcall as well as cdecl).

Another issue with implementing all of the Railgun API functionality on Linux was finding a way to read and write arbitrary memory locations within the current process. On Windows, the ReadProcessMemory and WriteProcessMemory functions are available and ideal because when the location is invalid and called from Python, the process will not crash. These functions don’t exist on Linux. Simply making a call to memcpy will result in a segmentation fault when given an invalid address, ultimately resulting in the Meterpreter session abruptly dying. In order to address this, the process_vm_readv and process_vm_writev system calls are used which will gracefully fail when given an invalid address. To get extended information on why the operation failed, the /proc/pid/maps file is parsed to check if the specified address is valid and permissions are correct. With this information Meterpreter can proactively determine the reason the call may fail.

 

All of this functionality is combined to create Railgun and power a large amount of the post-exploitation modules.