首页
社区
课程
招聘
[翻译]通过回溯调试分析脆弱性的根本原因
2019-10-30 21:17 6564

[翻译]通过回溯调试分析脆弱性的根本原因

2019-10-30 21:17
6564

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 Arcobat Reader脆弱性

有一个关于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的代码形似:

  • 第一次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
  • 第二次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

从GetMemoryBlock到得到0x1fb1c850内存块

这两个内存地址都是通过调用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_type参数的不同,该函数将通过不同偏移位置和memory_block_base地址返回一个内存块指针。

  • 第一次通过GetMemoryBlock获取内存块指针时memory_block_type值为0x01000000。

.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
  • 第二次通过GetMemoryBlock获取内存块指针时memory_block_type值为0x010000。

.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);

在CalcMemoryBlockType内计算出memory_block_type为0x00010000

位于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处,并直接影响内存分配行为。

RCA

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分类系统。


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

收藏
点赞2
打赏
分享
最新回复 (1)
雪    币: 17780
活跃值: (60073)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2019-10-30 21:42
2
0
感谢分享!
游客
登录 | 注册 方可回帖
返回