Android逆向分析案例——某点评APP登陆请求数据解密

今天,七夕,单身23载的程序汪,默默地写着博客~

上一次的逆向分析案例中讲了如何去分析某酒店的APP登陆请求,为了进一步学习如何逆向分析以及学习其他公司的网络传输加解密,本次案例将继续就登陆请求的数据加密进行分析,实现从网络抓包的密文到明文的转换,这次成为炮灰的是某点评的APP~

首先,分析的步骤还是和上一次分析某酒店的APP那样:

反编译-->找关键代码-->分析请求代码加解密原理-->验证密文到明文准确性

分析步骤的重点主要是第3步和第4步,而且这两步往往是相互结合的,就有点像软件上的单元测试一样,每分析完一个加解密的地方就编写相应的测试代码去验证是否是分析中得到的加解密原理。

本次用到的工具有:APKTool(反编译),Sublime Text(smali阅读器),Xposed(hook方法,验证是否调用),Cydia Substrate(hook方法),Android Studio(编写Xposed和Cydia的hook模块),Eclipse(编写测试代码验证加解密原理),wireshark(抓包),IDA PRO(看so库),测试机。

说明:1、和上次不同,之后的分析我基本不会再用dex2jar和jd-gui这两个工具,为什么呢?因为它从smali转java不可靠,错的地方很多,只能辅助用,而且现在对smali语法已经比较熟悉,所以一般会直接看smali文件进行分析,嗯,这是进步~

2、为什么用了Xposed进行hook了,还要用Cydia来hook?因为APK反编译后,若体积较大,往往会分割成几个smali子文件夹,Xposed只能hook第一个smali文件夹下的smali文件,第二个smali文件夹下的smali文件是无法hook得到的,原因不明;其次,Cydia还能hook本地方法,也就是标记为native的方法。那为什么还用Xposed呢?这个我表示实在惭愧,Cydia最近才用,还不是很熟悉。。。

3、Xposed和Cydia都需要手机root过,而且最好不要同时安装在同一部手机,因为有可能会死机,或者发生日志输出冲突等情况。       

明确需求:分析登陆请求数据的加密原理。

开始:

----------------------------------------------------------

利用APKTool对目标APK反编译,并用Sublime Text打开:


   我们可以看到在整个反编译的工程文件夹下有三个smali的文件夹,其中第二个和第三个smali文件夹下的smali文件是无法用Xposed工具来hook的,所以需要用到Cydia Substrate。

通过关键字符串找关键代码,我们可以从登陆请求的网络抓包观察,输入账号密码后,在wireshark的过滤器输入“login”,按登陆的那一瞬间看看wireshark:


我们可以抓到一个TCP包,那么打开这个TCP流看看:


从抓包的TCP流可以看到有一段明文头(json格式)和拼接了一大段密文,很好,那段密文就是我们要解密的~

这里,我们可以推断,登陆没有用HTTP协议,所以我们可以推测登陆请求是用Socket类写的请求,其次,我们可以看到明文中的关键字符“u”,“m”,“i”等。

So,我们在打开的sublime text代码阅读器上通过“Find in folder”的功能搜索关键词“u”:


有三个文件,先看第一个文件,可以看到m.smali文件里有“u”的身影,那么我们打开m.smali文件,并找到“m”的字符的位置:


可以看到“u”,"i"关键词,并且进行json的拼接,而且看到下面的Socket了吗?基本可以确定这个m类就是一个socket通信类,还有密文发送到网络的入口OutputStream~

那么既然知道了入口,我们就要开始逆方向跟踪数据是怎么到达这里的,并找出中间的加密位置,也即是关键代码:


我们编写Xposed模块hook该方法的第4个参数,也即是那个数组,并以十六进制的形式打印出来:


然后我们通过打印出来的日志,复制一小段去wireshark抓包窗口那里验证一下:


将数据显示设置为原始数据,然后ctrl+f查找复制过来的那一小段日志,发现抓包的数据里包含这一小段,说明确确实实是密文的byte[],验证成功。

既然知道实例m的字段f是数据的密文,那么我们就沿着该数据去追溯前面的代码。

