Why Nim?
Preamble
At this point I’ve had a few people ask me about Nim in general and why I’ve chosen it for malware development as it is a relatively unknown language, so I thought it might be a good idea to talk about it and hopefully get more people interested in using it.
As a bonus, more people programming in Nim will also force lazy antivirus developers to come up with real solutions to detecting Nim-based malware that don’t involve flagging anything written in the language.
What is Nim?
So, first things first. What is Nim? Nim-lang.org
Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula.
For a little more background Wikipedia:
Nim’s initial development was started in 2005 by Andreas Rumpf. It was originally named Nimrod when the project was made public in 2008. The first version of the Nim compiler was written in Pascal using the Free Pascal compiler. In 2008, a version of the compiler written in Nim was released. The compiler is free and open-source software, and is being developed by a community of volunteers working with Andreas Rumpf.
The language was officially renamed from Nimrod to Nim with the release of version 0.10.2 in December 2014. On September 23, 2019, version 1.0.0 of Nim was released, signifying the maturing of the language and its toolchain.
So, Nim has been around for a while, overshadowed by the likes of Rust and Go, which is unfortunate, but expected when you have the backing of huge corporations like Google, Mozilla, and Microsoft.
So, what exactly does Nim have to offer?
Python-like Syntax
Nim has Python-inspired syntax, which means easy to read and easy to write. Want to do “Hello World” in Nim? Easy.
echo "Hello World!"
What about reading from stdin?
var input = readLine(stdin)
echo input
What if you want to safely read a password from stdin?
import std/terminal
var pass = readPasswordFromStdin(prompt = "Password: ")
echo "Your password is: $1" % [pass]
Another fuller example can be found on the Nim website itself. Nim-lang.org
import std/strformat
type
Person = object
name: string
age: Natural # Ensures the age is positive
let people = [
Person(name: "John", age: 45),
Person(name: "Kate", age: 30)
]
for person in people:
# Type-safe string interpolation,
# evaluated at compile time.
echo(fmt"{person.name} is {person.age} years old")
# Thanks to Nim's 'iterator' and 'yield' constructs,
# iterators are as easy to write as ordinary
# functions. They are compiled to inline loops.
iterator oddNumbers[Idx, T](a: array[Idx, T]): T =
for x in a:
if x mod 2 == 1:
yield x
for odd in oddNumbers([3, 6, 9, 12, 15, 18]):
echo odd
# Use Nim's macro system to transform a dense
# data-centric description of x86 instructions
# into lookup tables that are used by
# assemblers and JITs.
import macros, strutils
macro toLookupTable(data: static[string]): untyped =
result = newTree(nnkBracket)
for w in data.split(';'):
result.add newLit(w)
const
data = "mov;btc;cli;xor"
opcodes = toLookupTable(data)
for o in opcodes:
echo o
But wait, didn’t we say Nim is a statically typed language? So, where are the types? A great question that has an easy answer. Nim also has type inference in most cases. Meaning we don’t always have to define the types. Case in point, both the readLine
and readPasswordFromStdin
functions (procedures, actually) return a string
, so Nim is smart enough to know our input variables are also strings.
We cannot rely on type inference in all situations though. The rule is (for most, if not all, statically typed languages) the type must be known at compile-time.
Nim also has a bit of a flexible syntax. We don’t always need to include the parentheses when we call our procedures. We can call them like so:
proc doEcho(strToEcho: string) =
echo strToEcho
"Hello World".doEcho
This is called the Uniform Function Call Syntax (UFCS). It only works with the first parameter of a procedure (beyond that we must use parentheses), but it is a cool feature allows us to do things like this:
type Vector = tuple[x, y: int]
proc add(a, b: Vector): Vector =
(a.x + b.x, a.y + b.y)
let
v1 = (x: -1, y: 4)
v2 = (x: 5, y: -2)
# all the following are correct
v3 = add(v1, v2)
v4 = v1.add(v2)
v5 = v1.add(v2).add(v4)
Transpiling
Another cool feature Nim has is transpiling. Because I’ve learned this single word has the potential to make developers and Computer Science majors cringe, I’ll just be saying compiling instead. So, as we mentioned above, Nim is a flexible language, and this extends down to its compilation process.
We won’t be diving too deep into the intricacies of how compilers and linkers work (mostly because I don’t have a good, or even decent, understanding of it myself), but Nim produces another language’s code prior to becoming machine code.
Which language’s code does it produce? That’s entirely up to you! Nim can compile down to C, C++, Objective-C or even Javascript! We can write valid C code without ever having to touch C itself. How cool is that? One language to rule them all. Maybe that’s why the logo is a crown.
Side note: I think everyone should still learn C (or C++). Abstractions are cool and all, and make all our lives much easier, but it’s good to know what is really happening under the hood.
Language Interoperability and Win32 API
In a similar vein to Nim’s ability to compile down to other language’s code, its language interoperability is topnotch as well. This means we can interface with other languages. The easiest languages to do this with is C and C++, but we can also use Assembly, .Net (C#, Powershell), VBA, and Python.
Want to use C/C++? Throw it into the .emit pragma:
{.emit: """
static int cvariable = 420;
""".}
{.push stackTrace:off.}
proc embedsC() =
var nimVar = 89
# access Nim symbols within an emit section outside of string literals:
{.emit: ["""fprintf(stdout, "%d\n", cvariable + (int)""", nimVar, ");"].}
{.pop.}
embedsC()
Or use the importc pragma like so:
proc printf(formatstr: cstring) {.header: "<stdio.h>", importc: "printf", varargs.}
Likewise, other “import” pragmas also exist for Objective-C, C++, and Javascript.
Want to run Assembly? Easy! This example is from the Offensive Nim Github repo
proc runsc(): void =
# msfvenom -p windows/x64/exec CMD=calc.exe EXITFUNC=thread -f csharp
asm """
.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,0x8b,0x52,0x20,0x48,0x8b,0x72,0x50,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x48,0x8b,0x52,0x20,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x8b,0x80,0x88,0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x67,0x48,0x01,0xd0,0x50,0x8b,0x48,0x18,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,0xe3,0x56,0x48,0xff,0xc9,0x41,0x8b,0x34,0x88,0x48,0x01,0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x4c,0x03,0x4c,0x24,0x08,0x45,0x39,0xd1,0x75,0xd8,0x58,0x44,0x8b,0x40,0x24,0x49,0x01,0xd0,0x66,0x41,0x8b,0x0c,0x48,0x44,0x8b,0x40,0x1c,0x49,0x01,0xd0,0x41,0x8b,0x04,0x88,0x48,0x01,0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,0x59,0x5a,0x48,0x8b,0x12,0xe9,0x57,0xff,0xff,0xff,0x5d,0x48,0xba,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x48,0x8d,0x8d,0x01,0x01,0x00,0x00,0x41,0xba,0x31,0x8b,0x6f,0x87,0xff,0xd5,0xbb,0xaa,0xc5,0xe2,0x5d,0x41,0xba,0xa6,0x95,0xbd,0x9d,0xff,0xd5,0x48,0x83,0xc4,0x28,0x3c,0x06,0x7c,0x0a,0x80,0xfb,0xe0,0x75,0x05,0xbb,0x47,0x13,0x72,0x6f,0x6a,0x00,0x59,0x41,0x89,0xda,0xff,0xd5,0x63,0x61,0x6c,0x63,0x2e,0x65,0x78,0x65,0x00
ret
"""
Powershell? Create your own runspace and go!
C#? Host the CLR and full speed ahead!
Python? SciNim Example
import nimpy
let py = pyBuiltinsModule()
discard py.print("Hello world from Python..")
Win32? Other languages can make accessing this a pain in the behind, but not Nim. Offensive Nim Github repo example
type
HANDLE* = int
HWND* = HANDLE
UINT* = int32
LPCSTR* = cstring
proc MessageBox*(hWnd: HWND, lpText: LPCSTR, lpCaption: LPCSTR, uType: UINT): int32
{.discardable, stdcall, dynlib: "user32", importc: "MessageBoxA".}
MessageBox(0, "Hello, world !", "Nim is Powerful", 0)
We can do things this way, which works perfectly fine, and for languages other than C/C++ and APIs other than Win32 may be necessary, but we can also use the winim library like so:
import winim/com
MessageBox(0, "Hello, world !", "Nim is Powerful", 0)
And this brings us to the final benefit of Nim we will discuss.
Dynamic Function Resolution
If we take a look at the winim library or even our first MessageBox example from earlier, we will notice something very interesting. We notice usage of the “dynlib” pragma. The dynlib pragma (and associated module) performs dynamic function resolution! This means Win32 API calls we make are not included in the Import Address Table (IAT)!
While there are numerous other ways malware can be caught by AV and EDR, taking a look at what API calls the executable will make, even before running it, is an easy win for them. With Nim, it’s also an easy bypass!
That concludes everything I wanted to discuss regarding the benefits of Nim. If there’s any negatives, I would say it’s just that Nim, being a relatively unknown language, does not have a large community behind it.
That means libraries to make certain tasks simpler, like PKCS7 padding or UUID generation, do not exist and must be created manually.