字符串类型混淆漏洞的研究

1. 简介

  • 现在几乎已经很少会看到关于恶意字符串所造成的漏洞了,更不用说那些可利用的字符串了
  • 这并不奇怪,因为SDL(安全开发生命周期)已经禁止了所有不安全的函数
  • 但是,如果开发人员错误地使用了增强的安全功能,仍然可能造成严重的安全漏洞。
  • 在adobe acrobat Reader DC中,开发人员已经实现了一些增强的安全字符串处理函数。但开发人员有可能会错误使用这些函数。
  • 在一般情况下,这不是什么大问题。然而,在这个特定的软件中处理字符串时,类型混淆的情况也很容易被触发。
  • 在某些场景中,可以利用这两个条件来实现代码执行。

2. 基本概念

Adobe Acrobat Reader DC使用SpiderMonkey(可能是24.2.0版本)作为其JavaScript引擎。

a. 字符串类型

  • 字符串在Windows上可以分为两类: ANSI字符串和Unicode字符串
  • ANSI字符串由一系列ANSI字符组成,其中每个字符编码为8位值;
    Unicode字符串由一系列Unicode字符组成。
  • Windows主要使用UTF-16编码的Unicode字符,其中每个字符编码为16位值。
  • ANSI字符串的结束符是\x00, Unicode字符串的结束符是\x00\x00

b. Byte Order Mark (BOM)

  • 如果一个字符有多个字节的数据,那么它可以用两种形式表示: 大端序(little-endian)和 小端序 (big-endian)
  • 大端序 将最重要字节放在前面,最不重要字节放在最后,而 小端序 顺序则相反
  • 例子
    character UTF-16 Encoding Little-Endian Big-Endian
    U + 4E2D 2D 4E 4E 2D
    U + 6587 87 65 65 87
  • 对于UTF-16字符串,可以在字符集名称中指定字节顺序。例如,UTF16LE表示字节顺序是little-endian,而UTF-16BE表示字节顺序是big-endian
  • 我们还可以使用字节顺序标记(byte order mark, BOM)字符U+FEFF来指定字符串的字节顺序
  • BOM字符本身的字节顺序表示整个字符串的字节顺序。如果没有显式地指定,字符串的字节顺序可以是特定于平台的
  • 例子
    UTF-16 String Little-Endian Big-Endian
    中文 FF FE 2D 4E 87 65 FE FF 4E 2D 65 87

注意:BOM字符将始终位于字符串的开头。把它放在别的地方是没有意义的。

  • 这张图片简单的概括了上面内容。注意:BOM字符不唯一。FE FFBOM字符,而图中的EF BB BF 也是BOM字符
    img

c. 一些字符串的处理函数

  • 这里只列举其中的一个函数:strncpy

  • 大部分认为strncpy会比strcpy更安全,原因是strcpy中的第三个参数指定了处理数据的大小

    1
    char *strncpy(char *destination, const char *source, size_t num);
  • 在大多数情况下,这看上去是正确的。但在处理一些特殊的情况时,它仍然很不安全。
    如果源字符串的长度等于或大于第三个参数num的值,则不会将在目标字符串的末尾添加NULL字符。
    而在处理没有终止符的字符串时,它将导致越界访问。以下是strncpy的源代码(来自glibc-2.23):

    1
    2
    3
    4
    5
    6
    7
    8
    char *
    STRNCPY (char *s1, const char *s2, size_t n)
    {
    size_t size = __strnlen (s2, n);
    if (size != n)
    memset (s1 + size, '\0', n - size);
    return memcpy (s1, s2, size);
    }
  • 这是为什么呢?原因是strncpy在设计时本来就不是strcpy的安全版本。

  • strncpy_s是微软实现的一个安全增强版本。在将内容复制到目标缓冲区时,这些函数总是确保可以追加终止null字符。否则,操作失败,将调用无效的参数处理程序。

  • 如果错误地使用了字符串处理函数的安全增强版本,则不能保证它们是安全的。例如,如果开发人员将错误的值作为目标缓冲区的大小,即使是函数strcpy_s也可能导致缓冲区溢出。

    1
    2
    3
    char src[32] = { "0123456789abcdef" };
    char dst[10] = { 0 };
    strcpy_s(dst, 0x7FFF /*dst_size*/, src);

