|
.NET 二进制文件通常比已编译的二进制文件更容易进行逆向工程,就像 Java 中的 .jars 一样,它们可以被反编译以显示底层源代码;然而,为这些二进制文件编写反混淆器对于新的分析师来说通常并不简单。本篇文章将介绍为 Blackguard 信息窃取器编写一个字符串去混淆器。
什么是 BlackGuard?
Blackguard 信息窃取器由 Zscaler 于 2022 年 3 月发现,零售价为每月 200 美元或终身价 700 美元。Blackguard 专注于从 Web 浏览器、VPN 客户端、messaging 客户端、FTP 客户端和加密货币钱包中窃取信息。它通过本文其余部分中描述的加密字节数组对其字符串进行了严重混淆。
字符串混淆
对于字符串混淆,Blackguard 使用一个大字节数组,在开始执行时通过 XOR 解密,多个函数将在解密后获取此字节数组的子集并返回 UTF-8 编码的字符串。由于每个反混淆函数都是随机放置的,因此偏移量没有规律。由于 DnSpy 缺乏在 Cutter 或其他反汇编程序中常见的自定义内联注释和脚本,因此反混淆是一项艰巨的任务。如果没有像 Dnlib 这样的库,分析师将不得不手动拉出数组的一部分并手动解码。
Dnlib 介绍
我们可以使用 Dnlib 读取 .NET 二进制文件并更改指令,该库是 DnSpy 和 De4Dot 反混淆工具等工具的基础。我们可以使用它来将 Blackguard 示例中的混淆函数重写为字符串表示形式。
最简单的方法是遍历二进制文件的类和方法来检索字节数组,接下来,我们可以创建一个返回去混淆字符串的所有方法的列表,并将对它们的调用替换为字符串本身。首先,我们需要从 Dnlib 中的二进制文件加载模块,这是通过定义一个 ModuleContext 并将其传递给 ModuleDefMD.Load 来完成的。这将返回一个对象,该对象可用于读取有关模块的元数据,包括类定义和 .NET 指令。
- ModuleContext modCtx = ModuleDef.CreateModuleContext();
- ModuleDefMD module = ModuleDefMD.Load(@"path\to\blackguard.exe", modCtx);
复制代码
检索初始字节数组
在检索初始字节数组时,了解我们正在寻找哪些指令非常重要。在 DnSpy 中,可以查看特定函数的中间语言 (IL) 指令。中间语言是 .NET 虚拟机在执行期间用于将高级代码转换为字节码的语言。可以将其比作 Java 虚拟机 (JVM) 在运行时解释的 Java 字节码。
查看设置初始字节数组的函数,可以看到初始化数组并将其压入堆栈的指令 newarr。我们还看到了对 InitializeArray 的调用。这个函数接受一个 System.Array 类型的对象和一个 RuntimeFieldHandle。RuntimeFieldHandle 是一个指向存储在模块中的数据的指针,用于用数据填充数组。
在调用 InitializeArray 之前,第 6 行的 ldtoken 指令用于将值压入堆栈。根据 .NET 文档,该指令用于 "将元数据令牌转换为其运行时表示"。这很可能是包含我们数组的初始数据的 RuntimeFieldHandle。 我们可以通过抓取操作数的 InitialValue 字段轻松地在 Dnlib 中抓取这些数据。
最后,我们看到稍后在该方法中使用的一些异或指令,它们是解密循环的一部分。这些很好地表明该函数设置了字节数组。
查找设置字节数组的函数的最简单方法是查找 newarr 和 xor 指令的用法,获取这个数组的整个代码是:
- // Will grab the key array from the binary
- // Params: type (TypeDef): The class to look for the key in
- static byte[] getKey(TypeDef type)
- {
- // Loop through class methods
- foreach (var method in type.Methods)
- {
- if (!method.HasBody)
- continue;
- // Check if method declares a new array and has an xor instruction
- Instruction newarr = containsOpCode(method.Body.Instructions, OpCodes.Newarr);
- Instruction xor = containsOpCode(method.Body.Instructions, OpCodes.Xor);
- if (newarr != null && xor != null)
- {
- // Pass instructions to the getArrayData Function Below
- byte[] bArr = getArrayData(method.Body.Instructions);
- // Decrypt the byte array once we have its initial value
- for (int i = 0; i < bArr.Length; i++)
- {
- int d = bArr[i] ^ i ^ 170;
- bArr[i] = (byte)d;
- }
- return bArr;
- }
- }
- return null;
- }
- // Gets array's data from method
- static byte[] getArrayData(System.Collections.Generic.IList<Instruction> instructions)
- {
- bool foundArr = false;
- foreach(var ins in instructions)
- {
- if(ins.OpCode == OpCodes.Newarr)
- {
- foundArr = true;
- }
- // If the current instructions opcode is ldtoken
- // and we already passed the newarr instruction then
- // this must be the array RuntimeFieldHandle
- if(ins.OpCode == OpCodes.Ldtoken && foundArr )
- {
- FieldDef field = (FieldDef)ins.Operand;
- // return array's initial value
- return field.InitialValue;
- }
- }
- return null;
- }
复制代码
替换反混淆函数
创建反混淆函数列表
要找到反混淆函数并替换它们,首先我们需要找到将从解密的字节数组中获取子集的函数,将其转换为字符串并返回值。最简单的方法是查找对 GetString 函数的调用。由于这是一个系统函数,编译器无法知道该函数在内存中的确切位置。为了解决这个问题,二进制文件使用 callvirt 指令来定位 GestString 函数。我们可以查找该指令的用法并验证它是否用于调用 GetString。代码看起来像这样:
- static MethodDef getDecryptMethod(TypeDef type)
- {
- // Loop through methods in class
- foreach(var method in type.Methods)
- {
- if (!method.HasBody)
- continue;
- // Loop through method instructions
- foreach(var ins in method.Body.Instructions)
- {
- // Check if instruction is using the callvirt opcode
- if(ins.OpCode == OpCodes.Callvirt)
- {
- // Verify that it is calling GetString
- if (ins.Operand.ToString().Contains("GetString"))
- {
- // Return the method definition itself
- return method;
- }
- }
- }
- }
- return null;
- }
复制代码
一旦有了 GetString 函数的方法定义,我们就可以寻找调用它的其他方法。一旦这样做了,就可以将方法定义及其去混淆的字符串存储在 HashMap 中,以便以后轻松提取。当我们去替换对这些反混淆函数的调用时,这将很有用。查找这些函数的代码如下所示:
- /* Params:
- TypeDef type: Class to look for deobfuscation functions in
- byte[] kArr: Byte array used for decryption
- MethodDef decryptMethod: Main Deobfuscation Method Definition
- */
- static Dictionary<MethodDef, string> decryptFuncs(TypeDef type, byte[] kArr, MethodDef decryptMethod)
- {
- Dictionary<MethodDef, string> funcs = new Dictionary<MethodDef, string>();
- foreach(var method in type.Methods)
- {
- if (!method.HasBody)
- continue;
- // Starting at index two to grab previous two instructions once we
- // find the call to the main deobfuscation method
- for (var i = 2; i < method.Body.Instructions.Count; i++)
- {
- // Previous two instructions will be paramaters for method
- Instruction prevIns = method.Body.Instructions[i - 1];
- Instruction prevprevIns = method.Body.Instructions[i - 2];
- Instruction ins = method.Body.Instructions[i];
- // Look for call instruction that calls the main deobfuscation method
- if(ins.OpCode == OpCodes.Call && ins.Operand == decryptMethod)
- {
- // Add method to hashmap with the method as the key and
- // the deobfuscated string as the value
- funcs.Add(method, decryptString(prevprevIns.GetLdcI4Value(), prevIns.GetLdcI4Value(), kArr));
- }
- }
- }
- return funcs;
- }
- // Returns deobfuscated string
- static string decryptString(int x, int y, byte[] kArr)
- {
- return Encoding.UTF8.GetString(kArr, x, y);
- }
复制代码
查找和替换反混淆函数用法
现在我们有了一个包含所有反混淆函数及其对应字符串的映射,我们可以再次循环遍历二进制文件并替换这些函数的用法。为此,可以简单地将调用指令替换为 ldstr 后跟字符串。 ldstr 指令会将一个新的字符串对象压入堆栈,这在字符串用作另一个函数的参数时很有用。这意味着代码仍将正常运行,并使用适当的混淆方法。执行此操作的代码如下:
- // Grab hashmap of deobfuscation functions
- var obfFuncs = getObfFuncs(module);
- // Loop through each class in module
- foreach (var type in module.Types)
- {
- if (!type.HasMethods)
- continue;
- // Loop through each method in class
- foreach (var method in type.Methods)
- {
- if (!method.HasBody)
- continue;
- // Loop through each instruction in method
- foreach (var inst in method.Body.Instructions)
- {
- // Check if the current instruction is for calling a function
- if (inst.OpCode == OpCodes.Call && inst.Operand is MethodDef)
- {
- // Check to see if method being called is in hashmap
- if (obfFuncs.ContainsKey((MethodDef)inst.Operand))
- {
- // Replace opcode with ldstr
- inst.OpCode = OpCodes.Ldstr;
- // Replace operand with deobfuscated string
- inst.Operand = obfFuncs[(MethodDef)inst.Operand];
- }
- }
- }
- }
- }
复制代码
可以在下图中看到我们反混淆工作的前后对比:
替换 Base64 解码函数
现在我们已经对二进制文件中的所有字符串进行了去混淆处理,可以看到另一个问题,这些字符串中有很多是 Base64 编码的,并且正在被另一个混淆函数解码。
快速浏览一下,这些函数只返回 Base64-Decoded 字符串:
我们可以使用相同的过程来查找这些 Base64-Decoding 函数,就像我们通过查找 FromBase64String 的调用指令来查找字符串反混淆函数一样。
- static List<MethodDef> getB64Funcs(ModuleDefMD module)
- {
- // Generate empty list to store function
- List<MethodDef> b64Funcs = new List<MethodDef>();
- // Loop through classes
- foreach (var type in module.Types)
- {
- if (!type.HasMethods)
- continue;
- // Loop through Methods
- foreach(var method in type.Methods)
- {
- if (!method.HasBody )
- continue;
- // Loop through instructions
- foreach(var ins in method.Body.Instructions)
- {
- // Look for call to FromBase64String
- if(ins.OpCode == OpCodes.Call && ins.Operand.ToString().Contains("FromBase64String"))
- {
- // Add found method to list
- b64Funcs.Add(method);
- }
- }
- }
- }
- return b64Funcs;
- }
复制代码
从这里,我们可以用它们的字符串表示来替换这些函数,就像我们对反混淆函数所做的那样。必须确保传递给 Base64-Decoding 函数的参数是一个字符串,所以在执行替换之前检查前面的指令是 ldstr。最终循环如下所示:
- // Get map of obfuscated functions
- var obfFuncs = getObfFuncs(module);
- // Get list of base64 functions
- var b64Funcs = getB64Funcs(module);
- // Loop through classes
- foreach (var type in module.Types)
- {
- if (!type.HasMethods)
- continue;
- // Loop through methods
- foreach (var method in type.Methods)
- {
- if (!method.HasBody)
- continue;
- // Loop to replace the obfuscated strings
- foreach (var inst in method.Body.Instructions)
- {
- if (inst.OpCode == OpCodes.Call && inst.Operand is MethodDef)
- {
- if (obfFuncs.ContainsKey((MethodDef)inst.Operand))
- {
- inst.OpCode = OpCodes.Ldstr;
- inst.Operand = obfFuncs[(MethodDef)inst.Operand];
- }
- }
- }
- // Loop through instructions again to find base64 functions
- for (var i = 1; i < method.Body.Instructions.Count; i++)
- {
- // Get previous instruction
- var prevIns = method.Body.Instructions[i - 1];
- var ins = method.Body.Instructions[i];
- if (ins.OpCode == OpCodes.Call && ins.Operand is MethodDef)
- {
- // Check to see if instruction is calling base64 function
- if (b64Funcs.Contains((MethodDef)ins.Operand))
- {
- // Verify that previous instruction is a string
- if (prevIns.Operand is string && prevIns.OpCode == OpCodes.Ldstr)
- {
- try
- {
- // base64 decode string
- string b64Dec = Encoding.ASCII.GetString(Convert.FromBase64String(prevIns.Operand.ToString()));
- // Replace with ldstr instruction
- ins.OpCode = OpCodes.Ldstr;
- ins.Operand = b64Dec;
- // Replace paramater passing with nop instruction as it's no longer needed
- prevIns.OpCode = OpCodes.Nop;
- }
- catch
- {
- continue;
- }
- }
- }
- }
- }
- }
- }
复制代码
现在二进制文件中的所有字符串都已完全去混淆,我们可以继续分析:
结论
如果想查看完整的代码,可以在我的 GitHub 或 我的项目 页面上找到它。如果对本文有任何问题或反馈,请随时在我的 Twitter 或 LinkedIn 上给我发消息。
感谢阅读,happy reversing!
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
x
|