https://darungrim.com/research/2019-10-10-vulnerability-root-cause-analysis-with-time-travel-debugging.html
找脆弱性有许多方法。最广泛使用的是fuzzing。本质上,fuzzing是一种暴力方法。多数例子中,先通过畸形输入造成程序崩溃。获得一次崩溃后,下一步是理解奔溃的根本原因。合适的RCA(根本原因分析)是理解bug本质的基础。这有助于决定bug是否可利用及付出更多努力开发该脆弱性的利用是否有意义。从安全工程师的视角,适当地对脆弱性进行归类并理解bug的本质对确定修复策略很有帮助。简单说,RCA是进行利用开发和制定防护策略的起始。
时间回溯调试是微软的一款记录程序执行过程并能够随时离线重现的工具。它用于从微软客户处搜集非可再现的软件bug。一旦bug在某个开启记录功能的客户环境中重现,客户可以提交记录内容到微软以便工程师分析、检查。
众所周知,理解脆弱性的本质是一个非常痛苦和乏味的过程。如果能访问源代码,可以通过重编译并调试来完全理解bug位置的上下文。如果不能,这就变成了一个试验、错误、猜测的重复游戏。
TTD(时间回溯调试)基于其记录、重现能力有助于RCA过程。TTD建立在Nirvana和iDNA技术基础上。Nirvana是一个二进制插装技术。程序执行过程由iDNA踪迹记录器记录并保存为踪迹文件。踪迹文件可以随后通过iDNA踪迹阅读器运行。这个和Pin很像,但TTD将保存执行日志和重现执行日志的功能都整合进了Windbg内,实用性更强。
有一个关于Adobe Acrobat Reader脆弱性的报告。和一段简短的描述一起,还包含一个POC。描述中说这是一个malformed JP2 stream record引起的double free问题。
下面是该POC引起奔溃的数据流和控制流的总览图。这个视图是通过TTD技术得到的。在接下来的内容中,我将解释如何通过一个有效的方法获得这个视图。
首先,需要搭建测试环境。我从官方发布页面获取了Adobe Acrobat Reader的老版本。启动Acrobat Reader后,可以在目标进程上附加一个TTD会话。演示进程是一个父进程为AcroRd32.exe的AcroRd32.exe进程。要使用TTD功能前需要先使用WinDbg。
要进行TTD记录需要以管理器权限启动WinDbg,并附加到目标进程(pid为2668的AcroRd32.exe进程)。
附加TTD后,通过打开从exploit-db下载的恶意格式PDF文档来重现奔溃。
我在这里分享了我的TTD运行过程记录文件,方便大家跟随我的分析过程。Archive的密码时DarunGrim。
打开TTD文件后,在WinDbg会话中会得到一个命令提示符,输入g(go)命令到达记录的末尾,在这里能看到在哪里发生了奔溃。
下面显示程序执行过程中遇到一个异常。
(2dc.13a0): Unknown exception - code c0000374 (first/second chance not available) TTD: End of trace reached. (2dc.13a0): Break instruction exception - code 80000003 (first/second chance not available) Time Travel Position: 224FB2:1 eax=000d0004 ebx=00000000 ecx=ffffd8f0 edx=770d2330 esi=00003a98 edi=00000000 eip=67ce7001 esp=010cc25c ebp=010cc2a8 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 67ce7001 0970ce or dword ptr [eax-32h],esi ds:002b:000cffd2=????????
通过查看调用栈可以看到异常来自哪里。显然, MSVCR120!free调用的ntdll!RtlFreeHeap调用了ntdll!RtlpLogHeapFailure来报告堆不一致异常。这意味着发生了堆破坏,并且在释放时被堆管理器检测到了。
0:001> kp 10 # ChildEBP RetAddr 00 010cd970 7712b763 ntdll!RtlpReportHeapFailure 01 010cd980 770d16cf ntdll!RtlpHeapHandleError+0x1c 02 010cd9b0 770e23be ntdll!RtlpLogHeapFailure+0x9f 03 010cd9e4 6b1becfa ntdll!RtlFreeHeap+0x4abce 04 010cd9f8 6a18b2a7 MSVCR120!free(void * pBlock = 0x1fb1c850)+0x1a [f:\dd\vctools\crt\crtw32\heap\free.c @ 51] WARNING: Stack unwind information not available. Following frames may be wrong. 05 010cdb0c 6a17bc96 AcroRd32!CTJPEGTiledContentWriter::operator=+0x12c83 06 010cdcd4 6a179c26 AcroRd32!CTJPEGTiledContentWriter::operator=+0x3672 07 010cdd08 6a171033 AcroRd32!CTJPEGTiledContentWriter::operator=+0x1602 08 010cdd1c 6a1654a7 AcroRd32!AX_PDXlateToHostEx+0x271448 09 010cddc4 69c7c595 AcroRd32!AX_PDXlateToHostEx+0x2658bc 0a 010cdde0 69c7c4a9 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x22d4d 0b 010cde00 69c119d7 AcroRd32!CTJPEGWriter::CTJPEGWriter+0x22c61 0c 010cde28 69c1198d AcroRd32!AcroWinBrowserMain+0x19eb3 0d 010cde3c 69cb0c16 AcroRd32!AcroWinBrowserMain+0x19e69 0e 010cde54 69d8d21a AcroRd32!CTJPEGWriter::CTJPEGWriter+0x573ce 0f 010cdea8 6a0ee398 AcroRd32!CTJPEGDecoderHasMoreTiles+0xf4a
可以在ntdll!RtlpLogHeapFailure设置一个断点并运行g-(回溯)命令来到达该位置。
Time Travel Position: 222B02:4E9 eax=00000000 ebx=7715c908 ecx=00000002 edx=00000000 esi=00000002 edi=1fb1c848 eip=7712cfb0 esp=010cd974 ebp=010cd980 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 ntdll!RtlpReportHeapFailure: 7712cfb0 8bff mov edi,edi
可以使用t-(向后步过)命令来向后一步步找到发生堆检测失败的条件判定。这是堆检查发生的位置。
Time Travel Position: 222B02:67 eax=6ae3fb4c ebx=1fb1c850 ecx=1fb1c850 edx=00000000 esi=1fb1c848 edi=01670000 eip=77097851 esp=010cd9c8 ebp=010cd9e4 iopl=0 ov up ei pl nz ac pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000a16 ntdll!RtlFreeHeap+0x61: 77097851 f646073f test byte ptr [esi+7],3Fh ds:002b:1fb1c84f=80
RtlFreeHeap函数内的安全检查功能的反汇编代码在Ghidra中如下:
0x1fb1c84f处的一字节长度空间被破坏了,我们想找到修改这个位置的代码。可以对这个地址使用ba命令来找到修改指令。
ba w1 1fb1c84f g-
下面显示的是修改0x1fb1c84f处内容的指令。
Time Travel Position: 222B02:B eax=0317022d ebx=1fb1c848 ecx=8317022d edx=0317022d esi=0317022d edi=078410c0 eip=77097953 esp=010cd990 ebp=010cd9bc iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 ntdll!RtlpLowFragHeapFree+0x93: 77097953 c6430780 mov byte ptr [ebx+7],80h ds:002b:1fb1c84f=88
根据调用栈,对内存0x1fb1c850处的修改是由MSVCR120!free发起的。所以这是一个针对地址0x1fb1c850的double-free错误。
0:001> kp # ChildEBP RetAddr 00 010cd9bc 7709787d ntdll!RtlpLowFragHeapFree+0x93 01 010cd9e4 6b1becfa ntdll!RtlFreeHeap+0x8d Unable to load image C:\Program Files (x86)\Adobe\Acrobat Reader DC\Reader\AcroRd32.dll, Win32 error 0n2 02 010cd9f8 6a18b296 MSVCR120!free(void * pBlock = 0x1fb1c850)+0x1a [f:\dd\vctools\crt\crtw32\heap\free.c @ 51] WARNING: Stack unwind information not available. Following frames may be wrong. 03 010cdb0c 6a17bc96 AcroRd32!CTJPEGTiledContentWriter::operator=+0x12c72 04 010cdcd4 6a179c26 AcroRd32!CTJPEGTiledContentWriter::operator=+0x3672 05 010cdd08 6a171033 AcroRd32!CTJPEGTiledContentWriter::operator=+0x1602 06 010cdd1c 6a1654a7 AcroRd32!AX_PDXlateToHostEx+0x271448
对内存0x1fb1c850执行了两次free操作。现在检查一下这个内存位置在第一次free之后是否被重新分配了。一种方法是对TTD对象使用LINQ请求。
下面的命令会返回所有返回值为0x1fb1c850的MSVCR120!malloc调用。
0:000> dx -r1 @$cursession.TTD.Calls("MSVCR120!malloc").Where(c => c.ReturnValue == (void *)0x1fb1c850) @$cursession.TTD.Calls("MSVCR120!malloc").Where(c => c.ReturnValue == (void *)0x1fb1c850) [0x14088] [0x14e29] [0x3d3dd] [0x3d8b9]
对所有调用进行检查后,发现0x14e29处是最后的调用,并且在两次free之前。所以很确定这是一个double-free问题。
发生double-free的代码形似:
6a18b286 8b8568ffffff mov eax,dword ptr [ebp-98h] 6a18b28c 85c0 test eax,eax 6a18b28e 7407 je AcroRd32!CTJPEGTiledContentWriter::operator=+0x12c73 (6a18b297) 6a18b290 50 push eax 6a18b291 e821eea6ff call AcroRd32!AcroWinBrowserMain+0x2593 (69bfa0b7) <-- call to free
6a18b297 8b8570ffffff mov eax,dword ptr [ebp-90h] 6a18b29d 85c0 test eax,eax 6a18b29f 7407 je AcroRd32!CTJPEGTiledContentWriter::operator=+0x12c84 (6a18b2a8) 6a18b2a1 50 push eax 6a18b2a2 e810eea6ff call AcroRd32!AcroWinBrowserMain+0x2593 (69bfa0b7) <-- call to free
其反编译代码如下。
void FreeJP2Resources(void) { ... if (*(int *)(unaff_EBP + -0x98) != 0) { free(*(int *)(unaff_EBP + -0x98)); } if (*(int *)(unaff_EBP + -0x90) != 0) { free(*(int *)(unaff_EBP + -0x90)); }
0x010cda74 (ebp-98h)和0x010cda7c (ebp-90h)存的都是指向0x1fb1c850的指针。现在需要看看为什么两个内存值相同。
0:001> dd 010cda74 010cda74 1fb1c850 00000000 1fb1c850 00000018 010cda84 00000000 00000000 0000000d 07843b1c 010cda94 000034a0 11001001 00000000 00000000 010cdaa4 07843b1c 00000000 00000000 00000000 010cdab4 00000004 00000001 00000b20 000005ac 010cdac4 00000563 00ecc304 00000666 0000bbe6 010cdad4 1fb1c808 07798d0b 1fb1c898 1fb1c700 010cdae4 00000000 05000000 03030303 01000000
向后回溯找到这两个内存地址被分配的位置,对这两个位置使用ba(访问断点)命令。
ba w4 010cda74 ba w4 010cda7c g-
定位到两个代码位置。
Time Travel Position: 222B00:1AD2 eax=1fb1c850 ebx=00000000 ecx=016afde0 edx=00000003 esi=1fb1c700 edi=00000000 eip=6a18ac72 esp=010cda04 ebp=010cdb0c iopl=0 nv up ei pl nz ac po nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000212 AcroRd32!CTJPEGTiledContentWriter::operator=+0x1264e: 6a18ac72 898570ffffff mov dword ptr [ebp-90h],eax ss:002b:010cda7c=0000077f
Time Travel Position: 222B00:1AA3 eax=1fb1c850 ebx=00000000 ecx=016afde0 edx=00000003 esi=1fb1c700 edi=00000000 eip=6a18ac59 esp=010cd9cc ebp=010cdb0c iopl=0 nv up ei ng nz na pe cy cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000287 AcroRd32!CTJPEGTiledContentWriter::operator=+0x12635: 6a18ac59 898568ffffff mov dword ptr [ebp-98h],eax ss:002b:010cda74=1ff69498
这两个内存地址都是通过调用0x6a18b2e5 (GetMemoryBlock)函数得到的。
undefined4 GetMemoryBlock(undefined4 memory_block_type,int *param_2,byte offsetVal1,byte offsetVal2, undefined4 offsetVal3,int param_6) { undefined4 retVal; int memory_block_base; byte offset; code *pcVar1; if ((char)memory_block_type == '\x03') { if ((param_2 == (int *)0x0) || (param_6 == 0)) { exception: CallThrowException(0x40000003,0); memory_block_type = 0; _CxxThrowException(&memory_block_type,0x7472e75c); pcVar1 = (code *)swi(3); retVal = (*pcVar1)(); return retVal; } *param_2 = *param_2 + 1; retVal = RetrieveMemoryBlock(param_6,*param_2 + -1); } else { offset = offsetVal1; if (((char)memory_block_type != '\0') && (offset = offsetVal2, (char)memory_block_type != '\x01')) { if ((char)memory_block_type != '\x02') goto exception; offset = (byte)offsetVal3; } if (offset == 0) goto exception; memory_block_base = (*_TlsGetValueStub)(_dwTlsIndexForMemoryBlockBase); retVal = *(undefined4 *)(memory_block_base + 0x20 + (uint)offset * 4); } return retVal; }
总的来说,根据memory_block_type参数的不同,该函数将通过不同偏移位置和memory_block_base地址返回一个内存块指针。
.text:6A18AC3F push esi .text:6A18AC40 push ebx .text:6A18AC41 push 0Fh .text:6A18AC49 lea eax, [ebp-58h] .text:6A18AC4C push 0Eh .text:6A18AC4E push eax .text:6A18AC4F push dword ptr [ebp-1Ch] .text:6A18AC52 call GetMemoryBlock <-- Getting memory location .text:6A18AC59 mov [ebp-98h], eax <-- 222B00:1AA3
0:001> dds esp L6 010cd9d4 01000000 <-- memory_block_type 010cd9d8 010cdab4 010cd9dc 0000000e 010cd9e0 0000000f 010cd9e4 00000000 010cd9e8 1fb1c700
.text:6A18AC57 push esi .text:6A18AC58 push ebx .text:6A18AC5F push 0Fh .text:6A18AC61 push 0Eh .text:6A18AC63 lea eax, [ebp-58h] .text:6A18AC66 push eax .text:6A18AC67 push dword ptr [ebp-1Bh] .text:6A18AC6A call GetMemoryBlock <-- Getting memory location (222B00:1AA9) .text:6A18AC6F add esp, 48h .text:6A18AC72 mov [ebp-90h], eax <-- 222B00:1AD2
0:001> dds esp L6 010cd9bc 00010000 <-- memory_block_type 010cd9c0 010cdab4 010cd9c4 0000000e 010cd9c8 0000000f 010cd9cc 00000000 010cd9d0 1fb1c700
第一次调用传输0x01000000作为memory_block_type,第二次调用传输0x00010000作为memory_block_type。但在GetMemoryBlock内,memory_block_type被截断为一个char类型。所以两次参数都被截断为0x00。
两次调用都通过下面的代码获取内存块。两次调用的其它参数都相同。所以,使用相同memory_block_type值调用两次GetMemoryBlock将给两个不同域分配相同的地址,最终导致double-free问题。
memory_block_base = (*_TlsGetValueStub)(_dwTlsIndexForMemoryBlockBase); retVal = *(undefined4 *)(memory_block_base + 0x20 + (uint)offset * 4);
位于0x6A18AC6A的第二次调用的memory_block_type参数来自于下面指令。
Time Travel Position: 222B00:1AA8 eax=010cdab4 ebx=00000000 ecx=016afde0 edx=00000003 esi=1fb1c700 edi=00000000 eip=6a18ac67 esp=010cd9c0 ebp=010cdb0c iopl=0 nv up ei ng nz na pe cy cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000287 AcroRd32!CTJPEGTiledContentWriter::operator=+0x12643: 6a18ac67 ff75e5 push dword ptr [ebp-1Bh] ss:002b:010cdaf1=00010000
通过下面的命令回溯这个内存值来自哪里。这里的地址0x010cdaf1是[ebp-1Bh]的内容。
0:001> ba w1 010cdaf1 0:001> g- Breakpoint 0 hit Time Travel Position: 222B00:19AE eax=00000000 ebx=1fbad818 ecx=00000002 edx=010cdb7c esi=010cdb54 edi=010cdaf4 eip=6a18ab57 esp=010cda04 ebp=010cdb0c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 AcroRd32!CTJPEGTiledContentWriter::operator=+0x12533: 6a18ab57 f3a5 rep movs dword ptr es:[edi],dword ptr [esi]
0x010cdaf4处的4字节从0x010cdb54处复制来。0x010cdaf1处的4字节从0x010cdb51处复制来。通过多次使用ba命令并结合静态分析,我发现memory_block_type由函数CalcMemoryBlockType计算得到。在0x6a17be56,al寄存器存的就是memory_block_type的值。
6a17be18 e809d5ffff call AcroRd32!CTJPEGTiledContentWriter::operator=+0xd02 (6a179326) <-- ReadInt 6a17be1d 0fb7c8 movzx ecx, ax <-- 222AAB:7DD 6a17be4f 8bc1 mov eax, ecx 6a17be51 c1e80a shr eax, 0Ah 6a17be54 22c3 and al, bl 6a17be56 88467d mov byte ptr [esi+7Dh], al <--- 222AAB:7F4
0x6a17be1d处寄存器ax里的值是在ReadInt函数中按一定规则从字节值转换得到的int值。
ret_val = (uint)**buffer; currente_byte = *buffer + 1; *buffer = currente_byte; if (1 < size) { iVar2 = size - 1; do { ret_val = ret_val * 0x100 + (uint)*currente_byte; currente_byte = currente_byte + 1; *buffer = currente_byte; iVar2 = iVar2 + -1; } while (iVar2 != 0);
在这里转换为int的字节是位于0x0763beac处的两个字节。
0:001> db 0763beac 0763beac 00 ff 00 00 05 63 20 00-77 65 55 23 00 00 00 00 .....c .weU#....
通过ba命令,可以发现这个内存位置的值是从0x0746ba24处复制来的。但是,地址0x0746ba24在执行到调用ReadInt之前没有被写过。TTD无法追踪内核代码执行的内存修改操作。到目前为止,可以假设0x0746ba24处的内容实在内核函数中赋值的,可能是ReadFile。为了验证我的猜测,我利用TTD请求查找针对内存0x0746ba24进行的ReadFile操作。ReadFile函数的第一个参数是缓存地址,第二个参数是缓存尺寸。
0:001> dx -r1 @$cursession.TTD.Calls("kernel32!ReadFile").Where(c => c.Parameters[1] <= 0x0746ba24 && 0x0746ba24 <= c.Parameters[1] + c.Parameters[2]) @$cursession.TTD.Calls("kernel32!ReadFile").Where(c => c.Parameters[1] <= 0x0746ba24 && 0x0746ba24 <= c.Parameters[1] + c.Parameters[2]) [0x7b]
这个命令只返回一个值0x7B,通过进一步分析可以确认这块内容是通过ReadFile函数从PDF文件中读到的。
0:001> dx -r1 @$cursession.TTD.Calls("kernel32!ReadFile").Where(c => c.Parameters[1] <= 0x0746ba24 && 0x0746ba24 <= c.Parameters[1] + c.Parameters[2])[0x7b] @$cursession.TTD.Calls("kernel32!ReadFile").Where(c => c.Parameters[1] <= 0x0746ba24 && 0x0746ba24 <= c.Parameters[1] + c.Parameters[2])[0x7b] EventType : 0x0 ThreadId : 0x13a0 UniqueThreadId : 0x3 TimeStart : 220141:4C [Time Travel] TimeEnd : 220143:14 [Time Travel] Function : UnknownOrMissingSymbols FunctionAddress : 0x74db9c40 ReturnAddress : 0x69c11134 ReturnValue : 0x1 Parameters
执行完这个ReadFile函数后,目标内存看起来如下:
0:001> db 07467850 07467850 00 00 00 0e 30 00 01 00-00 00 13 00 00 0b 20 00 ....0......... . 07467860 00 0e 44 00 00 2e 23 00-00 2e 23 00 8e 43 00 00 ..D...#...#..C.. 07467870 00 0f 00 01 01 00 00 23-46 00 01 00 00 02 3f 00 .......#F.....?. 07467880 00 02 3f b8 7e 00 c0 20-70 04 08 07 e0 e9 a4 7f ..?.~.. p....... 07467890 cf d8 ff ec 7c 43 f3 80-d9 3f 9f 9f ff c6 7f ff ....|C...?...... 074678a0 ff 3f c0 4f 69 1b 3e cb-cc 61 fd df 13 00 62 00 .?.Oi.>..a....b. 074678b0 08 08 2f 1d f8 00 e7 e3-ba 44 9c 96 7b bb be 0f ../......D..{... 074678c0 e7 38 a0 08 1c 61 80 e7-67 f7 dd ff df 3b ff 7f .8...a..g....;..
这些内容来自于文件的0x130DF偏移处。
也就是说,内存地址0x0746ba24处的内容来自于文件偏移0x172B3处,并直接影响内存分配行为。
Fuzzed文档中被修改的字节和我通过内存回溯发现的一样。
修改前的字节“00 1C”经过ReadInt转换后是0x1D。0x1D在CalcMemoryBlockType函数中被与3异或,将导致memory_block_type值为1,而不是修改为“00 ff” 后得到的0。从GetMemoryBlock函数重复得到相同的内存块是导致double-free的本质原因。
现在看总览图就更清楚了。一个被fuzzed的字节通过影响内存类型域导致重复使用一块内存。看上去这个字节无法直接影响内存的内容。
当bug发生时,引起bug的输入数据往往是通过多个数据拷贝过程产生的。有些输入数据经过了多段代码的一轮又一轮复制,并在引起bug或脆弱性前经过一些算数运算。RCA是回溯数据和控制流来确定引起bug或脆弱性原因的逆向工程技术。本文中显示的方法很像是个使用说明。结合符号执行及一些启发式方法,有可能使用二进制插装技术构建一个有效的bug分类系统。