逆向工程初体验

2,667 阅读10分钟
原文链接: www.jianshu.com

我一朋友最近在玩国内某三四线厂商的不知名手游,跟我说他已经充了不少钱了,但最近有个充值活动看起来很诱人,不知道要不要参加。我带着轻蔑的语气回复他道:“这种破游戏有什么好充钱的,我分分钟给你破解掉!”。于是就开启了这段hack之旅。(打脸之旅)

人生赢家

安装之后发现这游戏实时性要求不是特别高,类似于几年前流行的卡牌养成类游戏。目测网络请求用的是http,于是打开了Charles试着抓包看一下。果不其然,用的甚至都不是https。


接口.png

看到结尾的aspx我猜这家公司很早以前应该是做网站的,后来转型做的游戏。在我个人的认知里,现在好像很少看到有公司用asp写后台以及用windows做服务器了。

接着看这些接口都返回了什么。


返回的数据.png

简单,看到末尾的等号以及这串东西,显然是用base64编码过了。找个在线网站直接解码一下,我们得到了这么个东西。


base64解码结果.png

看来他们还对数据做了一层加密,我第一反应是异或加密,因为这种加密方式实现起来比较简单,也比较常见。这时我们就需要取到加密的密钥才解密了。

既然网络请求返回的都是密文,客户端要解密,本地一定存有一份密钥。所以接下来我决定对客户端进行反编译。因为本身是android开发,对android也比较熟悉,就去他们官网下载了android客户端。直接把.apk改成.zip解开后可以看到后缀为.dex的文件。在这里我用了dex2jar把dex转换成jar文件,然后用jd-gui来预览源码。

整个过程比我想象的要顺利的多,反编译之后所有代码一览无遗,这家公司甚至连混淆都没做。不做混淆的结果就是我一眼就注意到了AesUtil.class这个类。原来他们采用的是aes加密。


AesUtil.png

可以看到密钥写在了Constants这个类里。到此为止,我就成功解开了他们对网络请求的所有加密,这时我的内心是这样的:


陷入困境

接下来把抓包得到的密文解密后看到了他们请求的数据格式,伪造一份重新加密后发送过去,但返回给我的却是失败。看了下请求头User-Agent那边设置了他们app的名字。设置好UA后重新发送请求,这次终于拿到了正确的数据。

本来是想在代码里直接找到他们所有的网络接口的,但发现他们用了一个叫做corona的引擎,大概就是用lua写android和ios游戏,然后他们的网络请求都是用lua写的,虽然最后肯定还是会被编译成c的二进制文件或者是java的class文件,但我一时半会儿没找到在哪儿,而且找到了可读性可能也很差,所以还是决定通过抓包来测试他们的接口。

游戏第一天签到会赠送道具,使用后可以增加10万金币,我们先从这个接口入手,看看能不能让我们一夜奔小康。


根据字段的名称可以知道cmd是指令,num是使用的数量,propID是道具的ID,guid是用户id,cTime是时间戳,但是位数好像不太对,测试后发现是减去了2016/01/01/0:00的时间戳,hmVer比较重要,可以看到respose会返回一个hmVer且等于请求加一,这个是他们服务器做的验证,下次请求的hmVer需是上一次Response中的hmVer,但是经过测试发现-999是一个特殊的hmVer可以无视这个规则,这应该是他们为了方便留的一个后门吧。



很遗憾他们对数据做了一些判断,随后我试了下num = 0,num = -1以及别的一些异常数据,也试了别的几个接口,他们的后端对于一些边界条件和异常数据都做了处理还是比较细心的,看来接口这条路是走不通了,这时我的内心是这样的:


出现转机

当然有了所有的网络接口已经可以写个自动挂机的脚本了,但只是自动挂机的话,我要怎么做天下第一???回去继续看源码,寻找别的突破口,这时看到了这么一段代码。


转折点

看方法名就知道这是用来测试支付的,显然我手上这个包不会执行到这段代码,我们得通过一些手段来逆天改命。


知天易,逆天难

用dex2jar反编译的包是不能修改代码的,也不能重新编译回去,所以这时候我们需要另一个工具:apktool

他能帮你把apk反编译成Dalvik运行时所需的字节码。我们通过修改字节码来改变程序运行的流程。

反编译后找到支付方法所在的类,并找到支付这个方法

.method public pay(Lxxx/PayParams;)V
    .locals 5
    .param p1, "data"    # Lxxx/PayParams;

    .prologue
    ....
.end method

我们在程序开始处直接调用测试支付的方法并且让程序直接返回

.method public pay(Lxxx/PayParams;)V
    .locals 2
    .param p1, "data"    # Lxxx/PayParams;
    .prologue

    invoke-direct {p0, p1}, Lxxx/Pay;->checkPayResultTest(Lxxx/PayParams;)V
    return-void
    ....
.end method

好了,现在使用apktool重新build代码,并重新对apk进行签名就能安装了,安装好之后进商城点充值然后boom:


