猿人学攻防赛之第十一题:so文件破解

    这段时间猿人学的题目又更新了几道,后来有时间也玩了玩,除了第17题没搞以外,其他的都还蛮有意思的,17有点恶心人,算了吧。11题是当初比赛的最后一道题,后来主办方也把app直接放在网站上了,因为我一直想通过算法破,又没逆出来,最近看他服务器都炸了。。赶紧写一下,证明我来过。

    逆算法可以试试动态调试,我这边环境有点问题,搞了一天也没解决。所以这道题我的做法是RPC,题目的要求也是允许的,虽然主办方是希望通过算法解决。。。这道题RPC解决,只需要解决几个小坑即可,以下是寻坑经过。

    首先是抓包,相信这道题的抓包就足以难倒一些人了,不过这个防抓包意义不大,我最开始分析的时候是跳过抓包的。。因为代码里写的太明显了,这个App一共就这么点代码,还没混淆。所以很容易就能定位到发包函数:

    URL,参数,签名,有没有你想要的。。都在这里了。

    抱着学习的态度,我还是研究了一下他为什么抓不到包,我也试过JustTrustMe、PacketCapture、HttpAnalyzer抓包,走代理的、VPN的、既不走代理又不走VPN的,都试过了,确实是抓不到。那就是代码做了处理。一般来讲抓不到包,无非是单向校验(客户端校验服务器证书)、双向校验(客户端与服务器分别校验对方证书),一般的单向校验用JustTrustMe过掉就可以了,有一些会对校验的代码进行混淆或者处理,这时JustTrustMe就会失效,具体可以研究一下JustTrustMe的代码,他本质上就是Hook了几个常用的Http框架的证书校验,一遇到混淆就死了,改了函数名肯定Hook不到。双向校验麻烦一点,这里他也没用到。

    那是不是证书校验搞的鬼,我们去找一下。打开App就可以看见okhttp3这个包,所以网络请求用的就是okhttp3。那么我们就要知道okhttp3在开发的时候怎么做证书校验的。

    随便百度一篇:

    可以看到,okhttp3在做证书校验的时候,只需要在new Builder()的后面调用sslSocketFactory()方法就行了,里面是具体的校验,我们不用管。其余的用来检测的,有域名校验(.hostnameVerifier)、强制代理(.proxy)。我们找到对应代码看看他的Builder后面跟了什么,搜BuiIder()很容易就能定位到。

        所以这里根本就没有做证书方面的校验。Builder后面跟的方法只有proxySelector,coneectionPool。这个proxySelector就有点东西了,我的理解是他强制代理走了这个请求。。做法的话,Hook或者改一下代码。这个App并没有什么签名校验,所以直接暴力改也是没啥问题。好吧,这里我不会Hook。。希望知道的大大交流一下。

    打开Android Killer,干掉调用proxySelector的地方。搜索proxySelector:

    就这行,找到,删掉,反编译。

    然后装上我们改好的App,试试抓包。

    OK,已经抓到了。那看来就是proxy搞的鬼。

    再来看sign的代码,j就是要传的数字,这里是直接传到so里了。

    一个静态方法,我们试试直接frida主动调用。

    首先是肯定可以调用拿到的,但是一个结果要返回好几十秒。一万个数字。。。这样肯定是不行的。我们还是去看看他的so,拉倒IDA里反编译一波。先看看导出列表:

    搜不到,所以是动态注册。前面我们说过,动态注册一般可能在JNI_Onload或者init_array中,我们先进到JNI_OnLoad,简单还原一下,这里还原的步骤略了。。很简单,之前的文章里也讲过(安卓逆向:某西到家商品列表so层签名分析 - 简书):

    RegisterNatives,那么就是这里了。按照之前的逻辑,这里的v7就是注册的结构体。点过去:

    依旧是三个一组,分别是函数名、签名、地址,前三行实际上是一样的内容,看地址就能看出来,所以我们找的是最后一个,sub_336B8,这个就是getSign函数的地址了。直接F5,稍微修复一下:

    乱七八糟的,反正我是看不懂。。大概的逻辑就是取了一堆随机数,和他拼接好的字符串异或,然后传到一个sub_35164的方法里,简单看一眼35164,a9Fvwxng78pqrgh是重点

'

    这个东西是一个长度64的字符串。所以我猜,sub_35164是替换编码的base64。

    这些算法总体来讲不算太难,动态调试一波跟着走一走应该是可以还原出来的。我这动态调试一直不行,不知道是有坑没排掉还是环境有问题。所以还是解决主动调用的问题。

    在观察代码执行流程的时候,我发现336B8里有这样几行代码:

    别的不说,这个sleep,很有趣。其他的变量名是我修改过的,这个sleep我并没有改,他最开始就叫这个名。。

    他先反射调用looper.myLooper()方法,如果结果是非,就睡眠一定时间。。myLooper是啥,百度一下:

    获取当前线程。我好像知道了,写一段frida代码试试。这里我hook的是上面的callMethod4,名字是我改过的,他的地址是33C70,也就是调用myLooper得到的结果。

    这里是thumb指令,所以取偏移的时候要+1。

    然后手动点一下查询,可以看到Java层拿到的Looper是一个线程,包括tid和地址。so层拿到的结果也是一个地址,看不懂是什么,无所谓。

    我们再用frida主动调用getSign1,看看输出的looper分别是什么。因为是静态方法,我们也拿到了这个类,所以直接下面加一行就可以:

    保存,执行,然后等他睡眠几十秒,这里java层的结果是null,so层得到的结果是0x0,也就是没有拿到looper。我们主动调用的时候,并没有当前执行的线程,就会走进他if里的逻辑,sleep一段时间。

    解决方法也很简单,我们可以hook sub_33C70的返回值,如果是0x0,就给他改成一个别的值,这里需要用replace替换so层的返回值。写一个100的循环试一下:

    后面也打印了时间,证实是这个looper的问题了。接下来的任务就是拿着sign去请求了。其实也可以不用请求了,因为他那边的服务器挂了,全都是返回502,不知道什么时候能恢复,还会不会恢复..正常点都获取不到结果,更别说这种。。。

    Over,后面的几道题会不会更看情况吧,之前也都做过,比较懒,就没写过程。写文章的话。。。要比分析、扣代码花的时间还要长。其实已经有很多人在网上发解法了,都很详细,大家可以百度一下找找。

推荐阅读更多精彩内容