我们可以根据smali语法格式,编写需要找的f字段形式   “Lcom/dianping/nvnetwork/e/q;->f:[B”  来进行“Find in folder”,找到字段 f 被赋值的位置:


可以看到有4处地方,下面的m.smali文件不用看了,因为都是“iget-object”,就是取值,因为我们需要知道是谁给f 字段赋值了,所以我们只关注“iput-object”,因此,我们可以直接打开e.smali文件,并找到该字段被赋值的地方:


可以看到,数据byte[ ]是从一个InputStream转化而成的,那么这个这个InputStream又是从哪来的呢?从参数寄存器可以看到v2寄存器,那么从上面找v2寄存器的赋值位置:


该InputStream是有一个u的实例调用h( )方法得到的。那么疑问就来了,从InputStream到byte[ ]会不会有加密?先说,没有的,但还是怀疑?很好,hook吧,反正一开始我也是那么多疑,虽然费时间,但一步一步来,心里踏实~

但这里有个关键的地方,因为你要hook的参数是InputStream,也就是意味着你要将InputStream的流read到ByteArrayOutputStream内再转byte[ ],这时你必须要对进行了此操作的InputStream调用reset方法还原,因为处理过流的同学都清楚,InputStream只能读一次,若要重新使用,必须得进行mark,reset等操作,不然你的InputStream被修改了,原APP调用该方法时的参数前就已被Xposed修改,造成登陆失败,从而抓不了包,导致没有数据校验,所以即使你hook到InputStream的内容也没有抓包数据给你校验,这是很蛋疼的,反正我之前不知道reset这个方法,在这里浪费了不少时间,实在惭愧~

下面是示例:


好了,我们可以知道InputStream的内容还是密文,那么我们继续跟踪,我们去看看u类,因为该类相当于一个Request类,InputStream就是u类的g 字段。

那么我们hook一下h()函数吧:


可以看到,h( )方法返回的结果先是明文,然后是密文,因为h( )相当于javabean里的get( )方法,没有对InputStream进行处理,所以我们有理由推测,该h( )方法先是提供明文给加密函数,加密完后再调用该方法将InputStream提供给Socket的网络入口,那么h( )提供明文这段代码,会先在哪里调用呢?

接下来,我要放大招了,但在放大招之前先问:这样一步一步的找数据是不是有点低效率?没错,实在是太繁琐了,那么看招~

在hook方法中刻意抛异常,通过异常路径找数据来源:(因为h( )方法是没有参数的,我们刻意制造一个参数传进去,制造异常,并观察异常日志)


其实,使用这种方法是有一定风险的,因为虽然是可以看到方法一层一层的调用位置,但你不能确定数据是否在这一系列嵌套函数中是否贯穿,这是缺点。

但还是建议使用的,尤其是针对本本案例中存在两个常见的问题,使用该方法往往能事半功倍~

而且,当你在“Find in folder”中搜索“Lcom/dianping/nvnetwork/u;->h()”时,你会被吓一跳,因为很多地方都调用了h( )这个函数,难道你要一个一个smali文件都去找?显然不能,这样效率太低了,所以,还是使用该方法吧,哈哈~

问题实例:

第一种:假设  a 类   implements  b类,a类实现了b类的接口方法c( ),那么在Sublime Text中根据smali格式

                不能搜  Lcom/.../a;->c()    而是  搜  Lcom/.../b;->c(), 即只能搜接口类->方法,但在Xposed中必须hook方法a( )而不是接口类的方法a( )。

第二种:假设  a 类    extends   b类, a类实现了b类的抽象方法c(), 那么在Sublime Text中根据smali格式

                不能搜  Lcom/.../b;->c(),   而是 搜  Lcom/.../a;->c(), 即只能搜子类->方法,在Xposed中需要hook非抽象方法。

因为笔者之前不知道这规则,明明数据追溯到那里,但hook的时候却发现没有调用,原因就是那些继承和接口的原因,所以追溯数据会很绕,阻力很大,那么此时你就可以使用这种方法,让异常路径告诉你是在哪里被调用了,然后一层一层地追踪数据即可

从异常路径中,我们可以先找com.dianping.h.f.a.o文件中的b方法和a方法:

先看看b方法:


很显然,传一个u类实例进去,又出来一个u类实例,我们可以猜测,这中间是否有猫腻,那么事不宜迟,我们hook吧,比较前后两个u类实例的字段 g(即InputStream)是否是一致的,若不一样,那么说明关键代码就在里面了。这里hook需要掌握一些反射机制的知识,通过反射找到u类,再从u类找到字段g,Xposed写法如下(因为在Cydia中找不到相应的反射或找类的API,所以才一边用Xposed,一边用Cydia).


好,模块写好的了,安装到手机上,然后Xposed框架会在通知栏提醒,模块已更新,请软重启。。。

我们来看看日志:


可以看到,before hook 之前是明文, after hook 之后是密文,那么我们基本上已经可以确定,这个方法之后的代码就是我们需要的关键代码,也即是加密代码。

我们回到smali文件里,继续看看这个方法里面。

我们知道,h( )这个函数相当于get InputStream了,即从u类实例获取 g 字段,之前说过,h( ) 前后既有密文,又有明文,那么我们当时推断 h( )先是把 InputStream get出来后,进行加密,再把密文封装成 InputStream 的形式 回到 u类实例。

So,我们看看之前利用 异常路径 所知道的 h( )调用的位置在哪里。


我们可以在当前的smali文件里搜索 “ .line 68” 字符串(不需要 “Find in folder”,就在当前smali内搜索):


在上面提到的  传 u 返 u  的这个方法内,还有一个 InputStream 返 InputStream 的方法,我们可以继续hook,这里我就不贴出来了,结果一样,根据hook到的参数InputStream内容判断,确实就是 h( )提供的明文, 而返回InputStream的内容则是密文。

然后,我们找到p类,在p 类的 a(InputStream)InputStream 方法里,它将 InputStream 作为参数 初始化了 q 类,这是另外一个类,该类是一个处理加密的类:


因为数据被作为InputStream传到q类实例去,那么我们接着看q类,q.smali的代码很少,除了一个构造函数外,还有一个 a( ) 方法, 该方法返回一个InputStream,更重要的是浏览一下这个a( )方法,我们可以发现了用于加密的东西:


我们知道,因为java层容易被反编译,一般加密这种核心代码,往往会用C语言写在SO库里(即使被反编译了,也是一堆可读性很差的汇编语言),防止反编译,增强其保密性,然后通过本地调用,即native修饰的函数进行加密。

我们去找NativeHelper这个类:


这里我就说出来吧, ne方法就是请求数据的加密函数, ndug就是服务器返回数据的解密。

到这里,我们就不能用Xposed进行hook了,因为这个smali文件是在smali_classes2里,即第二个smali文件夹,而且这是个native方法,Xposed是无能为力的。那么我们用Cydia去hook这个ne函数吧:


接着我们将模块安装在装有Cydia Substrate的手机上,安装完后Cydia会在通知栏通知已更新模块,然后软重启,跟Xposed一样~

然后,我们看看日志:


可以看到,ne这个函数有4个byte参数,均为数组,其中before hook之前第一个数组是明文,第二个0数组,第三和第四个都是固定字节数组,after hook之后,第二个数组变成了密文,而其他三个数组没有变化。

因此,我们可以推测,第三第四个数组应该是加密的key。

我们此时在看看NativeHelper这个类的构造体:


由“nh”这个字符串可知,这是一个libnh.so库,那么我们去找APKTOOL反编译出来的那个文件夹下的lib文件夹,看看是否有这个so库:


为了查看这个so库,我们需要利用 IDA PRO 打开,该工具是世界上最强的反编译工具,网友如此说~那么我们就用它打开吧:


如果你不是大牛,劝你不要去阅读里面的汇编,我这里用的是IDA PRO 6.6,找到ne函数的代码,然后按“F5”功能键,将汇编转成C语言,这个转换并不一定准确,而且可读性很差,都是一些指针操作,但我们可以从里面找到一些有用的信息。

没错,AES_cbc,这是运用了AES加密的CBC模式,因为CBC模式需要用到一个key和一个初始化向量IV,所以前面ne方法中的第三第四个固定字节数组也就知道是什么回事了吧,对,第三个是key,第四个是向量IV,不懂什么是CBC加密模式的自行百度。

这里我不得不用红字吐槽一下,大哥,你煞费苦心地用C写个加密方法在so库,在java层你懂得用“ne”这个迷之字符串声明,在C里却用“AES_cbc”这么显眼的命名方式作为方法名,为什么不用迷之字符串+注释的方法呢?你是欺负我不懂什么是 AES的CBC模式吗?

好吧,菜鸟的我也没资格说别人,继续吧~

既然我们知道是AES加密,那就开始写测试代码测试吧,在eclipse上如此写道:

	public static byte[] AESDecrypt(byte[] content, byte[] keyBytes, byte[] iv){
		
		try{
			SecretKeySpec keySpec=new SecretKeySpec(keyBytes, "AES");
			Cipher cipher=Cipher.getInstance("AES/CBC/NoPadding");
			cipher.init(Cipher.DECRYPT_MODE, keySpec, new IvParameterSpec(iv));	
			byte[] result=cipher.doFinal(content);
			return result;
		}catch (Exception e) {
			// TODO Auto-generated catch block
			System.out.println("exception:"+e.toString());
		} 		
		return null;
	}
将wireshark上面的抓包密文的原始数据(十六进制)复制到eclipse上面,然后调用该函数,即可得到明文字符串。



其实,那段明文中还有一段加密的内容,就是   cx=密文   这一段。

其实要解cx这一段也很简单,首先,“Find in folder” 功能搜索 “cx” 字符串:


找到“cx”的smali文件有很多个,但我们可以随便挑一个,不用去hook验证是否是登陆的“cx”,因为我们只需要知道其加密原理而已,其它的“cx”估计应该也是用同一套加密方法而已,那么我们就打开第一个吧,刚好第一个跟login有关。

接着顺藤摸瓜,一直跟着String或者byte[ ]这样的数据去找,最后找到了一个加密类的方法a(String, String):


这个smali是在第二个文件夹里的,所以要hook的话需要用Cydia,这里我就不hook了,结果告诉大家,这是一个DES加密,第一个参数是明文,第二个参数是key,返回的结果就是一个   DES+Base64 的字符串, 我们在Eclipse上面编写一下测试代码,但我们先看一下刚才那个cx的密文:


可以看到,cx里面的密文含有%符号,这并不是Base64的编码符号啊?没错,实际上它还套了一层URLEncode编码上去,其中的“%2F”=“/”,“%0A”=换行符,“%2B”=“+”等,所以在调用下面的方法DES解密前先URL解码,再Base64解码,再DES。即  URL Decode-->Base64 Decode-->DES 的顺序。

	public static byte[] DESDecrypt(byte[] content, byte[] keyBytes){		
		try {
			DESKeySpec keySpec=new DESKeySpec(keyBytes);
			SecretKeyFactory keyFactory=SecretKeyFactory.getInstance("DES");
			SecretKey key=keyFactory.generateSecret(keySpec);
			
			Cipher cipher=Cipher.getInstance("DES/CBC/NoPadding");
			cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(keySpec.getKey()));
			byte[] result=cipher.doFinal(content);
			return result;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			System.out.println("exception:"+e.toString());
		}
		return null;
	}
