密码破解的利器——彩虹表(rainbow table)
确定 shellcode 依赖于哪些导入将使研究人员进一步了解其其余逻辑。不用动态分析shellcode,并且考虑到研究人员已经弄清楚了上面的哈希算法,研究人员可以自己构建一个彩虹表。
彩虹表是一个预先计算好的表,用于缓存加密哈希函数的输出,通常用于破解密码哈希。
以下 Python 代码段计算位于最常见系统位置的 DLL 导出的“Metasploit”哈希值。
例如,下面的PowerShell命令生成一个彩虹表,然后搜索下图中首先观察到的726774Ch哈希值。为了方便大家,研究人员发布了包含 239k 哈希值的 Rainbow.csv 版本。
如上所述,shellcode解析并调用的第一个导入是LoadLibraryA,它由32位和64位kernel32.dll导出。
执行流程分析
将导入解析排序后,理解剩余的代码变得更加容易。如下图所示,shellcode开始执行以下调用:
在㉗处LoadLibraryA确保ws3_32库加载,如果还没有加载,它将映射内存中的ws3_32.dll DLL,使shellcode能够进一步解析与Windows Socket 2技术相关的附加函数。
在㉙处,WSAStartup在 shellcode 的进程中启动套接字的使用。
WSASocketA 在㉙处创建一个新的套接字,这将是一个基于流(SOCK_STREAM)的IPv4套接字(AF_INET)。
套接字初始化的反汇编
创建套接字后,shellcode 继续调用 ㉝ 处的连接函数,并使用先前进入堆栈 (㉜) 的 sockaddr_in 结构。 sockaddr_in 结构包含从事件响应角度看的有价值的信息,例如协议(0x0200 是 AF_INET,又名 IPv4,小端)、端口(0x115c 是大端的默认 4444 Metasploit 端口)以及 C2 IPv4 地址㉛(0xc0a801ca 在大字节序中是 192.168.1.202)。
如果连接失败,shellcode 最多重试 5 次(在 ㉞ 处定义的计数器在 ㉚ 处递减),之后它将使用 ExitProcess (㉟) 中止执行。
如果连接成功,shellcode 将创建一个新的 cmd 进程并将其所有的标准错误、输出和输入(㊱)连接到已建立的 C2 套接字。进程本身是通过 ㊲ 处的 CreateProcessA 调用启动的。
逆向shell
最后,当进程运行时,shellcode执行以下操作:
通过调用 WaitForSingleObject 在 ㊳ 处无限期等待远程 shell 终止;
终止后,使用 GetVersion 在 ㊴ 处识别 Windows 操作系统版本,并使用 ExitProcess 或 RtlExitUserThread 在 ㊵ 处退出;
终止shellcode
总的来说,Metasploit 的 windows/shell_reverse_tcp shellcode 的执行流程架构如下:
Metasploit的TCP逆向shell执行流
Shellcode被中断
在完成了执行流分析之后,让我们看看如何在 shellcode 上打开表格并破坏它。从攻击者的角度来看,shellcode 本身被认为是可信的,而它运行的环境是被污染的。本节将假设研究人员不知道 shellcode 在内存中执行的位置,因此,挂钩/修改 shellcode 本身不是一个可接受的解决方案。
接下来,我们在介绍概念验证实现之前,会首先介绍一些理论方面的内容。
CWE-1288:输入一致性验证不当
当接收一个包含多个元素或字段的复杂输入时,这些元素或字段必须彼此一致,但它不验证或不会很正确地验证输入是否实际一致。
从shellcode的角度来看,只有两个外部交互提供了一个可能的攻击面。第一个也是最明显的表面是C2通道,在这个通道中,一些安全解析方案可以检测/削弱通信协议或周围的API调用。然而,这种攻击表面有大量的警告,安全解析方案必须区分合法和恶意行为,可能会导致一些中等/低置信度的检测。
第二个不太明显的攻击面是导入解析本身,从shellcode的角度来看,它依赖于外部进程数据。在这个导入解析例程中,研究人员观察到 shellcode 如何依赖 BaseDllName 属性为每个模块生成哈希。
哈希例程检索Buffer和MaximumLength以哈希模块的BaseDllName
虽然模块的导出是以 UTF-8 NULL 结尾的字符串,但 BaseDllName 属性是一个 UNICODE_STRING 结构。此结构包含多个属性:
Length:存储在Buffer中的字符串的长度,以字节为单位;
MaximumLength:缓冲区的长度,以字节为单位;
缓冲区:指向用于包含宽字符字符串的缓冲区的指针;
……
如果字符串以null结尾,Length不包括末尾的空字符。
MaximumLength 用于指示 Buffer 的长度,以便在将字符串传递给 RtlAnsiStringToUnicodeString 等转换例程时,返回的字符串不会超过缓冲区大小。
虽然上文没有明确提到,但我们可以隐约地感觉到缓冲区的MaximumLength属性与实际字符串的Length属性无关。Unicode字符串不需要消耗整个Buffer,也不能保证以null结束。理论上,Windows API应该只考虑Buffer的第一个Length字节进行比较,忽略Length和MaximumLength位置之间的任何字节。增加UNICODE_STRING的缓冲区(buffer和MaximumLength)不应该影响依赖于存储字符串的函数。
由于shellcode的哈希例程依赖于缓冲区的MaximumLength,不同大小的缓冲区中的类似字符串将生成不同的哈希。哈希例程中的这个漏洞可以用来消除潜在的Metasploit shellcode。从技术角度来看,由于安全解决方案已经挂钩进程创建并自我注入,因此可以通过增加 Metasploit 所需模块的 BaseDllName 缓冲区(例如:kernel32.dll)来在不知道其存在或位置的情况下干扰哈希例程。
研究人员接下来将利用这个哈希输入验证漏洞作为初始向量来导致拒绝服务和执行流劫持。
CWE-823:使用超出范围的指针偏移
该程序对有效指针执行指针运算,但它使用的偏移量可以指向结果指针的预期有效内存位置范围之外。
研究人员之前所做的一个观察是 shellcode 如何无限循环模块,直到找到匹配的导出。当他们发现一个改变哈希值的漏洞时,就试着分析如果所有哈希值都不匹配会发生什么情况。
当遍历双链表可以无限循环时,shellcode将在检查所有模块后实际生成一个“Access Violation”错误。此异常不是由shellcode生成的,而是由于代码没有验证列表的边界而发生的。鉴于对于列表中的每个项目,BaseDllName.Buffer 指针都是从偏移量 0x28 加载的,一旦研究人员访问列表中的第一个非 LDR_DATA_TABLE_ENTRY 项目,就会发生异常。如下图所示,一旦 shellcode 循环回到第一个 PEB_LDR_DATA 结构,就会出现这种情况,在这个阶段会发生越界读取,导致无效指针被取消引用。
遍历InMemoryOrderModuleList双链表时的越界读取
虽然从防御的角度来看,导致拒绝服务比执行 Metasploit shellcode 更好,但让我们看看如何进一步利用上述漏洞为防御者提供优势。
滥用CWE-1288劫持执行流
我们感兴趣的一个模块是kernel32.dll,正如之前在“执行流分析”部分所分析的,它是调用 LoadLibraryA 函数所需的第一个模块。在哈希例程中,kernel32.dll哈希被计算为0x92af16da。通过应用上述缓冲区调整技术,研究人员可以确保 shellcode 循环附加模块,因为原始哈希不会匹配。至此,安全解决方案有几个选项:
研究人员注入的安全解析方案的DLL可以命名为kernel32.dll。虽然它的哈希值是匹配的,但两个名为kernel32.dll的模块可能会对LoadLibraryA的合法调用产生意想不到的后果。
有两个名为 kernel32.dll 的模块可能会对 LoadLibraryA 的合法调用产生意想不到的后果。
同样,由于研究人员已经在 LDR_DATA_TABLE_ENTRY 结构中修改缓冲区,但却可以轻松保存 kernel32.dll 缓冲区的原始值并将它们分配给研究人员的安全解决方案的注入模块。虽然这在理论上可行,但如上所述,在内存中有一个被称为kernel32.dll的第二个缓冲区,这很容易引起运行风险。
另外,研究人员的安全解决方案的注入模块可以有不同的名称,只要与原始哈希存在哈希冲突。这种技术不会影响诸如 LoadLibraryA 之类的合法调用,因为它们依赖于基于值的比较,而不是 shellcode 的基于哈希的比较。
研究人员之前观察到 Metasploit shellcode 如何使用 ASCII 字符(1 字节)的添加和旋转来执行哈希。以下模式描述了 KERNEL32.DLL 在第三个循环中的哈希状态,其中 ASCII 字符 K 和 E 重叠。可以看到,NULL字符是对最初是Unicode字符串(2字节)的字符串执行1字节操作的直接结果。
第一个和第三个ASCII字符重叠
为了获得哈希冲突,研究人员需要识别在不改变结果哈希的情况下对初始KERNEL32.DLL字符串所执行的更改。下图强调了第一个和第三个ASCII字符之间的6位关系。通过减去第一个字符的第二位,我研究人员可以增加第三个字符的第八位(2+6),而不影响得到的哈希值。
第一个和第三个ASCII字符之间的哈希冲突
虽然上面的冲突是不实际的(ASCII或Unicode字符0xC5不在字母数字范围内),但研究人员可以应用相同的原则来识别可接受的关系。假设研究人员不改变字符串的长度,以下 Python 代码片段会暴力破解 KERNEL32.DLL 字符串的 Unicode 字符之间的关系。
如上所述,可以修改多个字符对以导致哈希冲突。例如,在Unicode位置0和11处的字符之间存发生了 2 位左移。
如果左移2位类似于乘以 4的结果,则位置 0 处的 Unicode 字符增加任何值都需要将位置 11 处的字符减少 4 倍相同的值,以保持 Metasploit 哈希不变。以下 Python 命令突出显示了 KERNEL32.DLL 的这两个字符之间的不同组合。
这种哈希冲突与缓冲区调整大小技术相结合,可以链接起来,以确保研究人员的自定义 DLL 在哈希例程中作为KERNEL32.DLL进行计算。现在,如果研究人员导出 LoadLibraryA 函数,则Metasploit 导入解析将错误地调用研究人员的实现,从而导致执行流程劫持。可以利用此劫持向安全解决方案发出高置信度 Metasploit 导入解析发生的信号。
概念证明
为了验证上述理论,首先需要构建一个概念验证 DLL,一旦加载,它将使用 CWE-1288 来模拟 EDR(终端检测和响应)解决方案如何在不知道其内存位置的情况下检测 Metasploit,不过为了利用上述哈希冲突,研究人员的 DLL 将被命名为 hernel32.dlx。
该概念验证目前已经发布在NVISO的GitHub存储库中了。
进程注入
为了模拟安全解析方案如何被注入到大多数进程中,让我们构建一个简单的函数,它将在我们选择的进程中运行研究人员的 DLL。
Inject函数将诱使目标进程加载一个特定的DLL(研究人员的hernel32.dlx)并执行其 DllMain 函数,研究人员将在此处触发缓冲区大小调整。虽然存在多种技术,但我们只需将DLL的路径写入目标进程,然后创建一个调用LoadLibraryA的远程线程即可。最后这个远程线程将加载我们的DLL,就像目标进程一样。
可能有人会注意到,上面的代码依赖于hPayload变量。这个变量将在DllMain函数中定义,因为研究人员的目标是获得当前DLL的模块,而不管其名称如何,而 GetModuleHandleA 将要求研究人员对 hernel32.dlx 名称进行硬编码。
导出 Inject 方法后,研究人员现在可以继续构建触发 CWE-1288 所需的逻辑。
缓冲区大小调整
可以使用以下逻辑完成从 kernel32.dll 模块调整 BaseDllName 缓冲区的大小。与 shellcode 的技术类似,研究人员将恢复 PEB,遍历 InMemoryOrderModuleList,一旦找到 KERNEL32.DLL 模块,将其缓冲区增加 1。
最好在注入发生后尽快触发此逻辑,虽然这可以通过TLS钩子来完成,但为了简单起见,研究人员将更新现有的 DllMain 函数以在 DLL_PROCESS_ATTACH 上调用 Metasplop。
分析的shellcode只有依赖于LoadLibraryA,才能使构建得以实现,它将简单地发出Metasploit警报,然后终止当前的恶意进程。下面的函数只会被shellcode触发,它本身不会从研究人员的 DLL中被调用。
上述方法可以用于其他变体,如LoadLibraryW、LoadLibraryExA和其他变体。
准备好模拟安全解决方案后,就可以继续演示了。现在将首先执行 Shellcode.exe,这是一个简单的 shellcode 加载程序(如下图所示)。这个 shellcode 加载程序提到它的进程ID(研究人员将其作为注入的目标),然后等待它需要执行的 shellcode 路径。
一旦我们知道shellcode在哪个进程中运行,我们就可以注入模拟的安全解析方案(如下图的右侧所示)。此过程通常由每个过程的安全解决方案执行,为了简单起见,仅在研究人员的 PoC 中手动完成。使用研究人员的自定义 DLL,我们就可以使用以下命令注入所需的进程,其中 hernel32.dlx 的路径和进程 ID 已进行了匹配。
手动模拟未来恶意进程的注入
执行注入后,Shellcode.exe 进程已被暂存(调整模块缓冲区大小,加载冲突 DLL),以便在任何 Metasploit shellcode 运行时利用 CWE-1288 漏洞。值得注意的是,在此阶段,没有加载任何 shellcode,也没有为其分配任何内存。这就确保了我们遵守了研究人员不知道 shellcode 在哪里执行的假设。
注入研究人员的模拟安全解决方案后,我们可以继续向即将成为恶意的 Shellcode.exe 进程(下图的左侧)提供初始生成的 shellcode(在研究人员的示例中为 shellcode.vir)的路径。
执行由 stager 执行的恶意 shellcode
一旦shellcode运行,我们就可以在下图中看到如何调用LoadLibraryA信号函数,从而对基于shellcode的导入分辨率进行高置信度检测。
将输入验证漏洞和哈希冲突链接起来向杀毒软件发出信号
总结
本文重点介绍了Metasploit shellcode的剖析,并着重介绍了动态导入解析。在这个动态导入解析方案中,我们又发现了两个漏洞,其中一个可以用来准确识别运行时Metasploit shellcode。
【网络安全学习资料】