Link Search Menu Expand Document

Railgun is a very powerful post exploitation feature exclusive to the Windows and Python Meterpreters. It allows you to have complete control of your target machine’s Windows API, or you can use whatever DLL you find and do even more creative stuff with it. For example: say you have a Meterpreter session on a Windows target. You have your eyes on a particular application that you believe stores the user’s password, but it is encrypted and there are no tools out there for decryption. With Railgun, you can either tap into the process and grep for any sensitive information found in memory, or you can look for the program’s DLL that’s responsible for the decryption, call it, and let it decrypt it for you. If you’re a penetration tester, obviously post exploitation is an important skill to have, but if you don’t know Railgun, you are missing out a lot.

Defining a DLL and its functions

The Windows API is obviously quite large, so by default Railgun only comes with a handful of pre-defined DLLs and functions that are commonly used for building a Windows program. These built-in DLLs are: advapi32, crypt32, dbghelp, iphlpapi, kernel32, netapi32, ntdll, psapi, shell32, spoolss, user32, version, winspool, wlanapi, wldap32, and ws2_32. The same list of built-in DLLs can also be retrieved by using the known_library_names method.

All DLL definitions are found in the “def” directory, where they are defined as classes. The following template should demonstrate how a DLL is actually defined:

# -*- coding: binary -*-
module Rex
module Post
module Meterpreter
module Extensions
module Stdapi
module Railgun
module Def

class Def_windows_somedll

  def self.create_library(constant_manager, dll_path = 'somedll')
    dll = Library.new(library_path, constant_manager)

    # 1st argument = Name of the function
    # 2nd argument = Return value's data type
    # 3rd argument = An array of parameters
    dll.add_function('SomeFunction', 'DWORD',[
      ['DWORD','hwnd','in']
    ])

    return dll
  end

end

end; end; end; end; end; end; end

In function definitions, Railgun supports these data types: BOOL, BYTE, DWORD, LPVOID, PBLOB, PCHAR, PDWORD, PULONG_PTR, PWCHAR, ULONG_PTR, VOID, WORD.

There are four parameter/buffer directions: in, out, inout, and return. When you pass a value to an “in” parameter, Railgun handles the memory management. For example, MessageBoxA has an “in” parameter named lpText, and is of type PCHAR. You can simply pass a Ruby string to it, and Railgun handles the rest, it’s all pretty straight forward.

An “out” parameter will always be of a pointer datatype. Basically you tell Railgun how many bytes to allocate for the parameter, it allocates memory, provides a pointer to it when calling the function, and then it reads that region of memory that the function wrote, converts that to a Ruby object and adds it to the return hash. Some datatypes such as LPVOID and ULONG_PTR have a size that is determined based on the host architecture, e.g. 32-bit versions of Windows use 4-byte/32-bit values. For cross compatibility, the number 4 (for 4-bytes) can be used as the size for these values on both 32-bit and 64-bit systems. The number four comes from the size used for these types in the original 32-bit implementation and was selected to maintain backwards compatibility when 64-bit support was added.

An “inout” parameter serves as an input to the called function, but can be potentially modified by it. You can inspect the return hash for the modified value like an “out” parameter.

The fourth type, “return” is used as the return data type. It is passed to #add_function after the function name argument.

A quick way to define a new function (or redefine an existing function) at runtime can be done like the following example:

client.railgun.add_function('user32', 'MessageBoxA', 'DWORD',[
	['DWORD','hWnd','in'],
	['PCHAR','lpText','in'],
	['PCHAR','lpCaption','in'],
	['DWORD','uType','in']
 ])

However, if this function will most likely be used more than once, or it’s part of the Windows API, then you should put it in to the library.

Usage

The best way to try Railgun is with irb in a Windows Meterpreter prompt. Here’s an example of how to get there:

$ msfconsole -q
msf > use exploit/multi/handler
msf exploit(handler) > run

[*] Started reverse handler on 192.168.1.64:4444
[*] Starting the payload handler...
[*] Sending stage (769536 bytes) to 192.168.1.106
[*] Meterpreter session 1 opened (192.168.1.64:4444 -> 192.168.1.106:55148) at 2014-07-30 19:49:35 -0500

meterpreter > irb
[*] Starting IRB shell...
[*] You are in the "client" (session) object

>>

Note that when you’re running a post module or in irb, you always have a client or session object to work with, both point to same thing, which in this case is Msf::Sessions::Meterpreter_x86_Win. This Meterpreter session object gives you API access to the target machine, including the Railgun object Rex::Post::Meterpreter::Extensions::Stdapi::Railgun::Railgun. Here’s how you simply access it:

