目前,诸多新的编程语言一经开发就成为攻击者的崭新的武器开发语言。在这些新的编程语言中,Nim语言尤其受到了攻击者的青睐。本次蓝帽杯初赛和半决赛的两道逆向工程方向题目都采用了nim语言编写。本篇文章分享一下我关于这两道nim逆向题目的复现心得,并且简单学习一下nim语言
0x00 前言
随着计算机技术的发展,计算机研究人员根据现有语言的缺陷,尝试创造出更好的编程语言,然而新技术的产生往往是一把双刃剑。对传统的安全检测设备和安全研究人员而言,新语言相对晦涩且冷门,具有语言本身的特性。在面对传统的安全措施时,绕过更加轻松,使得安全设备增大识别和检测难度,大大增加安全防御成本。因此诸多新的编程语言一经开发就成为攻击者的崭新的武器开发语言。在这些新的编程语言中,Nim语言尤其受到了攻击者的青睐。比如说,APT组织TA800在攻击中多次使用Nim语言开发的NimzaLoader下载器;APT28组织的攻击工具Zebrocy用Nim语言进行重构等。
本次蓝帽杯初赛和半决赛的两道逆向工程方向题目都采用了nim语言编写,主办方都这么暗示我们要与时俱进了,我们还不学是不是就说不过去了?
0x01 初识Nim
nim语言优势分析
语言本身
语法简洁,执行效率高(远高于python,与c不分伯仲),语法简单,内部函数封装完整。
具有隐藏性的win32 api调用
外部函数接口:指的是一种机制,使用一种编程语言编写的程序可以调用用另一种编程语言编写的服务(比如在Nim语言中可以调用使用C/C++编写的Messagebox函数)。
Nim有成熟的外部函数接口技术(FFI,使得Nim语言与Windows API交互的时候,具有OPSEC的特性,即使用Nim编写的程序的外部导入函数不会真正显示在可执行文件的静态导入表中 。
对抗特征码检测
特征码和哈希检测在恶意软件检测手法中占据一席之地。研究者从程序中提取特征码和哈希,编写yara规则,实现对恶意软件的匹配和检测。而使用Nim重构后的程序无论是签名哈希还是特征码都会有所变化,达到绕过规则检测的效果。
自带混淆效果
Nim等新兴语言对传统分析者来说相对不熟悉与晦涩,针对性的分析工具的缺失,更使得新兴语言本身几乎自带混淆效果。针对Nim语言二进制文件的分析工具目前还不完善,其作为新兴语言的红利期尚未结束。比起常见的高级语言和其他新兴语言,Nim语言给分析人员带来了更大的逆向难度和成本,相关的安全措施也尚不成熟,这也就导致了越来越多的武器开发者利用Nim语言等新兴语言编写加载器,用于部署RAT或Cobalt Strike等攻击软件。
nim语言基本语法
打印输出
打印输出使用echo函数
echo "Hello World"
变量声明和赋值
nim语言变量需要提前声明 声明采用 变量名:变量类型 的格式。赋值使用等号
var var1: int # int类型
var var2: string # 字符串类型
var1 = 3
var2 = "str"
选择结构
选择结构类似于python。
例:判断var1是否等于5
if
if var1 == 5:
echo "True"
elif var1 > 5:
echo "bigger"
elif var1 < 5:
echo "smaller"
switch:
switch case
case var1
of 5:
echo "Case:Yes,it's 5"
else:
echo "Case:No,it isn't 5"
循环结构
总体来说nim语言的循环和python还是很像的
关键词 while
echo("What's your name? ")
var name = readLine(stdin)
while name == "":
echo("Please tell me your name: ")
name = readLine(stdin)
一定要注意最后一行的缩进,如果没有这个缩进,会出问题
关键词for和..
countup是一个方法,这个方法每次返回一个整形值,在下面这段代码中,就返回1到10的值
echo("Counting to ten: ")
for i in countup(1, 10):
echo($i)
一定要注意最后面那个冒号
如果你打算让他返回从10到1,那么你应该使用countdown(10, 1)
函数使用
Procedures 使用,相当于 函数
discardable 用于 声明
proc Addpro(x, y: int): int {.discardable.} =
return x + y
数组与序列
构建索引从1到5,元素数量为5个的数组
type
IntArray = array[1..5, int]
声明的时候也可以直接赋值
var arr: IntArray
arr = [5,10,15,20,25]
序列类型标志为sep 相当于动态数组或python list
var arrSep: seq[int]
# arrSep = @[5,10] # 赋值方式和数组一样用[],但前面多了个@符号
结构体与指针
自定义一个对象,相当于结构体
type
MyObj = object
name: string
age: int
FFI使用
FFI(Foreign Function Interface)是用来与其它语言交互的接口,Nim语言最终编译成C语言,所以使用FFI很方便
proc strcmp(a, b: cstring): cint {.importc: "strcmp", nodecl.}
let cmp = strcmp("C?", "Easy!")
echo cmp
win32 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)
0x02 识nim逆向
[2022蓝帽杯]Loader
这个题 比赛的时候拿到手属实蒙蔽了,感觉非常陌生,不过不了解知识点不代表我们不能做题。遇到不会的点的时候无脑动调往往能收获一些奇效。通过以往的经验我们知道基本上程序进行输入输出的地方肯定是关键函数。不断f8单步跟进注意程序动向,看看call哪个函数之后程序输出东西了无法单步了,那么打个断点,然后重新开始程序再f9到这个地方f7进这个call,反复如是 大概率能找到关键函数。接下来我们就按照如此的思路进行动调。
这里找到了main_0 函数
继续动调一直断到call print 的函数 即为我们要找的main函数
采用动调读取数据的方式我们发现这里是检验flag前五位
同样采用动调的方式我们可以看到下面的一个do-while循环是逆序读取了输入的前18位
下面通过同样的方法发现了一个字符串转int的函数 我们把它命名为str2int
接下来我们可以看到4个乘法(mm_loadu_si128)和一个减法(sub7FF66E )
剩下的就是一个check 动调可以直接拿到密文9
最后我们可以得到一个式子
input1 ^ 2 - 11 * (input 2^2 ) = 9
拿到https://www.wolframalpha.com/网站上去解方程 限制x的范围 得到x,y的解 拼接即为flag(拼接完发现少一位 在x结尾y开头用-或 0作为连接符)
小小小后记
关于这个题的复现,我只是在这里和大家分享一种,当比赛实战的时候遇到新知识点的逆向通用对策。对于这题设计真正十分精妙之处鲜少涉及,不过好在有大佬师傅总结的非常深刻了,强烈推荐大佬的这篇文章,有助于大家了解无文件 PE 文件加载的相关技术。
https://www.r4ve1.xyz/p/2022-bluehat-loader/#locate-kernel32dll
[2022蓝帽杯半决]babynim
熟悉的nim逆向 味太冲了太冲了
首先通过main函数一路跟进 跟进到 NimMainModule函数
主体很复杂看不懂 发现了flag字数长度限制的验证,更加确定这是关键函数了
if ( *(_QWORD *)v8 != 5i64 || *(_DWORD *)(v8 + 16) != 'galf' || *(_BYTE *)(v8 + 20) != '{' )
goto LABEL_5;
v9 = (unsigned __int64 *)input__hello_2;
if ( !input__hello_2 )
{
v11 = -1i64;
LABEL_21:
raiseIndexError2(41i64, v11);
}
v10 = *(_QWORD *)input__hello_2;
if ( *(_QWORD *)input__hello_2 <= 0x29ui64 )
{
v11 = v10 - 1;
goto LABEL_21;
} // input_hello2的内容就是flag?
// flag 字符部分57位
v5 = check__hello_3;
if ( *(_BYTE *)(input__hello_2 + 57) == '}' )
然后发现好像就initBigInt__6758Z85sersZ65ZOnimbleZpkgsZbigints4548O53O48Zbigints_1987这个函数中带关键参数,跟进看看。在这里发现了大质数乘法。
多处 multiplication的函数名也在暗示我们。
接着找大质数就可以了,可以直接搜索字符串。
poc:
import z3
a=z3.IntVal("56006392793428440965060594343955737638876552919041519193476344215226028549209672868995436445345986471")
z=z3.Solver()
flag=z3.Int("flag")
z.add(a * flag==z3.IntVal("51748409119571493927314047697799213641286278894049840228804594223988372501782894889443165173295123444031074892600769905627166718788675801"))
if z.check() ==z3.sat:
print(z.model())
0x03 总结
nim类题目的难点主要在于语言反编译过程中自成混淆,诸多的语言伴随的陌生函数会给我们的反编译代码阅读造成很大的困扰,解决这类题目笔者总结了以下三点:
1.记忆nim语言常见自定义函数及用途(一般都是完成一些初始化、系统功能函数,不会涉及)主要目的是为了防止大量读不懂的函数影响我们的程序跟进过程
2.粗暴动调 既然乱花渐欲迷人眼,我们不如冲入迷雾,直导黄龙。基本上程序进行输入输出的地方肯定是关键函数。不断单步跟进注意程序动向,看看call哪个函数之后程序输出东西了,那么打个断点,然后重新开始程序再f9到这个地方f7进这个call,反复如是 大概率能找到关键函数
3.某大佬曾经说过 逆向这东西 三分解七分猜 无论是什么语言编写的,程序算法逻辑肯定还是常规类型 注意识别特征变量,在识别的基础上大胆猜测即可。
0x04 参考链接
https://www.r4ve1.xyz/p/2022-bluehat-loader/
https://cloud.tencent.com/developer/article/1921518
https://blog.csdn.net/blue_fantasy/article/details/122804155
http://blog.nsfocus.net/nim-summary/
https://zhuanlan.zhihu.com/p/394239363
https://www.cnblogs.com/J0o1ey/p/15717342.html
- 本文作者: 绿冰壶
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1830
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!