报了一个错,不慌,这是因为没有在ui线程修改了ui组件所造成的。我们修改字节码让其在ui线程运行就可以了。但是这里我犯了一个错误,我一开始没仔细看pay方法下面的字节码,其实下面是有调用到这个方法的。

因为我们new了一个Runnable,是一个匿名内部类,所以编译器会自动帮你生成一个静态方法,以及一个内部类,这和jvm是一样的。

.method static synthetic access$1(Lxxx/CKPay;Lxxx/PayParams;)V
    .locals 0

    .prologue
    .line 292
    invoke-direct {p0, p1}, Lxxx/Pay;->checkPayResultTest(Lxxx/PayParams;)V

    return-void
.end method

所以我们找到Pay$1.smali里面的run方法非常的长大概有300多行,所有的支付逻辑都在这里面了。本着帮他们优化一下程序性能的想法,我把300多行的代码优化成了6行:

# virtual methods
.method public run()V
    .locals 2

    .prologue

    iget-object v0, p0, Lxxx/Pay$1;->this$0:Lxxx/Pay;

    iget-object v1, p0, Lxxx/Pay$1;->val$tempData:Lxxx/PayParams;

    invoke-static {v0, v1}, Lxxx/Pay;->access$1(Lxxx/Pay;Lxxx/PayParams;)V

    return-void

好了现在看着干净多了,支付这一块的性能和内存占用也得到了极大的提升,我们重新构建apk并且签名。


选择成功后弹出,请稍后,待订单验证成功后重新登录,然后就没有然后了。


尾声

当然故事到了这里并没有结束,在阅读他们源码的时候发现他们的客户端程序猿还做了一件我所不能理解的事情--把他们的运营后台网址写进了代码里,于是我又试着从他们运营后台找突破口。先试了下sql注入,并不起效果,然后打开了Chrome的控制台,刷新了下页面。


并没有返回他们服务器的信息,但可以看到cookie里有设置一个open.session.id应该是通过这个id来判断是否登录的,是一串md5值,然而并没有什么卵用。

然后我又点开了Chrome控制台的source标签看了下都有些什么文件,根据目录结构我感觉服务器是nodejs或者python的可能性比较大,然后看了下js基本都是些jquery和bootstrap的库,直到common下两个js文件引起了我的注意。

注释里写着所有权归这家公司所有,还有作者的昵称。然后我去搜索了一下作者的昵称,搜到了一个博客。翻阅了一下最早的一篇文章是2008年的,也就是说这个作者至少是工作了8年的程序员。其中有一篇文章是作者的一个开源项目,是基于多个别的开源项目封装的快速开发平台,在github还有着4000+的star。然后在文档里巨细无遗的写了从后端到前端各个层级具体使用了什么技术。而且我还看到了和我反编译的apk所相似的包名前缀,所以我可以肯定这位程序员是那家公司的员工,而且前端技术选型里所提到的几个框架还有库和他们运营后台所用到的一摸一样,所以他应该是把他们运营后台给开源了吧🙄️。顺带提一下,common下的两个js都是些工具类,而且在登陆页只是引用了这两个文件,我并没有找到使用他们的地方。

这次hack之旅到这里就告一段落了,如果还要继续下去的话,可能我会去看一下他开源的代码。哦,对了,在他项目的文档里还给了个测试的账号密码。我试了下并没有登上去,但我相信账号肯定是对的。

总结

这个故事告诉我们这么几个道理:

  1. 打包时一定要对代码进行混淆,必要的话最好再加上壳
  2. 密钥什么的不要写死在代码里,可以选择放在native的so包里,或者存本地数据库也行
  3. 单元测试还是有必要的,并且尽量把测试用例补全
  4. 不要为了方便而留后门
  5. 用不到的代码都删掉或者注释掉
  6. 一些重要的逻辑都放到服务端去处理
  7. 不要把运营后台地址写进代码里,而且运营后台不要和app共用一个地址
  8. 不要随随便便立flag

把运营后台地址写到代码里真的是很危险的事,如果登进去了我不仅能对他们游戏数据做一些修改,还有可能拿到他们整个数据库的数据。一般情况下数据库里前几的账号都是他们内部人员的账号,而这些账号很有可能与他们的百度网盘、支付宝、qq之类是同一个,我甚至能拿到他们公司比较机密的数据。

最后说两句

其实在和朋友打赌之前,我完全没有做过这种尝试,我只是用自己这两年储备的知识和经验作出对应的分析和思考。大概过程是这个样子的:利用网络来改自己数据->抓包->有=号用base64解解看->解不出,还有一层加密->客户端也要解->去反编译客户端->得到密钥->测试他们接口->不可行->还有源码,去源码找找线索->找到测试支付代码->需要改字节码,不太会->查语法,自己先写个demo试一下->动手改,重新安装->不可行->接着去源码找线索->找到运营后台->去运营后台找线索->找到作者博客->找到后台所用技术

整个过程还是挺流畅的,我想说的其实是遇到问题不要慌,去找对应的解决方法就好了,如果找不到就换一个角度,那句话怎么说来着,条条大路通罗马

友情提醒:这么做其实是不对的,请不要模仿!