11. 一石多鸟——击溃全线移动平台浏览器

看雪编辑按:作为通向互联网世界的一扇窗,浏览器早已融入了我们的日常生活。然而近年来针对浏览器的攻击却不断增长,方式也更加多元化,一旦这方面的利用技术被意图不轨者掌握,那么后果会非常严重。另一方面,企业的业务很大程度上也依托于互联网来完成。因此,如何防止浏览器被攻击者利用成了安全人员不可回避的问题,同时也是不可推卸的责任。本次看雪安全开发者峰会,陈佳林从  Pegasus  间谍软件披露的漏洞开始谈起,由一个具体的  WebKit  中  JavaScript  内核漏洞出发,详细阐述了漏洞的成因、内核堆风水、垃圾回收及  JIT  编译机制,并通过综合利用地址泄漏、对象伪造、指针劫持、堆喷和  ROP  等技巧最终实现了任意代码执行,为我们展示了一套完整的浏览器漏洞利用过程。

以下内容为陈佳林演讲实录,由看雪学院(微信公众号:ikanxue)整理。


陈佳林

看雪ID:Roysue,看雪iOS版块版主,iOS独立安全研究员,iOS系列书籍作者。《iOS黑客养成笔记:数据挖掘与提权基础》电子工业出版社今年11月出版。《越狱!越狱!》正在撰写中,预计明年6月上市。

从小酷爱计算机,在网络空间和单机游戏中度过青少年生涯;大学没能读到计算机是最大的遗憾,却依旧戒不掉对计算机的这份酷爱,长时间浸淫在国内外各大论坛和技术书籍之中,心之所向,身之所往!



       首先聊一下为什么叫做“一石多鸟”,标题主要是受长亭科技在今年的BlackHat 2017上发表的《一石多鸟——击溃全线浏览器》所启发,他们讲的是挖到了一个sqlite的漏洞,这个sqlite可不简单,所有的浏览器、常用的App,基本上全部使用到了这款小巧精悍的微型数据库,这个情况就跟OpenSSL库出问题是一样的,所有的https都是用到了SSL协议,像去年的“心脏滴血”事件,在这种底层库这样的地基出现问题的情况下,所有上层协议、模块和产品的安全荡然无存,互联网上几乎百分百的https网站都要受到影响。Sqlite出问题的话,攻破所有的浏览器和网站也是轻而易举,我们这次所要研究的,正是另外一项移动平台浏览器的“基础建筑”——JavaScript的解释引擎:JavaScriptCore。


       首先自我介绍一下,我是看雪论坛iOS板块的版主,我叫陈佳林。刚刚听了很多大神的演讲,心之所向,身之所往,想要更多地与大神进行交流,我们看雪就是这样的一个平台。演讲的时间很短,但是日常的工作和学习生活很长,而看雪论坛是经历了十七年风风雨雨为大家服务的论坛,我们都在这里,等待你来相聚,希望大家在论坛里多多发帖,学习知识和分享经验。


       我的新书《iOS黑客指南:数据挖掘和提权基础》将在今年年内出版,书中具体介绍了逆向的一般方法论及iOS逆向入门的方方面面,也是我个人在学习和成长过程中的经验和技巧的总结,希望大家可以喜欢。