3. 漏洞学习

* 前置说明

  • adobe acrobat reader DC实现了一些增强的安全字符串处理功能,例如以下部分函数:

    Generic API ANSI Version Unicode Version
    strnlen_safe ASstrnlen_safe miUCSStrlen_safe
    strncpy_safe ASstrncpy_safe miUCSStrncpy_safe
  • 在处理字符串时,通用api将检查字符串的类型并将请求重定向到相应的函数。下面的代码展示了函数strnlen_safe是如何工作的:

    1
    2
    3
    4
    5
    6
    7
    8
    unsigned int strnlen_safe(char *str, unsigned int max_bytes, void *error_handler) {
    unsigned int result;
    if (str && str[0] == 0xFE && str[1] == 0xFF)
    result = miUCSStrlen_safe(str, max_bytes, error_handler);
    else
    result = ASstrnlen_safe(str, max_bytes, error_handler);
    return result;
    }
  • 在当前情况下,函数根据字符串的前两个字节检查字符串的类型。如果第一个字节是0xFE,第二个字节是0xFF,则该字符串将被识别为Unicode字符串,否则将被识别为ANSI字符串。而实际上,FE FFBOM的大端序标记字符

  • 触发漏洞需要满足以下两个条件:

    • 检查字符串的类型时可能会引发类型混淆。如果前两个字节是FE FF, ANSI字符串可以被识别为Unicode字符串。这可能导致越界访问,因为在ANSI字符串中找不到Unicode null终止符。例如以下表格:

      char . . F a k e U n i c o d e .
      HEX FE FF 46 61 6B 65 20 55 6E 69 63 6F 64 65 00
    • 开发人员不正确地使用了通用api。在大多数情况下,目标缓冲区的大小将设置为0x7FFFFFFF。如之前所述的那样,这可能会导致安全问题

      1
      strnlen_safe(a2,  0x7FFFFFFF, 0)

a. CVE-2019-7032

1) 简介

  • CVE-2019-7032是一个越界读取漏洞,可以利用该漏洞实现绕过ASLR的信息公开。
  • 该漏洞在天府杯中,结合UAF漏洞可以进行代码执行。

注:CVE-2020-3804与该CVE漏洞原理与漏洞补丁等等大致相同,本文中将不再赘述。

2) 具体细节

  • 此漏洞会被以下JS代码所触发

    1
    2
    3
    // Tested on Adobe Acrobat Reader DC 2019.010.20069
    var f = this.addField('f1', 'text', 0, [1, 2, 3, 4]);
    f.userName = '\xFE\xFF';
  • ANSI字符串被作为Unicode处理时,越界读取将会被触发,原因是处理函数无法找到Unicode字符串的终结符\x00\x00
    以下为miUCSStrlen_safe函数源代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    unsigned int miUCSStrlen_safe(wchar_t *src, unsigned int max_bytes,
    void *error_handler) {
    unsigned int result;
    wchar_t *str = src;
    if ( src ) {
    unsigned int bytes = 0;
    if ( max_bytes ) {
    do {
    char ch = *(char *)str;
    ++str;
    if ( !ch && !*((char *)str - 1) ) break; // check Unicode terminator
    bytes += 2;
    } while ( bytes < max_bytes );
    }
    if ( bytes == max_bytes ) {
    void *handler = hb_set_invert;
    if ( error_handler ) handler = error_handler;
    handler(L"String greater than maxSize", L"miUCSStrlen_safe", 0, 0, 0);
    result = max_bytes;
    } else {
    result = bytes;
    }
    } else {
    void *handler = hb_set_invert;
    if ( error_handler ) handler = error_handler;
    handler(L"Bad parameter", L"miUCSStrlen_safe", 0, 0, 0);
    result = 0;
    }
    return result;
    }
  • 该漏洞是在为字段对象分配用户名属性时触发的。关键的部分是原始字符串将被复制到新创建的堆缓冲区中,该缓冲区将与属性相关联。
    这意味着我们可以通过JavaScript代码读取泄漏的信息。下面的代码显示了简化的漏洞模型。

    1
    2
    3
    4
    5
    6
    7
    // src <- field.userName <- "\xFE\xFF....."
    // len <- number of bytes
    size_t len = strnlen_safe(src, 0x7FFFFFFF, 0); // Out-Of-Bounds Read
    char* dst = calloc(1, aligned_size(len + 4));
    memcpy(dst, src, len); // Information Disclosure
    dst[len] = dst[len + 1] = '\0';
    // field.userName <- dst

