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

The created in 2017.05.12 updated in 2017.05.15


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

0x01 小程序分析

小程序包结构

Segment Name Length Remark
Header FirstMark 1 byte 0xBE 固定值
Edition 4 bytes 0 -> 微信分发到客户端 1 -> 开发者上传到微信后台
IndexInfoLength 4 bytes 索引段的长度
BodyInfoLength 4 bytes 数据段的长度
LastMark 1 byte 0xED 固定值
FileCount 4 bytes 文件数量
Index NameLength 4 bytes 文件名长度
Name NameLength bytes 文件名,长度为NameLength
FileOffset 4 bytes 文件在数据段位置
FileSize 4 bytes 文件大小
LOOP......
Data Files Package......

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

  1. 头信息,包含一些包的标识,版本定义等,包含了三个冗余字段 --- 索引段和数据段的长度应该是用于做校验,但实质上没有用(设计者觉得需要设计一些冗余字段来确保设计的完整性,防止解析的时候溢出,但实际工程实践中并没有起到相应的作用),文件数量应该是用于简化包解析过程,实际上知道了索引段长度或数据段长度中任何一个皆可推算出文件数量。
  2. 索引段,包含文件的元信息 --- 文件名以及文件位置(通过FileOffset和FileSize定位数据段中的文件)。如果从精简包的大小的角度来看,FileOffset和FileSize只需一个存在即可,但是这样解析包的难度就大大增加了,还是以工程实践为主。
  3. 数据段,将所有的文件罗列在一起。

由此可见,数据完全没有经过压缩或者加密,连包的签名信息也没有。这导致只能在制品流程上进行严格控制,例如在开发者上传代码过程中需要授信,必须经过审查,也一定得由授信平台进行代码分发等。这些都无关风月,毕竟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版本传送门python版本传送门),其实非常简单,根据小程序包结构一步一步解析,基本上没啥难度。但如果要将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代码进行精简大致可以从以下几个方面入手:

常用的工具有很多:YUI CompressorUglifyJSGoogle Closure CompilerJS PackerJS Min...

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

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

2. 混淆(obfuscation)

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

一般来说,提供代码精简的工具都会提供一些混淆的方法,除此之外,比较知名的商业工具有jasobjscrambler,一般越商业的越难被反混淆,然而这些高级的代码混淆也常会被用于隐藏应用中的恶意代码。对恶意代码进行混淆是为了躲避杀毒软件的检测,这些代码在被混淆扩充后会难以被识别为恶意软件。相应的也有一些反混淆的工具出现,例如上面提到的JSNice工具能够对混淆的代码进行推理,另外反混淆工具JSDetox专门针对一些混淆方法做过专门的支持。反混淆一直是一项体力活,根据不同的混淆策略需要进行反推演算,这就是一场攻与防的游戏罢了。

3. 加密(encryption)

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

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

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

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

4. 编译(compile)

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

5. 防止被调试

对代码进行破解分析无非分为静态分析和动态分析,如果对代码进行混淆加密等形式操作,那么静态分析就很麻烦了,对代码调试跟踪分析可以对代码整体逻辑有一个宏观的把控。例如首先判断浏览器是否开启了开发者工具控制台(目前最完美的解决方案传送门),如果检测出控制台开启则堵塞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.

The World


[1] https://www.w3.org/TR/PNG/
[2] https://www.ietf.org/rfc/rfc1952.txt
[3] http://blog.yaq.qq.com/blog
[4] http://blog.knownsec.com/2015/08/use-estools-aid-deobfuscate-javascript/