将刚才AES解密的那一串明文中的 cx = 后面的数据截取出来,经过解码后,结果:



cx明文json里的的key-value中的key用了“A+数字”的形式,估计他们那边还有一张key对应的值表,不管了,反正我们已经大概能知道里面就是一些imei的手机状态信息。

到此为止,我们已经完成了登陆请求的数据解密任务。

请求的返回结果也一样,也是调用了AES_CBC进行解密,再通过GzipInputStream解压后的到序列化的字节流,这里就不继续讲了。

-----------------------------------------------------

结束。

纸上得来终觉浅,绝知此事要躬行~

别看我写得那么简单,这里博客我只是把正确的分析过程写出来了,但实际上为了解那么一个解密内容,走了N多弯路,毕竟本人还是菜鸟的原因吧,不过正是走的弯路多了,你才能有成长,有收获,才能在下一次少走弯路~

路漫漫其修远兮,吾将上下而求索~

最后,再次祝贺七夕快乐——单身23载的程序汪。

已标记关键词 清除标记
相关推荐
Android软件安全与逆向分析》由浅入深、循序渐进地讲解了Android系统的软件安全、逆向分析与加密解密技术。包括Android软件逆向分析和系统安全方面的必备知识及概念、如何静态分析Android软件、如何动态调试Android 软件、Android 软件的破解与反破解技术的探讨,以及对典型Android病毒的全面剖析。 作者简介:丰生强(网名非虫),Android软件安全专家,看雪论坛Android安全版版主,安卓巴士开发交流版版主。 目录 第1章  Android程序分析环境搭建 1.1  Windows分析环境搭建 1.1.1  安装JDK 1.1.2  安装Android SDK 1.1.3  安装Android NDK 1.1.4  Eclipse集成开发环境 1.1.5  安装CDT、ADT插件 1.1.6  创建Android Virtual Device 1.1.7  使用到的工具 1.2  Linux分析环境搭建 1.2.1  本书的Linux环境 1.2.2  安装JDK 1.2.3  在Ubuntu上安装Android SDK 1.2.4  在Ubuntu上安装Android NDK 1.2.5  在Ubuntu上安装 Eclipse集成开发环境 1.2.6  在Ubuntu上安装CDT、ADT插件 1.2.7  创建Android Virtual Device 1.2.8  使用到的工具 1.3  本章小结 第2章  如何分析Android程序 2.1  编写第一个Android程序 2.1.1  使用Eclipse创建Android工程 2.1.2  编译生成APK文件 2.2  破解第一个程序 2.2.1  如何动手? 2.2.2  反编译APK文件 2.2.3  分析APK文件 2.2.4  修改Smali文件代码 2.2.5  重新编译APK文件并签名 2.2.6  安装测试 2.3  本章小结 第3章  进入Android Dalvik虚拟机 3.1  Dalvik虚拟机的特点--掌握Android程序的运行原理 3.1.1  Dalvik虚拟机概述 3.1.2  Dalvik虚拟机与Java虚拟机的区别 3.1.3  Dalvik虚拟机是如何执行程序的 3.1.4  关于Dalvik虚拟机JIT(即时编译) 3.2  Dalvik汇编语言基础为分析Android程序做准备 3.2.1  Dalvik指令格式 3.2.2  DEX文件反汇编工具 3.2.3  了解Dalvik寄存器 3.2.4  两种不同的寄存器表示方法--v命名法与p命名法 3.2.5  Dalvik字节码的类型、方法与字段表示方法 3.3  Dalvik指令集 3.3.1  指令特点 3.3.2  空操作指令 3.3.3  数据操作指令 3.3.4  返回指令 3.3.5  数据定义指令 3.3.6  锁指令 3.3.7  实例操作指令 3.3.8  数组操作指令 3.3.9  异常指令 3.3.10  跳转指令 3.3.11  比较指令 3.3.12  字段操作指令 3.3.13  方法调用指令 3.3.14  数据转换指令 3.3.15  数据运算指令 3.4  Dalvik指令集练习--写一个Dalvik版的Hello World 3.4.1  编写smali文件 3.4.2  编译smali文件 3.4.3  测试运行 3.5  本章小结 第4章  Android可执行文件 4.1  Android程序的生成步骤 4.2  Android程序的安装流程 4.3  dex文件格式 4.3.1  dex文件中的数据结构 4.3.2  dex文件整体结构 4.3.3  dex文件结构分析 4.4  odex文件格式 4.4.1  如何生成odex文件 4.4.2  odex文件整体结构 4.4.3  odex文件结构分析 4.5  dex文件的验证与优化工具dexopt的工作过程 4.6  Android应用程序另类破解方法 4.7  本章小结 第5章  静态分析Android程序 5.1  什么是静态分析 5.2  快速定位Android程序的关键代码 5.2.1  反编译apk程序 5.2.2  程序的主Activity 5.2.3  需重点关注的Application类 5.2.4  如何定位关键代码--六种方法 5.3  smali文件格式 5.4  Android程序中的类 5.4.1  内部类 5.4.2  监听器 5.4.3  注解类 5.4.4  自动生成的类 5.5  阅读反编译的smali代码 5.5.1  循环语句 5.5.2  
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页