学习 | 从微信小程序看前端代码安全

-回复 -浏览
楼主 2018-07-07 06:54:32
举报 只看此人 收藏本贴 楼主


点击上方蓝字关注“公众号”


起初在研究对移动网络传输进行功耗优化,在一次意外的监听网络传输包中截获了微信小程序的请求包,借此来窥探当下前端代码安全。


0x01 小程序分析



小程序包结构



包结构非常清晰,分为三个部分:


头信息,包含一些包的标识,版本定义等,包含了三个冗余字段 --- 索引段和数据段的长度应该是用于做校验,但实质上没有用(设计者觉得需要设计一些冗余字段来确保设计的完整性,防止解析的时候溢出,但实际工程实践中并没有起到相应的作用),文件数量应该是用于简化包解析过程,实际上知道了索引段长度或数据段长度中任何一个皆可推算出文件数量。


索引段,包含文件的元信息 --- 文件名以及文件位置(通过FileOffset和FileSize定位数据段中的文件)。如果从精简包的大小的角度来看,FileOffset和FileSize只需一个存在即可,但是这样解析包的难度就大大增加了,还是以工程实践为主。

数据段,将所有的文件罗列在一起。


由此可见,数据完全没有经过压缩或者加密,连包的签名信息也没有。这导致只能在制品流程上进行严格控制,例如在开发者上传代码过程中需要授信,必须经过审查,也一定得由授信平台进行代码分发等。这些都无关风月,毕竟App Store就是这种模式,但是......


如何拆解这种自定义文件格式呢?

  1. 对多个相同格式的文件进行对比,对大体结构有宏观的感觉,很容易发现一些固定的字段以及一些结构的长度。对于像小程序这种有软件本体的例子,还可以通过微量修改来观察文件的变化来找到文件结构和意义。                                                                   

  2. 观察特殊形式,首先英文的字符串是很明显的,一般hex编辑器都自带字符串化窗口,如果发现常见的字符串,就可以继续去寻找字符串的边界,字符串在二进制文件里有两种储存方式:一种是不记录字符串的长度,读取字符串到0x00位置,另一种一定在某一个地方储存了这个字符串的长度,因此一旦得知了该字符串的内容,搜索该长度字段即可获取更多的信息。其次一些文件头也非常显眼,例如PNG[1]、ZIP[2]等通用标准文件格式都有固定的文件头,在小程序的自定义格式中很容易发现一些png、jpg等资源的文件头,因此可以定位数据区的位置。                                                                                     

  3. 对特定区域的二进制进行推理猜测,一般来说二进制文件里需要储存大量的offset和size的数据作为数据段的索引。offset相当于一个指针 - 索引文件在数据段的位置,工程实践中,大部分储存了offset的地方也会储存size字段,毕竟在解析文件的时候会方便很多,也可以防止校验数据出现指针越界。因此,一旦确认了文件中的数据段,就可以通过它的位置(offset)和大小(size)的实际数据进行搜索,逆向找到指向它的数据位置,并且继续逆向直到解析完整的文件。另外,如果要考虑设计的完备性,需要在二进制文件中加入一些冗余字段进行校验或者纠错,例如CheckSum、CRC32、Alder32、MD5、ECC等,这些通过hex编辑器很容易计算并发现。小程序中FileCount的字段,这完全是为了工程实践考虑的,在小程序中并没有出现这类的计算值,这是可能是因为小程序为了简单设计考虑,一旦发现包体被篡改或损坏就直接丢弃。


其实拆解小程序这种格式并不需要花费特别多的精力,因为其格式比较简单,而且从下图流程上来说,后缀为wx的二进制格式很可能与wxapkg格式是同源的。



开发者工具上传服务器分发原始代码后缀为wx代码包处理为wxapkg格式包体客户端

