本文为看雪论坛优秀文章
看雪论坛作者ID:50u1w4y
这篇文章主要是想分享一下CVE-2021-24074的漏洞细节,并根据博客作者的方案写了一个连poc都算不上的测试代码。所以如果你只是想找份能用的poc 的话,那么这篇文章是没法满足你的需求的。而如果你正打算分析该漏洞的话,这篇文章应该能对你有一点小帮助吧,或许对你理解 CVE-2021-24086 的细节也能有所帮助。(我没有去看这个IPv6 的洞,但是一开始接收数据包的函数栈应该是一样的)https://www.armis.com/resources/iot-security-blog/ 公司的文章展开,并添加了详细的细节和自己的一些理解。
拿到一个新的漏洞按理来说应该从补丁入手,bindiff 一下看看补丁打在哪,再去分析为什么打这个补丁以及之前的漏洞在哪。不过既然都有前人栽树了就可以省去大量的分析时间。
这次的漏洞出在处理网络数据包的 tcpip.sys 驱动中,与处理分片数据包和处理带有 route option 可选头的数据包的函数有关。漏洞产生的原因是因为在转发带有 route option 的分片数据包时,关于 option 的一些信息,比如 route option offset in IP header,来自于最后一个分片。而结合后的数据包的数据,也就是要转发时用的数据,是来自于第一个分片的 buffer 空间的。这种不一致就会导致潜在的漏洞危害。tcpip.sys 驱动更底层的驱动是 ndis.sys,这个驱动和网卡相关联,不过对于这个漏洞来说没必要跟它的细节,所以可以直接把它看成是透明的。只要是接收到 IP 数据包,都会有这么一个函数栈,从 IppReceiveHeaderBatch 开始对数据包进行预处理,然后分别调用不同的函数进行处理。第二列可以选择性地无视掉。在这个图中指出,数据包是由 3 个结构串起来的,_NET_BUFFER_LIST的第一个成员是next,形成一个 _NET_BUFFER_LIST 的链表,第二个成员是 FirstNetBuffer,指向该 _NET_BUFFER_LIST 携带的 _NET_BUFFER 结构。_NET_BUFFER 描述了数据包的相关信息,由 _MDL 真正指向数据包的 buffer 空间。一般来说,一个_NET_BUFFER_LIST只携带一个 _NET_BUFFER。比如若有两个分片,那么就会有两个_NET_BUFFER_LIST,这两个_NET_BUFFER_LIST 串在一起,每个_NET_BUFFER_LIST 各携带一个 _NET_BUFFER。这三个结构体都可以由 dt 得到,也有对应的文档。_MDL结构中的MappedSystemVa指向当前_MDL的起始地址,由 _NET_BUFFER 中的CurrentMdlOffset 指向起始数据的位置,也就是说 MappedSystemVa + CurrentMdlOffset 就是数据包的 buffer 空间。虽然在上图中 _NET_BUFFER_LIST 是第一层,但是在整个网络数据处理流程中,它上面至少还有两层结构。上上层在这次分析中不重要,不过 _NET_BUFFER_LIST的上层是一个重要的结构,我管它叫 _REASSEMBLE_LIST。_REASSEMBLE_LIST 的第一个成员也是 next,第二个成员指向_NET_BUFFER_LIST。里面还存放了很多数据包的相关信息,在各个函数执行中经常要用到。_REASSEMBLELIST这个名字应该不太准确,不过就分析这个漏洞来说还行。关于这个结构体不知道是我没找到它的定义还是真的没有,我把 `dt ndis!*` 浏览了一遍没有看到和它对得上的结构体,文档里似乎也没有。这应该是个叫什么 ndis ... list 或者 miniport ... list 这类的结构体,不过搜了 ndis 中的许多定义也没发现对得上的,而且这个结构体很大,是那种接近顶层的结构了。如果这真的是个有定义的已知结构体的话还请教教我 ,捂脸。虽然没找到,不过知道了它的宏观结构,然后里面大部分字段都是保存着数据包的相关信息的,所以在跟一个字段的时候还是可以知道它的具体含义的。以下是这个结构体的一小部分字段。struct _REASSEMBLE_LIST
{
_REASSEMBLE_LIST *next;
_NET_BUFFER_LIST *NetBufferList;
char unknow0[28];
unsigned int functionToCall;
unsigned int headersize;
char unknow1[188];
unsigned int OriDst;
char unknow2[12];
unsigned int IPSrc;
char unknow3[12];
IPHeader *IpPacketBuffer;
char unknow4[24];
unsigned __int8 RouteOptionOffsetInIpHeader;
unsigned __int8 RouteOptionSize;
...
};
知道了结构体的构成与宏观的组织,接下来就是函数的执行流了。2. 函数的执行流
与漏洞相关的函数有:
(1) IppReceiveHeaderBatch:对数据包进行预处理,然后根据数据包的类型调用不同的函数进行处理。(2) Ipv4pReceiveFragmentList:对分片数据包进行处理(3) Ipv4pReceiveRoutingHeader:对包含 route option 的数据包进行处理当发送一个正常的包含route option的分片数据包时,如使用ping 192.168.126.128 -l 1600 -j 192.168.126.128 -n 1 时,执行的函数栈为:
* IppReceiveHeaderBatch
* Ipv4pReceiveRoutingHeader
就这样而已,这说明Ipv4pReceiveRoutingHeader这个函数是可以直接处理 route option分片的。当发送的数据不一致时,这里以 armis 的方案为例,两个分片不一致,第一个分片携带了 IPSec option,第二个分片携带 route option,此时的执行流为:
* IppReceiveHeadersHelper
* IppReceiveHeadersHelper
* Ipv4pReceiveFragmentList
* Ipv4pReceiveRoutingHeader
* Ipv4pReceiveFragmentList
* IppReceiveHeadersHelper
* Ipv4pReceiveRoutingHeader
这里用IppReceiveHeaderBatch中的函数 IppReceiveHeadersHelper 来表示,因为该函数的调用次数可以表示分片的数量,如代码块所示:if ( ReassembleList )
{
do
{
v8 = ReassembleList->next;
a3a = (__int64)&a6a;
ReassembleList->next = 0i64;
IppReceiveHeadersHelper(
ReassembleList,
Ipv4Global_me,
(__int64)&a3a,
(__int64)&a4a,
(__int64)&v48,
a3a);
ReassembleList = v8;
}
while ( v8 );
}
(1) 首先调用 Ipv4pReceiveFragmentList 从第一个分片开始处理,(2) Ipv4pReceiveFragmentList 处理第二个分片的时候发现这是一个 route option。(3)然后调用Ipv4pReceiveRoutingHeader来处理,但是 Ipv4pReceiveRoutingHeader 又发现这是第二个分片。(4) 再次调用 Ipv4pReceiveFragmentList 来处理第二个分片,(5) 此时 Ipv4pReceiveFragmentList 开辟了一块新的空间将两个分片组合在了一起,(6) 然后调用 IppReceiveHeaderBatch 来处理这个组合后的分片,此时RouteOptionOffsetInIpHeader不为0。(7) 所以直接调用Ipv4pReceiveRoutingHeader来处理组合后的分片。RouteOptionOffsetInIpHeader 保存在 _REASSEMBLE_LIST,其含义为该数据包是否存在 route option,若有的话其值为 route option 在 IP Header 中的偏移字节,若不存在的话值为 0。到这里需要用到的知识应该差不多了,有关 route option 的相关知识网上应该挺多的,比如 Linux 的这篇文章:http://www.embeddedlinux.org.cn/linux_net/0596002556/understandlni-CHP-18-SECT-3.html#understandlni-CHP-18-FIG-4这里顺便提一句,route option 的 ptr 是从 type(第 0 个字节) 开始数的,只不过因为它的初始值为 1,所以一般用的时候都拿 ptr - 1 来用。比如说一开始的 ptr 是 4,指向第一个 route address。
ForwardNetBuffer 从组合后的 Net Buffer 复制过来,其头部 buffer 空间与第一个分片相关联。pointer_sub_one(也就是route option 的 ptr)来自于 buffer 空间,因此与第一个分片相关联,而ReassembleList 中保存的很多字段都是来自于最后一个分片的,比如RouteOptionOffsetInIpHeader。因此,ForwardOption 的偏移量是由最后一个分片决定的。由于这种不一致性,因此导致了潜在的漏洞危害。我们以 armis 提出的方案再来详细地看看这个漏洞:RouteOptionOffsetInIpHeader 来自于最后一个分片,因此 route option 的 offset 为 20 + 4,但是 buffer 空间是来自于第一个分片的,所以 ForwardOption 指向第一个分片的 Byte #3,同理 pointer_sub_one 的值为 5 而不是 4。OK,搞清楚漏洞的产生原因了,现在我们来看看 armis 提出来的利用方案,这也是我比较怀疑的地方。1. Writing 1-byte OOB
还是上面那张图,按 armis 的意思,option 的 11 个字节(从 1 开始数)后面的第 12 个字节是可控的 OOB。然而,对于一个正常的数据包来说,IP头需要是 4 字节的倍数,若不够的话就要在最后进行填充。也就是说,这第 12 个字节其实应该是个范围内的地址,而不是 OOB,也就不会造成安全隐患。我们来看看 ForwardNetBuffer 是怎么来的,要转发的包从重组后的包的空间复制而来。可以看到 Retreat 的头部大小为 IPHeaderWithOptionSize,这个值是包含那个 pad 字节的。从 windbg 中也可以看出来,当我们的两个分片的 option 头为这样时。char optionPacket_1[] = {
'\x82', '\x0b', '\x41', '\x41',
'\x41', '\x08', '\x05', '\x41',
'\xC0', '\xA8', '\x7E', '\x01' // 最后一个字节用来填充
};
char optionPacket_2[] = {
'\x01', '\x01', '\x01', '\x01',
'\x83', '\x07', '\x08', '\xC0',
'\xA8', '\x7E', '\x80', '\x00'
};
IPHeaderWithOptionSize 的值为:
32 字节。既然是在正常的 Retreat 空间中,那么就不算 OOB,也就不会造成安全隐患才对。我不知道这里是不是失误,毕竟按这种思想往后挪一个字节就对了。然而,前面也说到,最后一个字节为 pad 字节,按理来说应该填 0x00,不过填成 0x01(nop) 也行,从 0x02 开始数据包就发不出去了,windows 内核应该是认为这是一个错误的数据包。0x00 是个不合法的 IP 地址,比如 192.168.0.3。所以这里其实只有 0x01 这种选项,此时数据包长这样:char optionPacket_1[] = {
'\x82', '\x0b', '\x41', '\x41',
'\x41', '\x09', '\x06', '\x41',
'\x41', '\xC0', '\xA8', '\x01', // 最后一个字节用来填充
};
然而,这样的数据包是过不了 IppRouteToDestinationInternal 的 check 的(受害者IP: 192.168.126.128,攻击者IP: 192.168.126.1,子网掩码: 255.255.255.0),推测原因为不属于同一网段。(看到这儿我觉得这个洞的利用不太现实,就没有继续跟了,也没有逆 IppRouteToDestinationInternal,所以是推测)其次,我们还需要爆破最后一个字节的空间,因为需要保证这个 route address 等于我们的源 IP(也就是攻击者的 IP 地址,虽然可以伪造),这样才能进入转发的流程。这最后一个字节的空间还恰巧不能为 0xff 或 0x00。不过两次数据包存放的地址也不一致,爆破其实也不现实,比如你爆破到 0x02 了,由于现在是另外一个地址,此时的空间为 0x01。当然,如果我们有办法喷射 MDL 空间的话,那这个 check 还是比较容易过的,不过你都有办法喷射 MDL 了,然后拿来过这个洞的 check,那不是本末倒置了吗?2. Writing 4-byte OOB
再来看看 armis 说的另一种方法(在原文中是先说这种方法的,然后说它不太现实,进而提出 Writing 1-byte OOB 的方案的)。被当成 route option 的第一个分片对应的 len 和 ptr 的位置我们是完全可控的,因此,从理论上来说,我们可以对 ForwardOption 后面 256 个字节空间的任意 4 个字节进行替换(虽然只能替换成受害者的 IP 地址,但结合 dhcp 伪造我们可以控制受害者的 IP 地址的值,至少最后一个字节随便换都能正常运作)。然而,从上面就可以看到,这更不现实。首先,你要替换的空间原本是存在一些垃圾数据的,你得保证这个垃圾数据是一个合法的 LSRR 数据,不然过不了 IppRouteToDestinationInternal 的 check。可以从上面看到,那样都过不了 check,这里要过 check 有多难。其次,你也得猜或者爆破这个空间的数据以进入转发的流程。总之,write OOB 的条件非常的苛刻。不过不妨假设下我们能够对 ForwardOption 后面 256 个字节空间的任意 4 个字节进行替换,那么我们若有一个使用 MDL 空间的 UAF 洞或者是分配->替换关键数据->继续执行这样的劫持的话,或许还是能够有不错的效果的。通告上说这是一个 RCE,若是仅有这个地方存在问题的话,那么个人感觉是很难做到 RCE 的,当然也有很大可能是因为我利用技能太差了(捂脸
这篇文章主要还是出于一个分享与学习的目的,其实并未给出任何有效的代码。
最后附上 idb 文件(ida 7.0):
https://github.com/50u1w4y/50u1w4y.github.io/blob/master/docs/recurrence/code/CVE-2021-24074/tcpip.i64
https://github.com/50u1w4y/50u1w4y.github.io/blob/master/docs/recurrence/code/CVE-2021-24074/test.cpp
由于是从头开始逆向的,逆向的过程中认知会发生改变,所以里面可能会残留一些一开始的错误认知,而且由于本人水平有限,所以注释和各种变量名都仅作参考,要是感觉哪里很有问题就放心地改掉。跑代码时记得设置 UAC,使用 IP_HDRINCL 选项需要管理员权限。
看雪ID:50u1w4y
https://bbs.pediy.com/user-home-863283.htm
*本文由看雪论坛 50u1w4y 原创,转载请注明来自看雪社区。
在线申请SSL证书行业最低 =>立即申请
[广告]赞助链接:
关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
让资讯触达的更精准有趣:https://www.0xu.cn/