专栏首页腾讯玄武实验室的专栏Chrome PDFium 整数截断漏洞分析
原创

Chrome PDFium 整数截断漏洞分析

作者:刘科

0x01. 漏洞简介

chromium:697847 是 PDFium 里面由于 整数截断 引起的一个堆溢出漏洞(将 unsigned long 赋值给uint32),简单记录一下。

漏洞原理:

  1. PDFium 使用 zlib 的 inflate 接口解压数据;
  2. 在 zlib 中,解压后的数据的大小使用 unsigned long类型的变量 total_out来存储;
  3. PDFium 使用 uint32 类型的变量来接收 total_out 的值;
  4. 在 64 位环境中,当解压后的数据大小超过 4GB 时(即超过uint32的范围)会产生截断;
  5. 后续 PDFium 使用截断后的值分配堆块并拷贝解压后的数据,导致了堆溢出;0x02. 漏洞分析

2.1 崩溃信息

在 64 位 Ubuntu 上开启 AddressSanitizer 编译 PDFium,使用编译出来的 pdfium_test测试原贴提供的 PoC 文件,可以看到如下崩溃信息(已简化):

==43290==ERROR: AddressSanitizer: heap-buffer-overflow on address 
    0x60200000ecd1 at pc 0x0000004a5dad bp 0x7ffdcfa78c10 sp 0x7ffdcfa783c0
WRITE of size 8349028 at 0x60200000ecd1 thread T0
    #0 0x4a5dac in __asan_memcpy 
    #1 0x8e5d80 in (anonymous namespace)::FlateUncompress() 
        pdfium/core/fxcodec/codec/fx_codec_flate.cpp:603:9
    #2 0x8e5d80 in CCodec_FlateModule::FlateOrLZWDecode() 
    #3 0x6f3710 in FPDFAPI_FlateOrLZWDecode() 
    #4 0x6f49a3 in PDF_DataDecode()
    #5 0x6db1b5 in CPDF_StreamAcc::LoadAllData()
    #6 0x7a7e58 in CPDF_ContentParser::Start()
    #7 0x628de1 in CPDF_Page::StartParse()
    #8 0x628de1 in CPDF_Page::ParseContent()
    #9 0x50b0ab in FPDF_LoadPage()
    #10 0x4f8173 in GetPageForIndex()
    #11 0x4f8aea in RenderPage()
    #12 0x4fc753 in RenderPdf()
    #13 ...

0x60200000ecd1 is located 0 bytes to the right of 
    1-byte region [0x60200000ecd0,0x60200000ecd1)
allocated by thread T0 here:
    #0 0x4bc230 in calloc()
    #1 0x8e5c58 in FX_AllocOrDie()
    #2 0x8e5c58 in (anonymous namespace)::FlateUncompress()
        pdfium/core/fxcodec/codec/fx_codec_flate.cpp:595
    #3 0x8e5c58 in CCodec_FlateModule::FlateOrLZWDecode()
    #4 0x6f3710 in FPDFAPI_FlateOrLZWDecode()
    #5 0x6f49a3 in PDF_DataDecode()
    #6 0x6db1b5 in CPDF_StreamAcc::LoadAllData()

可以看出这里出发了堆溢出行为,目标堆块的大小只有 1 字节([0x60200000ecd0,0x60200000ecd1)),而程序尝试通过 memcpy往堆块上写入大量数据。总结如下:

  • memcpy调用位于 FlateUncompress 函数 (core/fxcodec/codec/fx_codec_flate.cpp的第 603 行);
  • 堆块的分配操作同样位于 FlateUncompress函数(fx_codec_flate.cpp 的第 595 行);2.2 POC 分析

原贴提供的 PoC 文件十分简单:4 号 obj 包含 0x3FB2B2 字节数据,/Filter的值为 /FlateDecode,即数据使用了 zlib/deflate 算法进行压缩,需要使用 zlib/inflate算法进行解压缩。

2.3 zlib 分析

先来看一下 zlib 在解压数据时需要用到的关键结构z_stream(注意这里 total_out 的类型为unsigned long):

typedef struct z_stream_s {
    z_const Bytef *next_in; /* 存储待解压数据的位置 */
    uInt     avail_in;      /* 还有多少字节的数据需要解压 */
    uLong    total_in;      /* 已经处理的数据大小(原始压缩数据) */

    Bytef    *next_out;     /* 存储已解压数据的位置 */
    uInt     avail_out;     /* 还可以存储多少字节的已解压数据 */
    uLong    total_out;     /* 已经处理的数据大小(已解压数据) */

    z_const char *msg;
    struct internal_state FAR *state;

    alloc_func zalloc;
    free_func  zfree; 
    voidpf     opaque;

    int     data_type;
    uLong   adler;
    uLong   reserved;
} z_stream;

在调用 zlib 的inflate 解压数据时,z_stream 的成员都会进行相应的更新,这里着重观察 total_out 成员。

