混淆与反混淆
在我们讨论混淆技术之前,让我们从缩小和美化的概念开始。
缩小化
缩小是减小JavaScript文件大小,同时保持其功能的过程。这是通过删除不必要的字符(例如空格或换行符)来实现的。有时缩小还包括缩短变量名称和重构源代码。许多 JavaScript 框架都会压缩默认发布的代码。
缩小的目标是提高JavaScript 的传输效率。以下是使用UglifyJS创建的缩小代码的(有点人为的)示例,但澄清了这一点。
通过执行以下转换,文件大小显着减少了约 40%:
-
空格、新行、注释和不必要的大括号被删除。
-
局部变量input已重命名为e.
-
if-else 语句被所谓的三元运算符 (x?a:b) 取代。
顺便说一句,您通常可以通过扩展名来识别缩小的源.min.js。
美化
美化是使缩小的代码再次变得易于人类阅读的过程。美化精简代码的方法有以下几种:
-
默认情况下, Chromium 调试器会漂亮地打印JavaScript 源代码。这会向代码添加缩进和换行符。
-
要美化本地 JavaScript 文件,可以使用js-beautify之类的工具:
# Install
$ pip install jsbeautifier
# Usage
$ js-beautify main.min.js > main.js
还有一个在线服务:
但请注意,此过程无法逆转变量重命名。
源maps
完全恢复缩小代码的一种方法是所谓的源映射。源映射文件仅包含一个 JSON 对象,如下所示。关键部分是mappings字符串,它包含原始代码和缩小代码之间的映射。
映射是VLQ Base64编码的,我不会在这里详细讨论。
您可以通过源映射可视化工具来可视化映射。例如,23->2:5意味着第 23 列的单词e=映射到第 2 行第 5 列的单词input=。
有时,源映射与缩小的代码一起提供。这是通过向包含源映射位置的缩小 JavaScript 添加注释来完成的。仅当打开开发人员工具时才会加载源地图。
缩小和分析
在分析 JavaScript 的过程中,缩小并不是什么大问题。是的,如果没有源映射,由于不可逆的转换,代码就不太容易理解。然而,如上所述,仍然可以通过静态分析来检测秘密、链接和对易受攻击的函数的调用。
在动态分析过程中,描述性标识符的缺乏可以通过Chromium 开发者工具中的调试器来弥补。它在执行后在 JavaScript 语句旁边显示变量的值。
混淆
JavaScript 混淆的目的是防止其分析(无论是静态还是动态),同时保持其功能。为了实现这一点,即使代码性能有所损失也是可以接受的。通常,混淆器会将代码中的单词和符号放入数组中。在执行期间,通过重复引用该数组来重建原始代码。该图显示了这种所谓的打包的示例。
正如您所看到的,发生了以下转变:
document.getElementById("input").value ➡️ document[d(0x0)](d(0x1))[d(0x2)];
d是一个引用数组的函数,在 function 中定义a。 0x0是第一个字符串,0x1是第二个字符串,依此类推。
其他一些经常使用的转换:
-
将标识符重命名为十六进制字符串
-
美化后就会崩溃的自卫代码
-
注入死代码会增加文件大小
有些工具甚至提供调试保护,使得“几乎不可能使用开发人员工具的调试器功能” 2。互联网上有几个混淆器。一种流行的工具是javascript-obfuscator, 您可以从命令行或在线使用它。
存在更多不同变体的混淆器。一个突出的例子是JSFuck。它仅使用这六个字符:![]+()
反混淆
反混淆是使混淆的代码再次变得可供人类阅读的过程。正如您从上面的示例中可以想象的那样,这并不是不可能的。美化混淆的代码只是第一步,但仍然使代码几乎不可读。
此外,反混淆器通常依赖于以下技术:
-
数组拆包
-
替换代理功能
-
删除死代码分支
-
重命名标识符
互联网上也有很多反混淆器。一个例子是javascript-deobfuscator。它可以从命令行或再次在线使用。我也喜欢JSNice的结果。它根据统计模型重命名标识符,这通常有助于更好地理解代码。
混淆与分析
不幸的是,反混淆的结果通常无法接近原始的 JavaScript。尽管如此,它们通常有助于简化代码流程,这对于后续的动态分析来说是一个优势。下图说明了从普通代码到混淆代码以及最终反混淆代码的转换。
实践:分析混淆代码
在介绍了 JavaScript 的静态分析、动态分析和混淆等主题后,我将使用示例应用程序对混淆的 JavaScript 进行动态分析。在每一步中,我都会解释我的方法、程序和结论。
在深入研究动态分析工具之前,我想先谈谈适当的心态。动态分析可能是一个艰苦的过程。有时,记住您正在搜索的任何客户端功能肯定存在于客户端代码中会有所帮助。举个例子:如果 Web 应用程序在将数据发送到服务器之前对数据进行签名,则签名密钥必须位于客户端代码中。这种态度可以帮助你更加努力。
应用示例
我要分析的应用程序是上面的ping 服务,但使用了混淆的 JavaScript。用户提交主机并接收服务器的 ping 响应。Burp Suite 中请求的拦截显示应用程序发送了一个 JWT,其中包含目标服务器的主机(IP)。
由于 JWT 已签名,因此无法在不使签名无效的情况下操纵主机值。这阻止我在 Burp Suite 中操纵请求,这对于渗透测试至关重要。但由于 JWT 是使用 JavaScript 计算的,因此密钥必须位于客户端代码中的某个位置。目的是提取密钥以对端点进行正确的分析。
寻找切入点
首先,我想了解单击“提交”按钮时会发生什么。为此,我访问开发人员工具的“元素”选项卡并检查事件侦听器。这会显示一个单击事件侦听器。
单击该链接将打开“源”选项卡并突出显示侦听器函数的位置。请注意 Chromium 的开发工具如何美化混淆的代码,这些代码实际上是在一行中编写的。
我在提交函数中的第一个语句上放置了一个断点,然后单击“提交”按钮。断点触发并暂停执行。这意味着,我找到了正确的位置,现在可以一步步执行了。
找到价值
有时,准备发送的 JWT 必须存储在 JavaScript 变量中。我使用开发人员工具的“跳过下一个函数调用”按钮来找到这个位置。下面突出显示的行包含 JWT 标头、有效负载以及最终编码的 JWT(以特征字符串开头)ey[...]
签名必须发生在计算 JWT 的行中的某个位置。我在该行中放置了一个断点并删除了旧的断点,以在分析过程中跳过不必要的语句。之后,我再次单击“提交”按钮。
拆包功能
现在,棘手的部分开始了。我使用“步骤”按钮继续执行。这导致了混淆代码的解包函数,其中 JavaScript 符号和字符串从数组中加载。
现在整个数组由一个变量引用。这可以从右侧开发者工具的“范围”部分观察到。
这一步表明将返回数组的第 20 个条目。
实际上返回了值JWS,其索引为 20。
重建签名通话
迭代地使用相同的方法,我观察到数组中的下一个解压值是:
-
sign
-
HS256
-
6465623837323564656533323462383134656535386133626434353431373866
这让我可以重构用于计算 JWT 的调用:计算 JWT 的行的结构是KJUR['jws'][a][b](c,d,e,f)。变量a、b和是c上面f的解包值。变量d和e是 JWT 的标头和有效负载。将所有内容放在一起并用点符号替换 array- 会导致以下函数调用:
KJUR.jws.JWS.sign("HS256", {header}, {payload}, "6465[...]3866")
继续下一步验证这一点。范围显示哪些变量已传递给函数。
KJUR.jws.JWS.sign()
功能范围
此时,最简单的方法就是研究这个函数是否来自 JavaScript 库以及JWS.sign到底在做什么。快速谷歌搜索表明,确实正在使用一个库。GitHub 用户kjur将其称为jsrsasign。
文档显示该方法使用指定密钥生成 JWS 签名。在本例中,位于函数调用位置 4 的键是一个十六进制值。这意味着上面的长整数是关键。
解码密钥
我解码十六进制密钥并将结果编码为 base64,因为这是 Burp Suite 扩展JWT Editor所需的。
签署被操纵的 JWT
在 JWT 编辑器扩展中,我创建了一个新的对称密钥,因为 HS256 是一种对称签名方法。我将上一步中的密钥粘贴到该"k"字段中。
我将 Burp Suite 的代理历史记录中的请求发送到 Repeater 并切换到 JSON Web Token 选项卡。在这里,我将主机替换127.0.0.1为infosec.exchange并单击“签名”按钮。
我将带有更新签名的请求发送到服务器并接收 的 ping 输出infosec.exchange。这意味着签名计算成功,我现在可以根据需要操纵该值。
使用 Burp Suite 扩展CSTC,我现在可以自动化签名过程并对目标执行主动扫描,从而检测命令注入漏洞。但这是另一篇博文的内容。
本地覆盖
本地覆盖是一种在页面加载时保持 JavaScript 更改的方法。在某些情况下,这可能有助于分析。
典型用例是:
-
更改代码流以绕过客户端保护
-
添加console.log语句来记录变量内容
-
重构代码以美化和反混淆
开发者工具的临时变化
Chromium 的开发人员工具允许您直接在“源”选项卡中更改源的内容。我将使用上面的ping 服务进行演示。第 5 行和第 6 行实现了一个客户端过滤器,用于从主机值中删除特殊字符。
为了绕过过滤器,我注释掉了这两行并使用 Ctrl+S 保存更改。顶部栏中的警告标志表明这些更改不是持久的。
正如您所看到的,过滤器已成功绕过并允许来自 Web 应用程序本身的命令注入。
但是,一旦页面重新加载,更改就会消失。现实世界的 Web 应用程序在浏览页面时通常会重新加载页面。为了有效地对应用程序进行渗透测试,对 JavaScript 进行持久化更改非常有用。
开发者工具的持续变化
Chromium 和 Chrome为此提供了所谓的本地覆盖功能。这使您可以使用本地文件夹中的文件覆盖页面资源。通过“源”选项卡中的“覆盖”选项卡进行配置。
允许 Chromium 访问本地文件夹后,您可以通过上下文菜单中的 Save for overrides项保存源。
这将在 overrides 文件夹中添加一个路径文件夹并将源代码保存到其中。如紫点所示,对此文件的更改现在是持久的。
Burp Suite 中的持续变化
遗憾的是,此功能无法在 Burp Suite 的嵌入式浏览器中使用,因为“Overrides”选项卡保持为空。其原因似乎是浏览器是使用该--disable-file-system标志启动的。2.5 年前已提交功能请求。
但幸运的是,有一个 BApp 可以做到这一点! 扩展HTTP Mock允许您定义将返回的响应而不是真实的响应。它的工作原理如下:
-
将请求-响应对发送到扩展。
-
进行更改,不要忘记按“保存”按钮。
-
在嵌入式浏览器中重新加载页面,您可以看到更改已被采用。
绕过代码保护
JavaScript 混淆器提供了保护混淆代码免遭反混淆和分析的功能。例如,obfuscator.io有以下保护措施:
-
自卫:美化或反混淆时破坏代码
-
调试保护:防止使用调试器语句
-
禁用控制台输出
本节向您展示如何绕过所有这些措施,并提供有关如何使用其他混淆器来绕过所有这些措施的提示。
设置
我会陆续对obfuscator.io的默认选项采取保护措施并分析结果。下面的屏幕截图显示了混淆选项。
我的演示应用程序使用 JavaScript 将文本字段的输入写入页面,如下所示:
function process() {
let input = document.getElementById("input").value;
document.getElementById("output").innerHTML = input;
}
在每个步骤中,我将使用deobfuscate.io对混淆后的脚本进行反混淆。请注意,我直接在源文件中更改 JavaScript 代码。在实际示例中,您可能会为此使用本地覆盖,如上所述。
自卫
此选项使输出代码能够抵抗格式化和变量重命名。如果尝试在混淆的代码上使用 JavaScript 美化器,代码将不再工作,从而使其更难以理解和修改。
— https://obfuscator.io
当访问 Chromium 中的页面时,它冻结了,让我根本无法分析发生了什么。
下一次尝试:火狐
结果是不同的并且更有希望:显示错误消息Uncaught InternalError: too much recursion和揭示问题原因的堆栈跟踪。
第 41 行包含正则表达式(((.+)+)+)+$,它表现出灾难性的回溯。
正如您所看到的,该函数_0x3b1622在第 43 行中调用,在网页的实际 JavaScript 代码之前(从第 44 行开始)。因此,绕过就像注释掉第 43 行(或分别注释掉第 41 行)一样简单。因此,该页面正在 Chromium 中加载并且仍然有效。
这种方法可以推广到 obfuscator.io 的任何自卫代码。自卫功能的模板在这里定义。正则表达式被硬编码到其中。
因此,绕过可以像删除这个特定的正则表达式一样简单,例如使用 sed:
sed -i 's/(((.+)+)+)+$//g' file.js
调试保护
该选项使得开发者工具的调试器功能几乎无法使用。
— https://obfuscator.io
在开发者工具打开时访问 Chromium 中的页面,会立即暂停执行并打开调试器。此外,人们似乎陷入了一次又一次调用调试器的匿名函数中。使用封闭的开发人员工具,一切都照常进行。
摆脱这个问题的最简单方法是阻止调用您陷入的匿名函数。为此,我建议使用开发人员工具的调用堆栈。单击特定的调用,直到找到一个似乎可以轻松注释掉而不会破坏某些内容的调用。您对堆栈进行得越深入,该函数就越有可能与网页的工作相关。因此,尽可能 靠近顶部。
在这种情况下,我决定删除else第 54 行中的语句,因为它包含 call _0x3c9b48(0),这最终导致调试器无休止的调用。事实上,该页面现在正在 Chromium 中加载,而无需启动调试器。
禁用控制台输出
console.log禁用、console.info、 、console.error、console.warn、的使用console.debug,console.exception并console.trace用空函数替换它们。这使得调试器的使用更加困难。
— https://obfuscator.io
出于演示目的,我console.log在 process 函数中添加了一条语句,该语句应将输入字段的值打印到控制台。但是,由于 obfuscator.io 的控制台输出被禁用,控制台输出保持为空。
有多种方法可以绕过此措施。
-
console从压缩数组中删除该符号。这是可行的,因为混淆的代码需要覆盖控制台对象,为此它需要其名称。尽管如此,打包数组可能会以某种方式被混淆,使得这不可能简单地实现。
-
禁用控制台输出的函数模板在这里。它包含要覆盖的控制台函数的名称:log, warn, info, error, exception, table, trace
从混淆的代码中找到并删除该数组。根据混淆程度,可能不容易识别。
使用嵌入式 iframe 中的控制台功能。这是迄今为止最可靠的方法。它的工作原理是通过 JavaScript 将 iframe 附加到当前文档。该 iframe 有自己的内部 DOM,可以通过其contentWindow属性进行访问。以下代码显示了这一点:
我将此代码添加到混淆代码中。
var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
iframe.contentWindow.console.log("This is logged!")
当然,您也可以console使用 iframe 中的方法覆盖文档的方法,如下所示:
console = iframe.contentWindow.console;
以下屏幕截图显示控制台输出再次正常工作。
建议
根据混淆程度和所使用的混淆器,上述技术可能不适用。如果可能的话,您应该始终尝试找出使用了哪个混淆器。 上面的最小示例很好地说明了混淆的工作原理,并且可以推广到更大的代码库。
始终尝试在不同的浏览器或 JavaScript 引擎中运行和分析 JavaScript 。如上所示,结果可能略有不同。在这种情况下,Chromium 冻结了,Firefox 抛出了带有重要信息的错误:带有递归的行号。您还可以使用jsconsole.com等服务。
回想一下,任何客户端保护都可以被删除。 最后,这就是 JavaScript 黑客的有趣之处
原文始发于微信公众号(红队笔记录):渗透测试人员之 JavaScript 分析(二)
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论