APP逆向分析
一、Root检测
- 将APP包安装在root过的手机上并打开,结果有提示:这应该是检测到root了。
- 使用
jadx
加载APK
,全局搜索关键字:检测到当前设备 - 已搜索到这个字符串信息,它的ID名叫
toast_app_fail
- 通过ID定位该字符串在哪里引用
- 搜索到许多处调用,主要在
MainActivity
与StartActivity
这两个文件里- 因为这两个文件通常是APP中最早启动的两个类,很多检测都会放在启动阶段去实现调用
- 注意到每次调用有不同的后缀,逗号,叹号等等。
- 提示的字符串是逗号,所以选择后缀为逗号的进入。
- 看到调用了
DeviceUtils.a()
函数,如果返回true
就会弹出检测信息的字符串,然后退出。 - 接下来就是分析.a()函数具体怎么实现
- 上面的代码是通过检查文件方式进行判断是否运行环境已经
root
了 - 测是否
root
的常规方式就是尝试打开这些/system
目录下的su
文件- 如果能打开,则说明有root权限
- 上面的代码是通过检查文件方式进行判断是否运行环境已经
- 使用
MT
管理器打开APK
包,找到DeviceUtils
类中a
函数- 通过修改
smali
代码,让a函数
不检测而直接返回false
即可(也可以用androidkiller、jeb等)。 - 将返回值中修改成返回v0即可。重新打包,安装
- 又弹出那串文字,不过后缀变成了感叹号!
- 通过修改
- 继续查找字符串信息,定位这个
感叹号
出现的位置 - 定位到
DeviceUtils.AntiRoot()
逻辑下面- 如此欲盖弥彰的安全保护,真令破解者狂喜。
- 接下来定位分析下
AntiRoot
函数- 点发现它是一个
native
函数,加载的so
是myapplication
:- 在通过
ida
在so
中全局搜索AntiRoot
关键字
- 在通过
- 点发现它是一个
- 发现还是在检查
su
文件- 如果
check_su_files()
检测到是root
就返回“yes
”,没有就返回“no
”,这和java
代码中检测相对应
- 如果
- 接下来可以直接用
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内部依然是一片白,查看返回的数据,发现是加密的
1.数据解密
- 一般情况下
APP客户端
会使用okhttp
作为http客户端
进行收发请求- 目前还有
cronet
做客户端的,但仅限于大厂使用,而且大多是在okhttp
上定制的 okhttp
工作方式是责任链,也叫管道传输(pipelien
)- 每一环节处理一些事情,比如在发送阶段
- 第一个pipline是添加基本信息,如设备id,时间戳等等
- 第二个pipline是计算签名值,第三个pipline是把发送数据加密,然后发出去。
- 目前还有
- 分析APP中的
okhttp
,发现被混淆保护了okhttp
的创建是在okhttpclient
这个类里做的- 混淆通常只能混淆函数名,类名,但无法混淆函数的具体实现,包括一些特殊的字符串
- 可以从字符串入手,找到
okhttpclient
。
- 下载一份
okhttp
的源码,查看okhttpclient
这个类有什么特征- 源码地址
https://github.com/square/okhttp
- 源码地址
- 发现在内部的
builder
里,有三个连续的字符串“timeout
”- 而这是无法被混淆的。我们在jadx里全局搜索“
timeout
”:
- 而这是无法被混淆的。我们在jadx里全局搜索“
- 点进去看看,发现结构和
okhttpclient
的结构如出一辙,这样就可以判断这就是okhttpclient
类。 - 查看引用,我们也因此找到APP创建
okhttpclient
的位置okhttp
中客户端另一个特征,对读写超时的设置- 从
TimeUnit
可以看出这是在设置http请求
的读写超时。
- 同时,看到后面一连串的
.a函数
,这是在添加一个一个pipline
- khttp中叫拦截器,不过我个人感觉本质就是pipline
- 分别查看这些
pipline
,一个是添加请求头的 - 还有一个它是和数据处理有关的:
- 看到里面的
response
,bodystring字符串
,还有d_key_three
,这是在做什么,为什么出现了秘钥和返回体?
- 看到里面的
- 点进去看
f5865a.a
这个函数:- 看到了
password,iv,SecretKeySpec,Cipher,doFinal
这些特征 - 显然是在做
aes解密
,通过在用AES解密
,模式是CBC
,填充方式是PKCS5padding
。 - 用frida hook这个a函数,看看解密后的值是什么?
- 直接将正确的秘钥强行塞给解密函数,终于解密完成了
- 看到了
- 返回的数据显示“签名验证失败”,接下来就是要分析下签名验证功能
三、签名校验
- 因为签名验证失败,所以没有拿到正确的返回数据。需要分析下签名是怎么计算的
- 在
http
请求头里看到了sign
字段:- 一般情况下,签名的命名方式为
sign,sig,authcode,sec
之类- 特殊的例如某音,使用了希腊神话中的神的名字来命名。
- 一般情况下,签名的命名方式为
jadx
全局搜"sign"
- 注意用带双引号的
sign
来搜索,这样会提高搜索效率。 - 因为通常生成
sign
时会把sign
作为key
,put
进一个map
里。 - 而
key
是一个String
,所以代码中生成sign
的地方一定有字符串"sign"
- 注意用带双引号的
- 分析下
a7
的生成过程,因为sign
来源a7
赋值。- 先加入了ts参数作为时间戳。然后把所有参数首尾相连。
- 再添加类似于aes秘钥的后缀。
- 然后通过另一个a函数计算a2,再将a2全部小写。
- 这就是签名的生成过程。
- 看看另一个a函数
- 是在计算MD5摘要。那么问题就出在了那个类似于秘钥的后缀上。
- 确实,因为签名错误了所以也返回了错误的后缀,不过使用的是
aaxx函数
- 不同于aes时候使用的
ddmm函数
。
- 不同于aes时候使用的
- 同样的套路,签名不正确就返回error,正确才返回真正的后缀。
- 看看本地是怎么做签名校验的?
- 通过反射获取
signature
,然后计算MD5,与硬编码的正确SIGN_MD5
进行比对。
- 通过反射获取
- 既然如此,我们
hook aaxx函数
让他返回正确的值: - 再次运行
frida
脚本后,成功得到了正确的数据: - 同时,
app界面
也正常了,不过显示已到期:
四、绕过限制
-
成功绕过了root检测,代理检测,签名校验。
- 但是每一台设备只有2小时的免费机会,我们想要白嫖,怎么办?
-
通常APP会通过唯一设备标识来跟踪设备,即设备指纹技术
- Deviceid可以直接获取安卓设备本身的唯一标志(高版本android禁用了)
- 也可以获取设备的MAC地址,也可以生成一个UUID,或者随机字符串,藏在设备的某个角落
- 当apk重新安装时,先去看看之前这里有没有藏过相关的文件
- 如果有,就直接读出来当做设备ID
- 如果没有,则创建一个
- 这样做可以保证apk删除前后依然能跟踪设备
- 当然,如果你能发现这个文件,并且把它删掉,那么下次安装的时候app就会认为是一个新的设备了
-
先找找
deviceid
在哪,全局搜索deviceid
-
发现了许多地方,有
from app
,有from sdcard
,跟入一个sdcard
相关的函数: -
看到了是从
f文件
读取的设备id
。看看f文件
的路径:- 果然,
sd卡
下的alarm文件
下有一个文件,打开后的确是发送数据时使用的设备id
- 删掉这个文件,重新启动,发现
设备id
还是没有变化,难道还有其他地方存着?
- 果然,
-
注意到上面的
from app
: -
看到
deviceid
是从b函数
获取的,看看b函数
:- 原来是从
sharedpreferences
里拿的SharedPreferences
是app内部存储数据的一种方式,而sd卡
是一种外部存储方式。
- 原来是从
-
我们打开
sharedPreferences
,果然看到了customdeviceid
:- 看看他的
设备id
是怎么获取到的? - 首先通过
a(10)
获取了一个10位的随机字符串a2
, - 所以设备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,然后就可以又白嫖两小时的免费时间了。
-
-