Android逆向学习笔记(九)
《安卓逆向这档事》六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向 - 吾爱破解 - 52pojie.cn
目录
什么是校验
是开发者在数据传送时采用的一种校正数据的一种方式
常见的校验有:签名校验(最常见)、dexcrc校验、apk完整性校验、路径文件校验等
这里我有一个疑问:矫正数据是什么意思,一般我们理解的都是修正的意思,但在这里我觉得应该是判断与之前一样不一样的意思,为什么要使用这个词??
在逆向和安全环境中(判断的意思)
但在你看到的安卓代码或逆向分析中,它更多是指 “校验” 。
- 核心逻辑 :判断“现在的”和“原来的”一不一样。
- 为什么也叫矫正? ** 因为它是在核对正误**。如果不一样,系统会认为当前数据是“歪的”(被篡改了),从而拒绝运行。
什么是apk签名
通过对 Apk 进行签名,开发者可以证明对 Apk 的所有权和控制权,可用于安装和更新其应用。而在 Android 设备上的安装 Apk ,如果是一个没有被签名的 Apk,则会被拒绝安装。在安装 Apk 的时候,软件包管理器也会验证 Apk 是否已经被正确签名,并且通过签名证书和数据摘要验证是否合法没有被篡改。只有确认安全无篡改的情况下,才允许安装在设备上。
简单来说,APK 的签名主要作用有两个:
- 证明 APK 的所有者。
- 允许 Android 市场和设备校验 APK 的正确性。
Android 目前支持以下四种应用签名方案:
v1 方案:基于 JAR 签名。
v2 方案:APK 签名方案 v2(在 Android 7.0 中引入)
v3 方案:APK 签名方案 v3(在 Android 9 中引入)
v4 方案:APK 签名方案 v4(在 Android 11 中引入)
V1 签名的机制主要就在 META-INF 目录下的三个文件,MANIFEST.MF,ANDROID.SF,ANDROID.RSA,他们都是 V1 签名的产物。
在某些情况下,直接对apk进行v1签名可以绕过apk的签名校验
v2方案会将 APK 文件视为 blob,并对整个文件进行签名检查。对 APK 进行的任何修改(包括对 ZIP 元数据进行的修改)都会使 APK 签名作废。这种形式的 APK 验证不仅速度要快得多,而且能够发现更多种未经授权的修改。
什么是签名校验
签名校验是应用在运行时,通过代码主动调取系统接口,核对当前 APK 的数字签名(指纹) 是否与开发者预设的 原始指纹 一致的过程。
怎么判断apk文件是不是有签名校验
不做任何修改,直接签名安装,应用闪退则说明大概率有签名校验
一般来说,普通的签名校验会导致软件的闪退,黑屏,卡启动页等
当然,以上都算是比较好的,有一些比较狠的作者,则会直接rm -rf /,把基带都格掉的一键变砖。
1 | kill/killProcess-----kill/KillProcess()可以杀死当前应用活动的进程,这一操作将会把所有该进程内的资源(包括线程全部清理掉).当然,由于ActivityManager时刻监听着进程,一旦发现进程被非正常Kill,它将会试图去重启这个进程。这就是为什么,有时候当我们试图这样去结束掉应用时,发现它又自动重新启动的原因. |
使用demo认识签名校验
直接使用MT管理器对apk文件进行签名
签名之后发现打开第五关之后会自己闪退,想办法将这个机制去掉。打开算法助手,打开wuaipojie的开关,选择 拦截应用闪退和防止应用闪退,在算法助手中将这个apk文件打开之后,查看日志。日志如下
1 | 调用堆栈: |
at com.zj.wuaipojie.ui.ChallengeFifth.onCreate(ChallengeFifth.kt:57)这句是至关重要的,根据日志可以知道system.exit是在这个函数中调用的。
可以看到,在这个函数中确实调用了这个函数,将他注释掉,就不会再黑屏卡住了。
当我在MT管理器中修改签名之后,在进行普通签名校验时没通过,看到源代码之后
其实可以发现,普通签名校验就是checksign,观察checksign函数的方法,是先对messageDigest进行sha编码,之后再进行base64编码,将得到的strEncodeToString与SIGNATURE进行对比,这里想绕过普通签名,有两种方法:
- 修改if,使得即使不相等,也能返回true
- 将SIGNATURE修改为换签名之后apk文件的签名
方法一:修改If
在MT管理器中搜索com.zj.wuaipojie.ui.ChallengeFifth.checkSign
根据变量名和方法名,可以判断这个if-nez就是我们需要改的那个if,将if-nez(这个是不等于not equal的意思),改成if-eql(这个是等于的意思)
方法二:修改SIGNATURE
还是在com.zj.wuaipojie.ui.ChallengeFifth.checkSign,在修改SIGNATURE之前,先使用算法助手得到修改之后的apk文件的校验值
在算法助手中,勾选读取应用签名监听,之后查看日志
这个Ye03foXThqjf7muGS9hbC/qlr4E=就是修改之后的签名
使用导航,切换到字符串,双击该签名就可以导航到该位置
将此位置换成Ye03foXThqjf7muGS9hbC/qlr4E=
可以看到这个普通签名就通过了
但是 新的API签名校验 还是没有通过,这两个校验实际用的都是同一种方法,先看源码:
还是通过与正确签名进行比较,还是可以使用上面的方法,使用算法助手得到修改之后的API签名,然后在修改smali代码,因为这里并没有使用If语句,所以不能修改If判断条件,打开算法助手,在算法助手中打开demo,查看日志,由于新的API签是有newsign这个字符串的,所以可以直接搜索这个。
直接将这个newsign替换成日志中这个就可以了
PM代理
PMS(PackageManagerService)
- 是什么:
PMS 是 Android 系统的一个核心服务(运行在system_server进程),全称为 PackageManagerService。它负责管理系统中所有已安装应用的信息,包括包名、版本、权限、签名、组件等。 - 作用:
当应用通过PackageManager接口查询信息(如调用getPackageInfo)时,实际是通过 Binder 通信向 PMS 发起请求,PMS 从系统数据库或 APK 文件中读取真实数据后返回。 - 关键点:
- 运行在系统进程,拥有最高权限。
- 返回的数据是真实的(基于当前安装的 APK)。
- 无法直接修改(除非 Root 后 Hook 系统服务)。
PM代理的原理
- 是什么:
PM代理是一种Hook技术,指在应用层或框架层拦截应用对PackageManager的调用,并返回伪造的数据(如篡改签名信息)。它不是系统服务,而是逆向工程师插入的中间层。 - 原理:
通过 Hook 框架(如 Xposed、Frida)或动态代理,替换PackageManager对象或拦截其方法,在调用 PMS 之前或之后修改返回值。例如,当应用请求签名时,PM代理将真实的签名替换为预先保存的原始签名。 - 关键点:
- 运行在应用进程(通过代码注入)。
- 返回的数据是伪造的(为了绕过校验)。
- 需要 Hook 环境(Root、Xposed 框架等)。
IO重定向
IO重定向(Input/Output Redirection)在安卓逆向中,是指通过技术手段拦截并修改应用程序的输入输出操作,使其实际访问的资源与原本意图不同。其核心原理是在数据流动的通路中插入一个中间层,劫持原始操作,并将其重定向到开发者指定的目标。
1 | sget-object p10, Lcom/zj/wuaipojie/util/ContextUtils;->INSTANCE:Lcom/zj/wuaipojie/util/ContextUtils; |
不知道为什么我使用io定向,但是没有过crc校验,并且在数据目录一中也没有lib目录,看评论区也有同学和我一样的问题,可能是因为Android版本问题。
但是不妨碍用这个方法去写,我将步骤写下来:
首先在搜索com.zj.wuaipojie.ui.ChallengeFifth.useNewAPICheck,点击导航,因为我要重定向的是crc,所以使用导航找到check_crc这个函数之后,长按选择
查看调用处,找到调用的函数之后,正在该函数开头加上上面的smali代码,保存安装。
之后在apk文件的数据目录1下新建文件夹files,在该文件夹中加入原始apk文件。
还有一种方法也可以绕过,就是通过修改If的判断条件,将nez改成eqz就可以了。
root校验
root校验是指应用程序检测设备是否已被获取超级用户权限(即root)的一种安全机制。开发者通过这种校验来判断当前运行环境是否为“不安全”的root设备,并据此采取相应措施,例如拒绝运行、隐藏敏感功能或直接退出应用。
smali语法之赋值
任务是将非会员改成会员,会员未到期,以及钻石数量不为零。
VIP
这个代码就是检测你是否是VIP的逻辑。
在MT管理器中搜索非会员并且将isvip的值改成99。
1 | .method public final isVip()I |
const/4 v0, 0x0我们需要将v0的值改成99,首先需要注意的就是const/4和const/16的区别是什么。
1. const/4
- 数据格式 :4位有符号整数,即 -8~7(包含)
- 指令格式:
const/4 vA, #+B - 特点:
- 指令长度 2 字节,非常紧凑,节省代码空间。
- 只能加载很小的整数(如 0、1、5、-3 等)。
- 如果尝试加载超出此范围的常量(如 16),汇编器会报错或自动改用更宽的指令。
2. const/16
- 数值范围:16 位有符号整数,即 -32768 到 32767。
- 指令格式:
const/16 vAA, #+BBBB - 特点:
- 指令长度 3 字节(操作码 1 字节 + 寄存器 1 字节 + 常量 2 字节)。
- 用于加载中等大小的常量,如 1000、-20000 等。
- 当常量超出 16 位范围时,需使用
const(32 位)或const/high16等。
所以如果想将v0的值赋值为99的话,需要将const/4改成const/16,并且是十六进制。
会员到期时间
1 | public final long vipEndTime() { |
1671889481513L是一个时间戳
1 | .method public final vipEndTime()J |
这是修改之后的代码。随便写了一个数,竟然10000多年了。
钻石数量
还是根据非会员找到了vip_coin,导航找到了所有调用处。
点击一个,选中,点击搜索,选择正则,就可以复制出来iget p0, p0, Lcom/zj/wuaipojie/ui/SmaliLearn;->vip_coin:I,接着正则搜索,这时候搜索,原本有两个地方调用,但是搜索出来只剩一个调用了,有时候有成千上万个地方都有,这时候我们就可以用到正则
注意这个.*后面应该没有,我写错了。
这个$1会自动转换成()框住的寄存器,也就是说对框住的寄存器进行赋值。
不知道为啥我替换完之后,是const/4 p0, p0, 0x2的,明显是错的,因为有三个寄存器。