int ZEXPORT inflate(z_streamp strm, int flush)
{
    // ......
    in -= strm->avail_in;       // 处理了多少压缩数据
    out -= strm->avail_out;     // 数据解压缩后的大小
    strm->total_in += in;       // 记录
    strm->total_out += out;     // 记录
    // ......
}

2.4 FlateUncompress 分析

为了提高代码的可读性,这里删除了 FlateUncompress中无关紧要的一些代码。

下面的代码展示了数据的解压过程,可以看出数据是分块进行解压的,且解压的结果存储在 result_tmp_bufs 中。

 // 初始化
  void* context = FPDFAPI_FlateInit(my_alloc_func, my_free_func);

  // 设置输入数据 (待解压数据)
  FPDFAPI_FlateInput(context, src_buf, src_size);

  // 对输入数据分块进行解压
  std::vector<uint8_t*> result_tmp_bufs;
  uint8_t* cur_buf = guess_buf.release();
  while (1) {
    // 调用 inflate 解压一块数据
    int32_t ret = FPDFAPI_FlateOutput(context, cur_buf, buf_size);
    // cur_buf 的剩余存储空间
    int32_t avail_buf_size = FPDFAPI_FlateGetAvailOut(context);

    // 解压出错
    if (ret != Z_OK) {
      last_buf_size = buf_size - avail_buf_size;
      result_tmp_bufs.push_back(cur_buf);
      break;
    }

    // cur_buf 还有剩余空间, 说明解压完毕
    if (avail_buf_size != 0) {
      last_buf_size = buf_size - avail_buf_size;
      result_tmp_bufs.push_back(cur_buf);
      break;
    }

    // 存储当前解压结果, 并为下一次解压做准备
    result_tmp_bufs.push_back(cur_buf);
    cur_buf = FX_Alloc(uint8_t, buf_size + 1);
    cur_buf[buf_size] = '\0';
  }

因为数据是分块进行解压的,所以解压完之后需要进行拼接操作。然而 FPDFAPI_FlateGetTotalOut返回类型为int,且dest_size 的类型为uint32,所以会发生截断,后面 FX_Alloc(uint8_t, dest_size)分配的堆块也无法存储全部解压数据。

  // 解压后总的数据大小
  dest_size = FPDFAPI_FlateGetTotalOut(context);

  if (result_tmp_bufs.size() == 1) {
    // 仅有一块数据
    dest_buf = result_tmp_bufs[0];
  } else {
    // 存在多块数据
    // 根据 dest_size 分块堆块
    uint8_t* result_buf = FX_Alloc(uint8_t, dest_size);
    uint32_t result_pos = 0;
    // 拷贝数据
    for (size_t i = 0; i < result_tmp_bufs.size(); i++) {
      uint8_t* tmp_buf = result_tmp_bufs[i];
      uint32_t tmp_buf_size = buf_size;
      if (i == result_tmp_bufs.size() - 1) {
        tmp_buf_size = last_buf_size;
      }
      // Crash
      FXSYS_memcpy(result_buf + result_pos, tmp_buf, tmp_buf_size);
      result_pos += tmp_buf_size;
      FX_Free(result_tmp_bufs[i]);
    }
    dest_buf = result_buf;
  }

2.5 gdb 调试

FlateUncompress下断点,可以看到待解压的数据以及大小(前面提到过大小为 0x3FB2B2):

(gdb) b (anonymous namespace)::FlateUncompress
(gdb) r crash.pdf

Breakpoint 1, (anonymous namespace)::FlateUncompress (
    src_buf=0xa672b0 "x\234\354\301\201",   // 压缩数据
    src_size=4174514,                       // 数据大小
    orig_size=0, 
    dest_buf=@0x7fffffffd350: 0x0, 
    dest_size=@0x7fffffffd2f0: 4294967295, 
    offset=@0x7fffffffd184: 0)
    at ../../core/fxcodec/codec/fx_codec_flate.cpp:508

(gdb) p /x src_size
$1 = 0x3fb2b2

(gdb) x /40xb src_buf
0xa672b0:    0x78    0x9c    0xec    0xc1    0x81    0x00    0x00    0x00
0xa672b8:    0x00    0x80    0x20    0xd6    0xfd    0x25    0x16    0xa9
0xa672c0:    0x0a    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0xa672c8:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00
0xa672d0:    0x00    0x00    0x00    0x00    0x00    0x00    0x00    0x00

在调用FPDFAPI_FlateGetTotalOut 函数所在的行(第 590 行)下断点,可以看到 total_out 的值为 0x100000000,当赋值给 uint32 时会截断为 0

(gdb) b 590
Breakpoint 2 at 0x5de5a1: 
    file core/fxcodec/codec/fx_codec_flate.cpp, line 590.

(gdb) c
Continuing.
Breakpoint 2, (anonymous namespace)::FlateUncompress ...

