更多精彩内容,欢迎关注作者的微信公众号:码工笔记
2024年7月19日,全球出现大量Windows系统蓝屏问题。主要原因是有一家名为CrowdStrike的公司出品的安全软件Falcon在内核态出现了内存越界,导致内核崩溃。
本文主要看一下他们官方出的根因分析文档,看看究竟发生了什么事。
背景
Falcon有一套动态下发防护策略的机制,主要下发以下两种内容:
-
Sensor content
这种更新是包含新代码逻辑的,相对来说不太频繁,要支持新的模板类型时才会用这种更新方式,可以参考iOS/Android上需要发版才能实现的需求。
对这种新模板类型的支持,一般都要涉及定义一种数据格式,生产方按这种格式生产数据,消费方消费这种数据。数据格式规定了字段的数量和格式。
在CrowdStrike这次的事故中,数据格式看起来是通过正则表达式来指定的。
-
RapidRespose Content
这种是基于Sensor content中已支持的模板类型快速下发新的配置,下发到客户侧以后Sensor content中的代码就能解析它并按它指定的策略执行具体数据获取逻辑,下发相对更频繁,风险也更低,可以类比不需要发版、产品通过CMS就能配置上线的策略。
问题原因
问题根本原因是今年2月份下发的Sensor Content代码中新支持了一种模板(支持命名管道相关检测),模板中定义了21个参数,这21个参数在运行时有对应的生产者和消费者,其中消费者是按21个参数读取的,但生产者其实只提供了20个,所以运行时出现了访问越界,读到了非法地址,在内核态下直接崩了,从而导致Windows蓝屏。
其实CrowdStrike针对这种新模板在3月和5月还成功下发过几次数据,但那几次正好因为下发的策略里并没有真正用到第21个参数(官方说法是之前几次针对第21个参数都是用通配符匹配的),所以程序没有走到消费它的逻辑,所以没有触发相应的问题。
但7月19日这天下发的策略数据中,使用了第21个参数(也就是消费者会真正读这第21个参数的数据),从而触发了崩溃。
具体崩溃现场分析
CrowdStrike的工程师在报告中附上了崩溃现场的WinDbg crashdump信息和相应的分析,我们一起看一下:
kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
PAGE_FAULT_IN_NONPAGED_AREA (50)
Invalid system memory was referenced. This cannot be protected by try-except.
Typically the address is just plain bad or it is pointing at freed memory.
Arguments:
Arg1: ffffd6030000006a, memory referenced.
Arg2: 0000000000000000, X64: bit 0 set if the fault was due to a not-present PTE.
bit 1 is set if the fault was due to a write, clear if a read.
bit 3 is set if the processor decided the fault was due to a corrupted PTE.
bit 4 is set if the fault was due to attempted execute of a no-execute PTE.
- ARM64: bit 1 is set if the fault was due to a write, clear if a read.
bit 3 is set if the fault was due to attempted execute of a no-execute PTE.
Arg3: fffff8020ebc14ed, If non-zero, the instruction address which referenced the bad memory
address.
Arg4: 0000000000000002, (reserved)
READ_ADDRESS: ffffd6030000006a Paged pool
MM_INTERNAL_CODE: 2
IMAGE_NAME: csagent.sys
MODULE_NAME: csagent
FAULTING_MODULE: fffff8020eae0000 csagent
PROCESS_NAME: System
TRAP_FRAME: ffffae035f57eca0 -- (.trap 0xffffae035f57eca0)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=ffffae035f57f280 rbx=0000000000000000 rcx=0000000000000003
rdx=ffffae035f57f250 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8020ebc14ed rsp=ffffae035f57ee30 rbp=ffffae035f57ef30
r8=ffffd6030000006a r9=0000000000000000 r10=0000000000000000
r11=0000000000000014 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei ng nz na po nc
csagent+0xe14ed:
fffff802`0ebc14ed 458b08 mov r9d,dword ptr [r8] ds:ffffd603`0000006a=????????
Resetting default scope
STACK_TEXT:
ffffae03`5f57ea78 fffff802`05add2da : 00000000`00000050 ffffd603`0000006a 00000000`00000000 ffffae03`5f57eca0 : nt!KeBugCheckEx
ffffae03`5f57ea80 fffff802`05947efc : ffffd603`000ed454 00000000`00000000 00000000`00000000 ffffd603`0000006a : nt!MiSystemFault+0x1bc19a
ffffae03`5f57eb80 fffff802`05a2707e : 00000000`00000000 ffffd603`e33a019e ffffae03`5f57f0a0 ffffae03`5f57f0a0 : nt!MmAccessFault+0x29c
ffffae03`5f57eca0 fffff802`0ebc14ed : 00000000`00000000 ffffae03`5f57ef30 ffffd603`f208200c ffffd603`f207a05c : nt!KiPageFault+0x37e
ffffae03`5f57ee30 fffff802`0eb9709e : 00000000`00000000 00000000`e01f008d ffffae03`5f57f202 fffff802`0ed6aaf8 : csagent+0xe14ed
ffffae03`5f57efd0 fffff802`0eb98335 : 00000000`00000000 00000000`00000010 00000000`00000002 ffffd603`f207a01c : csagent+0xb709e
ffffae03`5f57f100 fffff802`0edd20c7 : 00000000`00000000 00000000`00000000 ffffae03`5f57f402 00000000`00000000 : csagent+0xb8335
ffffae03`5f57f230 fffff802`0edcec44 : ffffae03`5f57f6e8 fffff802`060abae0 ffffd603`ed408580 00000000`00000003 : csagent+0x2f20c7
ffffae03`5f57f4b0 fffff802`0eb47a31 : 00000000`0000303b ffffae03`5f57f770 ffffd603`edc908a0 ffffc189`7fcd4098 : csagent+0x2eec44
ffffae03`5f57f670 fffff802`0eb46aee : ffffd603`edc908a0 fffff802`0ebf1e7e 00000000`00006820 fffff802`0ed3f8f0 : csagent+0x67a31
ffffae03`5f57f7e0 fffff802`0eb4685b : ffffae03`5f57fa58 ffffd603`edc97830 ffffd603`edc908a0 ffffc189`7f90f4b8 : csagent+0x66aee
ffffae03`5f57f850 fffff802`0ebe99ea : 00000000`f047f4ef ffff49ac`ca0f55d4 00000000`00000000 ffffd603`ec18fc30 : csagent+0x6685b
ffffae03`5f57f8d0 fffff802`0eb3efbb : 00000000`00000000 ffffae03`5f57fad9 ffffc189`7f90f010 ffffc189`7f7ea470 : csagent+0x1099ea
ffffae03`5f57fa00 fffff802`0eb3edd7 : ffffc189`7ab79000 00000000`00000000 ffffc189`7f90f010 ffffc189`00000001 : csagent+0x5efbb
ffffae03`5f57fb40 fffff802`0ebde681 : 00000000`00000000 00000000`00000000 ffffc189`7f5a97d0 ffffc189`7f7ea470 : csagent+0x5edd7
ffffae03`5f57fb70 fffff802`05879ca7 : ffffc189`7faa8040 00000000`00000080 fffff802`0ebde510 00000000`00000000 : csagent+0xfe681
ffffae03`5f57fbb0 fffff802`05a1af64 : ffffe601`bcf51180 ffffc189`7faa8040 fffff802`05879c50 00000000`00000000 : nt!PspSystemThreadStartup+0x57
ffffae03`5f57fc00 00000000`00000000 : ffffae03`5f580000 ffffae03`5f579000 00000000`00000000 00000000`00000000 : nt!KiStartSystemThread+0x34
崩溃信息显示是访问了无效地址,访问者是csagent.sys(CrowdStrike Falcon的文件系统事件过滤器)。它监听了命名管道的创建等事件,并在收到事件时,事件中的数据连同其他环境信息一起作为输入提供下游用作模板字段匹配的输入。
看一下trap frame的具体内容:
TRAP_FRAME: ffffae035f57eca0 -- (.trap 0xffffae035f57eca0)
NOTE: The trap frame does not contain all registers.
Some register values may be zeroed or incorrect.
rax=ffffae035f57f280 rbx=0000000000000000 rcx=0000000000000003
rdx=ffffae035f57f250 rsi=0000000000000000 rdi=0000000000000000
rip=fffff8020ebc14ed rsp=ffffae035f57ee30 rbp=ffffae035f57ef30
r8=ffffd6030000006a r9=0000000000000000 r10=0000000000000000
r11=0000000000000014 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei ng nz na po nc
csagent+0xe14ed:
fffff802`0ebc14ed 458b08 mov r9d,dword ptr [r8] ds:ffffd603`0000006a=????????
可以看到出错的指令位于 0xfffff8020ebc14ed(rip寄存器中),是一个内存读操作错误了。
打印一下出问题处之前的10条指令:
kd> u @rip-16 L0n10
csagent!TemplateGetString+0xe:
//1. rax中存放的是参数表的开始地址,r11中存放的是0x16,即20,每个参数占8字节,即取出来第21个参数的地址,存到r8中
fffff802`0ebc14d7 4e8b04d8 mov r8,qword ptr [rax+r11*8]
//2. 这里条件成立,跳到下面的0ebc14e8处
fffff802`0ebc14db 750b jne csagent!TemplateGetString+0x1f (fffff802`0ebc14e8)
fffff802`0ebc14dd 4d85c0 test r8,r8
fffff802`0ebc14e0 7412 je csagent!TemplateGetString+0x2b (fffff802`0ebc14f4)
fffff802`0ebc14e2 450fb708 movzx r9d,word ptr [r8]
fffff802`0ebc14e6 eb08 jmp csagent!TemplateGetString+0x27 (fffff802`0ebc14f0)
//3. 从上面第2步跳到这里后,判断一下r8是否为NULL
fffff802`0ebc14e8 4d85c0 test r8,r8
//4. 如果是NULL,则跳到fffff802`0ebc14f4
fffff802`0ebc14eb 7407 je csagent!TemplateGetString+0x2b (fffff802`0ebc14f4)
//5. 不是NULL,则读取此内存地址的内容并存入r9中
fffff802`0ebc14ed 458b08 mov r9d,dword ptr [r8]
fffff802`0ebc14f0 4d8b5008 mov r10,qword ptr [r8+8]
- 在进入这段代码前,rax中存放的是生产者提供的21个参数的开始地址,看寄存器r11值是0x14(10进制为20),rax + r11 * 8,这条指令也就是把第21个参数的地址存放了r8中(每个参数占8个字节);
- 跳到0ebc14e8处
- 判断一下r8中的内容是否为空,为空则跳走
- r8内容非空,则以r8的内容为地址,取对应内存的数据
命令:
u @eip-16 L0n10
- @eip-16 表示当前指令地址往前数0x16个byte,(看样子这个session里默认的立即数是16进制的,因为实际打印的长度是 0xed - 0xd7 = 0x16)
- L是u命令的参数,0n10代表十进制数10,0n前缀代表后面的数字是10进制的,0x是16进制,0t是8进制,0y是二进制 [5]
- 命令整体意思是反汇编从@eip-0x16地址开始的10条指令
再看一下rax处开始的内存数据:
kd> dp @rax l0n21
ffffae03`5f57f280 ffffae03`5f57f320 ffffae03`5f57f330
ffffae03`5f57f290 ffffae03`5f57f340 ffffae03`5f57f350
ffffae03`5f57f2a0 ffffae03`5f57f360 ffffae03`5f57f370
ffffae03`5f57f2b0 ffffae03`5f57f380 ffffae03`5f57f390
ffffae03`5f57f2c0 ffffae03`5f57f3a0 ffffae03`5f57f3b0
ffffae03`5f57f2d0 ffffae03`5f57f3c0 ffffae03`5f57f3d0
ffffae03`5f57f2e0 ffffae03`5f57f3e0 ffffae03`5f57f3f0
ffffae03`5f57f2f0 ffffae03`5f57f400 ffffae03`5f57f410
ffffae03`5f57f300 ffffae03`5f57f420 ffffae03`5f57f430
ffffae03`5f57f310 ffffae03`5f57f440 ffffae03`5f57f450
ffffae03`5f57f320 ffffd603`0000006a
以上命令指定打印rax中的地址开始的21 * 8-byte的数据,可以看到,第21个数据是 0xffffd6030000006a,也就是说,上面的第1步中给r8中存了0xffffd6030000006a,然后在第4步中加载地址0xffffd6030000006a处的内存数据。
下面我们看一下0xffffd6030000006a的页表项情况:
kd> !pte ffffd603`0000006a
VA ffffd6030000006a
PXE at FFFFFE7F3F9FCD60 PPE at FFFFFE7F3F9AC060 PDE at FFFFFE7F3580C000 PTE at FFFFFE6B01800000
contains 0A00000107A00863 contains 0000000000000000
pfn 107a00 ---DA--KWEV contains 0000000000000000
not valid
contains后面的数据中的前缀部分本应存放的是虚拟地址0xffffd6030000006a对应的物理地址的前缀(不包含页面偏移),但这里显示的是0,是无效地址。
参考资料
- [1] CrowdStrike官方分析初版
- [2] CrowdStrike官方分析详版
- [3] WinDbg的u命令
- [4] WinDbg命令中指定Address range
- [5] n命令指定数值基数
- [6] !pte命令
- [7] [!pte命令讲解](www.cnblogs.com/bianchengna…