开篇之前,我一直考虑这算不算侵权?只是兴趣,如果有人告知侵权的话,立马删除。
我所说的数据不是指拿别人app中的图片资源。而是程序运行所需要的数据。
App中的数据无非两种,一种是网络数据,一种是本地数据。网络请求我们一般用抓包工具(Mac上的Charles 或者Windows上的Fiddler)来获取Api,然后再用程序循环请求,获取所有的数据。而另一种是app存储到本地的数据,一种是存储到文件中的,一种是存储到数据库中的。
一次偶然的机会,发现快快查汉语词典
这个app作为汉语词典来说,还算是比较良心的app,免费,而且界面还是比较干净的。而且词库比较全。所以我想获取这个词典的所有数据,作为自己的词库使用。像这种词典,一般汉字都用拼音散列开了。所以我想获取汉字和拼音的映射。好吧,用了一个周的时间,边学习,边反编译,并且学习了Smali语法。最终还是把数据解析出来了。
大体流程:数据解密
--->数据格式化
--->格式化后的数据
作为一个词典来说,我感觉这个app做的相当不错。有很多值得我们学习的地方。so,我给反编译了。
-
首先确定数据来源。
分析一下数据到底来源于网络呢,还是本地数据存储呢。这个很容易判断,第一次安装好app之后,断开网络,如果仍然能查询,说明本地有数据缓存,如果不能查询,说明必须要访问网络。好,测试之后我们发现能查询,说明本地可定有一份可供查询的数据存储。
有些app会做双重数据存储。也就是说数据源,既有网络也有本地,比如有道词典,会做一部分本地存储,详细信息要从网络查询。所以还要做,有网和无网情况下查询详情是不是一样,来判断本地是否存有所有的数据。经过比较发现本地确实有完整的数据存储
-
如果数据存储在本地,存在哪了?
猜测: 本地数据
理由: 像
字--拼音--解释
这种数据结构,一般不适合存放到文件中。实践:数据库存放无非三个位置,一个是
/data/data/包名
目录下,一个是/sdcard/Android/data/包名
,最后一个就是SD卡了,随便建个文件夹就可以存储。参考,Android文件存放位置,那么如何判断数据库文件存放在这三个位置中的那个呢?我们来一个个排除:
要访问真机的/data/data目录必须要root权限。当然我的测试机是root了的。发现里面并没有任何有价值的数据库。
/sdcard/Android/data/包名
这个目录是可以访问的,adb shell
的方式访问之后也没有发现任何有价值的数据。那就剩下最后一个sd卡了。这个比较麻烦,因为这个路径不固定,可以任意创建文件夹。如果是测试机,那么sd卡上没有几个文件夹比价好排查。这里可以用一些小技巧。比如我们app的包名为
com.kk.dict
所以按照一般程序员命名规则,这个文件夹如果真的在SD卡上的话,那么有很大的可能性和kk
或者dict
有关,哈哈,经过查找,在sd卡上有个叫kkdict
的文件夹,最后发现所有的离线数据库都在这里面/sdcard/kkdict/dict/
-
其实上面的三种排查方式比较大众化,也就是可以用于所有的情况下的排查,但是在分析别人的app的时候,我们的方法还是比较灵活的,不拘泥于这么几种,比如我们要分析的这个快快查词典。并不需要这么麻烦。这个app在
我的-->设置-->功能包下载
里面能够设置离线的数据库的存储路径。
由上图可以发现,在这之前的所有分析,都不用了,从离线路径可以分析出来,这数据查询就保存在本地,而且保存路径都可以看到了。
结果: 和我们猜想的一样。
-
好那接下来我们就从数据库中拿数据了哦~
可以看到
/sdcard/kkdict/dict/
目录下主要有以下数据库:
从名字结合app的界面可以大体分析出数据库中存储的内容。我们来看详解这个库,应该存储的是每个字的详细解释。用数据库查看软件查看一下我用的
Navicate
(在Mac上没发现什么好用的sqlite3可视化查看软件)。打开数据库,找到里面主要的表,这下懵逼了。表里的数据是用二进制Blob存储的。
下面的主要任务就是把这个zhujie的字段表示的数据解析出来:how?这种分析主要从哪下手、我一般主要从两方面下手:直接分析数据库;从界面反编译代码后,从代码查询。显然后者难度很大,如果代码混淆了,读起来相当困难。我们先来实验第一种方式,看是否能够成功。也就是直接解析
zhujie
字段的二进制,翻译成字符串。猜测这个二进制是什么:有这么几种可能,
对象
,直接把数据所对应的对象存到数据库,以二进制的方式;字符串
,把字符串转换成二进制数据写入数据库;加密
,这也是最头疼的方式,如果真的加密了,必须去源码中找到加密算法和相应的解密密钥。试验:试着猜想一下,这种数据库中可能存放对象吗?我感觉以我的经验来说不大可能,这种存对象的方式,平台适用性太低。如果使用java存的对象,只能用java读出来,那么这个库完全无法在别的平台(如:
IOS
)上使用。这种不是不行只是不太理想,极少有程序员这么做。当时我猜测的最大可能是字符串
的二进制格式,因为我一直以为这种数据没必要加密。所以,我就试着验证了一下。如何读数据库就不说了,参考;从数据库以二进制的方式读入,然后用String解码。一运行,额。。。乱码??
从上图看出,以我多年解决乱码的经验(瞎搞),出现乱码<font color="#ff0000">大多数</font>是由于编码方式不正确引起的。好,编码不对那我改还不行吗?
于是有了下图的测试代码:
额。。。全是乱码,从查词的结果来看一定包含中文,我知道的能处理中文的常用编码也就这么三种,你说全是乱码。这。。。
结果:上面猜想错误,0.0,只能继续猜测呗。
继续猜测: 会不会数据库存放的是Base64的二进制方式,作者会不会用Base64的方式对数据进行了简单的加密? 但是仔细一想,Base64主要作用是把二进制转换成字符来显示和表达的。把一个字符串,转换成二进制在转换成Base64再换成二进制,再存储。好像能有这种想法的人有点,那啥吧??既然没有别的办法也只能试试喽。测试代码就不给了,就是Base64的基本操作,就是流程绕了点,后来测试发现也是乱码。(:-
继续猜想:看了数据库的这个字段的数据真的加密了。只从数据库入手好像无法解决。加密有难有易,但是无论如何,他既然会显示到界面的数据是正常的明文,说明即使加密了,apk源码中也有解密代码。看了现在找到这个解密的代码是非常关键的。这个解密的代码也有难有易,如果做的简单点就是一个java的Util类。如果做的难了,用jni做到so文件中。但愿他在java中吧。这样反编译起来还简单些。
测试:要把apk中的dex拿出来反编译成jar,然后查看这个jar中的源文件。具体如何反编译拿到jar,不再给出,可以参考这个。参考 至于查看浏览这个jar包中的java代码,我主要借助两个工具jd-gui-1.3.0.jar
和jadx-0.6.0
为什么要接着两个,我能力有限,查看反编译后的java代码,如果这个代码被混淆了,读起来相当困难,而这两个tool反编译后的java代码各有优缺点。前者有代码跟进功能,后者的代码反编译的可读性比较好,但是后者代码综合性比较强,比如能简写的他会简写。这样读起来也比价困难。结合来看就轻松多了。
好,我们可以看到最终数据会被现实到,汉字查询结果页面。如下:
那么我们找到这个页面,然后就可以进行数据分析了,接下来的问题,这个页面怎么找??
猜测:这是个Activity,既然是详情页面,那么根据中国程序员的英语水平命名的话,应该和Detail 和 Activity有关。好找一下。
如图所示,可以看到,这个jar里面大多数是没有用的,都是些第三方的引入。被打包进来的。真正我们写的代码在如图箭头指向的包中。是主要的Activity。找来找去,还真找到了一个叫DetailActivity.class
的类。哈哈...
打开一看彻底懵逼:全是a啊b啊的。这特么怎么看。没办法,反编译比人的工程代码,需要极大的耐心。慢慢来看,有些小技巧在里面,我们并不需要全看,要想理解大体思路,也需要一定的代码基础。至少自己独立架构过一个项目。或者参与过很多项目。
思路:接下来要做的就是找到显示着段文字的View
--->找到这个View的赋值过程
--->找到这个值得来源
--->这个来源中一定包含解密过程
这个思路想起了简单,但是操作起来想当复制,我们可以看到这个DetailActivity.class
类大约有几千行代码啊。如何找?
猜测:据我所知,在Android常用的显示文本的控件也就那么三个TextView
,EditText
,WebView
。到底是哪个,我们要借助于一个工具叫Android Device Monitor
这是分析比人程序布局的利器啊。如何使用?这个地方先空一下,改天补上 因为用起来不难,但是描述起来很烦人。来看分析截图:
从红色箭头标出的部分看,6不6,这界面用了什么View,View的层级关系,View的Id都能得到。拿到了这些再去DetailActivity.class
中分析是不是就简单多了。可以确定这个就是为WebView赋值而已嘛。找到WebView的赋值的地方,也就找到了数据源了。这时候就兴奋的去DetailActivity.class
中找WevView
了,我去,没有???why?
猜测:页面用了Fragment,数据在Fragment中,界面用了组合View,WebView在组合View中。分析到这里似乎难以用一种固定的方式分析了,好像要凭感觉?怎么办,转向layout 的xml文件,找点突破口。这时候就用到jadx出厂了,为啥?自己对比。
从图片中可以看到,我们很容易拿到了,这个activity的布局文件。打开它。
分析后发现果然,主要的数据都放到了ViewPager中。好啊,接下来的主要工作就转战这个ViewPager,在这个DetailActivity中可定有这个Viewpager的引用。继续用jadx。搜索这个ViewPager的id找到,他的引用。如下图。
好嘛,兄弟,你在Activity中的引用叫this.B啊,让我这个好找啊。既然找到了ViewPager,我们想要ViewPager中的每一个Pager的数据,怎么找,找啥?找Adapter
不论是啥,一定会有setAdapter这个方法。继续搜索。this.B.setAdapter
一切如上图。其实感觉好的可以看到我们的方框框起来的就是我们最终要找的。好这个PagerAdapter的名字竟然叫j
。Adapter有两种方式存在,一种是单独的文件,另一种是匿类的方式,这就是我们的开发常识了。先猜测就在本文件中吧。搜索Adapter
.如下图:
从上图中我们可以分析出ViewPager的View集合了,this.a.C
认真读下代码,这个this
指的是j
也即是PagerAdapter的实例对象。而a,指的是DetailActivity.this
,好了,集合对象找到了就是DetailActivity中的C
,搜索this.C
,结果如下图:
图片中包含唯一的自定义View就是DetailContentView
,打开它看看。哈,果然WebView就在这里面。如下图:
这货对外暴露的设置内容的方法为a(string)
,好。在DetailActivity中可以看到:
this.U = (DetailContentView) inflate.findViewById(R.id.detail_zhujie_id);
this.V = (DetailContentView) inflate.findViewById(R.id.detail_xiangjie);
this.W = (DetailContentView) inflate.findViewById(R.id.detail_guhanyu);
this.X = (DetailContentView) inflate.findViewById(R.id.detail_kangxi);
this.Y = (DetailContentView) inflate.findViewById(R.id.detail_shuowen);
this.Z = (DetailContentView) inflate.findViewById(R.id.detail_wys_id);
我们从这里面随便拿一个分析就行。我就选this.W
吧。这个对应详情页的古汉语
展开的详情。
那么这个this.W要想设置数据可定调用了a方法。搜索this.W 大小写敏感。注意自己过滤一些无关的搜索。结果可以看到唯一符合条件的是:
可以看到,这个p.c(this,this.aX.c)+this.aX.c)
就是获取数据源的方法。猜测,这个p.c
方法可能是某个帮助类。对数据进行处理,也可能是某个查询类根据传入的参数来查询,到底是什么呢?我们用jd-gui来帮助我们跟进代码。来跟进参数this.aX.c
打这个东西。(P.S.这个p.c也是相当有用的。剧透一下用于字符串的格式化)如下图:
可以看到 aX就是上图中a这个类的对象,而c就是这个对象的成员,在结合上面的拼音,我们断定,这个a类是和数据库表结构对应的Bean类。进而我断定这个a的外部类就是数据库表的查询类。查询方法在上面。数据库表结构如下图:
从上上个图中,我们从a类的定义往上看,猜我看到了啥。哈哈,getBlob
方法,很多时候感觉还是很重要的。一种熟悉的感觉有木有。一开始的时候我们就是用他读的数据的那个二进制字段。从这行代码往下读,看不懂?我也看不懂。但是我读出来,这个a类中的c字段就是我们要的东西。继续看这个c是怎么获取来的?
locala.c = q.a(paramList,i) //paramlist 就是我们的二进制byte数组,是数组的长度,哈哈,这个方法就是解密方法喽。
我们要做的就是找到,这个方法,把他提取出来,就能解密数据所有的二进制了。这是个静态方法,是比较独立的工具类。所以提取没有什么难度?但是也遇到很多问题,关键是有些加密代码看不懂。
万里长征走一半了?No,才刚刚开始。还记得我们要干嘛嘛?要拿到解密后数据的数据。继续吧:
用jd-gui
跟进代码。q.a(byte[],int)
。反编译查找方法时候一定注意方法签名。有可能都是特么的a
但都是重载,别找错了。这个方法传递一个加密后的二进制数据和他的长度进去,返回一个解密后的字符串。
我发现不能再往下写了,再写人家的加密算法就给拿出来了。
来看这个方法的签名:
P.S.看反编译后的代码要学会自动过滤无关代码
这个方法有用的即使三行。而这三行中唯一不知道的就是d也就是d.getBytes()
中的d,继续分析这个类看看这个d是如何获取的。a(byte[],int)是个静态方法,所以d不可能在构造方法中初始化,一定在这个方法内初始化的,为啥找不到???对比jadx
,效果如下图:
从上图可以看到,jadx的反编译结果,比jd-gui多了一个调用a()
a的空方法,这才符合我的猜想嘛。接下来看看这个a()的定义,只要能拿到d的值。这个解密过程就算完成了。哈哈,这个不能贴了,这是人家的密钥生成方式。不过这个方法我是真心看不懂,不过没关系,这个方法没有引用其他的东西,直接拷出来,引入生成一个d就行了。至此解密完成。如下图所示:
这不太对啊。这数据格式太丑了,还包含什么乱七八糟的字符?要处理这些就是前面所说的字符串格式化。加入css样式和加入html标签。这个过程更是相当复杂。各种猜测和尝试,其中还借助了Smali文件,重新打包,打印log等。不过还好,最终成功了,我给封装成ExplainUtil
,哈哈他的app中也是这么命名的,不过混淆过后就成了a,b,c。。。这种乱七八糟的东西了。
这篇文章写了五个小时~~~