“炙热”的“大”前端


       我们先从目录开始说起,首先是“炙热”的“大”前端,前端的火热程度想必在座的各位都是非常感同身受的。



       “全栈”语言——JavaScript,作为大前端的基础组件,如果没有JavaScript,那么所有的网页都“动”不起来,并且无法与用户进行交互,JavaScript一开始只是运行在客户端的浏览器中,现在也可以运行在服务器上,作为Server与数据库、HTTP、Router等协议进行沟通,因此称为“全栈”语言。


       我们来介绍这次分享的主角——Webkit及其组件JavaScriptCore中存在的一个漏洞,介绍如分析、调试、和利用这个漏洞,完成我们任意代码执行,弹出计算器的目的。



       “大”前端有多“炙热”,去年前年就已经出现产品经理不再雇佣安卓或者iOS程序员的倾向,做宣传推广也好、做活动也好,都以H5作为主要的传播介质,不知道大家有没有发现,但凡是一些“现象级”、“病毒式”的营销,大多是由一个H5页面作为“PoC”的概念验证,造成疯狂传播的现象。


       产品经理非常重视这一点,他们会要求链接在微信里一点就可以打开,一点就能看,比如前年去年很火的直播,如果还要点开再点开,再下载App,这个转化率就非常低了,大多数产品经理都会要求“点开就能看”,什么广告呀、条幅呀都跟着弹出来,至少可以马上养活这个平台,像“点开点开再下载App”这个套路用户就会迅速损失一大批,这对于直播平台来说是无法接受的。


       基于H5能做的事情还有很多,覆盖了日常生活中的方方面面,包括朋友圈、各种公众号,公众号已经事实上成为了众多新媒体·自媒体的主战场,“10万+”也已经成为一个专有名词,在如此巨大的装机量和传播能级下,没有哪一个商家、甚至是个人,能够忽略这一块。


       H5仅仅是一个网页,JavaScript在其中发挥的作用与普通浏览器并无二致。功能更加强大的“小程序”与今年年初正式发布,发布初期很多人并没有找到其正确的玩法,或者说没能理解到其精髓,小程序在发布之初并没有爆发,而是循序渐进、稳扎稳打、暗中渗透。



       时至今日,我们的日常生活又被小程序给包围了,不管走到哪里,“红包店”遍地都是,现在没有红包店的接入的店铺已经很少了,买个战斗鸡排都可以抢红包,更不提肯德基、全家这些“重量级”玩家了。



       然后就是一些稍微“中度”、“重度”的应用,移动支付自不必说,成为中国的“新四大发明”。



       共享单车也是其中之一,饿了么的客户端我已经卸载了,用小程序足矣,腾讯企业邮等等,这些产品原先自己都是单独的一个App,现在都以“寄居”的方式,生存在微信、支付宝、淘宝等App中。


       小程序虽然表面上采用的是腾讯独立开发的wx接口和MINA框架,但是其底层调用的还是iOS或者安卓的Native的JavaScript解析引擎,比较直白的一个例子就是每当Chomium发布漏洞披露的时候,腾讯应急中心也会发布相应的版本更新,因为用户时常不更新自己的安卓系统,微信必须做到规避漏洞带来的影响。当然这种现在在iOS上会好很多,新版系统的更新频率远远大于安卓系统。


“全栈”语言:JavaScript


        JavaScript其诞生之初,仅仅是Netscape(网景),也就是火狐的前身,为了让单调的网页活跃一点,动起来,而在其浏览器上加入的一种动态网页技术。


        当这项技术越来越流行越来越广泛的时候,为了保护JavaScript不被微软抢走,因为微软是有名的把别人的idea抄过来,自己做一个来代替他的copycat。事实上他们也这么做了,同样这么做了,微软抄袭之后推出了Jscript,Macromedia公司(后被Adobe收购)推出了ActionScript,Netscape为了保护JavaScript,将其提交给欧洲计算机制造商协会ECMA。


        ECMA在博取众长的基础上,推出了ECMAScript标准,这只是一项标准,大家可以采用也可以不采用,这就是为什么IE会成为很多前端程序员的噩梦的原因,IE总是“特立独行”,或者干脆不兼容,有效地“狙击”了JavaScript的发展。


       当时间进入到2010年往后,ECMA 5、6、7甚至8的发布,大家还是很给面子的,基本上做到了JavaScript这门语言的统一,尤其是谷歌在推出开源的v8解析引擎之后,在nodejs的帮助下,JavaScript这门语言完成了在客户端上执行代码,到在服务器上执行代码的蜕变,JavaScript成为了“全栈”语言。



       目前开源世界主流的三大网页浏览器是Chrome、Mozilla、和Safari。Mozilla自然其本身就是JavaScript的发明者,其网页渲染引擎叫做Gecko,JavaScript解析引擎叫做SpinderMonkey。Safari的网页渲染引擎是Webkit,网页解析引擎叫做JavaScriptCore,这也是我们今天的主角。


       Chrome的Blink渲染引擎最初脱胎于Webkit,后来Chrome为了更加激进地提高JavaScript的性能,另起炉灶,牟足了劲把所有黑科技都给安上,推出了完全性能优先的v8引擎,这也是为什么刚刚使用Chrome时,能够体验到无与伦比的速度的原因,但是其缺陷也很明显,Chrome带来的开销和占用太大了,老电脑根本不敢开,几个页面就可以把性能全部吃光。


       今天的主角是聊JavaScriptCore,也就是Webkit的JavaScript解析引擎,Webkit是老牌的网页渲染引擎了,距今已经有十多年的历史,而且只要是与苹果相关的平台,都是强制使用Webkit渲染引擎的,比如说哪怕即使是Chrome,iOS版本的Chrome和Firefox其内核都是Webkit,因为苹果不允许其使用其自己的内容,比较iOS是“我的地盘我做主”嘛。当然安卓和PC平台苹果就管不到了。


主角:Webkit



       Webkit由于其老牌,并且脱胎于KHTML、GNOME等linux社区的特性,收到了移动设备厂商的广泛欢迎,比如我们的游戏机PlayStation、任天堂Nintendo,都是采用的Webkit内核加个自己的皮肤就拿出来当做默认浏览器了,安卓在4.3之前其WebView也是采用的Webkit内核,因为大家知道Android这个项目是谷歌买过来的,要知道鲁宾领导的安卓团队可是诞生于2003年,那时候并没有什么更好的选择。


       后来谷歌逐渐收紧对安卓的控制,一切以Chromium开源项目为基础,以v8引擎为核心,打造超高速度的WebView排版引擎。最后,Webkit在PC平台上当然也是全平台的,macOS自不必说,Linux和Windows上都有其相对应的实现和产品。


       关键是在移动平台上,Webkit占据了全部浏览器和App市场的“半壁江山”,而移动平台在今年双十一中,占据了92%的市场份额,这就很能说明问题,Webkit漏洞的影响能力,究竟可以大到什么程度。



       iOS/macOS设备越狱的典型流程,利用Webkit漏洞构造任意代码执行,再利用其XNU内核漏洞打穿内核,做好持久化之后越狱成功!


PlayStation、Nintendo设备破解的典型流程,利用Webkit漏洞构造任意代码执行,再利用FreeBSD漏洞打穿内核,持久化之后破解成功!


       安卓也是一样的道理,安卓由于其基于规则的权限管理机制,跟Windows非常像,如果只要获取数据的话,在网页挂马就可以了,都不需要给设备进行root,就可以获得全套的敏感资料。


       现在的很多Pwn类型的比赛中,获取Webkit漏洞都是第一步,以Webkit漏洞为跳板,只要访问一个他们提供的网站,先打穿Webkit,然后打穿内核,获取整个系统最高权限,最后弹计算,越狱是一模一样的流程。



       去年有一个三叉戟漏洞就是非常典型的一个案例,其首先利用CVE-2016-4657的Webkit漏洞达到任意代码执行,然后综合利用XNU的两个漏洞在后台“帮”机主静默越狱,全部过程机主完全不知情,越狱完成之后再“为”用户装上全套监听软件;


       今年的Pwn2Own2017上爆出的CVE-2017-2533也是从Webkit的一个特殊的漏洞构造一个符号链接开始引爆攻击链,最终完成的全套内核提权操作。



       其作者Saelo去年曾经在Phrack上放出了一整套Webkit的JavaScriptCore引擎漏洞利用思路,介绍的非常详细,在这里我们对这个经典案例进行学习,来掌握一整套漏洞利用的经典技巧,并最终给它“安装”上Shellcode,在macOS平台上弹出计算器。


CVE-2016-4622



       接下来我们来看一下什么是JavaScript引擎?JavaScript引擎是专门处理JavaScript脚本并执行的虚拟机,一般会附带在网页浏览器之中。第一个JavaScript引擎由布兰登·艾克在网景公司开发,用于Netscape Navigator网页浏览器中。JavaScriptCore就是一个JavaScript引擎。


       一个JavaScript引擎不外乎包括以下几个部分:

1. 编译器:将源代码编译成为抽象语法树及字节码;

2. 解释器:主要是接受字节码并且解释执行;

3. JIT工具:可以将字节码或者语法树转化为本地机器码的工具;

4. 垃圾回收器和其余配件:符合内存和堆中的垃圾回收,帮助改进引擎的性能和功效。


       接下来我们来看看这个漏洞,由于JavaScriptCore的源代码是开源的,我们可以git clone到本地,然后逐行阅读和确认漏洞到底问题出在哪里。



       该漏洞于2016年初被`yours truly`团队发现,并且上报给ZDI团队(番号ZDI-16-485),利用该漏洞可以导致JavaScript对象地址泄漏以及伪造。结合一些其他的提权手段的话,攻击者可以做到浏览器中的任意代码执行。


       这个bug的`650552a`的git提交版本中被修复,下文中引用的代码来自于320b1fc版本,也就是最后一个受影响的版本,也是前文我们一直在讨论和实验的版本。这个漏洞是在2fa4973版本中引入的,历时一年之久才被发现和修复,分配的CVE编码为CVE-2016-4622。


       该漏洞主要存在于切片函数slice()之中。查询该函数引用关系可知该函数位于Array.prototype.slice()的实现过程中,然后我们来到源代码的相关位置,也就是./Source/JavaScriptCore/runtime/ArrayPrototype.cpp:848行的位置。


       在执行切片的时候,有两种方式。


       如果数组是密集存储(FastPath)的本地数组,则使用快速切片——`fastSlice`,使用给定的索引和长度将内存值直接写入新的数组。


       如果不是,则取不到快速路径,这时候只有使用简单的循环来获取每个元素并将其添加到新的数组。



       然后,在第二种较慢的方法中,属性检查会检查数组的边界,而快速切片中却没有进行边界检查...这就有意思了,我们暂且不深入,先往下看,`result = constructEmptyArray(exec, nullptr, end - begin);`这行代码在构造空的result数组的时候,用`end - begin`来的大小来构建数组的长度么?


       那我们用脚趾头都能猜到程序猿的“臆测”绝对是这个差值肯定是小于原数组的长度的咯?对呀,切片之后的数组当然比切片之前的短的咯?然而,我们可以在JavaScript的类型转化过程中耍一些手段,来毁掉程序猿的美梦,我们继续往下看。



       上文代码中,在执行具体切片的前方,有一个argumentClampedIndexFromStartOrEnd 函数来决定end的大小,接受的参数是数组原先的长度length。大家也看到了,所有地方都不会对新的`length`进行检查。


       综上所述,在PoC代码中的这句话:`var b = a.slice(0, {valueOf: function() { a.length = 0; return 10; }});`,如果我们在valueOf方法的内部修改数组的长度,slice()函数却会依旧使用之前的长度,导致指针访问到了本不该它能访问的内容,导致内存越界读。


       在完美利用这一点之前,我们还必须保证数组确实是可以被缩小的。怎样来缩小数组呢?使用数组的.length属性就好了,我们来看下`.length`方法的实现,在`JSArray::setLength`中(`./Source/JavaScriptCore/Runtime/JSArray.cpp`文件的第431行):



       为了节省开销,数组缩小的元素个数小于64,系统就不执行缩小了,折腾一下带来的性能损耗还不如不节约呢。也就是说我们得“显著”地缩小一下,系统才愿意为我们折腾一下。


       这里我们进行一次从100个元素缩小到10个元素的实验,用Array.prototype.slice来切割数组,同时在内部修改其长度。再来看一眼PoC.js,PoC先使用setLength来缩小数组的长度,再使用ValueOf来返回固定的10,而在执行slice()方法的过程中不会再检查a数组的长度,因此取回的结果b,已经不再是a数组里的数字了,因为a数组早就被置0了。



        取回的结果应该是10个**undefined**数据才对,但是最终结果却是一些浮点数,貌似我们的指针访问到了一些,本不该我们可以访问到的东西。这个东西是什么呢?其实就是内存中紧靠着它的下一个对象的地址。接下来我们只要在这个位置进行构造一个对象,就可以通过内存越界读直接获取到它的地址,如果构造的非常完美,通过这个对象来获取任意地址读写的能力。


Fake Object Injection



       在构造对象的过程中有几个难点,最为困难的是伪造SturctureID这个部分,我们就需要知道结构表中Float64Array的正确的结构ID。


       所以首先是预测结构ID,而结构ID是运行时动态产生和分配的,随着各个引擎、版本的不同,结构ID也会改变。而且更要命的是,我们也不能随便填个ID上去,因为ID有可能是字符串的、符号链接的、各种对象的,甚至结构ID自己的。


       在MethodTable里调用方法的时候,ID不对,JavaScriptCore就Crash了。这些ID是在引擎启动的时候动态确定的,也就是说他们都会有ID,但是你就是不知道哪个是哪个的,总不能把受害人的电脑抢过来看一下吧。


       为了解决这个问题,我们想了个办法,结合堆喷和instanceof()函数,来猜测一下结构ID。我们在堆里喷几千个Float64Array对象,带着不同的结构ID。然后挨个检查下这个对象是否被引擎识别为Float64Array对象,如果是,那么它的结构ID肯定就是对的了。



       用instanceof()函数来判断对象是否是Float64Array,如果不是,下一个再上。Instanceof()是一个非常安全的函数,它只检查从结构ID里抽出属性来和默认的对比一下,其他没有任何操作。



       找到正确的结构ID之后,终于可以放心大胆的开始伪造对象以劫持指针(弄虚作假)了。然后要过垃圾回收也有点小小的问题,我们又采用了一点小小的手段,由于时间的关系,这里没办法做过多的深入,欢迎大家到bbs来参与学习和讨论。


JIT Function Overwrite


       在得到任意地址读写的能力之后,我们使用覆盖JIT编译之后的函数的方式,来触发我们的shellcode执行。由于现代浏览器全都采用JIT进行即时编译,这就意味着把指令写到内存、然后去执行这块指令,也就是说这块内存是可读也可写的,这就给我们运行shellcode帮了大忙。我们使用刚刚讲完的任意地址内存读写功能来找到一块经过JIT引擎编译之后的JavaScript函数的地址,然后使用shellcode来覆盖这块地址,最终调用这个函数的时候,执行的就是我们的shellcode。请看下面的这段代码。



Arbitrary Code Execution


        shellcode是从hacking team泄露的网络军火库中扒下来的。



       给Expolit装上之后运行,立刻就弹出计算器了。



       当然,这才是通向内核的第一步,接下来我们只要手中拥有内核的漏洞,就可以进行内核的破解了,还有一个好消息就是,macOS/iOS的内核XNU也都是开源的。



这次的分享就到这里,谢谢大家!