从开发者工具的代码中的pack.js很容易发现一些对wx格式封装的痕迹,只不过其中unpack.js的代码被隐去了。通过实际的分析发现(wxapkg文件可以通过截获网络包请求获得或者在本地的微信appbrand目录下可以发现),wxapkg格式就是将wx格式进行了转化:Wxml -> Html、 Wxml -> JS、Wxss -> Css,其二进制格式跟后缀名为wx二进制格式完全一致。我写了两个版本的解析二进制包的代码(

Javascript版本传送门(https://github.com/thedreamwork/unwxapkg)

python版本传送门(https://gist.github.com/thedreamwork/01691af73eeda292e7327bc6bd3e1d08)),其实非常简单,根据小程序包结构一步一步解析,基本上没啥难度。但如果要将Html -> Wxml, JS -> Wxml, Css -> Wxss进行还原,其中JS -> Wxml的过程中需要将if语句转变成wx-if标签、for语句转化成wx-for标签有点麻烦,需要对解析包后的page-frame.html中 JS 代码进行修改,修改细节太多就不再详说了,总之微信小程序的代码没有经过额外的保护措施,比较容易进行还原。 


(PS:暴露一下微信小程序未公开的API,openUrl - 在小程序中打开外部网页;getGroupInfo - 获得群的名称,群内成员的昵称等数据;getOpenidToken - 获得用户openid;这些权限微信应该是没有准备开放的。每次在进入小程序时,客户端都需要先去请求该小程序的元数据,例如应用名、版本号、一些权限列表、代码包下载地址等描述信息,修改这些元数据可以获得相应的权限,小程序的关键信息完全由后台控制进行配置,另外小程序的本地文件存储采用HASH映射机制进行文件定位,文件存储在外部存储,本身通过自定义算法实现完整性校验 - 首先,小程序最终存储的文件名是:对称加密(文件流内容Alder32校验和 | 原始文件名)生成的,最终文件名和文件内容会通过自校验判断完整性;其次,本地缓存是通过HASH映射查找文件。所以即使能破解文件名和文件内容,绕过文件自身签名校验,篡改为攻击者的伪造文件,小程序APP也无法映射到该伪造文件进行使用。)


0x02 前端代码安全



由上可见,微信并没有在代码安全上进行过多的考虑。这导致需要在应用审核过程中花费比较多的功夫,不然作品太容易被复制窜改,以至于会失去渠道先机,这对流量是致命打击。由于历史原因,前端的代码安全技术发展的比较缓慢,相比其他被编译成二进制的应用,前端这种纯文本应用,太容易被辨识与窜改。


对前端代码进行保护的目的在于让机器容易识别相关的指令,而使人难以理解代码的逻辑,但往往在对前端代码进行保护过程中,很难既兼顾指令效率又能使可读性降低。因此,常常需要在现有的代码中增加一些额外的验证逻辑,例如一些增加无效的代码进行混淆、采用守护代码保护业务代码不能在其他的域名下正常运行、增加一些防止调试跟踪的断点等,这些措施都是使得破解代码时人工成本增加,从而增加代码的安全性。


下面提供一些能够增加前端代码安全性的策略:


1. 精简(minify)


这是最简单且无害的方法,精简代码能减少代码体积,从而减小数据传输的负荷,同时也能降低代码的可读性。在小程序开发者工具中也提供该选项。对Javascript代码进行精简大致可以从以下几个方面入手:

  • 删除注释,删除无意义或者多余的空白,删除可以省略的符号      


  • 删除一些没有调用的代码(Dead code),对函数进行精简(三元运算符?:、字符串操作、对象函数、对象继承、函数引用、无名函数、递归函数)


  • 将变量名进行简化,将零散的变量声明合并,缩短语句 ...... 


常用的工具有很多:

YUICompressor(http://yui.github.io/yuicompressor/)

UglifyJS(https://github.com/mishoo/UglifyJS)

GoogleClosureCompiler(https://developers.google.com/closure/compiler/)

JSPacker(https://www.npmjs.com/package/packer)

JSMin(https://github.com/douglascrockford/JSMin)...


使用工具对代码进行精简时需要注意:1. 最好备份原始代码,方便调试与后期修改。 2. 用于调试精简代码时保存的sourcemap,在线上应该删除。 3.编写代码的时候应该严格按照规范,最好使用lint工具对代码进行检查,精简代码后导致代码不可用时,调试非常困难。


这种简单的方法虽然很实用,但是也很容易被还原出源代码,使用一些代码格式化工具可以补齐被删除的空格、换行、符号等,例如jsbeautifier(http://jsbeautifier.org/)。另外2015年就有相关的研究,从大量的代码中推测出被精简的代码,因为人写代码总有固定的范式,所写的代码相似性都非常的高,如果用统计方式就很容易反推源代码,苏黎世联邦理工大学Martin Vechev教授领带下开发的工具JSNice(http://jsnice.org/)就是一款运用条件随机场(Conditional Random Fields)机器学习和程序分析方法来还原Javascript代码利器,利用大量的开源代码,去学习命名和类型的规律。JSNice可以用于以下不同的方面:反精简的JavaScript代码、对当前的代码提供更多的更有意义的变量名、自动化程序的注释等。相关论文传送门(http://www.srl.inf.ethz.ch/papers/jsnice15.pdf)后台代码传送门(https://github.com/eth-srl/Nice2Predict)



2. 混淆(obfuscation)

混淆可以减低代码的可读性,防止被轻易追踪出程序逻辑。常见的混淆方法有如下几种:

  • 通过编码混淆代码,这篇文章《Javascript常用混淆方法》             (http://www.cnblogs.com/yiruoyun/archive/2007/02/21/653246.html)里面介绍了很多不错的编码加密方法。但是这些方法有个明显缺点,增加代码体积,而且编码加密都是可逆的。


  • 将标识符混淆和控制逻辑混淆(分离静态资源、打乱控制流、增加无义的代码等),例如aaencode(http://utf-8.jp/public/aaencode.html)jjencode(http://utf-8.jp/public/jjencode.html)。标识符混淆的方法有多种,有些与编码混淆代码方法有些重叠,常用方法有哈希函数命名、标识符交换和重载归纳等。哈希函数命名是简单地将原来标识符的字符串替换成该字符串的哈希值,这样标识符的字符串就与软件代码不相关了;标识符交换是指先收集软件代码中所有的标识符字符串,然后再随机地分配给不同的标识符,该方法不易被攻击者察觉;重载归纳是指利用高级编程语言命名规则中的一些特点,例如在不同的命名空间中变量名可以相同,使代码中不同的标识符尽量使用相同的字符串,增加攻击者对软件源代码的理解难度。


  • 控制混淆是改变程序的执行流程,从而打断逆向分析人员的跟踪思路,达到保护软件的目的。一般采用的技术有插入指令、伪装条件语句、断点等。伪装条件语句是当程序顺序执行从A到B,混淆后在A和B之间加入条件判断,使A执行完后输出TRUE或FALSE,但不论怎么输出,B一定会执行。控制混淆采用比较多的还有模糊谓词、内嵌外联、打破顺序等方法。模糊谓词是利用消息不对称的原理,在加入模糊谓词时其值对混淆者是已知的,而对反混淆者却很难推知。所以加入后将干扰反汇编者对值的分析。模糊谓词的使用一般是插入一些死的或不相关的代码(bogus code),或者是插入在循环或分支语句中,打断程序执行流程。内嵌(in-line)是将一小段程序嵌入到被调用的每一个程序点,外联(out-line)是将没有任何逻辑联系的一段代码抽象成一段可被多次调用的程序。打破顺序是指打破程序的局部相关性。由于程序员往往倾向于把相关代码放在一起,通过打破顺序改变程序空间结构,将加大破解者的思维跳跃。


  • 另外还有些混淆方式是专门针对于反混淆工具设计的,这就需要去仔细分析反混淆工具的原理,在一些特定的地方插入代码使反混淆器进入死循环或者异常跳出。


一般来说,提供代码精简的工具都会提供一些混淆的方法,除此之外,比较知名的商业工具有jasob(http://www.jasob.com/)、jscrambler(https://jscrambler.com/),一般越商业的越难被反混淆,然而这些高级的代码混淆也常会被用于隐藏应用中的恶意代码。对恶意代码进行混淆是为了躲避杀毒软件的检测,这些代码在被混淆扩充后会难以被识别为恶意软件。相应的也有一些反混淆的工具出现,例如上面提到的

JSNice(http://jsnice.org/)工具能够对混淆的代码进行推理,另外反混淆工具JSDetox(http://www.relentless-coding.com/projects/jsdetox/ww.jasob.com)专门针对一些混淆方法做过专门的支持。反混淆一直是一项体力活,根据不同的混淆策略需要进行反推演算,这就是一场攻与防的游戏罢了。



3. 加密(encryption)

加密的关键思想在于将需要执行的代码进行一次编码,在执行的时候还原出浏览器可执行的合法的脚本,在某个角度也可以认为是一种混淆的形式,看上去和可执行文件的加壳有点类似。Javascript提供了将字符串当做代码执行(evaluate)的能力,可以通过 Function constructor、eval、setTimeout、setInterval、Worker、DOM event等将字符串传递给JS引擎进行解析执行,由于有些需要用到eval函数,会导致代码性能会减低。以Worker执行举例:


var URL = window.URL || window.webkitURL;
var Blob = window.Blob || window.webkitBlob;
var blobURL = URL.createObjectURL(
   new Blob(['console.log("Hello World!")'], {type: 'application/javascript'})
);
new Worker(blobURL);
URL.revokeObjectURL(blobURL);


有以下常见的几种加密方法:

  • base64编码,一种简单的方法就是将代码转化成base64编码,然后通过atob以及eval进行解码然后运行,另外一种采用base62编码(http://dean.edwards.name/packer/)技术更为常见,其最明显的特征是生成的代码以eval(function(p,a,c,k,e,r))开头。无论代码如何进行变形,其最终都要调用一次eval等函数。解密的方法不需要对其算法做任何分析,只需要简单地找到这个最终的调用,改为console.log或者其他方式,将程序解码后的结果按照字符串输出即可。                                                                             
    (PS: 从算法上看,packer(http://dean.edwards.name/packer/)是一种base64编码字典压缩策略,packer的base64编码的压缩率很高,精简后代码依然可以减少50%体积以上,因为带有解压器和字符表,越长的代码理论上压缩率更高,想要了解详情可以看看这篇文章《Packer,你对我的JS做了什么!》(http://fed.renren.com/2011/12/350))


  • 使用复杂化表达式,在Javascript中可以把原本简单的字面量(Literal)、成员访问(MemberExpression)、函数调用(CallExpression)等代码片段变得难以阅读。例如这个方法(http://discogscounter.getfreehosting.co.uk/js-noalnum.php)仅用+!等符号就足以实现几乎任意Javascript代码。在 JS 代码中可以找到许多这样互逆的运算,通过使用随机生成的方式将其组合使用,可以把简单的表达式无限复杂化。


  • 隐写术,将 JS 代码隐藏到了特定的介质当中。如通过最低有效位(LSB)算法嵌入到图片的 RGB 通道、隐藏在图片 EXIF 元数据、隐藏在 HTML 空白字符、放到css文件中(利用content样式能存放字符串的特性)等。比如一张图片黑掉你:在图片中嵌入恶意程序(https://conference.hitb.org/hitbsecconf2015ams/wp-content/uploads/2015/02/D1T1-Saumil-Shah-Stegosploit-Hacking-with-Pictures.pdf),这个正是使用了最低有效位平面算法,结合HTML5的canvas或者处理二进制数据的TypeArray,抽取出载体中隐藏的数据(如代码)。隐写的方式同样需要解码程序和动态执行,所以破解的方式和前者相同,在浏览器上下文中劫持替换关键函数调用的行为,改为文本输出即可得到载体中隐藏的代码。


  • 混合加密,单个方法容易被破解,但组合起来就不会那么容易了,破解成本也会指数增长,例如jdists(https://github.com/zswang/jdists/wiki/%5Bcase%5DCode-mixed-encryption)采用组合加密和嵌套加密的方式。


这些加密的方式都很容易通过对源代码进行词法分析、语法分析进行还原,首先将代码的字符串转换为抽象语法树(Abstract Syntax Tree, AST)的数据形式,然后从语法树的根节点开始,使用深度优先遍历整棵树的所有节点,根据节点上分析出来的指令逐个执行,直到脚本结束返回结果。这种方法大多数用于对代码进行优化,例如最近Facebook开源了代码优化工具Prepack(https://prepack.io/),可以自动消除冗余代码,降低打包体积和执行时间,基本上就可以用来将这些加密的字符串进行还原,毕竟编码这些字符串都是可以通过词法语法推测出来的。



4. 编译(compile)

Github上有一份清单(https://github.com/jashkenas/coffeescript/wiki/List-of-languages-that-compile-to-JS)记录了所有Javascript扩展语言,这些语言都可以通过编译器转化为Javascript语言,这也是前端发展的一个趋势,原来写的html,css,Javascript已经开始变成了一个“中间语言”,而且越来越多的团队也有了自己的一套前端编译系统。Javascript越来越像Web中的汇编语言,特别是近些年Node的普及,让前端变得越来越复杂,大量前端框架的出现,使得Javascript代码可以通过手工编写,也可以从另一种语言编译而来,详情参考几年前Brendan Eich(JavaScript之父)、Douglas Crockford(JSON之父),还有Mike Shaver(Mozilla技术副总裁)的邮件(http://www.hanselman.com/blog/JavaScriptIsAssemblyLanguageForTheWebPart2MadnessOrJustInsanity.aspx)。通过编译后的Javascript代码越方便机器的理解,降低可读性,在某一定角度上讲,这也不愧为一种代码保护措施。据说几大科技巨头正在酝酿给浏览器应用设计一款通用的字节码标准——WebAssembly(https://github.com/WebAssembly),一旦这个设想得以实现,代码保护将可以引入真正意义上的“加壳”或者虚拟机保护,对抗技术又将提升到一个新的台阶。目前在桌面端,使用NW.js(https://github.com/nwjs/nw.js)框架可以JavaScript应用程序的源代码可以被编译为本地代码,在运行时通过NW.js动态还原出源代码,但是这种方法(http://docs.nwjs.io/en/latest/For%20Users/Advanced/Protect%20JavaScript%20Source%20Code/)目前会比正常的JS代码慢30%左右。



5. 防止被调试

对代码进行破解分析无非分为静态分析和动态分析,如果对代码进行混淆加密等形式操作,那么静态分析就很麻烦了,对代码调试跟踪分析可以对代码整体逻辑有一个宏观的把控。例如首先判断浏览器是否开启了开发者工具控制台(目前最完美的解决方案传送门(http://stackoverflow.com/a/30638226/1068602)),如果检测出控制台开启则堵塞Javascript执行或让代码异常跳出。另外Android 4.4及以上和iOS是支持webkit remote debug的,因此应该在debug模式下,设置代码可以被debug,release模式下,禁止debug以保护数据安全。



6. 前后端协作

首先得强调的事情是不要在前端放敏感数据,前端容易破解,因此需要配合后端进行安全防护,例如微信小程序的登录,必须利用授信的后端配合才能完成此项功能,另外在小程序的网络请求中的referer是不可以设置的,格式固定为https://servicewechat.com/{appid}/{version}/page-frame.html,其中{appid}为小程序的appid,通过验证appid字段可以抵御一些直接的山寨,其次就是加快迭代速度更改代码保护策略,这样可以让之前的分析失效,增加破解的成本。


以上就是对当前前端代码安全进行的探索,最后用一句话结束: 

Beneath this mask, there is more than flesh. Beneath this mask, there is an idea. And ideas are bulletproof.


本文转自:https://share.whuboy.com/weapp.html



我要推荐
转发到