(gdb) p /x context
$2 = 0xe625f0

(gdb) x /20xw content
No symbol "content" in current context.
(gdb) x /20xw 0xe625f0
0xe625f0:    0x00e62562    0x00000000    0x00000000    0x00000000
0xe62600:    0x003fb2b2    0x00000000    0xf68d5d48    0x00007ffe
0xe62610:    0x0048f82c    0x00000000    [0x00000000    0x00000001]    // total_out
0xe62620:    0x00000000    0x00000000    0x00e62670    0x00000000
0xe62630:    0x005dcca0    0x00000000    0x005dccca    0x00000000

(gdb) s
FPDFAPI_FlateGetTotalOut (context=0xe625f0) at 
    core/fxcodec/codec/fx_codec_flate.cpp:29
29      return ((z_stream*)context)->total_out;

(gdb) p /x ((z_stream*)context)->total_out
$3 = 0x100000000

2.6 补丁分析

f6d0146 对相关的代码进行了 Patch 以防止出现 Heap Overflow ,但是新的代码仍然无法处理 4GB 以上的数据,如下所示:

@@ -594,14 +598,17 @@
     } else {
       uint8_t* result_buf = FX_Alloc(uint8_t, dest_size);
       uint32_t result_pos = 0;
+      uint32_t remaining = dest_size;
       for (size_t i = 0; i < result_tmp_bufs.size(); i++) {
         uint8_t* tmp_buf = result_tmp_bufs[i];
         uint32_t tmp_buf_size = buf_size;
         if (i == result_tmp_bufs.size() - 1) {
           tmp_buf_size = last_buf_size;
         }
-        FXSYS_memcpy(result_buf + result_pos, tmp_buf, tmp_buf_size);
-        result_pos += tmp_buf_size;
+        uint32_t cp_size = std::min(tmp_buf_size, remaining);
+        FXSYS_memcpy(result_buf + result_pos, tmp_buf, cp_size);
+        result_pos += cp_size;
+        remaining -= cp_size;
         FX_Free(result_tmp_bufs[i]);
       }

后续相关的 commit 对这一段代码进行了清理,如 7b8e8c1e8c39

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 一个 PC上的 “ WormHole ” 漏洞

    最近安全界关注的焦点 WormHole 实际是一类不安全的开发习惯所导致的,在 PC 上类似问题也毫不罕见,只不过很多风险被微软默认自带的防火墙缓解了。联想公司...

    腾讯玄武实验室
  • 使用 python 自动化分析 CrashDump

    本文介绍了一下自动化分析 CrashDump 的方法由于项目原因,需要批量分析CrashDump文件,正常的手动分析流程是:使用windbg载入CrashDum...

    腾讯玄武实验室
  • 四个字节的安全 :一次固件加密算法的逆向分析

    这篇文章源自我们的一个检测项目,项目中我们需要对设备的固件进行分析,在整个固件分析的过程中我们克服了很多困难,最后完整解密了设备固件的内容,这里将相关的内容做个...

    腾讯玄武实验室
  • js中匿名函数自调用

    * 全称: Immediately-Invoked Function Expression 立即调用函数表达式

    李才哥
  • 如何利用cBioPortal分析基因家族?

    cBioPortal数据库是探索肿瘤的基因组学特征,是从DNA水平进行的,是对机制的进一步研究。基因差异表达、生存分析和免疫浸润分析,上述分析严格意义上讲均属于...

    百味科研芝士
  • spring注解是如何实现的

    用过spring的人都知道,spring简单的通过注解就可以完成很多时间,但这些东西是如何实现的呢以及如何应用到我们自己的代码中?接下来,让我们一起开启注解的旅...

    shengjk1
  • 组学分析神器:cBioPortal

    cBioPortal网站目前存储DNA拷贝数数据(每个基因的假定,离散值,例如“深度缺失”或“扩增”,以及log2水平),mRNA和microRNA表达数据,非...

    芒果先生聊生信
  • 风控缺陷?支付宝曝“致命”漏洞,他人能改你的密码

    今天,支付宝被曝光“熟人可以修改登录密码”漏洞。 据说“陌生人有1/5的机会登录你的支付宝,而熟人甚至100%可以登录你的支付宝”,而且登录方式并没有什么技术含...

    FB客服
  • Clean Code之JavaScript代码示例

    作为一个开发者,如果你关心代码质量,除了需要认真测试代码能否正确执行以外,还要注重代码的整洁(clean code)。一个专业的开发者会从将来自己或则他人方便维...

    Fundebug
  • [Java小工匠]CSS背景1-概述

    background-image 属性为元素设置背景图像。 元素的背景占据了元素的全部尺寸,包括内边距和边框,但不包括外边距。 默认地,背景图像位于元素的左上角...

    Java小工匠

扫码关注云+社区

领取腾讯云代金券