railgun

If you run the above in irb, you will see that it returns information about all the DLLs, functions, constants, etc, except it’s a little unfriendly to read because there’s so much data. Fortunately, there are some handy tricks to help us to figure things out. For example, like we mentioned before, if you’re not sure what DLLs are loaded, you can call the known_dll_name method:

>> railgun.known_library_names
=> ["kernel32", "ntdll", "user32", "ws2_32", "iphlpapi", "advapi32", "shell32", "netapi32", "crypt32", "wlanapi", "wldap32", "version", "psapi", "dbghelp", "winspool", "spoolss"]

Now, say we’re interested in user32 and we want to find all the available functions (as well as return value’s data type, parameters), another handy trick is this:

railgun.user32.functions.each_pair {|n, v| puts "Function name: #{n}, Returns: #{v.return_type}, Params: #{v.params}"}

Note that if you happen to call an invalid or unsupported Windows function, a RuntimeError will raise, and the error message also shows a list of available functions.

To call a Windows API function, call the Ruby function of the desired name on the corresponding library (DLL) object. For example to call user32!MessageBoxA:

>> railgun.user32.MessageBoxA(0, "hello, world", "hello", "MB_OK")
=> {"GetLastError"=>0, "ErrorMessage"=>"The operation completed successfully.", "return"=>1}

As you can see, this API call returns a hash. The “return” key is the return value of the function, as defined by its defined datatype. If the return type is a pointer to a known type (a pointer other than LPVOID, such as PCHAR), then the “return” key will be the value of that type and an additional “&return” key will be included. The “&return” key, when present, is the address in memory at which the “return” value is located. This is useful when the caller needs to both access the value but also have the ability to free it at a later time. Note that in these cases, if the pointer is NULL, “return” will always be Ruby’s nil value and “&return” will be 0.

The “GetLastError” key is the threads last-error code, as returned by kernel32!GetLastError. This value is useful for determining if the function call was successful and not not, why it failed. The “ErrorMessage” key is a string to a human readable name of the corresponding “GetLastError” code. When making a function call through railgun, it s important to inspect the results to determine if it was successful before processing any results. There is no error handling for native API calls, so simple mistakes like accessing invalid memory locations will cause the session to close as the host process crashes.

Memory Reading and Writing

The Railgun class also has useful methods that you will probably use: memread and memwrite. The names are pretty self-explanatory: You read a block of memory, or you write to a region of memory. We’ll demonstrate this with a new block of memory in the payload itself:

>> process = sys.process.open(sys.process.getpid, PROCESS_ALL_ACCESS)
=> #<#<Class:0x007fe2e051b740>:0x007fe2c5a258a0 @client=#<Session:meterpreter 192.168.1.106:55151 (192.168.1.106) "WIN-6NH0Q8CJQVM\sinn3r @ WIN-6NH0Q8CJQVM">, @handle=448, @channel=nil, @pid=2268, @aliases={"image"=>#<Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessSubsystem::Image:0x007fe2c5a25828 @process=#<#<Class:0x007fe2e051b740>:0x007fe2c5a258a0 ...>>, "io"=>#<Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessSubsystem::IO:0x007fe2c5a257b0 @process=#<#<Class:0x007fe2e051b740>:0x007fe2c5a258a0 ...>>, "memory"=>#<Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessSubsystem::Memory:0x007fe2c5a25738 @process=#<#<Class:0x007fe2e051b740>:0x007fe2c5a258a0 ...>>, "thread"=>#<Rex::Post::Meterpreter::Extensions::Stdapi::Sys::ProcessSubsystem::Thread:0x007fe2c5a256c0 @process=#<#<Class:0x007fe2e051b740>:0x007fe2c5a258a0 ...>>}>
>> address = process.memory.allocate(1024)
=> 5898240

As you can see, the new allocation is at the previously allocated address. Let’s first write some data to it:

>> railgun.memwrite(address, "AAAA\x00".b)
=> true

memwrite returns true, which means successful. Now let’s read 4 bytes from the same address:

>> railgun.memread(address, 4)
=> "AAAA"

Be aware that if you supply a bad pointer, you can cause an access violation and crash Meterpreter.

Reading and Writing Strings

Railgun also has a number of useful utility methods in railgun.util. For example, the #read_string method can be used to read an ASCII string from memory. A read_wstring variant can be used to read UTF-16 strings.

>> railgun.util.read_string(address)
=> "AAAA"

References: