厚客户端渗透介绍(四):程序集测试
导语:厚客户端渗透测试介绍是一个系列博客文章,上一篇我们讲到了文件系统和注册表的测试,这一章我们来看看程序集的测试。
厚客户端渗透测试介绍是一个系列博客文章,上一篇我们讲到了文件系统和注册表的测试,这一章我们来看看程序集的测试。
示例应用下载地址:BetaFast Github repo。
已发布文章
· GUI测试
· 网络测试
· 文件系统和注册表测试
程序集控制
我们在编译库和可执行文件时,可以采取一些安全措施来防止代码被利用:
· 地址空间布局随机化(ASLR)—应用程序在加载时,在内存中的地址是随机的,这样可以防御return-to-libc攻击,这种攻击手法可以通过覆写特定地址导致命令执行。
· SafeSEH—将安全异常处理函数存储到二进制文件中,防止攻击者在调用恶意的异常时强制应用程序执行代码。
· 数据执行保护(DEP)—内存区域标记为不可执行,防止攻击者在这些区域存储代码来执行缓冲区溢出攻击。
· 代码验证/强命名—程序集可以通过签名来进行保护。如果没有签名,攻击者能够修改并替换为恶意内容。
· 控制流防护(CFG)—ASLR和DEP的扩展,用于对可执行代码的地址进行限制。
· HighentroyVA—64位ASLR
我们的安全研究人员发布了一款工具PESecurity,这款工具可以检测二进制文件是否使用了代码执行防御技术。我们测试的很多厚客户端程序中,它们的安装目录中都使用了程序集,文件数量很多,而PESecurity可以检测大量的文件,对我们测试非常有帮助。
在下面的例子中,我们用PESecurity来测试一下BetaBank.exe。它采用了ASLR和DEP防御技术。SafeSEH只适用于32位的程序集。不过,该可执行文件没有进行签名:
反编译
反编译是测试厚客户端程序时我最喜欢最常用的一种技术。我以前写程序时也犯了很多编程错误,因此找其他程序员的错误是很爽的一件事情。借助下面这些工具,可以将.Net程序集还原成源代码:
这是因为.Net程序集是托管代码。当一个.Net程序被编译时,它会被编译成一个中间语言代码。只有在运行时环境中,中间语言代码会被编译成机器码。.Net程序集很容易被还原成源代码,因为中间语言包含了很多信息,比如类型和名称。
非托管代码,如C或者C++,它们直接编译成二进制。不像C#一样通过公共语言运行库环境来运行,而是直接加载到内存中。
信息泄露
托管代码
下面的案例将使用BetaBank.exe这个程序,可以在我们的github上找到该程序。这里我们也会用dnSpy作为我们的反编译器。
我一般测试厚客户端时,第一件事就是查找是否存在敏感信息硬编码,比如凭证,加密密钥和连接字符串等。查找敏感信息甚至都不用进行反编译,查找敏感信息最好的方式当然是先按配置文件。当一个.net程序集运行时,它会搜索配置文件来查找在二进制文件中引用的全局值,如连接字符串,web端点,或者是密码。这里推荐一款工具,Procmom,还有上一篇文章中提到的方法和步骤,能够有效的识别这些文件。
在这个例子中,我们在BetaBank程序的配置文件中找到了一个连接字符串。借助工具SQL Server Management Studio可以直接连到数据库中。
10.2.2.55是我运行docker的虚拟机ip:
不过对于源代码中的信息泄露,首先我们需要反编译程序的二进制文件。
如下图,用dnSpy加载BetaBank.exe程序:
dnSpy的搜索功能非常强大。我只要输入关键词”key”,”IV”,”connection”,”password”,”encrypted”和“decrypt”,就可以查找程序是如何处理加密,身份认证和数据库连接信息。软件的搜索功能中还有一个筛选器。下面,我限定了只搜索指定的文件,当然,也可以限定只搜索特定的二进制文件和对象类型。在BetaBank程序中,好像找到了一个硬编码密钥,在BetaBank.ViewModel.RegisterViewModel和 BetaBank.ViewModel.LoginViewModel模块中:
搜索“password”,找到一个硬编码的加密密码。显然,开发人员在客户端上进行授权检查。用户名”The_Chairman”直接与用户名输入框中的值进行比较,而加密的密码也是直接与密码输入框中密码的加密值进行比较。
BetaEncryption这个类可以进行反编译,可以看到一些自定义的加密逻辑:
非托管代码
前面也提到过,在测试非托管代码时,我们是无法如此清晰的查看到源代码的。为此我们看了微软Azure的CTO的推特,发现了一款工具Strings。Strings也已经包含在Sysinternals工具包里了,这个工具可以输出存储在可执行文件中的字符串列表。当分析内存转储或者是非托管代码的二进制文件时,我会用它来获取所有字符串的列表(虽然大部分的字符串都是没有意义的),然后在这一堆字符串列表中搜索上面提到的关键词。
修改程序集
使用dnSpy,我们可以修改一个类,也可以重新编译二进制文件。下面的例子中,我已经逆向了加密函数,当用户通过身份验证成功登录到应用程序时,我会用一个弹窗来显示已经解密的管理员密码,代码修改如下:
将模块另存为一个新的可执行文件:
最后,当我以某用户身份登录时,弹窗显示了“The_Chairman”的明文密码:
在进行信息收集时,在程序中添加弹窗是非常有用的,这就像是我在代码中添加一条print语句,而不是开启调试模式。
BetaBank开发人员可能会通过移除硬编码加密密钥来修复这个漏洞,然而,作用不大,因为BetaBank自定义的加密逻辑依然可以被逆向分析出来,然后获得解密的密钥。下面,我添加了一个函数,对明文密码和加密密码进行异或处理,然后公开加密密钥。在解密函数中,我通过传递一个已知的明文密码及其加密值来获取密钥:
但是那样工作量有点大。如果我们后面不需要“The_Chairman”的密码,可以直接把用户名和加密的密码放到login函数中。布尔逻辑也可以修改,这样任何经过服务端验证的逻辑,比如IsAdmin(),可以修改为始终返回true。
混淆
源代码也并不总是清晰可读的,有时候,源代码是经过混淆的。虽然混淆并没有什么安全性可言, 但还是给安全人员阅读代码增加了不少麻烦和难度。
下面的代码是上面提到的BetaEncrypted类,不过我添加了一些混淆,重命名了类型,方法名和参数名:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; public static class jinjnhglkjd { public static string dsfggd(byte[] erttr, string dgfhfgi) { byte[] plaintextBytes = Encoding.ASCII.GetBytes(dgfhfgi); byte[] ciphertextBytes; int messageLength = plaintextBytes.Length; while (messageLength % erttr.Length != 0) { Array.Resize(ref plaintextBytes, plaintextBytes.Length + 1); plaintextBytes[plaintextBytes.Length - 1] = 0x00; messageLength += 1; } ciphertextBytes = new byte[messageLength]; int startingIndex = 0; for (int i = 0; i < (messageLength / erttr.Length); i++) { for (int j = 0; j < erttr.Length; j++) { ciphertextBytes[j + startingIndex] = Convert.ToByte(plaintextBytes[j + startingIndex] ^ erttr[j]); } startingIndex++; } return Convert.ToBase64String(ciphertextBytes); } }
所有的功能都正常,而且能够正常运行。有些混淆技术做的更深入,直接混淆代码,让代码变得不可读。这些技巧会让我们在源代码中搜索关键词或者你想变得十分困难。dnSpy软件的程序集资源管理面板显示的都是毫无意义的类名,搜索特定的关键词如“Encrypt”,也没什么结果。
当然,也有很多反混淆的工具,比如de4dot。下面的代码就是用这款工具进行反混淆之后的,至少类名有一些可读性,方法名也命名为了method的一些变形,而不是dsfggd这些乱码:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; public static class class_1 { public static string method_1(byte[] byte_1, string string_1) { byte[] plaintextBytes = Encoding.ASCII.GetBytes(string_1); byte[] ciphertextBytes; int messageLength = plaintextBytes.Length; while (messageLength % byte_1.Length != 0) { Array.Resize(ref plaintextBytes, plaintextBytes.Length + 1); plaintextBytes[plaintextBytes.Length - 1] = 0x00; messageLength += 1; } ciphertextBytes = new byte[messageLength]; int startingIndex = 0; for (int i = 0; i < (messageLength / byte_1.Length); i++) { for (int j = 0; j < byte_1.Length; j++) { ciphertextBytes[j + startingIndex] = Convert.ToByte(plaintextBytes[j + startingIndex] ^ byte_1[j]); } startingIndex++; } return Convert.ToBase64String(ciphertextBytes); } }
混淆并不能保护代码,但是阅读源代码时会让人头疼,甚至比读其他人的大型代码库还令人头疼。
发表评论