函数CQmPacket::CQmPacket分析
对UserMessage包各部分的处理
已经通过IDA反编译了该函数,但都是伪代码,基本读不懂什么意思,还需要进一步反编译分析。
从函数名猜测,该函数用来处理客户端消息队列数据报,所以直接用发送的数据包进行猜测。函数主要是对几个参数进行操作,再返回一个值。那么就先从几个参数入手,分别分析出他们是什么。
参数一:是数据包本身
参数二:是BaseHeader头
修改a2为BaseHeader,BaseHeader偏移2个int是数据包大小,BaseHeader地址+数据包大小,等于BaseHeader结束地址。
所以可以优化如下伪代码
为
typedef struct {
uint8_t VersionNumber; // VersionNumber
uint8_t Reserved; // Reserved
uint16_t Flags; // Flags
uint32_t Signature; // Signature
uint32_t PacketSize; // PacketSize
uint32_t TimeToReachQueue; // TimeToReachQueue
// 其他字段...
} BaseHeader;
然后把结构体导入IDA中,就可以优化伪代码显示形式,由baseHeader + 偏移量
优化成对象->字段
的形式。
再之后,根据前边对this + 5
的初始化,我们知道其指向BaseHeader
类型的结构体。修改如下
这个函数的主要功能,是对UserMessage包进行处理。UserMessage 数据包始终包含完整的消息。用户消息包用于在发送方和接收方之间传送应用程序定义的消息和管理确认消息。
UserMessage 数据包包含许多必需的标头,并且可以包含其他可选标头。必须出现在所有 UserMessage 数据包中的必需标头是:BaseHeader、UserHeader和MessagePropertiesHeader。可选标头包括:TransactionHeader、SecurityHeader、 DebugHeader、 SoapHeader、 MultiQueueFormatHeader和SessionHeader。
这是文档的描述,实际分析过程中,还出现了一些其他未公开的结构体头。
第一个头BaseHeader通过参数传递,不用再次获取。通过调用CBaseHeader::SectionIsValid对该函数进行验证。
第二个头UserHeader从BaseHeader地址偏移---获取。之后同样通过类似函数SectionIsValid进行验证。
接着几个头XactHeader、SecurityHeader、PropertyHeader、BaseMqfHeader、MqfSignatureHeader、MsgGroupHeader、MsgDeadletterHeader、ExtendedAddressHeader等要末从**::GetNextSection
中获取,要末直接从前一个头中偏移对应的偏移量获取,但都通过**::SectionIsValid
进行有效性验证。
然后还有几个,没有通过**::SectionIsValid
进行验证,只在CQmPacket::CQmPacket中进行了简单的验证,就开始获取下一个头了。
验证和获取
来看看CBaseHeader::SectionIsValid都做了什么。一共传递了三个参数,第一个参数是baseHeader,前边有个指向。第二个参数是消息最大限制。
第三个参数是一个布尔类型,作用未知。但是在代码中写死了,固定为1
接着进入函数内部。
先验证BaseHeader头大小是不是大于最大限制,如果不正确,就抛出异常。
接着验证BaseHeader数据体是否超出范围
再往下验证签名是否等于固定值0x524F494C
最后验证版本是否为0x10
整个验证还是比较全面的。
其他几个验证函数也类似,验证一些固定值,验证以下头和数据是否超出范围,是否在合理范围内。
::GetNextSection,获取下一个节,这里是头结构体
该函数接收两个参数,第一个参数是上一个头地址,第二个参数是是0,或者直接没有。
通过队列获取结构体头中数据大小,然后在传入结构体头的地址上进行偏移,从而获取下一个节地址,同时也是下一个结构体头的地址。
未验证的结构体头
还有一些结构体头未通过**::SectionIsValid
进行验证,并不代表这些结构体头都是未公开的结构体头。
通过前边验证函数的类的名字,其实可以获取对应结构体头所属的类型,如下,那么这里的v13就可以认为是UserHeader数据类型。
通过这种方法,我们梳理了CQmPacket::CQmPacket
中this的结构体CQmPacket,如下。
typedef struct {
int64_t field1; // 'this' pointer.
int64_t field2; // Skipped in the given initializer.
int64_t field3; // Skipped in the given initializer.
int64_t pPacket; // Assigned by 'a3'.
int64_t pBaseHeader; // Assigned by 'baseHeader'.
int64_t pUserHeader; // this + 6
int64_t pXactHeader; // this + 7
int64_t pSecurityHeader; // this + 8
int64_t pPropertyHeader; // this + 9
int64_t pDebugSection; // this + 10
int64_t pBaseMqfHeader; // this + 11
int64_t pBaseMqfHeader; // this + 12
int64_t pBaseMqfHeader; // this + 13
int64_t pMqfSignatureHeader; // this + 14
int64_t unKnown15; // this + 15 未验证
int64_t unKnown16; // this + 16 未验证
int64_t unKnown17; // this + 17 未验证
int64_t unKnown18; // this + 18 未验证
int64_t unKnownHeader; // this + 19 未验证
int64_t unKnownHeader; // this + 20 未验证
int64_t unKnownHeader; // this + 21 未验证
int64_t unKnownHeader; // this + 22 未验证
int64_t unKnownHeader; // this + 23 未验证
int64_t unKnownHeader; // this + 24 未验证
int64_t pMsgDeadletterHeader; // this + 25 未验证
int64_t unKnownHeader; // this + 26 未验证
int64_t pExtendedAddressHeader; // this + 27 未验证
int64_t unKnownHeader; // this + 28 未验证
} CQmPacket;
接着,通过UserHeader的标志位,来识别其他数据结构体头。
this+6是userHeader数据结构,偏移44个_QWORD是FLAGS字段。&符号按位与运算,判断对应位是否为1。
this + 15
指向的值判断的是0x2000000
,对应二进制0000 0010 0000 0000 0000 0000 0000 0000
对应了J标志位。
这个标志位指示数据包最初是否通过HTTP发送。也就是说,如果设置,表示用用HTTP。如果未设置,用二进制协议发送。
如果设置的情况下,表示正在接收的SRMP 消息最初通过 HTTP 发送的。会多出SRMPEnvelopeHeader和CompoundMessageHeader头
所以this + 15 = SRMPEnvelopeHeader
,this+16=CompoundMessageHeader
this + 17
指向的值判断的是0x4000000
,对应二进制0000 0100 0000 0000 0000 0000 0000 0000
,对应X3标志位。这是一个保留位。可能是文档还没有更新。
this + 18
指向的值判断的是0x8000000
,对应二进制0000 1000 0000 0000 0000 0000 0000 0000
,对应X3标志位。这是一个保留位。可能是文档还没有更新。
this+19
指向的值判断的是0x10000000
,对应二进制0001 0000 0000 0000 0000 0000 0000 0000
,对应K标志位。对应SoapHeader头
this+20
通过其转换获取,又在同一条件下,可以认为是未知头1结构体的body部分。
this+21
指向的值判断的是0x20000000
,对应二进制0010 0000 0000 0000 0000 0000 0000 0000
,对应X4保留位第一位。
依次类推,最终再次整理后的CQmPacket结构体如下:
typedef struct {
int64_t field1; // 'this' pointer.
int64_t field2; // Skipped in the given initializer.
int64_t field3; // Skipped in the given initializer.
int64_t pPacket; // Assigned by 'a3'.
int64_t pBaseHeader; // Assigned by 'baseHeader'.
int64_t pUserHeader; // this + 6
int64_t pXactHeader; // this + 7
int64_t pSecurityHeader; // this + 8
int64_t pPropertyHeader; // this + 9
int64_t pDebugSection; // this + 10
int64_t pBaseMqfHeader; // this + 11
int64_t pBaseMqfHeader; // this + 12
int64_t pBaseMqfHeader; // this + 13
int64_t pMqfSignatureHeader; // this + 14
int64_t pSRMPEnvelopeHeader; // this + 15 未验证
int64_t pCompoundMessageHeader; // this + 16 未验证
int64_t unKnownReservedX31; // this + 17 未验证
int64_t unKnownReservedX32; // this + 18 未验证
int64_t SoapHeader; // this + 19 未验证
int64_t SoapHeaderBody; // this + 20 未验证
int64_t unKnownReservedX41; // this + 21 未验证
int64_t unKnown22; // this + 22 未验证
int64_t pMsgGroupHeader; // this + 23 验证
int64_t pExtensionHeader; // this + 24 未验证
int64_t pMsgDeadletterHeader; // this + 25 未验证
int64_t pSubqueueHeader; // this + 26 未验证
int64_t pExtendedAddressHeader; // this + 27 未验证
int64_t pSessionHeader; // this + 28 未验证
} CQmPacket;
漏洞原理
其中有一部分结构体头未进行验证,也就意味着可以伪造结构体大小和实际大小的差异,从而误导后续节获取的指针地址,从而导致访问未分配的地址而崩溃。
可以随机挑选一个未验证的头进行伪造利用,不过越往后,构造的难度就越大。因为后边可变字段的增加,会导致前边头中size字段数值的增加。
由于第一个和第二个未验证的结构体是一体的,所以直接选择第三个未验证的结构体unKnownReservedX31进行伪造。
可以看出来交互过程,漏洞触发数据在User Messages包中。为了发送User Messages包,需要先发送EstablishConnection Packet (section 2.2.3) 和ConnectionParameters Packet (section 2.2.2)
可以根据包详情构造数据包,也可以直接用官方的示例直接发送。
Establish Connection Request:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/f9bbe350-d70b-4e90-b9c7-d39328653166
Connection Parameters Request:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/6f79f2fb-1612-4685-8c85-a90a83f75f23
User Message:https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-mqqb/f0bf84a8-aba5-4ce9-a6ec-31ad9ca90d00
前边两个包不需要修改,直接把16进制转换为2进制,用tcp协议向目标1801端口发送即可。
User Message
包需要修改对应的位,来触发漏洞。
为了简化POC,可以根据User Messages包的定义,只保留必选头,然后加上触发漏洞的那个头。
下边分析一下漏洞构造,如下:
这里未对上一个头进行验证,直接获取下一个头的地址。
首先需要启动这个头,使if语句为真,进入这个循环。this+6是UserHeader
数据结构,偏移44对应Flags字段的一个标志位,体现在16进制数据中一个字节就是04
然后是构造后边的指针,计算公式大概是:指针地址 + (指针地址+4)+(指针地址+8)+15
在(指针地址+4)和(指针地址+8)对应的值处可以随意写入数字,最后指针地址偏移对应的量。
在之后调用的时候触发未分配的地址,导致程序崩溃
没有评论