wnagzihxa1n

wnagzihxa1n

iOS/Android Security

© 2021

浏览器安全周报 2019.10.21 - 2019.10.25

有段时间没有更新浏览器了

JS Fuzz,有点意思,我还没有看源码

  • https://github.com/fuzzitdev/jsfuzz

最近分析了CRBUG-944971,对应的CVE编号是CVE-2019-13698,正则模块的漏洞

  • https://bugs.chromium.org/p/chromium/issues/detail?id=944971

触发漏洞的原理很简单,根据报告者的Exp去找漏洞的触发路径很有意思

首先正则的Replace函数认为传入的是未经修改的对象,也就是说没有被重新定义过的正则对象,它会调用ToString(),这是一个可以在上层进行定义的函数

V8_WARN_UNUSED_RESULT MaybeHandle<String> RegExpReplace(Isolate* isolate, Handle<JSRegExp> regexp, Handle<String> string, Handle<Object> replace_obj) {
    // Functional fast-paths are dispatched directly by replace builtin.
    DCHECK(RegExpUtils::IsUnmodifiedRegExp(isolate, regexp));
    DCHECK(!replace_obj->IsCallable());

    Factory* factory = isolate->factory();

    const int flags = regexp->GetFlags();
    const bool global = (flags & JSRegExp::kGlobal) != 0;
    const bool sticky = (flags & JSRegExp::kSticky) != 0;

    Handle<String> replace;
    ASSIGN_RETURN_ON_EXCEPTION(isolate, replace, Object::ToString(isolate, replace_obj), String); <== 1
    replace = String::Flatten(isolate, replace);

官方给的回归测试文件可以看到就是在toString()里进行操作

let re = /x/y;
let cnt = 0;
let str = re[Symbol.replace]("x", {
    toString: () => {
        cnt++;
        if (cnt == 2) {
            re.lastIndex = {valueOf: () => {
                re.x = 42;
                return 0;
            }};
        }
        return 'y$';
    }
});
assertEquals("y$", str);

具体这个漏洞的利用细节我会单独写一篇文章分析,周报里简单讲讲,首先我们使用下面的代码测试一个特性

var pattern = new RegExp("AA","g");
%DebugPrint(pattern);

pattern.__defineGetter__('x', ()=>2);
%DebugPrint(pattern);

pattern.__defineGetter__('x', ()=>2);
%DebugPrint(pattern);

while(true);

%DebugPrint是V8调试版本下一个用于打印对象的接口,输出的数据我简化一下,第二次__defineGetter__执行完后,可以看到lastIndex被移动到properties存储,同时对象长度减少8字节

DebugPrint: 0x1fb102d4dcc1: [JSRegExp]
 - map: 0x1d23bf341359 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - properties: 0x1878b9400c71 <FixedArray[0]> {
    #lastIndex: 0 (data field 0)
 }
0x1d23bf341359: [Map]
 - type: JS_REGEXP_TYPE
 - instance size: 56

------------------------------------------------------------

DebugPrint: 0x1fb102d4dcc1: [JSRegExp]
 - map: 0x1d23bf34a9f9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - properties: 0x1878b9400c71 <FixedArray[0]> {
    #lastIndex: 0 (data field 0)
    #x: 0x2d807e95f4c1 <AccessorPair> (const accessor descriptor)
 }
0x1d23bf34a9f9: [Map]
 - type: JS_REGEXP_TYPE
 - instance size: 56

------------------------------------------------------------

DebugPrint: 0x1fb102d4dcc1: [JSRegExp]
 - map: 0x1d23bf34aa49 <Map(HOLEY_ELEMENTS)> [DictionaryProperties]
 - properties: 0x1fb102d4f631 <NameDictionary[29]> {
   #lastIndex: 0 (data, dict_index: 1, attrs: [W__])
   #x: 0x2d807e95f4d9 <AccessorPair> (accessor, dict_index: 2, attrs: [WEC])
 }
0x1d23bf34aa49: [Map]
 - type: JS_REGEXP_TYPE
 - instance size: 48

记住上面的这点,我们来看内存角度

var pattern = new RegExp("AA","g");
gdb-peda$ x/20gx 0x99f7870dcd8
0x99f7870dcd8:	0x000030b004981359	0x00003df557b80c71
0x99f7870dce8:	0x00003df557b80c71	0x0000099f7870f561
0x99f7870dcf8:	0x000000a31509f099	0x0000000100000000
0x99f7870dd08:	0x0000000000000000	0x00003df557b808a1

以下因为重新运行导致内存地址变化,问题不大,两次执行过后可以看到核心的地方出现了,注意箭头指向的地方,原先是`0`,现在变成了一个地址
pattern.__defineGetter__('x', ()=>2);
pattern.__defineGetter__('x', ()=>2);
gdb-peda$ x/20gx 0x30e12f60dce0
0x30e12f60dce0:	0x0000365cc160aa49	0x000030e12f60f651
0x30e12f60dcf0:	0x00003cb956540c71	0x000030e12f60f569
0x30e12f60dd00:	0x00000d083935f099	0x0000000100000000
0x30e12f60dd10:	0x00003cb956540321 <==	0x00003cb9565408a1
0x30e12f60dd20:	0x0000018300000000	0x0000000100000000
0x30e12f60dd30:	0x0000000000000000	0x0000008000000000
0x30e12f60dd40:	0x00003cb9565404d1	0x00003cb9565404d1
0x30e12f60dd50:	0x00003cb9565404d1	0x00003cb9565404d1
0x30e12f60dd60:	0x00003cb9565404d1	0x00003cb9565404d1
0x30e12f60dd70:	0x00003cb9565404d1	0x00003cb9565404d1

我们通过job命令查看,它是一个对象的Map,也就是说,这里是一个新的对象内存空间

gdb-peda$ job 0x00003cb956540321
0x3cb956540321: [Map]
 - type: FILLER_TYPE
 - instance size: 8
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x3cb9565404d1 <undefined>
 - prototype_validity cell: 0
 - instance descriptors (own) #0: 0x3cb956540259 <DescriptorArray[0]>
 - layout descriptor: (nil)
 - prototype: 0x3cb9565401d9 <null>
 - constructor: 0x3cb9565401d9 <null>
 - dependent code: 0x3cb9565402c1 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

这个特性有什么用呢?

我们再回到Replace函数里,它竟然可以直接设置这个lastIndex位,也就是说,在Replace里处理的时候,可以通过toString()修改掉这个值,而修改的时候,原先的lastIndex已经变成了另一个对象的Map,这就造成了一个越界写操作,sticky对应的y,比如"AA/y"

if (match_indices_obj->IsNull(isolate)) {
    if (sticky) regexp->set_last_index(Smi::kZero, SKIP_WRITE_BARRIER);
        return string;
}

auto match_indices = Handle<RegExpMatchInfo>::cast(match_indices_obj);

const int start_index = match_indices->Capture(0);
const int end_index = match_indices->Capture(1);

if (sticky) {
    regexp->set_last_index(Smi::FromInt(end_index), SKIP_WRITE_BARRIER);
}

参加了今年的数字校招面试工作,一共是四批,第一批是内推,剩下三批是大流程,内推的好处就是万一挂了可以参加大流程,完全不会有影响,但是大流程只能参加一次

整个部门的候选人简历一眼刷下来几乎全是硕士学历,本科生屈指可数,全公司的比例情况我不清楚,没有看到表格

感慨还好毕业的早,不然今年可能真的找不到工作了

记得17年的春招,我在寝室里参加了数字的春招视频面试,大早上有个人打电话给我问我能不能参加面试,然后我就早早地在那里准备了,然后时间一到就有面试官Call我,一早上三面聊得很快,一面工程师,二面leader,三面HR

另外,今年的秋招差不多结束了,春招的时候大家投简历请踊跃一点,不要太羞涩了,有坑,缺人!需要内推360的后台联系我啊!!!!!!