The Nim programming language has become increasingly attractive to malware developers due to its robust compiler and its ability to work easily with other languages. Nim’s compiler can compile Nim to JavaScript, C, C++, and Objective-C, and cross-compile for major operating systems such as Windows, Linux, macOS, Android, and iOS. Furthermore, Nim supports importing functions and symbols from the languages mentioned above, and importing from dynamically linked libraries for Windows and shared libraries for Linux. Nim wrapper modules are also available, such as Winim, that make interaction with the operating system painless. All these capabilities allow easy integration of Nim into development pipelines using these languages and boost the development of new tools, both benign and malicious.
It is no surprise, then, that ESET Research has seen an ongoing use of malware developed in Nim in the wild. As far back as 2019, Sednit was spotted using a malicious downloader written in Nim. Another notorious group playing the Nim game, and the impetus for developing Nimfilt, is the Mustang Panda APT group. ESET Research recorded Mustang Panda using Nim in its toolset for the first time in a campaign against a governmental organization in Slovakia in August 2023. The malicious DLL detected – and used as part of the group’s classic trident Korplug loader – was written in Nim.
For researchers tasked with reverse engineering such binaries, Nimfilt is a powerful tool to speed up analysis. While Nimfilt can be run as a Python script both on the command line (with a subset of its functionality) and in Hex-Rays’ IDA program, it will be presented here mainly as a Python plugin for IDA.
Initializing Nimfilt in IDA
When IDA is first opened, it loads and initializes any plugins in the IDA plugins directory. During the initialization of Nimfilt, the plugin uses basic heuristics to determine whether the disassembled binary was compiled with the Nim compiler. If one of the following checks is passed, Nimfilt determines that this compiler was used:
- The binary contains both of the following strings:
- The binary contains any of the following well-known Nim function names:
- NimMain
- NimMainInner
- NimMainModule
- The binary contains at least two of the following error message strings:
- @value out of range
- @division by zero
- @over- or underflow
- @index out of bounds
YARA rules are provided along with Nimfilt that make similar checks to determine whether an ELF or PE file has been compiled with Nim. Together, these checks are far more robust than the approach taken by other tools, such as Detect It Easy, which currently only checks the .rdata section of PE files for the string io.nim or fatal.nim.
As the final initialization step, if Nimfilt’s AUTO_RUN flag is set to true, the plugin runs immediately. Otherwise, Nimfilt can be run as usual from IDA’s plugins menu, as shown in Figure 1.
Demangling with Nimfilt
Nim uses a custom name mangling scheme that Nimfilt can decode. During a run, Nimfilt iterates through each function name in the binary, checking whether the name is a Nim package or function name. Discovered names are renamed to their demangled forms.
Interestingly, these names can leak information about the developer’s environment, in much the same way as PDB paths. This is due to the Nim compiler adding the file path to the name during mangling – Nimfilt reveals the path upon demangling.
For example, function names from third-party packages are stored as absolute paths during the mangling process. Figure 2 shows a function name that is stored as an absolute path revealing the version and checksum of the nimSHA2 package used, along with the developer’s installation path for nimble – Nim’s default package manager.
python nimfilt.py GET_UINT32_BE__6758Z85sersZ85serOnameZOnimbleZpkgs50Znim837265504548O49O494554555453d57a4852c515056c5452eb5354b51fa5748f5253545748505752cc56fdZnim83726550_u68
C:/Users/User.name/.nimble/pkgs2/nimSHA2-0.1.1-6765d9a04c328c64eb56b3fa90f45690294cc8fd/nimSHA2::GET_UINT32_BE u68
Figure 2. Demangling the name of a function from a third-party package
In contrast, Figure 3 shows the name of a function from a standard Nim package stored as a relative path (that is, relative to the Nim installation path).
python nimfilt.py toHex__pureZstrutils_u2067
pure/strutils::toHex u2067
Figure 3. Demangling the name of a function from a standard Nim package
However, names are not always mangled in the same way. Figure 4 shows that the same function name above from the nimSHA2 package is stored on Linux as a relative path.
python nimfilt.py GET_UINT32_BE__OOZOOZOOZhomeZalexZOnimbleZpkgs50Znim837265504548O49O494554555453d57a4852c515056c5452eb5354b51fa5748f5253545748505752cc56fdZnim83726550_u49
Figure 4. Demangling the name of a function from a third-party package on Linux
Package initialization functions are mangled in a completely different way: the package name is stored as a file path (including the file extension) positioned before the function name and an escaping scheme is used to represent certain characters like forward slashes, hyphens, and dots. Upon demangling, Nimfilt cleans up the package name by removing the .nim file extension, as shown in Figure 5.
python nimfilt.py atmdotdotatsdotdotatsdotnimbleatspkgsatswinimminus3dot9dot1atswinimatsincatswinbasedotnim_DatInit000
Figure 5. Demangling the name of an initialization function from a third-party package
Figure 6 shows how names of initialization functions from native packages are stored as absolute paths.
python nimfilt.py atmCatcatstoolsatsNimatsnimminus2dot0dot0atslibatssystemdotnim_Init000
C:/tools/Nim/nim-2.0.0/lib/system::Init000
Figure 6. Demangling the name of an initialization function from a native package
In IDA, Nimfilt’s name demangling process is followed by the creation of directories in the Functions window to organize functions according to their package name or path, as shown in Figure 7.
Applying structs to Nim strings
The last action performed during a run of Nimfilt is applying C-style structs to Nim strings. Just as strings in some other programming languages are objects rather than null-terminated sequences of bytes, so are strings in Nim. Figure 8 shows how the string ABCDEF appears in IDA before and after running Nimfilt. Note that in disassembled form, a Nim-compiled binary uses the prefix _TM as a part of the temporary name of some variables; these are often Nim strings.
Nimfilt iterates through each address in the .rdata or .rodata segment, and in any other read-only data segment, looking for Nim strings. Structs are applied to any discovered strings; the struct contains a length field and a pointer to the payload consisting of the characters in the string.
Wrap-up
On its way to being compiled as an executable, Nim source code is typically translated to C or C++; however, this process doesn’t entirely remove all traces of Nim. By taking a journey through the Nim compiler source code, we have unraveled some of the paths taken in the compilation process and were thus able to build Nimfilt as a Python tool, and IDA plugin, to assist in this untangling.
In short, whether or not you are new to Nim, turning to Nimfilt will make your reverse engineering work with Nim-compiled binaries almost instantly easier and more focused. By no means, however, is Nimfilt’s development at a standstill; we are working on additional features to handle double mangling, and improve the formatting of demangled names and the grouping of package names.
Nimfilt’s source code and documentation are available in a repository hosted on ESET’s GitHub organization at https://github.com/eset/nimfilt.