困了想睡觉,先发一版~
一、简介
今天发现希谭实验室-ABC123发的方程式工具包GUI工具包,话说回来这份泄露的工具其实里面有很多地方都是非常值得学习,无论是从漏洞利用到后门框架等等。所以打算写一波分析文
DoublePulsar是由美国国家安全局(NSA)方程组开发的后门植入工具,于2017 年初被影子经纪人泄露。该工具在短短几周内感染了超过 200,000 台Microsoft Windows计算机,并在 2017 年 5 月的WannaCry 勒索软件攻击中与EternalBlue一起使用。 Symantec于 2016 年 3 月首次在野外发现了 DoublePulsar 的变种。
DoublePulsar是一种恶意软件,它利用了Windows操作系统的漏洞,特别是针对SMB和RDP协议的漏洞。它通过在系统内核中注入代码来实现对系统的控制,从而允许攻击者执行恶意操作。DoublePulsar的一个显著特点是它可以绕过Windows的PatchGuard保护机制,这是Windows操作系统用于保护核心部分不被修改的重要机制之一。由于DoublePulsar的高度隐蔽性和对PatchGuard的绕过能力,它被认为是一种非常危险的后门工具。
PatchGuard 是微软 Windows 操作系统中的一种内核保护机制,旨在防止恶意软件对内核进行未经授权的修改。它通过监视内核数据结构和代码,以检测和阻止未经授权的内核模块加载和内核代码修改。PatchGuard 的主要目标是增强系统的稳定性和安全性,防止恶意软件绕过操作系统的安全性措施。PatchGuard 会定期检查内核的完整性,如果发现异常则会触发系统崩溃或者拒绝加载未经授权的模块,从而保护系统免受恶意软件的攻击。
二、复现
首先使用MS17-010 攻击模块进行漏洞利用,成功加载DoublePulsar后门。(利用模块为自己利用框架上的模块,Target SP未加载属于当时不熟悉Python,后续考虑优化)
图:MS-17-010
并且使用DoublePusar攻击模块进行RCE操作。弹calc还有上线美少妇就不展示了。
图: DoublePusar成功利用
二、永恒之蓝分析
2.1:漏洞存在文件信息:
漏洞类型:内存池溢出漏洞
分析存在驱动:srv.sys
漏洞驱动版本:6.1.7601.17514
漏洞所在函数:SrvOs2FeaListToNt (需符合表)
漏洞成因:在 SrvOs2FeaListToNt 主要是将 FEA LIST 转换为 NTFEA LIST 。内部调用的SrvOs2FeaListSizeToNt由于使用错误类型进行强制转换导致获得错误的内存长度,无法进行安全长度校验从而导致OOB,从而可以构造超出长度的内存数据覆盖MDL指针从而实现任意内存写入最后导致远程代码执行。
2.2:漏洞代码分析:
通过bindiff进行分析,发现漏洞点的差异,错误使用WORD类型。
图:补丁对比
通过查看国外大佬逆向的漏洞函数代码发现并且核实了这一点。
图:漏洞产生点
通过分析漏洞调用栈分析SrvOs2FeaListToNt代码,将 OS/2 1.2 格式的 FEALIST 转换为 NT 格式的全量 EA 列表。在转换过程中,需要进行内存分配和数据结构转换。
NTSTATUS
SrvOs2FeaListToNt (
IN PFEALIST FeaList,
OUT PFILE_FULL_EA_INFORMATION *NtFullEa,
OUT PULONG BufferLength,
OUT PUSHORT EaErrorOffset
)
/*++
Routine Description:
将一个 OS/2 1.2 的 FEALIST 转换为 NT 格式。从非分页池分配内存来存储 NT 格式的全量 EA 列表。调用方负责在使用完后释放此内存。
警告!调用方有责任确保 FeaList->cbList 的值在分配给 FeaList 的缓冲区内。这可以防止恶意重定向器在服务器上引发访问冲突。
Arguments:
FeaList - 指向要转换的 OS/2 1.2 FEALIST 的指针。
NtFullEa - 指向指向 NT 格式全量 EA 列表的指针。将分配一个缓冲区,并由 *NtFullEa 指向。
BufferLength - 分配的缓冲区长度。
Return Value:
NTSTATUS - STATUS_SUCCESS 或 STATUS_INSUFF_SERVER_RESOURCES。
--*/
{
PFEA lastFeaStartLocation;
PFEA fea = NULL;
PFEA lastFea = NULL;
PFILE_FULL_EA_INFORMATION ntFullEa = NULL;
PFILE_FULL_EA_INFORMATION lastNtFullEa = NULL;
PAGED_CODE( );
// 找出将 OS/2 1.2 FEALIST 转换为 NT 格式后的大小。这是为了确定分配用于接收 NT EA 的缓冲区的大小。
*BufferLength = SrvOs2FeaListSizeToNt( FeaList );
// 从非分页池中分配一个缓冲区来存储 NT 列表。
*NtFullEa = ALLOCATE_NONPAGED_POOL( *BufferLength, BlockTypeDataBuffer );
if ( *NtFullEa == NULL ) {
INTERNAL_ERROR(
ERROR_LEVEL_EXPECTED,
"SrvOs2FeaListToNt: 无法从非分页池中分配 %d 字节。",
*BufferLength,
NULL
);
return STATUS_INSUFF_SERVER_RESOURCES;
}
// 找到最后一个 FEA 可以开始的位置。
lastFeaStartLocation = (PFEA)( (PCHAR)FeaList +
SmbGetUlong( &FeaList->cbList ) -
sizeof(FEA) - 1 );
// 遍历 FEA 列表,将 OS/2 1.2 格式转换为 NT 格式。
for ( fea = FeaList->list, ntFullEa = *NtFullEa, lastNtFullEa = ntFullEa;
fea <= lastFeaStartLocation;
fea = (PFEA)( (PCHAR)fea + sizeof(FEA) +
fea->cbName + 1 + SmbGetUshort( &fea->cbValue ) ) ) {
// 检查无效标志位。如果设置,返回错误。
if ( (fea->fEA & ~FEA_NEEDEA) != 0 ) {
*EaErrorOffset = (USHORT)( (PCHAR)fea - (ULONG)FeaList );
return STATUS_INVALID_PARAMETER;
}
lastNtFullEa = ntFullEa;
lastFea = fea;
ntFullEa = SrvOs2FeaToNt( ntFullEa, fea );
}
// 确保 FEALIST 大小参数正确。如果我们结束于不是最后一个 FEA 之后的位置,则大小参数错误。返回引起错误的 EA 的偏移量。
if ( (PCHAR)fea != (PCHAR)FeaList + SmbGetUlong( &FeaList->cbList ) ) {
*EaErrorOffset = (USHORT)( (PCHAR)lastFea - (PCHAR)FeaList );
DEALLOCATE_NONPAGED_POOL( *NtFullEa );
return STATUS_UNSUCCESSFUL;
}
// 设置最后一个完整 EA 的 NextEntryOffset 字段为 0,表示列表结束。
lastNtFullEa->NextEntryOffset = 0;
return STATUS_SUCCESS;
}
那么错误类型使用导致什么问题?
以下做一个小演示。
DWORD:长度4个字节 32位 而WORD:长度2个字节16位 这个会有什么影响。我们写一个C语言复现一下错误类型的问题。
发现DWORD错误类型,仍然适用dowrd的值,但是长度变成了0xffff(65535)。
图:错误类型
现在已经知道smb在SrvOs2FeaListSizeToNt进行长度获取,但是使用错误类型导致长度非真实长度,导致按照要求进行长度校验,并且复制并且在SrvOs2FeaToNt进行非分页内存拷贝。
在拷贝溢出后会覆盖到srvnet.sys(Windows内核驱动的内存不采用独立划分)的非分页内存,永恒之蓝通过覆盖该驱动的SRVNET_BUFFER_HDR对应的MDL指针,实现任意内存写操作。
struct SRVNET_BUFFER_HDR {
LIST_ENTRY list;
USHORT flag; // 2 least significant bit MUST be clear. if 0x1 is set, pmdl pointers are access. if 0x2 is set, go to lookaside.
char unknown0[6];
char *pNetRawBuffer; // MUST point to valid address (check if this request is "xfdSMB")
DWORD netRawBufferSize; // offset: 0x20
DWORD ioStatusInfo;
DWORD thisNonPagedPoolSize; // will be 0x82e8 for netRawBufferSize 0x8100
DWORD pad2;
char *thisNonPagedPoolAddr; // 0x30 points to SRVNET_BUFFER
PMDL pmdl1; // point at offset 0x90 关键MDL指针
DWORD nByteProcessed; // 0x40
char unknown4[4];
QWORD smbMsgSize; // MUST be modified to size of all recv data
PMDL pmdl2; // 0x50: if want to free corrupted buffer, need to set to valid address
QWORD pSrvNetWskStruct; 假WSL地址
DWORD unknown6; // 0x60
char unknown7[12];
char unknown8[0x20];
};
struct SRVNET_BUFFER {
char transportHeader[80]; // 0x50
char buffer[reqSize+padding]; // 0x8100 (for pool size 0x82f0), 0x10100 (for pool size 0x11000)
SRVNET_BUFFER_HDR hdr; //some header size 0x90
//MDL mdl1; // target
};
查看漏洞利用脚本代码,可以看到对应利用流程。https://gist.github.com/worawit/bd04bad3cd231474763b873df081c09a?permalink_comment_id=2311451
大概流程就先控制MDL,间接性控制srvnet驱动,后通过srvnet,进行任意内存写入。看图构造恶意结构体的对应数据。
fakeSrvNetBufferNsa 是一个用于构造伪造的 SRVNET_BUFFER 结构的数据块。这个结构用于利用漏洞进行内存泄漏和任意写入。这个数据块包含了一些字段的值,这些字段的值将被用来覆盖 SRVNET_BUFFER 结构中的相应字段。攻击者利用这些字段的不当值来控制程序行为,最终实现代码执行。具体来说:
-
fakeSrvNetBufferNsa 中的数据被设计为符合 SRVNET_BUFFER 结构的布局,以便在漏洞利用过程中替换原始 SRVNET_BUFFER 结构的内容。
-
这些数据中包含了一些特定值,如指向函数指针的地址、MDL 结构的描述、以及用于触发特定行为的值。这些值的设定是为了在利用漏洞时能够控制程序的执行流程。
-
通过精心构造的 fakeSrvNetBufferNsa 数据块,攻击者可以在程序中引发内存泄漏、任意写入等行为,最终实现对程序的控制和攻击。
图:fakeSrvNetBufferNsa构造
并且分析fake_recv_struct构造,通过控制SrvNetWskReceiveComplete()和SrvNetCommonReceiveHandler的指针,指向HAL,因为HAL地址是固定的(CVE-2020-0796是因为无法攻克地址随机化和RWX问题,导致很多Exp不稳定利用)
图:构造代码
注意发送完后,其实最终执行在srvnet的会话取消。
图:最终执行
二、DoublePulsar分析
以上内容有点跑题了,开始正式分析DoublePulsar。其实DoublePulsar巧妙在于可以保证会话持久因为频繁进行漏洞利用会导致BSOD问题。
简单分析一下
图:流程图
我简单做个图,首先进行一些比较基础向的SMB Header会话建立。然后都会建立一个ping测试,它类似于ICMP协议,主要是看后门是否存活。然后一起带有一个key用于对下面的shellcode进行加密。fuzzbunch的dopu里面的dll注入是基于APC注入的一段shellcode实现的,所以我们把RunShellCode和DLL Inject归结在一起。这里并不对DLL注入进行分析。
通过分析导出Shellcode,可以使用ida或者ghidra,首先分析头部,开发者利用x64和x86区别的特性,利用opcode区别x86 会多出一个inc。 因为eax被执行inc,ZF也会被清0,JZ跳转会失效会执行下一跳Call调用。代码会继续执行流程。x64没有执行inc会跳转会被执行,会跳转。
代码差异
图:头部差异
接着分析Shellcode,具体流程查找ntoskrnl.exe基地址并且通过Hash定位ZwQuerySystemInformation和一些内存池申请释放API的地址(ExAllocatePool, ExFreePool )。
图:基础地址定位
接下来,Shellcode通过调用ZwQuerySystemInformation,将SystemInformationClass设置为0x0B(请求系统模块信息结构的未记录选项)来获取已加载的驱动程序列表,使用路径哈希比较在列表中搜索Srv.sys。
图:ZwQuerySystemInformation
图:定位srv.sys
遍历 .sys到 PE 部分,直到到达 .data 部分.data 部分内部通常是全局读/写内存,这里存储的是 SrvTransaction2DispatchTable,一个处理不同 SMB 任务的函数指针数组。
shellcode 存储名为 SrvTransactionNotImplemented() 的调度的函数指针(以便它可以从钩子代码中调用它)。然后它用钩子覆盖 SrvTransaction2DispatchTable 中的这个成员。
就是这样。后门完成。现在它只是返回自己的调用堆栈并做一些小的清理工作。
三、DoublePulsar通讯原理
这个后门它是通过SMB Trans2进行协议会话。我们前面复现例子举例,比如我们先进行Ping测试。我们ping时会设置一个随机的MID(MultiplexID ),发送到目标服务器,然后触发后门。后门一旦返回消息后,会将返回的Mid与原Mid进行减法计算。最终会得到一个状态码。 然后怎么触发后门呢? 他是通过Timeout字段进行计算取后两位。
SMB_Header
{
UCHAR Protocol[4];
UCHAR Command; //命令码
SMB_ERROR Status;//错误信息
UCHAR Flags; //
USHORT Flags2;
USHORT PIDHigh;
UCHAR SecurityFeatures[8];
USHORT Reserved;
USHORT TID; //tree id
USHORT PIDLow;/进程ID
USHORT UID; //用户ID
USHORT MID; //multiplex ID
}
比如0x723 它是23。等于ping
下面是状态码 和操作码
状态代码(通过 MultiplexID delta)是:
0x10 = 成功
0x20 = 无效参数
0x30 = 分配失败
操作码列表如下:
0x23 = ping
0xc8 = 执行Shellcode
0x77 = 杀死
然后查看参数数据。
然后用来加密shellcode key通过SecurityFeatures字段里面的数据进行计算得出,传输时通过XOR加密。
图:Shellcode
构造发送数据
图:构造发送数据
(声明:曾在圈子社区发布过此文,后续经过修改细化重新发布。)
原文始发于微信公众号(猫头鹰安全团队):DoublePulsar 分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论