记一次抓包和破解App接口

第一章 · 起源

某日,想做个爬虫工具,爬某个网站上的数据已做实验之用。大家都知道爬pc网页上的数据有几个常见的问题:首先是数据不规范需要自己解析html,第二现在很多网站是前端动态渲染的,直接爬取的html可能就是个静态页面什么也没有,还需要执行js才能生成最终的页面。因此就考虑,能否用它App的接口去爬数据,因为一般App调用的接口返回的都是json格式,解析起来比较方便。

第二章 · 尝试

既然要抓取App的接口,肯定需要抓包,现在接口一般都用的https通信,直接抓二进制的tcp包是无法解密的。想到用Fiddler,它的原理很简单,就是启动一个https代理服务器,手机上设置Fidder启动的代理服务ip和端口。这是第一步,第二步就是证书,Fiddler会自己生成一个证书,把这个证书作为根证书导入的手机中,这时手机对Fiddler返回的证书就是可信任的。通信的时候,Fiddler作为客户端它会和服务端建立https连接,得到解密后的数据,然后自己启动一个https的服务,把数据返回给手机端。手机端这个时候实际访问的是Fillder启动的https服务,验证的证书也是Fillder自己颁发的。至于https如何验证签名如何加密通讯的这里就不做展开了,有兴趣的可以自己查阅。

但是,必须要说的是这样配置代理用Fiddler抓包的确可以抓到用手机浏览器访问的内容,现在App为了防止代理模式抓https的包,一般都会把证书放到自己的App中,验证https server返回的证书和App中的证书是否一致,这个时候server返回的证书是Fiddler自己颁发的,肯定和App本地的不一样所以App这时是无法访问接口的。

为什么Fiddler要自己颁发一个证书呢?这是由于Fiddler需要把和服务端通信的内容解密,所以它作为一个正常的client端先和服务端建立连接,验证服务端的证书,用服务端的证书上的公钥加密自己生成的对称秘钥,服务端拿到对称秘钥后用私钥解密,双方都知道对称秘钥,之后的通信用对称秘钥加解密。同理Fiddler作为App的代理服务器,也是要和app建立https连接的,这时如果直接返回服务端上的证书,App用服务端上证书的公钥加密App生成的对称秘钥。但是Fiddler没有证书的私钥,所以Fiddler需要自己颁发一个证书,有公私钥,才能和App正常的用https通信。

尝试用Fiddler代理抓App的包失败了

第三章 · 脱狱

遇到了问题,肯定要解决问题。现在问题的关键就是App验证本地证书和Fiddler返回的证书不一致,要做的就是绕开这验证,或者让App验证这个证书永远通过。不得不说,道高一尺魔高一丈,还真有这样的东西,根据维基百科的解释Xposed框架(Xposed framework)是一套开放源代码的、在Android高权限模式下运行的框架服务,可以在不修改APK文件的情况下修改程序的运行(修改系统),基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。
安装Xposed是有风险的,弄不好手机会变成砖,这里我就用模拟器替代手机安装了,下载MuMu模拟器,安装Xposed框架

同时还需要安装一个Xposed的一个插件Just Trust Me

安装好之后,我们再打开这个App,同时配置模拟器网络的代理,再用Fiddler抓包可以看到内容

第四章 · 柳暗花明

看到上面的截图,的确是找打了请求的地址,但是发现发出的请求内容是

{
  "pagesize": 10,
  "page": 1,
  "platid": "0",
  "langid": "0",
  "typeid": "0",
  "saletime": "全部",
  "time": 1595672934792,
  "sign": "c2522179dd87522f0d2997e28d48289e"
}

没错,有一个签名,这个签名一看就知道是md5做的,但是我不知道规则,而且生成的签名肯定是这些请求的参数加上一个字符串,最后md5一下,服务端还要验证签名,才能正常返回结果。这时仿佛又回到了原点,不解决这个签名,我们是无法正常请求服务的。要解决这个签名唯一的办法就是反编译包,从代码中发现蛛丝马迹。
apktool是一个开源的用于反编译apk包的工具,用它试试看能不能找到对应的md5加密的源码。用它反编译apk后,代码非常庞大,在里面搜索接口的名称djscreen竟然找到代码了

    invoke-static {}, Ljava/lang/System;->currentTimeMillis()J
    move-result-wide v8
    .line 493
    .local v8, "time":J
    new-instance v10, Ljava/lang/StringBuilder;
    invoke-direct {v10}, Ljava/lang/StringBuilder;-><init>()V
    const-string v11, "10"
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    iget v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->mPage:I
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
    move-result-object v10
    iget-object v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->platid:Ljava/lang/String;
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    iget-object v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->langid:Ljava/lang/String;
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    iget-object v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->typeid:Ljava/lang/String;
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    iget-object v11, p0, Lcom/ws3dm/app/activity/GameCategoryActivity;->saletime:Ljava/lang/String;
    invoke-virtual {v10, v11}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v10
    invoke-virtual {v10, v8, v9}, Ljava/lang/StringBuilder;->append(J)Ljava/lang/StringBuilder;
    move-result-object v10
    invoke-virtual {v10}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object v7
    .line 494
    .local v7, "validate":Ljava/lang/String;
    invoke-static {v7}, Lcom/ws3dm/app/util/StringUtil;->MD5(Ljava/lang/String;)Ljava/lang/String;

根据代码可以看到MD5的规则就是10+mPage+platid+langid+typeid+saletime+time然后送达工具com.ws3dm.app.util.StringUtil执行MD5方法,进入这个类看看怎么MD5的

    .method public static MD5(Ljava/lang/String;)Ljava/lang/String;
    .locals 9
    .param p0, "string"    # Ljava/lang/String;
    .prologue
    .line 77
    const-string v4, "e8S8Ho0N25z78u6qn4kHyN"
    .line 78
    .local v4, "key":Ljava/lang/String;
    new-instance v5, Ljava/lang/StringBuilder;
    invoke-direct {v5}, Ljava/lang/StringBuilder;-><init>()V
    invoke-virtual {v5, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v5
    invoke-virtual {v5, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    move-result-object v5
    invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
    move-result-object p0

惊喜的发现传入的字符串+e8S8Ho0N25z78u6qn4kHyN然后再MD5就是结果
试试101000全部1595672934792e8S8Ho0N25z78u6qn4kHyN正好是c2522179dd87522f0d2997e28d48289e和抓包中的参数一致,搞定,到此基本上是破解结束了。后续还发现基本上所有的接口都是采取这种模式进行md5然后请求的。

第五章 · 终结

至此整个的抓包加破解的流程就结束了,整个过程其实是做二件事情,解决https验证证书,找到md5签名的规则。整个App相对而且是比较好破解的,整个MD5的过程都是Java写的,比较好反编译。这也是和后端通信,常见的一种处理方式。但是这种方式,的确不是很安全,没有很好的保护Api不被外部破解,比较安全的做法是把签名方法放到so中,或者对数据进行加密解密,同时也不要忘记混淆代码。