3) 漏洞修补分析

  • 该漏洞已在adobe acrobat reader DC 2019.010.20091中修复。
  • 通过放置额外3个NULL字节(实际上4个)至堆缓冲区的末端。
  • 即便ANSI字符串是由函数miUCSStrlen_safe处理的,它也会正常工作,因为Unicode-NULL终止符总是可以被找到。

b. CVE-2019-8199

1) 简介

  • CVE-2019-8199是一个越界读写漏洞,可以利用它来实现代码执行。

2) 具体细节

  • 此漏洞会被以下JS代码触发

    1
    2
    // Tested on Adobe Acrobat Reader DC 2019.010.20099
    Collab.unregisterReview('\xFE\xFF');
  • 与上面的CVE相同,之所以可以触发越界读写,是因为处理ANSI字符串时无法找到Unicode终止符。
    以下是miUCSStrcpy_safe源代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    signed int miUCSStrcpy_safe(wchar_t *dst, unsigned int max_bytes,
    wchar_t *src, void *error_handler) {
    wchar_t *ptr = dst;
    if ( dst ) {
    if ( src ) {
    if ( max_bytes > 1 ) {
    unsigned int max_len = max_bytes >> 1;
    do {
    wchar_t e = *(wchar_t *)((char *)ptr + (char *)src - (char *)dst);
    *ptr = e;
    ++ptr;
    if ( !e ) break; // check Unicode terminator
    --max_len;
    } while ( max_len );
    if ( !max_len ) {
    *(ptr - 1) = 0;
    void *handler = Handler;
    if ( error_handler ) handler = error_handler;
    handler(L"Destination too small", L"miUCSStrcpy_safe", 0, 0, 0);
    }
    return 0;
    }
    } else if ( max_bytes > 1 ) {
    *dst = 0;
    }
    }
    void *handler = Handler;
    if ( error_handler ) handler = error_handler;
    handler(L"Bad parameter", L"miUCSStrcpy_safe", 0, 0, 0);
    return -1;
    }
  • 该漏洞在调用Collab.unregisterReview函数时被触发。关键的部分是原始字符串将被复制到新创建的堆缓冲区中,该缓冲区的大小是通过调用ASstrnlen_safe计算的。但是复制请求是由函数strcpy_safe处理的。下面的代码显示了简化的漏洞模型

    1
    2
    3
    4
    5
    // src <- arg of unregisterReview / unregisterApproval
    // src = "\xFE\xFF......"
    size_t len = ASstrnlen_safe(src, 0x7FFFFFFF, 0); // ANSI Function
    char* dst = (char *)malloc(len + 1);
    strcpy_safe(dst, 0x7FFFFFFF, src, 0); // Generic API -> Unicode Function
  • 所以,我们需要通过控制内存的布局来利用这个漏洞,以达到任意地址读写的目的

    ArrayBuffer对象在任意读写时起到了很大的作用。
    当该对象中的成员byteLength > 0x68时,该对象的后备存储将从系统堆中分配。
    堆内存分配时,所分配的缓冲区会比之前大0x10。这部分0x10大小的内存用于存储ObjectElements中的变量。

    • 新建大量的字符串和ArrayBuffer对象来占据内存。在这里我们创建了五个对象作为一个单元。

      1
      2
      3
      ┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐
      │ ArrayBuffer │ String │ ArrayBuffer │ ArrayBuffer │ ArrayBuffer │
      └─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘
    • 将第一个和第三个内存区域释放,以创建大量的内存空洞

      1
      2
      3
      ┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐
      │ Hole │ String │ Hole │ ArrayBuffer │ ArrayBuffer │
      └─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘
    • 使原始字符串的堆缓冲区分配到其中一个单元的第一个内存空洞,目标堆缓冲区分配到其中一个单元的第二个内存空洞。

      1
      2
      3
      ┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐
      │ Src │ String │ Dst │ ArrayBuffer │ ArrayBuffer │
      └─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘
    • 通过strcpy_safe函数漏洞的越界拷贝,覆盖第四个ArrayBuffer的成员byteLength0xFFFF
      其中,String的内容(每个单元中的第二个对象)将用于覆盖byteLength
      之后通过该单元中的第四个对象(被覆盖byteLength后的ArrayBuffer),修改单元中的第五个对象(也就是图中的最后一个ArrayBuffer)的byteLength0xFFFFFFFF,以取得全局读写权限。

      1
      2
      3
      4
      5
      6
      7
                           (2)byteLength to 0xFFFF             (4)Global Access
      ┌───>───>───>───>───>───>───┐ <──────┬──────>
      ┌─────────────┬┼────────────┬─────────────┬┼────────────┬──────┼──────┐
      │ Src │ String │ Dst │ ArrayBuffer │ ArrayBuffer │
      └──────┼──────┴─────────────┴┼────────────┴┼────────────┴┼────────────┘
      └──>───>───>───>────>─┘ └──>───>───>──┘
      (1)strcpy_safe (3)byteLength to 0xFFFFFFFF
  • 一旦获得了全局读写权限,我们就可以向后搜索来计算ArrayBuffer对象的后备存储缓冲区的基址,从而获得任意地址读写权限。

  • 一旦获得了任意读写权限后,就很容易实现代码执行。之后要做的就是下面这些操作,本文中不再赘述。

    • EIP劫持
    • ASLR绕过
    • DEB绕过
    • CFG绕过

