yangyunhui

功不唐捐,玉汝于成

Android逆向学习笔记(九)

Android逆向学习笔记(九)

《安卓逆向这档事》六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向 - 吾爱破解 - 52pojie.cn

目录


什么是校验

是开发者在数据传送时采用的一种校正数据的一种方式
常见的校验有:签名校验(最常见)、dexcrc校验、apk完整性校验、路径文件校验等

这里我有一个疑问:矫正数据是什么意思,一般我们理解的都是修正的意思,但在这里我觉得应该是判断与之前一样不一样的意思,为什么要使用这个词??

在逆向和安全环境中(判断的意思)

但在你看到的安卓代码或逆向分析中,它更多是指 “校验” 。

  • 核心逻辑 :判断“现在的”和“原来的”一不一样。
  • 为什么也叫矫正? ** 因为它是在核对正误**。如果不一样,系统会认为当前数据是“歪的”(被篡改了),从而拒绝运行。

什么是apk签名

通过对 Apk 进行签名,开发者可以证明对 Apk 的所有权和控制权,可用于安装和更新其应用。而在 Android 设备上的安装 Apk ,如果是一个没有被签名的 Apk,则会被拒绝安装。在安装 Apk 的时候,软件包管理器也会验证 Apk 是否已经被正确签名,并且通过签名证书和数据摘要验证是否合法没有被篡改。只有确认安全无篡改的情况下,才允许安装在设备上。

简单来说,APK 的签名主要作用有两个:

  1. 证明 APK 的所有者。
  2. 允许 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
2
3
4
5
kill/killProcess-----kill/KillProcess()可以杀死当前应用活动的进程,这一操作将会把所有该进程内的资源(包括线程全部清理掉).当然,由于ActivityManager时刻监听着进程,一旦发现进程被非正常Kill,它将会试图去重启这个进程。这就是为什么,有时候当我们试图这样去结束掉应用时,发现它又自动重新启动的原因.

system.exit-----杀死了整个进程,这时候活动所占的资源也会被释放。

finish----------仅仅针对Activity,当调用finish()时,只是将活动推向后台,并没有立即释放内存,活动的资源并没有被清理

使用demo认识签名校验

直接使用MT管理器对apk文件进行签名

签名之后发现打开第五关之后会自己闪退,想办法将这个机制去掉。打开算法助手,打开wuaipojie的开关,选择 拦截应用闪退和防止应用闪退,在算法助手中将这个apk文件打开之后,查看日志。日志如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
调用堆栈:
at hJr.sE.hP.to.QCVJPblCp.XposedBridge$LegacyApiSupport.handleBefore(Unknown Source:24)
at J.callback(Unknown Source:179)
at LSPHooker_.exit(Unknown Source:15)
at java.lang.System.exit(System.java:1502)
at java.lang.reflect.Method.invoke(Native Method)
at gEBwZia.zy.PkaMim.Q.ASHZ.hZju.HookBridge.invokeOriginalMethod(Native Method)
at J.callback(Unknown Source:193)
at LSPHooker_.exit(Unknown Source:12)
at com.zj.wuaipojie.ui.ChallengeFifth.onCreate(ChallengeFifth.kt:57)
at android.app.Activity.performCreate(Activity.java:7284)
at android.app.Activity.performCreate(Activity.java:7275)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2938)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3093)
at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1823)
at android.os.Handler.dispatchMessage(Handler.java:106)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6838)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:860)

at com.zj.wuaipojie.ui.ChallengeFifth.onCreate(ChallengeFifth.kt:57)这句是至关重要的,根据日志可以知道system.exit是在这个函数中调用的。

可以看到,在这个函数中确实调用了这个函数,将他注释掉,就不会再黑屏卡住了。

当我在MT管理器中修改签名之后,在进行普通签名校验时没通过,看到源代码之后

其实可以发现,普通签名校验就是checksign,观察checksign函数的方法,是先对messageDigest进行sha编码,之后再进行base64编码,将得到的strEncodeToString与SIGNATURE进行对比,这里想绕过普通签名,有两种方法:

  1. 修改if,使得即使不相等,也能返回true
  2. 将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
2
3
4
5
6
7
sget-object p10, Lcom/zj/wuaipojie/util/ContextUtils;->INSTANCE:Lcom/zj/wuaipojie/util/ContextUtils;  

invoke-virtual {p10}, Lcom/zj/wuaipojie/util/ContextUtils;->getContext()Landroid/content/Context;

move-result-object p10

invoke-static {p10}, Lcom/zj/wuaipojie/util/SecurityUtil;->hook(Landroid/content/Context;)V

不知道为什么我使用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
2
3
4
5
6
7
.method public final isVip()I
.registers 2

const/4 v0, 0x0

return v0
.end method

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
2
3
public final long vipEndTime() {
return 1671889481513L;
}

1671889481513L是一个时间戳

在线时间戳转换工具

1
2
3
4
5
6
.method public final vipEndTime()J
.registers 3

const-wide v0, 0xf69584f71111L

return-wide v0

这是修改之后的代码。随便写了一个数,竟然10000多年了。

钻石数量

还是根据非会员找到了vip_coin,导航找到了所有调用处。

点击一个,选中,点击搜索,选择正则,就可以复制出来iget p0, p0, Lcom/zj/wuaipojie/ui/SmaliLearn;->vip_coin:I,接着正则搜索,这时候搜索,原本有两个地方调用,但是搜索出来只剩一个调用了,有时候有成千上万个地方都有,这时候我们就可以用到正则

注意这个.*后面应该没有,我写错了。

这个$1会自动转换成()框住的寄存器,也就是说对框住的寄存器进行赋值。

不知道为啥我替换完之后,是const/4 p0, p0, 0x2的,明显是错的,因为有三个寄存器。