混淆SWF的另一种更安全更通用的思路

Posted on November 5, 2013

简单说下SWF文件的混淆原理,(已经明白的请跳过本段):我们的AS源代码被编译完成后,SWF内部会形成一个字符串映射表,包含源码里出现的所有字符串(类名,包名,成员变量名,常量等)。一个数字(相当于地址指针)对应一个全局唯一的字符串。而原本的代码片段包含的字符串都会被替换为对应的数字指针。代码实际执行过程用的都是这些数字指针。所以即使你修改了映射表内的字符串内容(不能修改Flash API的关键字,会导致调用接口失效),通常对代码执行过程也不会产生影响。而反编译就是根据数字指针从这个映射表取出对应字符串填回代码,最后生成人可以阅读的源代码。其实所有语言生成的程序都是可以被反编译的。只是其他语言编译后没有存储明文字符串。还原了也没有可读性。SWF可以被反编译破解出来正是因为存储了明文的字符串。所以我们选择性的替换这个映射表的内容为毫无意义的其他字符串。这个程序就像二进制一样没有可读性了。这就是混淆的基础。

还有另一种流行的SWF保护方式:加密。简单说就是对SWF文件的字节流按一定规则篡改,然后运行时再还原回来。但是加密无论如何层层处理,最终都是要在内存中还原出原始的swf。所以有经验的破解者根本不用去猜你的加密算法,直接从最终结果内存抓取即可。不像加密,混淆才是真正二进制安全的方法。因为生成的SWF文件本身已经不包含可读性的代码了,这是一个不可逆的过程。

混淆之所以一直没有被主流采用,我认为最主要的原因是它很容易带来隐藏Bug。先说下传统的混淆方式。都是在导出发新版的SWF后,用第三方的库去解析SWF文件字节流,然后修改里面的字符串映射表内容。(这个方式有现成的第三方库,可以参考这个帖子)因为所有的字符串都是合并起来的,你修改一个字符串,就会同时影响到所有引用它的地方。举个例子:你自定义了一个类叫Sprite,那么这个类的类名会和flash.display.Sprite的“Sprite”共用一个数字指针。你要是修改了这个字符串的内容,你可以实例化你自定义的那个类(只是名字改了无所谓),但想实例化Flash API的Sprite时就会出错,会提示在那个包里找不到这个修改过的类名。还有通过字符串属性去访问的方式,被修改了之后也会带来各种问题。

所以混淆的关键其实是:确定哪些是可以安全修改的关键字列表。答案是没有通用办法。大家都只能根据自己项目的特点来写。很少能做到一个工具通吃所有情况。而且即使针对自己项目写的,也不敢100%保证获取的关键字列表是安全的,因为项目代码一直在变化。你每次发版本都得测试一遍,如果是隐藏很深的Bug,基本都很难发现,也无从测试。加上学习成本,大部分人自然就优先选择加密的保护方式了。因为写一次工具就可以无限复用。

知道了问题所在:这个关键字列表我们没法100%保证是安全的,只能无限接近100%。要解决这个问题,我们可以换一种思路:继续用我们读取关键字的列表的方法,但不去修改发行版的SWF,改成修改AS源代码。类似IDE的全局重命名功能,把源码的包路径,类名,变量名,都替换成无意义的唯一字符串,用这份已经没有可读性的源码再导出SWF文件。这跟传统方式的区别是,再次导出的过程中IDE会自动帮我检查编译错误。大部分情况直接就能导出成功。如果出了报错,手动修复一两个地方即可。我们还可以针对这种方式来优化关键字读取方式,避开编译器不能检查到的错误。让这无穷小的不确定性由编译器帮我们暴露出来。

根据这个思路,我写了一个通用的AS源码解析以及重新生成的工具。下面是源码和air发行包的下载地址。使用方式很简单,把目录下的encrypt.xml配置文件拖拽到窗口即可。配置文件内已经写好了注释。同时支持纯AS工程和Flex工程的混淆。

SwfEncryptor

[更新]

发现有个别同学还在问:”源代码开源吗?”。答:压缩包里的fxp就是标准的Flash Builder工程,导入就是源代码了。

打包的附件项目已经比较旧了,这里再附上项目的开源地址,可以从这里检出最新的代码:https://github.com/domchen/SwfEncryptor