3) 漏洞修补分析

  • 该漏洞已在adobe acrobat reader DC 2019.012.20034 版本中被修复。

  • 在堆缓冲区的末尾增加了两个额外的NULL字节(总共3个)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void *__cdecl sub_2383F4F8(int a1, int a2) {
    // --------------------------- cut ---------------------------
    if ( string ) {
    length = ASstrnlen_safe(string, 0x7FFFFFFFu, 0);
    if ( length < 0xFFFFFFFC ) {
    buffer = calloc(1, length + 3); // put 3 '\x00' at the end
    memcpy(buffer, string, length);
    }
    }
    // --------------------------- cut ---------------------------
    }
  • 这个补丁只能阻止漏洞被利用。POC仍然可能造成进程崩溃,因为如果目标堆缓冲区不够大,那么就无法存储Unicode字符串结束符。

    1
    2
    3
    4
    5
    // src <- arg of unregisterReview / unregisterApproval
    // src = "\xFE\xFF......"
    size_t len = ASstrnlen_safe(src, 0x7FFFFFFF, 0); // ANSI Function
    char* dst = (char *)malloc(len + 1); // only sufficient for ANSI string
    strcpy_safe(dst, 0x7FFFFFFF, src, 0); // Generic API -> Unicode Function
  • 该漏洞最终在adobe acrobat reader DC 2019.021.20047版本中得到修复。通过为目标堆缓冲区分配2个额外字节来存储Unicode字符串终止符,解决了这个问题。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void *__cdecl sub_2212A50B(char *src) {
    // --------------------------- cut ---------------------------
    signed int bytes = strnlen_safe(src, 0x7FFFFFFF, 0);
    void *dst = malloc(bytes + 2);
    memset(dst, 0, bytes + 2);
    strcpy_safe_wrapper(dst, src);
    // --------------------------- cut ---------------------------
    }
    int __cdecl strcpy_safe_wrapper(int dst, int src) {
    return strcpy_safe(dst, 0x7FFFFFFF, src, 0);
    }

白皮书后剩余的CVE-2020-3805由于原理与上述两个漏洞大致相同,故不再赘述。

4. 总结

  • 在这篇针对adobe acrobat Reader DC的漏洞分析中,大部分漏洞都是因为字符串类型混淆,导致ANSI字符串被 本用于处理Unicode字符串的函数 错误操作,其中以strnlen_safe为代表。
  • 在此类型漏洞中,因为在ANSI字符串中找不到Unicode字符串终结符,所以会造成越界 读/写,形成了一种漏洞模型,产生了4个CVE
  • 漏洞修补的方式也大同小异:在缓冲区末尾追加2个及以上的NULL字符,防止越界。

学习所使用的 白皮书PPT 如有需要请自取。议题的连接请点击这里

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2020-2021 Kiprey
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~