SESSION_ACTIVE :: RED_TEAM_ENGAGEMENT

Weaponizing Nim: Advanced Malware Development and Evasion Guide for Red Teamers

An advanced guide to using Nim for offensive development: syscalls, memory work, opsec, and stealth execution.

Weaponizing Nim: Advanced Malware Development and Evasion Guide for Red Teamers

Weaponizing Nim: Advanced Malware Development and Evasion Guide for Red Teamers

If you have been doing pentesting or Red Teaming lately, you have probably noticed a shift in the tooling ecosystem. For years, C# and PowerShell were the kings, but signatures and logging (AMSI, Script Block Logging) have made it harder to operate without detection. This is where Nim comes in.

Recently, Nim has become the preferred option for many malware developers. Why? Because it has a Python-like friendly syntax, but it compiles to native Windows executables or DLLs very easily, without relying on a heavy virtual machine like Java or even Golang.

This article is not a basic introduction. We will go deeper into how to use Nim to interact directly with the Windows Native API (syscalls), how to handle memory offensively, and how to avoid the common mistakes people make when porting code from C to Nim.


Setup and Compilation for OpSec

First, forget Visual Studio. You do not need that heavy IDE. A simple editor like VS Code and the Nim compiler are enough. One of Nim’s biggest advantages is cross compilation. You can compile a Windows binary from Linux by installing the MinGW toolchain and passing a flag to the compiler.

Critical Compilation Flags

For offensive operations, you cannot use default settings. We need to reduce size and strip debug information that AV analysts can use to create signatures. Based on Nim documentation and field experience, this is the winning setup:

nim c -d:danger -d:strip --opt:size payload.nim
  • -d:danger: Removes all runtime checks (like array bounds). Risky, but needed for optimized malware.
  • -d:strip: Strips symbols.
  • --opt:size: Optimizes binary size.

From Win32 to Syscalls: Hook Evasion

Most malware injects shellcode using a well known sequence of four methods:

  1. OpenProcess: Get a handle.
  2. VirtualAllocEx: Reserve memory (RWX or RW).
  3. WriteProcessMemory: Write the payload.
  4. CreateRemoteThread: Execute.

The problem is that EDRs and AVs place hooks in these functions at user level (in ntdll.dll or kernel32.dll) to inspect the flow and redirect it to their analysis engine.

To avoid this, we use syscalls. This lets us call the Native API directly, bypassing AV hooks. In Nim, that means mapping our calls to their native equivalents:

  • Windows API -> Native API
  • OpenProcess -> NtOpenProcess
  • VirtualAllocEx -> NtAllocateVirtualMemory
  • WriteProcessMemory -> NtWriteVirtualMemory
  • CreateRemoteThread -> NtCreateThreadEx

When implementing a runner with these syscalls, I have observed that solutions like Windows Defender often ignore the binary, especially if the payload is not embedded in the executable but downloaded over the network.


Low-Level Nim: Pointers, Memory, and Traps

This is where most people get stuck. Nim is a high-level language, but for malware we need to touch raw memory.

Shellcode: Array or Sequence?

Use seq[byte] (sequence) instead of a fixed array. Sequences are dynamic, which lets you load payloads of variable size from a file or the network.

let shellcode: seq[byte] = @[byte 0x41, 0x41, 0x41]

The “addr” vs “unsafeAddr” problem

This is the most critical point. In C, getting the address of an array is trivial. In Nim, you cannot simply use addr to get a direct pointer from a seq[byte]. You must use unsafeAddr and index the first element of the sequence.

If you try to pass the sequence directly to a Windows API like VirtualAllocEx, it will fail. The correct approach is:

VirtualAllocEx(..., unsafeAddr shellcode[0], ...)

It is vital to index the variable (shellcode[0]) to get the pointer to the start of the data in memory.

Type Casting and Structs

Windows requires specific types (HANDLE, DWORD, etc.). In Nim, you can define these structs as objects. A key detail is using the {.pure.} pragma to ensure memory compatibility. Also, when passing initialized structs to functions (like NtOpenProcess), you may need to use unsafeAddr instead of addr, because addr may not behave as expected with objects managed by Nim.

Example of quick casting from an integer to DWORD:

let pid: int = 1337
let dPid: DWORD = cast[DWORD](pid)

Real Weaponization: A C2 Runner (Sliver/Cobalt Strike)

Let’s put this into practice. A modern runner should not carry the shellcode inside. It should download it.

I have experimented with a main module that receives parameters (HTTP or SMB), downloads the payload, removes the Initialization Vector (IV) if it comes from Sliver (the first 16 bytes), decrypts it (AES128 CBC), and injects it.

Decryption and Cleanup Logic

If you use Sliver, the payload often comes with a prepended IV. The Nim code must clean this before decryption:

# If this is Sliver, remove the IV from the first 16 bytes
if $paramStr(1) == "sliver":
  for i in 16 ..< shellcode.len:
    actual.add(shellcode[i])
  shellcode = decrypt(actual, key, iv)

Safer Injection

An OpSec tip: injecting into processes like Notepad.exe is a massive red flag for Defender. If you do that, your session will die quickly. It is better to let syscalls create their own process or thread, or inject into a process you spawned and suspended yourself, avoiding sensitive system process boundaries.


Advanced Capabilities: The “OffensiveNim” Repository

If you want to go further, the OffensiveNim repository by Byt3bl33d3r is the must have reference. It contains snippets for almost every modern technique:

  • Bypassing AMSI: Patching process memory to disable script scanning.
  • Bypassing ETW: Disabling Event Tracing for Windows to blind telemetry.
  • Unhooking ntdll: Reloading a clean copy of ntdll from disk to remove EDR hooks.
  • Shellcode Injection: Multiple methods, including classic injection and direct syscalls.
  • CLR Hosting: Executing .NET assemblies directly from memory without touching disk.

Conclusion

Nim offers the elegance of a scripting language with the low-level power of C. It lets you build custom offensive tools quickly, manipulate memory precisely using pointers (be careful with unsafeAddr), and compile binaries that, for now, evade many security solutions simply because they are not C# or PowerShell.

If you are looking to upgrade your Red Team arsenal, porting your loaders and tools to Nim is the next logical step.