NjRat
NjRat 样本分析
1. 样本信息
NjRat
(Bladabindi
) 是一种 .NET
RAT
(远程访问木马),允许攻击者控制受感染的机器。
IOC
类别 | 特征值 |
---|---|
MD5 | 556FE886EDD2DB888EE3A33A103C2364 |
SHA1 | 9D58E7B157FE41D86398FF587E10AE2FF3FB3EE9 |
SHA256 | 833F86074592648C0A758098E34AB605A2B922D94DBAB7141E2CE87ACEC03C35 |
C2 | 44gang44.duckdns.org:2222 |
2. 样本分析
采用dnSpy
加载样本,得知样本原始文件名为:ClassLibrary1.exe
2.1. 初始命令参数判断
样本执行后,会检查是否存在参数命令,如果参数命令为UP:[ProcessID]
,则设置HKEY_CURRENT_USER\di
的值为!
,关联指定进程并等待5000
毫秒后退出;如果参数命令为..
,则休眠5000
毫秒:
2.2. 互斥锁检查
打开指定的已命名的互斥体OK.RG
("49e91d08e684b1770e0cefa60401157a"
),如果存在,关闭当前进程,如果不存在,则新建此互斥锁:
2.3. 准备工作
2.3.1. 检查文件路径
作者定义了一个OK.CompDir
函数,将当前执行路径与%AppData%\services64.exe
进行比较:
如果当前执行的文件不是%AppData%\services64.exe
,且存在%AppData%\services64.exe
,则将%AppData%\services64.exe
删除,然后将自身复制到%AppData%\services64.exe
,采用Process.Start
启动目标进程,并采用ProjectData.EndApp()
关闭当前进程:
OK.DR
与OK.EXE
的内容如下:
2.3.2. 关闭附件管理器检查
在检查完执行路径后,会修改环境变量SEE_MASK_NOZONECHECKS
的值为1
,已关闭关闭附件管理器检查的弹框提醒:
2.3.3. 修改防火墙规则
接着会执行netsh firewall add allowedprogram
命令,添加出栈规则:
其中OK.LO
便是当前执行的文件信息类(FileInfo
类):
2.3.4. 开机自启项
在注册表HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
和HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Run
中,添加49e91d08e684b1770e0cefa60401157a
项,指向样本的执行路径(%AppData%\services64.exe
):
OK.sf
与OK.RG
的值如下:
2.3.5. 文件复制功能
将文件复制到用户的“开始”程序组目录下,并已49e91d08e684b1770e0cefa60401157a.exe
命名:
注:由于OK.IsF
的定义为:public static bool IsF = Conversions.ToBoolean("False");
,故该功能未执行!
2.4. 网络通信
准备工作结束后,将多线程执行OK.RC
函数:
2.4.1. 连接C2
该函数用于TCP
网络通信连接,其中存在一个OK.connect
函数,用于同C2
(44gang44.duckdns.org:2222
)连接,并发送关于主机的相关信息(OK.inf()
返回内容):
OK.H
与OK.P
的值如下:
2.4.2. 发送本地主机信息
发送的内容是已lv
开头,已|'|'|
分割的字符串(OK.Y
的定义为public static string Y = "|'|'|";
)。
获取系统磁盘序号
首先检索注册表HKEY_CURRENT_USER\Software\49e91d08e684b1770e0cefa60401157a\vn
的值是否为空,再通过OK.HWD()
获取信息,然后将OK.VN
与信息拼接起来再通过OK.ENB()
编码:
其中OK.VN
的定义是:c3BsaXRnYXRldWtyYXluYQ==
,通过Base64
解码得到的字符串是:splitgateukrayna
(分裂门乌克兰)不知道这是不是组织的标签。
OK.HWD()
将首先获取当前系统磁盘符,然后通过GetVolumeInformation
获取磁盘序号:
获取主机名和用户名
通过调用系统API
:Environment.MachineName
、Environment.UserName
,获取此本地计算机的 NetBIOS 名称和当前线程相关联的用户的用户名:
获取文件最后一次修改时间
然后将调用OK.FR()
获取上次修改当前文件的时间:
获取操作系统名称
从My.Computer.Info.OSFullName
属性中获取完整的操作系统名称:
获取操作系统 ServicePack 版本
从Environment.OSVersion
属性中获取ServicePack
版本:
判断操作系统版本
通过检索x86 Program Files
文件夹(Program Files (x86)
)是否存在,判断操作系统版本并记录:
判断摄像头是否存在
调用Ok.Cam()
,判断是否存在摄像头:
Ok.Cam()
中调用了Win32API
:capGetDriverDescriptionA
,检索捕获驱动程序的版本说明,成功则返回TRUE
,否则返回FALSE
:
获取当前窗口信息
调用OK.ACT()
函数,获取当前窗口的相关信息,并采用OK.ENB
进行Base64
编码,API
调用如下:
- 调用
GetForegroundWindow()
,检索前景窗口(用户当前正在使用的窗口)的句柄; - 调用
GetWindowTextLength()
,检索指定窗口标题栏文本的长度; - 调用
GetWindowText()
,将指定窗口标题栏的文本复制到缓冲区中; - 调用
GetWindowThreadProcessId()
,检索创建指定窗口的线程的标识符,以及创建该窗口的进程的标识符; - 调用
Process.GetProcessById().MainWindowTitle
,获取进程主窗口的标题;
创建注册表项
样本会创建HKEY_CURRENT_USER\Software\49e91d08e684b1770e0cefa60401157a
2.4.3. 响应内容处理
样本采用Receive
将响应的内容存储到本地缓冲区,其长度为5121
字节(private static byte[] b = new byte[5121];
)
创建新的线程执行OK.Ind
以处理提取的响应命令(该部分将放在后面的命令解析中进行说明):
2.5. 键盘监控
新建一个kl
类,用于存储键盘记录相关的信息:
然后创建线程执行WRK()
函数:
使用File.ReadAllText()
读取日志文件,然后进行按键捕获,最终通过File.WriteAllText()
写入日志到当前路径下的services64.exe.tmp
中:
调用GetAsyncKeyState()
检测键盘是否被按下,然后调用this.Fix()
捕获按键内容,特殊及组合按键的捕获方式如下:
普通按键的的捕获则是通过来kl.VKCodeToUnicode()
实现的:
kl.VKCodeToUnicode()
中使用GetKeyboardState()
将256个虚拟键的状态复制到指定的缓冲区,然后通过MapVirtualKey()
将虚拟键代码转换(映射)为扫描代码或字符值,再使用GetKeyboardLayout()
检索活动输入区域的键盘布局,最后调用ToUnicodeEx()
将指定的虚拟键代码和键盘状态转换为相应的 Unicode 字符:
调用this.AV()
,将获取的信息整合起来,准备写入到日志中去:
2.6. 命令解析
在OK.Ind
函数中,会对响应内容进行分割,其中分割符OK.Y
的值为|'|'|
,这为命令的下发组合的反推提供了依据:
2.6.1. proc 命令
当命令组合为:proc|'|'|~
时
将发送当前活动进程的Id
及数量到C2
服务器:
采用process.MainModule.FileVersionInfo.FileDescription
获取进程描述代码后进行Base64
编码,然后构造成包含进程ID、完整进程路径、编码描述代码的字符串:
对于Windows
系统进程处理略有不同:
最后将收集到的信息发送到C2
服务器:
当命令组合为:proc|'|'|k|'|'|<ProcessID>..
时
当采用了k
子命令时,将循环检索<ProcessID>
,杀死进程,每次执行后都将反馈结果至C2
服务器:
当命令组合为:proc|'|'|kd|'|'|<ProcessID>..
时
当采用了kd
子命令时,将循环检索<ProcessID>
,杀死进程并删除进程文件,每次执行后都将反馈结果至C2
服务器(彩蛋:这里Delete
后作者的标识依然是ER
-.-!):
当命令组合为:proc|'|'|re|'|'|<ProcessID>..
时
当采用了re
子命令时,将循环检索<ProcessID>
,重启进程,每次执行后都将反馈结果至C2
服务器:
2.6.2. rss 命令
当命令组合为:rss|'|'|
时
新建一个ProcessStartInfo
类并启动,该类的作用是附加一个子进程到当前进程,其中进行了如下配置:
- 设置
RedirectStandardInput
属性为true
,应用程序的输入是从StandardInput
流中读取的值; - 设置
RedirectStandardOutput
属性为true
,将应用程序的文本输出写入StandardOutput
流中的值; - 设置
RedirectStandardError
属性为true
,将应用程序的错误输出写入StandardError
流中的值; - 设置子进程名为
cmd.exe
; - 指定
OK.RS()
处理OutputDataReceived
(输出)和ErrorDataReceived
(报错)事件; - 指定
OK.ex()
处理Exited
(退出)事件; - 设置
UseShellExecute
设置为false
,表示子进程将继承调用进程的标准输入、标准输出和标准错误流; - 设置
CreateNoWindow
属性为true
,表示启动子进程而不创建包含它的新窗口; - 设置启动进程时使用的窗口状态为
Hidden
(隐藏); - 设置
EnableRaisingEvents
属性为true
,表示如果关联的进程终止时应引发Exited
事件。
OK.RS()
函数从后期绑定值中检索Data
字段,该字段包含子进程的StandardOutput/StandardError
流,然后采用Base64
编码后发送到C2
服务器:
2.6.3. rs 命令
当命令组合为:rs|'|'|<EnBase64[command]>
时
将通过Base64
解码第二个参数,然后通过StandardInput.WriteLine
向之前创建的StandardInput
流写入数据:
2.6.4. rsc 命令
当命令组合为:rsc|'|'|
时
将调用Kill()
,杀死之前创建的子进程:
2.6.5. kl 命令
当命令组合为:rsc|'|'|
时
将对键盘记录的日志进行Base64
编码后发送到C2
服务器:
2.6.6. inf 命令
当命令组合为:inf|'|'|
时
检索注册表HKEY_CURRENT_USER\Software\49e91d08e684b1770e0cefa60401157a\vn
的值是否为空,再通过OK.HWD()
获取系统磁盘卷序列号,与OK.VN
拼接后通过Base64
编码,然后将C2
域名、端口、AppData
、可执行文件名、当前进程名拼接后发送到C2
服务器:
2.6.7. prof 命令
当命令组合为:prof|'|'|~|'|'|<name>|'|'|<value>
时
将调用OK.STV()
添加注册表项:
OK.STV()
函数则是编辑注册表HKEY_CURRENT_USER\Software\49e91d08e684b1770e0cefa60401157a
,为其添加新的项,并设置对应键值:
当命令组合为:prof|'|'|!|'|'|<name>|'|'|<value>
时
将调用OK.STV()
添加注册表项,然后调用OK.GTV()
获取!
的键值,并将键值发送到C2
服务器。
OK.GTV()
函数内容如下:
当命令组合为:prof|'|'|@|'|'|<name>
时
将调用OK.DLV()
删除对应名称的键值:
OK.DLV()
函数内容如下:
2.6.8. rn 命令
当命令组合为:rn|'|'|<Extension_Name>|'|'|<URL>/<Base64[Gzip_Bit]>
时
首先将判断第三个参数是否是http
开头的连接,如果不是,则调用FromBase64String
解码第三个参数,再调用OK.ZIP()
对解码内容进行处理:
OK.ZIP()
函数会调用GZipStream
压缩和解压缩数据内容,不过当前的执行流程是解压缩:
如果第三个参数是http
开头的链接,则会使用WebClient.DownloadData()
下载文件:
上述两个步骤二选一执行后,将对在%Temp%
目录下,将所有字节写入随机命名的.<Extension_Name>
文件,新建进程执行该文件并向C2
服务器发送文件名:
2.6.9. inv 命令
当命令组合为:inv|'|'|<Registry_Name>|'|'|<String1>|'|'|<String2>
时
首先检索HKEY_CURRENT_USER\Software\49e91d08e684b1770e0cefa60401157a
中<Registry_Name>
的键值,如果其不为空,则将该键值返回到C2
服务器,并继续执行;
如果<Registry_Name>
的键值为空,且<String2>
长度为1
,则返回C2
服务器错误信息并结束此轮命令解析;如果<String2>
长度不为1
,则调用OK.ZIP()
对<String2>
进行解压缩,并设置<Registry_Name>
键值为<String2>
解压缩后的内容,成功则发送信息至C2
服务器;
但从整个流程来看,<Registry_Name>
中存放的是一个插件。样本首先检索<Registry_Name>
是否已包含插件,如果没有,则判断<String2>
是否是插件内容,如果是则写入<Registry_Name>
,然后通过OK.Plugin()
进行插件调用:
OK.Plugin()
函数首先调用Assembly.Load()
加载插件程序集,然后通过Assembly.GetModules()
枚举程序集的所有模块,再寻找.A
结尾的类,找到后使用Assembly.CreateInstance()
创建它的实例:
然后使用NewLateBinding.LateSet()
方法向后期绑定字段写入:h
(C2
域名)、p
(C2
端口)、osk
(<String1>
),然后调用NewLateBinding.LateCall()
执行start
函数,最后写入off
为true
:
2.6.10. ret 命令
当命令组合为:ret|'|'|<Registry_Name>|'|'|<String>
时
从函数结构可以看出功能同inv
命令类似,首先查找本地注册表<Registry_Name>
的键值,不存在则将<String>
写入并进行插件加载:
然后调用NewLateBinding.LateGet()
获取GT
的值,编码后发送给C2
服务器:
2.6.11. CAP 命令
当命令组合为:CAP|'|'|<Width>|'|'|<Height>
时
将通过Bitmap.GetThumbnailImage()
方法,截取<Width>
宽,<Height>
高的屏幕,然后使用OK.getMD5Hash()
将截图加密,最后通过OK.Sendb()
发送到C2
服务器:
OK.getMD5Hash()
函数内容如下:
OK.Sendb()
函数内容如下:
2.6.12. P 命令
当命令组合为:P
时
将向C2
服务器发送P
字符:
2.6.13. un 命令
un
命令存在三个子项:~
、!
、@
:
当命令组合为:un|'|'|~
时
调用OK.UNS()
会首先执行OK.pr(0)
将当前进程设置为非关键进程,然后删除被修改的注册表项及防火墙规则,删除启动文件夹下的样本文件,调用cmd
隐藏窗口删除自身文件,最后结束当前进程:
OK.pr(0)
调用了未公开的API
函数NtSetInformationProcess()
将当前进程设置为非关键进程,防止进程结束时触发BSOD
:
当命令组合为:un|'|'|!
时
调用OK.pr(0)
将当前进程设置为非关键进程,然后退出当前进程。
当命令组合为:un|'|'|@
时
调用OK.pr(0)
将当前进程设置为非关键进程,然后新进程启动自身并退出当前进程,可以理解为安全的重启。
2.6.14. up 命令
当命令组合为:up|'|'|<URL>/<Base64[Gzip_Bit]>
时
前半段与rn
命令下载功能类似,不过up
命令指向的文件只能是exe
,首先会根据第二个参数的内容获取目标文件的二进制,设置注册表di
为空,将二进制文件保存到本地%temp%
目录下,然后将随机生成的文件名发送到C2
服务器,然后调用Process.Start()
启动该文件,参数命令为UP:<当前进程ID>
,然后进入一个循环,当di
被设置为!
时,卸载自身:
2.6.15. RG 命令
当命令组合为:RG|'|'|~|'|'|<Registry_Item>
时
首先会调用OK.GetKey()
获取<Registry_Item>
项,然后通过GetSubKeyNames()
和GetValueNames()
枚举该项中的所有子项和对应键值,最后发送到C2
服务器:
OK.GetKey()
函数构造如下:
当命令组合为:RG|'|'|!|'|'|<Registry_Item>|'|'|<Registry_Name>|'|'|<Registry_Value>|'|'|<Registry_Kind>
时
设置<Registry_Name>
项的键值为<Registry_Value>
,类型为<Registry_Kind>
:
当命令组合为:RG|'|'|@|'|'|<Registry_Item>|'|'|<Registry_Name>
时
将删除<Registry_Name>
的项:
当命令组合为:RG|'|'|#|'|'|<Registry_Item>|'|'|<Registry_Name>
时
将创建<Registry_Name>
的项:
当命令组合为:RG|'|'|$|'|'|<Registry_Item>|'|'|<Registry_Name>
时
将递归删除<Registry_Name>
项和它的其它子项:
3. 参考资料
https://cybergeeks.tech/just-another-analysis-of-the-njrat-malware-a-step-by-step-approach/
https://www.secpulse.com/archives/73878.html
https://app.any.run/tasks/78913e0b-1419-4571-8611-ac3372ffd578/#
https://www.virustotal.com/gui/file/833f86074592648c0a758098e34ab605a2b922d94dbab7141e2ce87acec03c35