本文为看雪论坛优秀文章
看雪论坛作者ID:乐子人
最近tz不太好用,之前用的破解版的某洞已经彻底挂了。于是在网上找了一个某豹加速器。但是下载后发现只有两小时的会员时间,用完后还要花重金续费。作为社会主义爱国青年,面对这种违法app还在肆无忌惮的收费的现象,当然是不能忍了,于是决心破解之,以达到维护中华人民共和国宪法与法律尊严的目的!
违法app截图:
一
root检测
将app装在手机上打开,结果有提示:
应该是检测到root了,因为我在用一台root过的手机进行实验。
使用jadx加载apk,全局搜索关键字:检测到当前设备。
确实有这个字符串,他的id名叫 toast_app_fail,再次搜索这个id名看看哪里引用了这个:
发现了很多处调用,主要在MainActivity与StartActivity里。出现在这两个文件里也正常,因为这通常是android程序最早启动的两个类,很多检测都会放在启动阶段做。注意到每次调用有不同的后缀,逗号,叹号等等。我们的是逗号,所以选择后缀为逗号的进入。
看到调用了DeviceUtils.a()函数,如果返回true就会弹出 检测设备…字符串,然后退出。看看.a()函数是什么。
好家伙,果然是在检查root。检查设备是否root的常规方式就是尝试打开这些/system目录下的su文件,如果能打开,则说明设备有root权限。
使用MT管理器打开包,找到a函数,通过修改smali代码,让a函数不检测而直接返回false即可:
直接让他返回v0即可。
重新打包,安装。又弹出了那串文字,不过后缀变成了感叹号:
查看这个感叹号出现的位置:
好家伙,看到那个DeviceUtils.AntiRoot()了。如此欲盖弥彰的安全保护,真令破解者狂喜。本着学习的态度,我们还是看一下这个函数怎么antiroot的:
点进去发现是一个native函数,而加载的so叫myapplication:
使用ida加载libmyapplication.so这个文件,全局搜索AntiRoot:
发现还是在检查 su 文件,如果发现有就返回“yes”,没有就返回“no”,这和java代码中检测对上了。
而check_su_files()还是在本地尝试打开各种su文件:
既然如此,可以直接用MT管理器修改java代码,将比对的“yes”改成“f*ck”,这样就算检测到root,java层的比对也会失败。
至此,root检测就全部绕过了。可以打开app了。但是打开后一片空白:
这种情况通常是因为没有拿到正确的返回数据导致的。app打开后肯定会向后端请求各种数据,如果数据异常,则无法正常显示。这时候就要抓包看看了。
二
代理检测
打开手机代理:
启动fiddler,结果app又无法正常启动,这次弹出了这个:
app生怕你不知道他在检测代理,给出了非常温馨的提示。那我们就按图索骥。
看到字符串id是 toast_api_proxy_fail,查找引用:
和root检测的流程基本相同。不过是调用了一个b函数来检测是否有代理的,看看b函数:
可以看到,代理检测的逻辑是要确保 http.proxyHost(代理地址) 为空字符串,或者http.proxyPort(代理端口)为-1。
我们直接修改java代码让他始终返回false就好。
这样之后,就可以成功打开app,并且抓到了数据包:
但是,app内部依然是一片白,查看返回的数据,发现是加密的:
看到后面的== 以为是base64,但是base64解码失败,于是猜测是aes或者des加密。
三
数据解密
去哪里找解密的地方呢?这需要研究andorid系统网络请求的数据流了。
通常,客户端会使用okhttp作为http客户端进行收发请求。(目前还有使用cronet做客户端的,但仅限于字节这样的大厂,而且大多是在okhttp上嫁接的。)
okhttp工作方式是责任链,或者说pipline,每一环节处理一些事情,比如在发送阶段,第一个pipline是添加基本信息,如设备id,时间戳,等等。第二个pipline是计算签名值,第三个pipline是把发送数据加密,然后发出去。
收包时候也有pipline,解密操作一般就放在某个pipline中。所以我们的目的是找到okhttp客户端创建的地方,然后找到他添加pipline的地方。
查看okhttp,好家伙,被混淆了:
通常,okhttp的创建是在okhttpclient这个类里做的。混淆通常只能混淆函数名,类名,但是无法混淆函数的实现,包括一些特殊的字符串,我们可以从字符串入手,找到okhttpclient。
下载一份okhttp的源码,查看okhttpclient这个类有何特征:
发现在内部的builder里,有三个连续的字符串“timeout”,而这是无法被混淆的。我们在jadx里全局搜索“timeout”:
果然有。
点进去看看,发现结构和okhttpclient的结构如出一辙,我们断定这就是okhttpclient类。
查看引用,我们也因此找到了app创建okhttpclient的位置:
okhttp客户端另一个特征是对读写超时的设置,从TIMEUnit也可以看出这是在设置http请求的读写超时。
同时,看到后面一连串的.a函数,这是在添加一个一个pipline。(okhttp中叫拦截器,不过我个人感觉本质就是pipline)
分别查看这些pipline。
一个是添加请求头的:
还有一个是和数据处理有关的:
看到里面的response,bodystring字符串,好有d_key_three,不由的让人浮想联翩,这是在做什么,为什么出现了秘钥和返回体?
点进去看f5865a.a这个函数:
实锤!看到了password,iv,SecretKeySpec,Cipher,doFinal这些特征。显然是在做aes解密。
为了更准确,看看SecretKetSpec 中的f5866b与Cipher.getInstance(f)中的f分别是什么?
f5766b与f都反编译崩了,转到smali看看:
确信了,在用AES解密,模式是 CBC,填充方式是PKCS5padding。
我们hook这个a函数,看看解密后的值是什么。
写一个简单的frida脚本:
hook后发现,打印出了奇怪的东西:
传入的参数确实是加密的返回数据,但是解密后没有东西。而且秘钥很怪,以error结尾?
查看系统日志:
发现解密失败,报错Unsupported key。
仔细看了一下,所使用的的秘钥和iv 确实是14字节。而aes通常有128,192,256三种秘钥,所以秘钥只能是16,24,32字节,那么显然秘钥错了。
找找秘钥生成的地方,发现秘钥是由三部分相加得来:
第一部分是从资源文件中获取的:
第二部分是通过app内置数据库获取的,不过采用的是默认值:
第三部分是从native获取的:
查看这个native函数:
原来,在native获取秘钥的时候又做了一起签名检查!如果签名不对则返回error,只有签名正确才会返回正确的后缀。
修改frida脚本:
直接将正确的秘钥强行塞给解密函数,这次终于解密了,不过:
返回的数据显示“签名验证失败”。
四
签名校验
因为签名验证失败,所以没有拿到正确的返回数据。我们看看签名是怎么算的。
在http请求头里看到了sign字段:
这个就是签名。
因为通常,签名的命名方式为sign,sig,authcode,sec之类。特殊的例如某音,使用了希腊神话中的神的名字来命名。
jadx全局搜 “sign”。(注意用带双引号的sign来搜索,这样会提高搜索效率。因为通常生成sign时会把sign作为key,put进一个map里。而key是一个String,所以代码中生成sign的地方一定有字符串”sign”)
果然有:
看看a7的生成:
阅读a函数,可以知道:
先加入了ts参数作为时间戳。然后把所有参数首尾相连。然后添加了类似于aes秘钥的后缀。然后通过另一个a函数计算a2,再将a2全部小写。这就是签名的生成。
看看另一个a函数:
是在算MD5摘要。
那么问题就出在了那个类似于秘钥的后缀上。
确实,因为签名错误了所以也返回了错误的后缀,不过使用的是aaxx函数,不同于aes时候使用的ddmm函数。
同样的套路,签名不正确就返回error,正确才返回真正的后缀。
看看本地是怎么做签名校验的?
通过反射获取signature,然后计算MD5,与硬编码的正确SIGN_MD5进行比对。
既然如此,我们hook aaxx函数让他返回正确的值:
再次运行后,成功得到了正确的数据:
同时,app界面也正常了,不过显示已到期:
五
无限续杯
我们成功绕过了root检测,代理检测,签名校验。但是每一台设备只有2小时的免费机会,我们想要白嫖,怎么办?
通常app会通过唯一设备标识来跟踪设备,即deviceID。
deviceId可以有很多种计算方式。可以直接获取安卓设备本身的唯一标志(高版本android禁用了),可以获取设备的MAC地址,也可以生成一个UUID,或者随机字符串,藏在设备的某个角落。当apk重新安装时,先去看看之前这里有没有藏过相关的文件,如果有,就直接读出来当做设备ID,如果没有,则创建一个。这样做可以保证apk删除前后依然能跟踪设备。当然,如果你能发现这个文件,并且把他删掉,那么下次安装的时候app就会认为是一个新的设备了。
我们先找找deviceid在哪,全局搜索deviceid:
发现了跟多地方,有from app,有from sdcard。
跟入一个sdcard相关的函数:
看到了是从f文件读取的设备id。我们看看f文件的路径:
好家伙,在alarms文件夹呢。打开看看:
果然,sd卡下的alarm文件下有一个文件,打开后的确是发送数据时使用的设备id。
删掉这个文件,重新启动,发现设备id还是没有变化,难道还有其他地方存着?
注意到上面的from app:
看到deviceid是从b函数获取的,看看b函数:
懂了,原来是从sharedpreferences里拿的。
SharedPreferences 是app内部存储数据的一种方式,而sd卡是一种外部存储方式。具体知识可以参考Android数据存储相关文章。
我们打开sharedPreferences
果然看到了customdeviceid:
看看他的设备id是怎么获取到的?
首先通过a(10)获取了一个10位的随机字符串a2:
然后计算了“33dfdfer21”+a2+”sddddsfe”的md5值 a3。
最后用“33dfdfer21”+a3+a3[0:10]为最终的设备id。
所以设备id是随机生成的。
每台新设备有2小时的免费时间,也就是7200秒。我们考虑每7000秒重新生成一次设备id,不就可以无限续杯了吗?
看看app在那里获取到这个deviceid并发送给服务器的,注意到之前的okhttp 的pipline中有一个就是添加http头的:
看到deviceid是通过a()方法获得的。查看a()方法:
最终是返回了一个字符串。其实不用继续跟下去了,无非是从app获得或者从sd卡获得。我们在这里hook就行。
理论上只要写一个字符串随机生成算法,保证2小时内保持一直就行了。我这里使用时间戳除以7000的方式获取:
字节写一个函数,然后使用android studio编译出smali代码。将对应的smalidaima插入到a()函数返回之前即可:
随机设备id的smali代码:
插入到a()函数返回之前:
这样,我们每隔两小时会使用全新的deviceid,对服务端来说好像是新的设备安装了他的app,然后就可以又白嫖两小时的免费时间了。
六
后记
这些不符合社会主义核心价值观的软件,都要统统破解了才好。为了维护社会正气,我辈当仁不让。
同时仅就安卓安全保护而言,这款app该做的都做了,但路数都比较常规。
唯一的亮点是将秘钥分段存储,在签名错误时返回错误的秘钥以对包体篡改提供更强的保护。
本次教程以学习研究为目的,请勿以此做违法犯罪的事情。遵纪守法的社会主义好公民,从我做起。
看雪ID:乐子人
https://bbs.kanxue.com/user-home-872365.htm
# 往期推荐
球分享
球点赞
球在看
点击“阅读原文”,了解更多!
原文始发于微信公众号(看雪学苑):无限续杯——从app破解角度学习安卓保护手段