文章封面:安卓逆向学习

安卓逆向学习

安卓逆向学习

跟着52破解的文章学:《安卓逆向这档事》一、模拟器环境搭建 - 吾爱破解 - 52pojie.cn

环境搭建

安装magisk

这里我用的mumu

image-20260530003353013

image-20260530003410814

先下Kitsune Mask,这玩意好像删库了,然后github有保留版本(后面看好像直接下magisk里面那个apk也行)

image-20260530003941258

安装完后显示all done,其实就结束了

安装lsposed模块

下github里面那个zygisk那个,然后进magisk的设置把zygisk打开

然后在模块里面安装就行,这里用mumu的共享文件夹把zip传进去就行,这个是一个hook的模块

image-20260530165609803

初识apk

先下np管理器和mt管理器

apk结构

本质是一个压缩包,可以直接解压

文件注释
assets目录存放APK的静态资源文件,比如视频,音频,图片等
lib 目录armeabi-v7a基本通用所有android设备,arm64-v8a只适用于64位的android设备,x86常见用于android模拟器,其目录下的.so文件是c或c++编译的动态链接库文件
META-INF目录保存应用的签名信息,签名信息可以验证APK文件的完整性,相当于APK的身份证(验证文件是否又被修改)
res目录res目录存放资源文件,包括图片,字符串等等,APK的脸蛋由他的layout文件设计
AndroidManifest.xml文件APK的应用清单信息,它描述了应用的名字,版本,权限,引用的库文件等等信息
classes.dex文件classes.dex是java源码编译后生成的java字节码文件,APK运行的主要逻辑
resources.arsc文件resources.arsc是编译后的二进制资源文件,它是一个映射表,映射着资源和id,通过R文件中的id就可以找到对应的资源

双开及原理

双开:简单来说,就是手机同时运行两个或多个相同的应用,例如同时运行两个微信

原理解释
修改包名让手机系统认为这是2个APP,这样的话就能生成2个数据存储路径,此时的多开就等于你打开了两个互不干扰的APP
修改Framework对于有系统修改权限的厂商,可以修改Framework来实现双开的目的,例如:小米自带多开
通过虚拟化技术实现虚拟Framework层、虚拟文件系统、模拟Android对组件的管理、虚拟应用进程管理 等一整套虚拟技术,将APK复制一份到虚拟空间中运行,例如:平行空间
以插件机制运行利用反射替换,动态代{过}{滤}理,hook了系统的大部分与system—server进程通讯的函数,以此作为“欺上瞒下”的目的,欺骗系统“以为”只有一个apk在运行,瞒过插件让其“认为”自己已经安装。例如:VirtualApp

后面三种比较困难,现在我们学习第一种修改包名

现在用mt管理器提取安装包,用给的demoapk

image-20260530175057091

功能里面点apk共存

image-20260530175324636

勾选自动签名点确定就行,然后这里是默认签名,会生成一个clone.apk在同目录下,安装之后桌面下就有两个一样的软件了,然后两个都点开,就发现实现双开了,如果实际上开发者做了签名校验的话,修改包名这种办法可能就不行了

汉化apk

基本上字符串都是在arsc里面,所以可以先一键汉化再润色,没汉化到的可以先定位再汉化,流程图如下

image-20260530185338800

现在完成附件里面的挑战,完成汉化和图片替换

mt管理器打开apk查看,然后搜索字符串,找到对应文件进行反编译,替换完字符串保存,它又进行了一次签名,这个时候,这个文件的签名跟我们之前的签名不一样了,就会出现安装失败,最简单的办法就是把原来软件卸载了,再用这里的安装包安装

但是如果汉化非英文的,我们不知道字符串怎么搜索,这里用到开发者助手,可以提取界面资源信息,复制了继续去mt管理器搜索,这里找到了arsc文件

选择翻译模式,找到之前的字符串就可以进行修改了

image-20260530191738715

这里保存的话,又进行一次签名,但是这里直接安装却没有失败,因为我们两次的签名都是一样的,所以并不会报签名不一致的错误

最后一个字符串在dex文件里面,用dex编辑器++查看,然后依旧搜索字符串修改

初识AndroidManifest.xml

AndroidManifest.xml文件是整个应用程序的信息描述文件,定义了应用程序中包含的Activity,Service,Content provider和BroadcastReceiver组件信息。每个应用程序在根目录下必须包含一个AndroidManifest.xml文件,且文件名不能修改。它描述了package中暴露的组件,他们各自的实现类,各种能被处理的数据和启动位置。

属性定义
versionCode版本号,主要用来更新,例如:12
versionName版本名,给用户看的,例如:1.2
package包名,例如:com.zj.52pj.demo
uses-permission android:name=""应用权限,例如:android.permission.INTERNET 代表网络权限
android:label="@string/app_name"应用名称
android:icon="@mipmap/ic_launcher"应用图标路径
android:debuggable=“true”应用是否开启debug权限

所以可以修改这个来实现更改图标,应用名称和版本号之类的

image-20260530193531991

初识smali,vip终结者

什么是JVM、Dalvik、ART

JVM是JAVA虚拟机,运行JAVA字节码程序 Dalvik是Google专门为Android设计的一个虚拟机,Dalvik有专属的文件执行格式dex(Dalvik executable) Art(Android Runtime)相当于Dalvik的升级版,本质与Dalvik无异

smali及其语法

smali是Dalvik的寄存器语言,smali代码是dex反编译而来的。

关键字

名称注释
.class类名
.super父类名,继承的上级类名名称
.source源名
.field变量
.method方法名
.register寄存器
.end method方法名的结束
public公有
protected半公开,只有同一家人才能用
private私有,只能自己使用
.parameter方法参数
.prologue方法开始
.line xxx位于第xxx行

数据类型对应

smali类型java类型注释
Vvoid无返回值
Zboolean布尔值类型,返回0或1
Bbyte字节类型,返回字节
Sshort短整数类型,返回数字
Cchar字符类型,返回字符
Iint整数类型,返回数字
Jlong (64位 需要2个寄存器存储)长整数类型,返回数字
Ffloat单浮点类型,返回数字
Ddouble (64位 需要2个寄存器存储)双浮点类型,返回数字
stringString文本类型,返回字符串
Lxxx/xxx/xxxobject对象类型,返回对象

常用指令

关键字注释
const重写整数属性,真假属性内容,只能是数字类型
const-string重写字符串内容
const-wide重写长整数类型,多用于修改到期时间。
return返回指令
if-eq全称equal(a=b),比较寄存器ab内容,相同则跳
if-ne全称not equal(a!=b),ab内容不相同则跳
if-eqz全称equal zero(a=0),z即是0的标记,a等于0则跳
if-nez全称not equal zero(a!=0),a不等于0则跳
if-ge全称greater equal(a>=b),a大于或等于则跳
if-le全称little equal(a<=b),a小于或等于则跳
goto强制跳到指定位置
switch分支跳转,一般会有多个分支线,并根据指令跳转到适当位置
iget获取寄存器数据

其余指令可用语法工具查询

定位方法:搜索弹窗关键字、抓取按钮id

安装新的教程demo,如果没有进行核心破解的话,由于签名不一致是会安装失败的,这里的核心破解其实就是github.com/LSPosed/CorePatch

关卡2有明显字段“大会员”,直接在jadx里面搜,定位到函数

image-20260601230822919

然后查看smali代码

image-20260601230847318

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
.method private static final onCreate$lambda-2(Lkotlin/jvm/internal/Ref$IntRef;Lcom/zj/wuaipojie/ui/ChallengeSecond;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/widget/ImageView;Landroid/view/View;)Z //Z代表返回值类型是布尔型的
    .registers 7 //寄存器有7个

    .line 33 //代码所在行数
    iget p0, p0, Lkotlin/jvm/internal/Ref$IntRef;->element:I //读取p0(第一个参数)中element的值赋值给p0

    const/4 p5, 0x1 //p5赋值1

    const/16 v0, 0xa //v0赋值10

    if-ge p0, v0, :cond_15 //判断p0的值是否大于或等于v0的值,如果大于或等于则跳转到:cond_15

    .line 34 //以下是常见的Toast弹窗代码
    move-object p0, p1 

    check-cast p0, Landroid/content/Context; 

    const-string v0, "\u8bf7\u5148\u83b7\u53d610\u4e2a\u786c\u5e01\u54e6"

    check-cast v0, Ljava/lang/CharSequence;

    invoke-static {p0, v0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object p0

    invoke-virtual {p0}, Landroid/widget/Toast;->show()V //当看到这个Toast;->show你就应该反应过来这里是弹窗代码

    .line 36
    :cond_15
    invoke-virtual {p1}, Lcom/zj/wuaipojie/ui/ChallengeSecond;->isvip()Z //判断isvip方法的返回值是否为1

    move-result p0 //结果赋值给p0

    if-eqz p0, :cond_43 //如果结果为0则跳转cond_43地址

    .line 37
    check-cast p1, Landroid/content/Context; //设置图片资源

    const-string p0, "\u5f53\u524d\u5df2\u7ecf\u662f\u5927\u4f1a\u5458\u4e86\u54e6\uff01"

    check-cast p0, Ljava/lang/CharSequence;

    invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object p0

    invoke-virtual {p0}, Landroid/widget/Toast;->show()V

    const p0, 0x7f0d0018

    .line 38
    invoke-virtual {p2, p0}, Landroid/widget/ImageView;->setImageResource(I)V

    const p0, 0x7f0d0008

    .line 39
    invoke-virtual {p3, p0}, Landroid/widget/ImageView;->setImageResource(I)V

    const p0, 0x7f0d000a

    .line 40
    invoke-virtual {p4, p0}, Landroid/widget/ImageView;->setImageResource(I)V

    .line 41
    sget-object p0, Lcom/zj/wuaipojie/util/SPUtils;->INSTANCE:Lcom/zj/wuaipojie/util/SPUtils;

    const/4 p2, 0x2

    const-string p3, "level"

    invoke-virtual {p0, p1, p3, p2}, Lcom/zj/wuaipojie/util/SPUtils;->saveInt(Landroid/content/Context;Ljava/lang/String;I)V

    goto :goto_50

    .line 44
    :cond_43
    check-cast p1, Landroid/content/Context;

    const-string p0, "\u8bf7\u5148\u5145\u503c\u5927\u4f1a\u5458\u54e6\uff01"

    check-cast p0, Ljava/lang/CharSequence;

    invoke-static {p1, p0, p5}, Landroid/widget/Toast;->makeText(Landroid/content/Context;Ljava/lang/CharSequence;I)Landroid/widget/Toast;

    move-result-object p0

    invoke-virtual {p0}, Landroid/widget/Toast;->show()V

    :goto_50
    return p5
.end method
# virtual methods
.method public final isvip()Z
    .registers 2

    const/4 v0, 0x0

    return v0
.end method        

寄存器

在smali里的所有操作都必须经过寄存器来进行:本地寄存器用v开头数字结尾的符号来表示,如v0、 v1、v2。 参数寄存器则使用p开头数字结尾的符号来表示,如p0、p1、p2。特别注意的是,p0不一定是函数中的第一个参数,在非static函数中,p0代指“this",p1表示函数的第一个 参数,p2代表函数中的第二个参数。而在static函数中p0才对应第一个参数(因为Java的static方法中没有this方法)

想要完成这个,修改方法:修改判断、强制跳转、修改寄存器的值

image-20260601232903056

现在可以尝试修改判断条件

image-20260601233817883

这里如果p0>=10的话就会跳转,那我们修改成小于等于其实就行,把if-ge修改成if-le

image-20260601233949055

这里判断是否是vip,否则跳转其他地方,我们直接把判断注释了就能直接继续下面的操作

还可以修改寄存器的值让它满足判断就行了,比如第一个要求大于等于v0,那直接把v0改成0就行了,然后把isvip函数的返回值也改成1,就满足条件了,当然也可以直接goto到想要跳转的地址

除了之前直接搜索字段来获取,可以通过查看布局的id来查找

image-20260601235224699

用这里的hex就能找到对应的代码所在

关闭广告和弹窗

打开一些app都会出现一些广告,要么是开屏广告,要么是更新提示或者嵌入在界面里面的广告,这次学习怎么去除

安卓四大组件

组件描述
Activity(活动)在应用中的一个Activity可以用来表示一个界面,意思可以理解为“活动”,即一个活动开始,代表 Activity组件启动,活动结束,代表一个Activity的生命周期结束。一个Android应用必须通过Activity来运行和启动,Activity的生命周期交给系统统一管理。
Service(服务)Service它可以在后台执行长时间运行操作而没有用户界面的应用组件,不依赖任何用户界面,例如后台播放音乐,后台下载文件等。
Broadcast Receiver(广播接收器)一个用于接收广播信息,并做出对应处理的组件。比如我们常见的系统广播:通知时区改变、电量低、用户改变了语言选项等。
Content Provider(内容提供者)作为应用程序之间唯一的共享数据的途径,Content Provider主要的功能就是存储并检索数据以及向其他应用程序提供访问数据的接口。Android内置的许多数据都是使用Content Provider形式,供开发者调用的(如视频,音频,图片,通讯录等)

activity的切换

教程demo里面的activity逻辑:先启动activity->广告activity->主页activity

那修改的方法可以有两种,一种就是修改广告加载时间为0,另一种就是切换activity的定位,让启动之后直接跳转到主页

这里有快速定位的办法,mt管理器内置了activity监听的服务,先去启动监听,再打开软件

image-20260603212512025

这里找到AdActivity,也就是广告的activity,去dex里面找这个类,或者直接jadx先看,进去直接改smail估计就行了

image-20260603213103814

可以看到load广告用了3秒,如果把它改成0秒,就不会加载广告了

image-20260603213802881

也就是把寄存器v2改成0,就行了

接下来学习切换activity,首先学习一下manifest.xml

image-20260603214303420

 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
26
27
28
29
30
31
<!---声明实现应用部分可视化界面的 Activity,必须使用 AndroidManifest 中的 <activity> 元素表示所有 Activity。系统不会识别和运行任何未进行声明的Activity。----->
<activity
       android:name="com.zj.wuaipojie.ui.ChallengeSixth"
       android:exported="false"/>
    <activity
       android:name="com.zj.wuaipojie.ui.ChallengeFifth"
       android:exported="true"/>
    <activity
       android:name="com.zj.wuaipojie.ui.ChallengeFourth"
       android:exported="true"/>
    <activity
       android:name="com.zj.wuaipojie.ui.ChallengeThird"
       android:exported="false"/>
    <activity
       android:name="com.zj.wuaipojie.ui.ChallengeSecond"
       android:exported="false"/>
    <activity
       android:name="com.zj.wuaipojie.ui.AdActivity"/>
    <activity
       android:label="@string/app_name"
       android:name="com.zj.wuaipojie.ui.MainActivity"
       android:exported="true"><!--当前Activity是否可以被另一个Application的组件启动:true允许被启动;false不允许被启动-->
      <intent-filter><!---指明这个activity可以以什么样的意图(intent)启动--->
        <action
           android:name="android.intent.action.MAIN"/><!--表示activity作为一个什么动作启动,android.intent.action.MAIN表示作为主activity启动--->
        <category
           android:name="android.intent.category.LAUNCHER"/><!--这是action元素的额外类别信息,android.intent.category.LAUNCHER表示这个activity为当前应用程序优先级最高的Activity-->
      </intent-filter>
    </activity>
    <activity
       android:name="com.zj.wuaipojie.ui.ChallengeFirst"/>

也就是说如果我们把第三关的activity替换主activity,那么第三关就会优先加载,从而跳过广告,不过一般不推荐这样做,容易使得应用闪退,所以还有另一种在dex里面修改

image-20260603215653943

复制类的smail路径

image-20260603215838525

np管理器不知道为什么搜不到,依旧jadx

image-20260603220241433

可以看到第三关这里优先加载了AdActivity这个类,而不是第三关那个类

image-20260603220622681

也是改这个就行

Activity生命周期

函数名称描述
onCreate()一个Activity启动后第一个被调用的函数,常用来在此方法中进行Activity的一些初始化操作。例如创建View,绑定数据,注册监听,加载参数等。
onStart()当Activity显示在屏幕上时,此方法被调用但此时还无法进行与用户的交互操作。
onResume()这个方法在onStart()之后调用,也就是在Activity准备好与用户进行交互的时候调用,此时的Activity一定位于Activity栈顶,处于运行状态。
onPause()这个方法是在系统准备去启动或者恢复另外一个Activity的时候调用,通常在这个方法中执行一些释放资源的方法,以及保存一些关键数据。
onStop()这个方法是在Activity完全不可见的时候调用的。
onDestroy()这个方法在Activity销毁之前调用,之后Activity的状态为销毁状态。
onRestart()当Activity从停止stop状态恢进入start状态时调用状态。

image-20260603221452395

弹窗定位&堆栈分析

修改方法: 1.修改xml中的versioncode 2.Hook弹窗(推荐算法助手开启弹窗定位) 3.修改dex弹窗代码 4.抓包修改响应体(也可以路由器拦截)

image-20260603222933693

也就是这玩意,版本号改高点就不会跳版本更新的弹窗了,不过有时候改了这个还是会跳出更新

接下来用算法助手,还是lsp上的插件,可以hook掉弹窗

github.com/Xposed-Modules-Repo/com.junge.algorithmaide/releases/tag/212-2.1.2

image-20260603224306585

还有更好的办法,可以屏蔽关键词弹窗,可以选择我们看到的关键词,就会自动hook弹窗

image-20260603224841544

这边可以看到hook后java的堆栈信息,可以跟到第三关对应的方法名里面,这里跟到onCreate里面

image-20260603225810147

可以看到下面有个show方法,之前看到show就是弹窗,所以这里直接注释也是可以的,找到所有的show都注释就不会弹窗了

去掉内置的横幅广告

用到之前的开发助手

image-20260603230219206

mt管理器进行xml搜索,搜索资源id

image-20260603230522438

把图片宽度和高度改成0dp就行了,或者用代码给它隐藏起来

1
android:visibility="gone"

动态调试&Log插桩

动态调试

步骤

1.修改debug权限

方法一:在AndroidManifest.xml里添加可调试权限

1
android:debuggable="true"

image-20260604204932853

方法二:XappDebug模块hook对应的app

GitHub - Palatis/XAppDebug: toggle app debuggable · GitHub

方法三:Magisk命令(重启失效)

1
2
3
4
1.  adb shell #adb进入命令行模式
2.  su #切换至超级用户
3.  magisk resetprop ro.debuggable 1
4.  stop;start; #一定要通过该方式重启

方法四:刷入MagiskHide Props Config模块

image-20260604202711550

修改ro.debuggable的值为1

2.端口转发以及开启adb权限

版本号点击七次开启开发者模式并开启adb调试权限

image-20260604205738264

打开这个usb调试

mumu的可以看官方文档:MuMu模拟器12如何连接adb?_MuMu模拟器_安卓模拟器

然后现在打开jeb,加载教程demo,第四关这里输入密钥错误会报提示,我们在jeb中搜索

image-20260604211810878

反编译成java代码看看

image-20260604212129193

有个check函数,判断传入的值是否符合条件,进行弹窗,跟进这个函数看看逻辑

image-20260604212434040

开头要是flag,结尾要是大括号,关键是中间字符串的内容

image-20260604212713876

最后是判断我们传入的值,和内部base64加密后的字符串是否匹配,直接逆上面逻辑太麻烦了,我们jeb是可以动态调试的,所以直接在base64这个函数这里下断点

image-20260604213336090

ctrl+b就能快捷下断点,启动adb

1
2
adb.exe connect 127.0.0.1:16416
adb.exe shell

image-20260604214211283

然后接下来就是要调试这个app了,首先要知道应用的包名

1
adb shell am start -D -n com.zj.wuaipojie/.ui.MainActivity

前面进的shell先退出,然后还是-s指定

image-20260604215903057

image-20260604215908497

然后jeb起调试

image-20260604221335971

应用里面输入flag{123}试试,然后查看jeb展示的局部变量

image-20260604223329368

可以发现确实断下来了,就可以进行调试了

跟进encodeToString方法,直到调试出我们想要的值p1

image-20260604225128341

image-20260604225351096

log插桩

定义:Log插桩指的是反编译APK文件时,在对应的smali文件里,添加相应的smali代码,将程序中的关键信息,以log日志的形式进行输出。

调用的命令

1
invoke-static {对应寄存器}, Lcom/mtools/LogUtils;->v(Ljava/lang/Object;)V

这里我们想要v0,所以捕获这个v0

image-20260604232845773

然后去算法助手里面开启log捕获,启动后查看算法助手捕获的日志也能完成这个挑战

签名校验对抗、PM代理、IO重定向

校验

是开发者在数据传送时采用的一种校正数据的一种方式 常见的校验有:签名校验(最常见)、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 签名的产物。

MANIFEST.MF

MANIFEST.MF:这是摘要文件。程序遍历Apk包中的所有文件(entry),对非文件夹非签名文件的文件,逐个用SHA1(安全哈希算法)生成摘要信息,再用Base64进行编码。如果你改变了apk包中的文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是程序就不能成功安装。

image-20260607145310601

ANDROID.SF

ANDROID.SF:这是对摘要的签名文件。对前一步生成的MANIFEST.MF,使用SHA1-RSA算法,用开发者的私钥进行签名。在安装时只能使用公钥才能解密它。解密之后,将它与未加密的摘要信息(即MANIFEST.MF文件)进行对比,如果相符,则表明内容没有被异常修改。

image-20260607145355467

ANDROID.RSA

ANDROID.RSA文件中保存了公钥、所采用的加密算法等信息。

在某些情况下,直接对apk进行v1签名可以绕过apk的签名校验

但是v2方案会将 APK 文件视为 blob,并对整个文件进行签名检查。对 APK 进行的任何修改(包括对 ZIP 元数据进行的修改)都会使 APK 签名作废。这种形式的 APK 验证不仅速度要快得多,而且能够发现更多种未经授权的修改。

签名校验

如何判断是否有签名校验?

不做任何修改,直接签名安装,应用闪退则说明大概率有签名校验

一般来说,普通的签名校验会导致软件的闪退,黑屏,卡启动页等

1
2
3
kill/killProcess-----kill/KillProcess()可以杀死当前应用活动的进程这一操作将会把所有该进程内的资源包括线程全部清理掉.当然由于ActivityManager时刻监听着进程一旦发现进程被非正常Kill它将会试图去重启这个进程这就是为什么有时候当我们试图这样去结束掉应用时发现它又自动重新启动的原因.
system.exit-----杀死了整个进程这时候活动所占的资源也会被释放
finish----------仅仅针对Activity当调用finish()只是将活动推向后台并没有立即释放内存活动的资源并没有被清理

三角校验,就是so检测dex,动态加载的dex(在软件运行时会解压释放一段dex文件,检测完后就删除)检测so,dex检测动态加载的dex

这里用教程demo体验一下被签名校验检测到的结果,直接用mt管理器对apk进行签名,然后安装,发现会直接闪退,这里用算法助手,打开拦截应用退出和防止应用闪退,然后再次启动程序,查看算法助手的日志

image-20260607150945932

image-20260607151112276

可以看到调用了system.exit导致应用退出,查看一下后面的com.zj.wuaipojie.ui.ChallengeFifth.onCreate

image-20260607151339373

所以如果把这行注释了,应用就不会因为签名校验而闪退

算法助手还能进行应用签名的监听,重新安装原包,打开监听

image-20260607151625190

这里确实也可以看到对应读取签名的方法com.zj.wuaipojie.ui.ChallengeFifth.checkSign,直接jadx看一下反编译源码

image-20260607151917220

就是经过签名和加密后跟原字符串比对,然后返回值,所以我们想要改的话,两种办法,一个是直接改判断语句,另一个是改这个原字符串的值跟我们当前值相等,这个值我们可以通过日志观察到,由于有关键词zj2595,我们可以快速定位到

image-20260607153927042

替换就会发现过了普通签名校验

普通获取签名校验:系统将应用的签名信息封装在 PackageInfo 中,调用 PackageManager 的 getPackageInfo(String packageName, int flags) 即可获取指定包名的签名信息

签名校验对抗

方法一:核心破解插件,不签名安装应用 方法二:一键过签名工具,例如MT、NP、ARMPro、CNFIX、Modex的去除签名校验功能 方法三:具体分析签名校验逻辑(手撕签名校验) 方法四:io重定向–VA&SVC:ptrace+seccomp SVC的TraceHook沙箱的实现&无痕Hook实现思路

手动实现PM代理

1.什么是PMS

思路源自:Android中Hook 应用签名方法

PackageManagerService(简称PMS),是Android系统核心服务之一,处理包管理相关的工作,常见的比如安装、卸载应用等。

2.实现方法以及原理解析

hookPMS代码

 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.zj.hookpms;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Log;

public class ServiceManagerWraper {

    public final static String ZJ = "ZJ595";

    public static void hookPMS(Context context, String signed, String appPkgName, int hashCode) {
        try {
            // 获取全局的ActivityThread对象
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Method currentActivityThreadMethod =
                    activityThreadClass.getDeclaredMethod("currentActivityThread");
            Object currentActivityThread = currentActivityThreadMethod.invoke(null);
            // 获取ActivityThread里面原始的sPackageManager
            Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");
            sPackageManagerField.setAccessible(true);
            Object sPackageManager = sPackageManagerField.get(currentActivityThread);
            // 准备好代{过}{滤}理对象, 用来替换原始的对象
            Class<?> iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");
            Object proxy = Proxy.newProxyInstance(
                    iPackageManagerInterface.getClassLoader(),
                    new Class<?>[]{iPackageManagerInterface},
                    new PmsHookBinderInvocationHandler(sPackageManager, signed, appPkgName, 0));
            // 1. 替换掉ActivityThread里面的 sPackageManager 字段
            sPackageManagerField.set(currentActivityThread, proxy);
            // 2. 替换 ApplicationPackageManager里面的 mPM对象
            PackageManager pm = context.getPackageManager();
            Field mPmField = pm.getClass().getDeclaredField("mPM");
            mPmField.setAccessible(true);
            mPmField.set(pm, proxy);
        } catch (Exception e) {
            Log.d(ZJ, "hook pms error:" + Log.getStackTraceString(e));
        }
    }

    public static void hookPMS(Context context) {
        String Sign = "原包的签名信息";
        hookPMS(context, Sign, "com.zj.hookpms", 0);
    }
}

ActivityThread的静态变量sPackageManager ApplicationPackageManager对象里面的mPM变量

IO重定向

什么是IO重定向?

例:在读A文件的时候指向B文件

平头哥的核心代码 Virtual Engine for Android(Support 12.0 in business version)

IO重定向可以干嘛?

1,可以让文件只读,不可写

2,禁止访问文件

3,路径替换

具体实现: 过签名检测(读取原包) 风控对抗(例:一个文件记录App启动的次数) 过Root检测,Xposed检测(文件不可取)

从教程demo里面看,其中的crc校验是写在其他类里面的,如果还是跟之前一样,每次修改dex文件都要修改crc的值就很麻烦

image-20260609190911043

首先在mt管理器里面找到这个函数,导航里面查找用法

然后把下面代码复制进去,用来获取上下文

1
2
3
4
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   

image-20260609191613293

然后在这个安装包的数据目录下新建files文件夹,把原包复制到目录下

image-20260609192208058

但是确没有过crc校验,就很奇怪,可能是安卓版本太高,so没生效?我从adb的日志里面都没发现有native hook的日志,但是这个方法本质调用的是实现hook的so里面的函数

  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
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
using namespace std;  
string packname;  
string origpath;  
string fakepath;  

int (*orig_open)(const char *pathname, int flags, ...);  
int (*orig_openat)(int,const char *pathname, int flags, ...);  
FILE *(*orig_fopen)(const char *filename, const char *mode);  
static long (*orig_syscall)(long number, ...);  
int (*orig__NR_openat)(int,const char *pathname, int flags, ...);  

void* (*orig_dlopen_CI)(const char *filename, int flag);  
void* (*orig_dlopen_CIV)(const char *filename, int flag, const void *extinfo);  
void* (*orig_dlopen_CIVV)(const char *name, int flags, const void *extinfo, void *caller_addr);  

static inline bool needs_mode(int flags) {  
    return ((flags & O_CREAT) == O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE);  
}  
bool startsWith(string str, string sub){  
    return str.find(sub)==0;  
}  

bool endsWith(string s,string sub){  
    return s.rfind(sub)==(s.length()-sub.length());  
}  
bool isOrigAPK(string  path){  

    if(path==origpath){  
        return true;  
    }  
    return false;  
}  
//该函数的功能是在打开一个文件时进行拦截,并在满足特定条件时将文件路径替换为另一个路径  

//fake_open 函数有三个参数:  
//pathname:一个字符串,表示要打开的文件的路径。  
//flags:一个整数,表示打开文件的方式,例如只读、只写、读写等。  
//mode(可选参数):一个整数,表示打开文件时应用的权限模式。  
int fake_open(const char *pathname, int flags, ...) {  
    mode_t mode = 0;  
    if (needs_mode(flags)) {  
        va_list args;  
        va_start(args, flags);  
        mode = static_cast<mode_t>(va_arg(args, int));  
        va_end(args);  
    }  
    //LOGI("open,  path: %s, flags: %d, mode: %d",pathname, flags ,mode);  
    string cpp_path= pathname;  
    if(isOrigAPK(cpp_path)){  
        LOGI("libc_open, redirect: %s, --->: %s",pathname, fakepath.data());  
        return orig_open("/data/user/0/com.zj.wuaipojie/files/base.apk", flags, mode);  
    }  
    return  orig_open(pathname, flags, mode);  

}  

//该函数的功能是在打开一个文件时进行拦截,并在满足特定条件时将文件路径替换为另一个路径  

//fake_openat 函数有四个参数:  
//fd:一个整数,表示要打开的文件的文件描述符。  
//pathname:一个字符串,表示要打开的文件的路径。  
//flags:一个整数,表示打开文件的方式,例如只读、只写、读写等。  
//mode(可选参数):一个整数,表示打开文件时应用的权限模式。  
//openat 函数的作用类似于 open 函数,但是它使用文件描述符来指定文件路径,而不是使用文件路径本身。这样,就可以在打开文件时使用相对路径,而不必提供完整的文件路径。  
//例如,如果要打开相对于当前目录的文件,可以使用 openat 函数,而不是 open 函数,因为 open 函数只能使用绝对路径。  
//  
int fake_openat(int fd, const char *pathname, int flags, ...) {  
    mode_t mode = 0;  
    if (needs_mode(flags)) {  
        va_list args;  
        va_start(args, flags);  
        mode = static_cast<mode_t>(va_arg(args, int));  
        va_end(args);  
    }  
    LOGI("openat, fd: %d, path: %s, flags: %d, mode: %d",fd ,pathname, flags ,mode);  
    string cpp_path= pathname;  
    if(isOrigAPK(cpp_path)){  
        LOGI("libc_openat, redirect: %s, --->: %s",pathname, fakepath.data());  
        return  orig_openat(fd,fakepath.data(), flags, mode);  
    }  
    return orig_openat(fd,pathname, flags, mode);  

}  
FILE *fake_fopen(const char *filename, const char *mode) {  

    string cpp_path= filename;  
    if(isOrigAPK(cpp_path)){  
        return  orig_fopen(fakepath.data(), mode);  
    }  
    return orig_fopen(filename, mode);  
}  
//该函数的功能是在执行系统调用时进行拦截,并在满足特定条件时修改系统调用的参数。  
//syscall 函数是一个系统调用,是程序访问内核功能的方法之一。使用 syscall 函数可以调用大量的系统调用,它们用于实现操作系统的各种功能,例如打开文件、创建进程、分配内存等。  
//  
static long fake_syscall(long number, ...) {  
    void *arg[7];  
    va_list list;  

    va_start(list, number);  
    for (int i = 0; i < 7; ++i) {  
        arg[i] = va_arg(list, void *);  
    }  
    va_end(list);  
    if (number == __NR_openat){  
        const char *cpp_path = static_cast<const char *>(arg[1]);  
        LOGI("syscall __NR_openat, fd: %d, path: %s, flags: %d, mode: %d",arg[0] ,arg[1], arg[2], arg[3]);  
        if (isOrigAPK(cpp_path)){  
            LOGI("syscall __NR_openat, redirect: %s, --->: %s",arg[1], fakepath.data());  
            return orig_syscall(number,arg[0], fakepath.data() ,arg[2],arg[3]);  
        }  
    }  
    return orig_syscall(number, arg[0], arg[1], arg[2], arg[3], arg[4], arg[5], arg[6]);  

}  

//函数的功能是获取当前应用的包名、APK 文件路径以及库文件路径,并将这些信息保存在全局变量中  
//函数调用 GetObjectClass 和 GetMethodID 函数来获取 context 对象的类型以及 getPackageName 方法的 ID。然后,函数调用 CallObjectMethod 函数来调用 getPackageName 方法,获取当前应用的包名。最后,函数使用 GetStringUTFChars 函数将包名转换为 C 字符串,并将包名保存在 packname 全局变量中  
//接着,函数使用 fakepath 全局变量保存了 /data/user/0/<packname>/files/base.apk 这样的路径,其中 <packname> 是当前应用的包名。  
//然后,函数再次调用 GetObjectClass 和 GetMethodID 函数来获取 context 对象的类型以及 getApplicationInfo 方法的 ID。然后,函数调用 CallObjectMethod 函数来调用 getApplicationInfo 方法,获取当前应用的 ApplicationInfo 对象。  
//它先调用 GetObjectClass 函数获取 ApplicationInfo 对象的类型,然后调用 GetFieldID 函数获取 sourceDir 字段的 ID。接着,函数使用 GetObjectField 函数获取 sourceDir 字段的值,并使用 GetStringUTFChars 函数将其转换为 C 字符串。最后,函数将 C 字符串保存在 origpath 全局变量中,表示当前应用的 APK 文件路径。  
//最后,函数使用 GetFieldID 和 GetObjectField 函数获取 nativeLibraryDir 字段的值,并使用 GetStringUTFChars 函数将其转换为 C 字符串。函数最后调用 LOGI 函数打印库文件路径,但是并没有将其保存在全局变量中。  

extern "C" JNIEXPORT void JNICALL  
Java_com_zj_wuaipojie_util_SecurityUtil_hook(JNIEnv *env, jclass clazz, jobject context) {  
    jclass conext_class = env->GetObjectClass(context);  
    jmethodID methodId_pack = env->GetMethodID(conext_class, "getPackageName",  
                                               "()Ljava/lang/String;");  
    auto packname_js = reinterpret_cast<jstring>(env->CallObjectMethod(context, methodId_pack));  
    const char *pn = env->GetStringUTFChars(packname_js, 0);  
    packname = string(pn);  

    env->ReleaseStringUTFChars(packname_js, pn);  
    //LOGI("packname: %s", packname.data());  
    fakepath= "/data/user/0/"+ packname +"/files/base.apk";  

    jclass conext_class2 = env->GetObjectClass(context);  
    jmethodID methodId_pack2 = env->GetMethodID(conext_class2,"getApplicationInfo","()Landroid/content/pm/ApplicationInfo;");  
    jobject application_info = env->CallObjectMethod(context,methodId_pack2);  
    jclass pm_clazz = env->GetObjectClass(application_info);  

    jfieldID package_info_id = env->GetFieldID(pm_clazz,"sourceDir","Ljava/lang/String;");  
    auto sourceDir_js = reinterpret_cast<jstring>(env->GetObjectField(application_info,package_info_id));  
    const char *sourceDir = env->GetStringUTFChars(sourceDir_js, 0);  
    origpath = string(sourceDir);  
    LOGI("sourceDir: %s", sourceDir);  

    jfieldID package_info_id2 = env->GetFieldID(pm_clazz,"nativeLibraryDir","Ljava/lang/String;");  
    auto nativeLibraryDir_js = reinterpret_cast<jstring>(env->GetObjectField(application_info,package_info_id2));  
    const char *nativeLibraryDir = env->GetStringUTFChars(nativeLibraryDir_js, 0);  
    LOGI("nativeLibraryDir: %s", nativeLibraryDir);  
    //LOGI("%s", "Start Hook");  

    //启动hook  
    void *handle = dlopen("libc.so",RTLD_NOW);  
    auto pagesize = sysconf(_SC_PAGE_SIZE);  
    auto addr = ((uintptr_t)dlsym(handle,"open") & (-pagesize));  
    auto addr2 = ((uintptr_t)dlsym(handle,"openat") & (-pagesize));  
    auto addr3 = ((uintptr_t)fopen) & (-pagesize);  
    auto addr4 = ((uintptr_t)syscall) & (-pagesize);  

    //解除部分机型open被保护  
    mprotect((void*)addr, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);  
    mprotect((void*)addr2, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);  
    mprotect((void*)addr3, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);  
    mprotect((void*)addr4, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC);  

    DobbyHook((void *)dlsym(handle,"open"), (void *)fake_open, (void **)&orig_open);  
    DobbyHook((void *)dlsym(handle,"openat"), (void *)fake_openat, (void **)&orig_openat);  
    DobbyHook((void *)fopen, (void *)fake_fopen, (void**)&orig_fopen);  
    DobbyHook((void *)syscall, (void *)fake_syscall, (void **)&orig_syscall);  
}

所以干脆直接用一键去除签名校验,np管理器里面提供了多种工具

image-20260609201621237

其他常见校验

root检测

反制手段 1.算法助手、对话框取消等插件一键hook 2.分析具体的检测代码 3.利用IO重定向使文件不可读 4.修改Andoird源码,去除常见指纹

举个例子,检测方法

 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
26
27
28
29
30
fun isDeviceRooted(): Boolean {
    return checkRootMethod1() || checkRootMethod2() || checkRootMethod3()
}

fun checkRootMethod1(): Boolean {
    val buildTags = android.os.Build.TAGS
    return buildTags != null && buildTags.contains("test-keys")
}

fun checkRootMethod2(): Boolean {
    val paths = arrayOf("/system/app/Superuser.apk", "/sbin/su", "/system/bin/su", "/system/xbin/su", "/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
            "/system/bin/failsafe/su", "/data/local/su", "/su/bin/su")
    for (path in paths) {
        if (File(path).exists()) return true
    }
    return false
}

fun checkRootMethod3(): Boolean {
    var process: Process? = null
    return try {
        process = Runtime.getRuntime().exec(arrayOf("/system/xbin/which", "su"))
        val bufferedReader = BufferedReader(InputStreamReader(process.inputStream))
        bufferedReader.readLine() != null
    } catch (t: Throwable) {
        false
    } finally {
        process?.destroy()
    }
}

定义了一个 isDeviceRooted() 函数,该函数调用了三个检测 root 的方法:checkRootMethod1()checkRootMethod2()checkRootMethod3()

checkRootMethod1() 方法检查设备的 build tags 是否包含 test-keys。这通常是用于测试的设备,因此如果检测到这个标记,则可以认为设备已被 root。

checkRootMethod2() 方法检查设备是否存在一些特定的文件,这些文件通常被用于执行 root 操作。如果检测到这些文件,则可以认为设备已被 root。

checkRootMethod3() 方法使用 Runtime.exec() 方法来执行 which su 命令,然后检查命令的输出是否不为空。如果输出不为空,则可以认为设备已被 root。

模拟器检测

1
2
3
fun isEmulator(): Boolean { 
        return Build.FINGERPRINT.startsWith("generic") || Build.FINGERPRINT.startsWith("unknown") || Build.MODEL.contains("google_sdk") Build.MODEL.contains("Emulator") || Build.MODEL.contains("Android SDK built for x86") || Build.MANUFACTURER.contains("Genymotion") || Build.HOST.startsWith("Build") || Build.PRODUCT == "google_sdk" 
        }

通过检测系统的 Build 对象来判断当前设备是否为模拟器。具体方法是检测 Build.FINGERPRINT 属性是否包含字符串 "generic"

模拟器检测对抗

反调试

安卓系统自带调试检测函数

1
2
3
4
5
6
fun checkForDebugger() {  
    if (Debug.isDebuggerConnected()) {  
        // 如果调试器已连接,则终止应用程序  
        System.exit(0)  
    }  
}

debuggable属性

1
2
3
4
5
6
public boolean getAppCanDebug(Context context)//上下文对象为xxActivity.this
{
    boolean isDebug = context.getApplicationInfo() != null &&
            (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
    return isDebug;
}

ptrace检测

1
2
3
4
int ptrace_protect()//ptrace附加自身线程 会导致此进程TracerPid 变为父进程的TracerPid 即zygote
{
    return ptrace(PTRACE_TRACEME,0,0,0);;//返回-1即为已经被调试
}

每个进程同时刻只能被1个调试进程ptrace ,主动ptrace本进程可以使得其他调试器无法调试

调试进程名检测

 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
26
27
28
29
30
31
32
33
34
int SearchObjProcess()
{
    FILE* pfile=NULL;
    char buf[0x1000]={0};

    pfile=popen("ps","r");
    if(NULL==pfile)
    {
        //LOGA("SearchObjProcess popen打开命令失败!\n");
        return -1;
    }
    // 获取结果
    //LOGA("popen方案:\n");
    while(fgets(buf,sizeof(buf),pfile))
    {

        char* strA=NULL;
        char* strB=NULL;
        char* strC=NULL;
        char* strD=NULL;
        strA=strstr(buf,"android_server");//通过查找匹配子串判断
        strB=strstr(buf,"gdbserver");
        strC=strstr(buf,"gdb");
        strD=strstr(buf,"fuwu");
        if(strA || strB ||strC || strD)
        {
            return 1;
            // 执行到这里,判定为调试状态

        }
    }
    pclose(pfile);
    return 0;
}

[原创]对安卓反调试和校验检测的一些实践与结论

frida检测

一些Frida检测手段

smali赋值学习

int类型

const/4和const/16的区别?

const/4 最大只允许存放4个二进制位(4bit), const/16 最大值允许存放16个二进制位(16bit), 第一位(即最高位)默认为符号位。单位换算 1byte=8bit

1
2
3
4
5
6
7
const/4 v0,0x2 # 最大只允许存放半字节数据 取值范围为 -8 and 7

const/16 v0 , 0xABCD # 定义一个寄存器变量,最大只允许存放16位数据 比如short类型数据 取值范围为-32768~32767

const v0 , 0xA# 定义一个寄存器, 最大只允许存放32位数据,比如int类型数据 将数字10赋值给v0 取值范围-2147483647~2147483647

const/high16 #定义一个寄存器, 最大只允许存放高16位数值 比如0xFFFF0000末四位补0 存入高四位0XFFFF

Xposed模块编写

Xposed是一款可以在不修改APK的情况下影响程序运行的框架,基于它可以制作出许多功能强大的模块,且在功能不冲突的情况下同时运作。在这个框架下,我们可以编写并加载自己编写的插件APP,实现对目标apk的注入拦截等。

说白了跟frida一样就是hook

Xposed原理

用自己实现的app_process替换掉了系统原本提供的app_process,加载一个额外的jar包,入口从原来的: com.android.internal.osZygoteInit.main()被替换成了: de.robv.android.xposed.XposedBridge.main(), 创建的Zygote进程就变成Hook的Zygote进程了,从而完成对zygote进程及其创建的Dalvik/ART虚拟机的劫持(zytoge注入)

image-20260611203111763

xposed的历史发展和免root框架

xposed能做什么

1.修改app布局:上帝模式 2.劫持数据,修改参数值、返回值、主动调用等。例:微信防撤回、步数修改、一键新机 应用变量

xposed环境配置

虽然教程用的unbuntu,但是这些工具windows都有,所以我用windows

android studio基本是抄idea的,所以很熟悉,这里还是选择用java开发,框架选择no activity

image-20260611210721754

libs目录下添加这个xposedbridgeapi.jar,然后添加到库

接下来修改AndroidManifest.xml

image-20260611211936388

修改build.gradle,将此处修改为compileOnly ,默认的是implementation

1
2
implementation 使用该方式依赖的库将会参与编译和打包
compileOnly 只在编译时有效不会参与打包

image-20260611212527442

新建–>Folder–>Assets Folder,创建xposed_init(不要后缀名):只有一行代码,就是说明入口类

image-20260611212921138

image-20260611212927785

新建的hook类实现IXposedHookLoadPackage接口,然后在handleLoadPackage函数内编写Hook逻辑

1
2
3
4
5
6
7
8
9
import de.robv.android.xposed.IXposedHookLoadPackage; 
import de.robv.android.xposed.callbacks.XC_LoadPackage;

public class Hook implements IXposedHookLoadPackage {
    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {

    }
}

继承这个接口,就有实现hook的能力

Xposed常用api

kotlin可用:An efficient Hook API and Xposed Module solution built in Kotlin

手上没真机,首先这里需要模拟器连接上as,如何使用 MuMu模拟器和 Android Studio 调试应用?_MuMu模拟器_安卓模拟器

adb连接,然后as里面就自动识别了,打开logcat

image-20260611220539188

hook普通方法

日志过滤zj5295,也就是之前看过日志必然输出的这个

image-20260611221250714

jadx看一下代码

image-20260611221505855

这里普通方法实际上调用了a方法,所以我们要hook这个a方法,jadx里面可以右键方法名复制xposed片段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo", classLoader, "a", "java.lang.String", new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
    }
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
    }
});

复制到前面那个hook类里面,现在可以看到有个before和after方法,我们可以重写它,这里可以让它的值输出到日志,也可以修改值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Hook implements IXposedHookLoadPackage {
    @Override
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
        if(!loadPackageParam.packageName.equals("com.zj.wuaipojie")){
            return;
        }
        XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo", loadPackageParam.classLoader, "a", "java.lang.String", new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                super.beforeHookedMethod(param);
                Log.d("zj2595",param.args[0].toString());
                XposedBridge.log(param.args[0].toString());
                String a = "0d00";
                param.args[0] = a;
                Log.d("zj2595",param.args[0].toString());
            }
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
            }
        });

    }
}

这里值得注意的是,运行会跳没有默认的activity,但是本身我们就没,所以不用管,这里看模拟器里面lsposed已经多了一个我们写的模块了,模块勾选demo就行,重启demo就发现log里面已经变成我们想要修改的值了

image-20260611233210239

同理我们可以修改after方法

1
2
3
4
5
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                Log.d("zj2595",param.getResult().toString());
                param.setResult("0721");
            }

模拟器就这点不好,每次都要卸载模块再安装

image-20260611234033329

Hook复杂方法&自定义参数

image-20260611234538226

这里需要map参数,但是下面都没调用这个,直接置空

1
2
3
4
5
6
7
Class a = classLoader.loadClass("类名");
XposedBridge.hookAllMethods(a,"方法名",new XC_MethodReplacement() {  
    @Override  
    protected Object replaceHookedMethod(MethodHookParam methodHookParam) throws Throwable {  
        return "";  
    }  
});

写完长下面这样

1
2
3
4
5
6
7
8
Class a = loadPackageParam.classLoader.loadClass("com.zj.wuaipojie.Demo");
        XposedBridge.hookAllMethods(a, "complexParameterFunc", new XC_MethodHook() {
            @Override
            protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                super.beforeHookedMethod(param);
                Log.d("zj2595",param.args[0].toString());
            }
        });

image-20260612000010545

确实输出了

Hook加固通杀

1
2
3
4
5
6
7
8
XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {  
    @Override  
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {  
        Context context = (Context) param.args[0];  
        ClassLoader classLoader = context.getClassLoader();
        //hook逻辑在这里面写  
    }  
});

Hook变量

教程说静态变量在jadx里面静态变量没法直接复制xposed代码,但是新版已经可以了,不过实际上类名还是要用findClass获取,然后这里获取到的也只是get方法

1
XposedHelpers.getStaticObjectField(/*runtimeObject*/, "静态变量名");

教程里面的方法

1
2
final Class clazz = XposedHelpers.findClass("类名", classLoader);  
XposedHelpers.setStaticIntField(clazz, "变量名", 999);

如果不是静态变量,而是实例变量,就要先hook所有的构造方法,然后再赋值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
final Class clazz = XposedHelpers.findClass("类名", classLoader);  
XposedBridge.hookAllConstructors(clazz, new XC_MethodHook() {  
     @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {  
        super.afterHookedMethod(param);  
        //param.thisObject获取当前所属的对象
        Object ob = param.thisObject;  
        XposedHelpers.setIntField(ob,"变量名",9999);  
    }  
});

简单改一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        final Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo", loadPackageParam.classLoader);
        XposedHelpers.setStaticObjectField(clazz, "staticField","0d00");
        XposedBridge.hookAllConstructors(clazz, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                //param.thisObject获取当前所属的对象
                Object ob = param.thisObject;
                XposedHelpers.setIntField(ob,"publicInt",721);
            }
        });

image-20260616235257097

Hook构造函数

无参构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
XposedHelpers.findAndHookConstructor("com.zj.wuaipojie.Demo", classLoader, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
    }
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
    }
});

有参构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
XposedHelpers.findAndHookConstructor("com.zj.wuaipojie.Demo", classLoader, String.class, new XC_MethodHook() {
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
    }
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
    }
});

Hook multiDex方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
XposedHelpers.findAndHookMethod(Application.class, "attach", Context.class, new XC_MethodHook() {  
    @Override  
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {  
        ClassLoader cl= ((Context)param.args[0]).getClassLoader();  
        Class<?> hookclass=null;  
        try {  
            hookclass=cl.loadClass("类名");  
        }catch (Exception e){  
            Log.e("zj2595","未找到类",e);  
            return;        
        }  
        XposedHelpers.findAndHookMethod(hookclass, "方法名", new XC_MethodHook() {  
            @Override  
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {  
            }        
        });  
    }  
});

主动调用

静态方法

1
2
Class clazz = XposedHelpers.findClass("类名",lpparam.classLoader);
XposedHelpers.callStaticMethod(clazz,"方法名",参数(非必须));

实例方法

1
2
Class clazz = XposedHelpers.findClass("类名",lpparam.classLoader);
XposedHelpers.callMethod(clazz.newInstance(),"方法名",参数(非必须));

Hook内部类

1
2
3
4
5
6
7
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo$InnerClass", lpparam.classLoader, "innerFunc",String.class,  new XC_MethodHook() {  
    @Override  
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {  
        super.beforeHookedMethod(param);  

    }  
});

反射大法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Class clazz = XposedHelpers.findClass("com.zj.wuaipojie.Demo", lpparam.classLoader);
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.Demo$InnerClass", lpparam.classLoader, "innerFunc",String.class,  new XC_MethodHook() {  
    @Override  
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {  
        super.beforeHookedMethod(param);  
        //第一步找到类
        //找到方法,如果是私有方法就要setAccessible设置访问权限
        //invoke主动调用或者set修改值(变量)
        Class democlass = Class.forName("com.zj.wuaipojie.Demo",false,lpparam.classLoader);  
        Method demomethod = democlass.getDeclaredMethod("refl");  
        demomethod.setAccessible(true);  
        demomethod.invoke(clazz.newInstance());  
    }  
});

遍历所有类下的所有方法

 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
26
27
28
29
30
XposedHelpers.findAndHookMethod(ClassLoader.class, "loadClass", String.class, new XC_MethodHook() {  
    @Override  
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {  
        super.afterHookedMethod(param);  
        Class clazz = (Class) param.getResult();  
        String clazzName = clazz.getName();  
        //排除非包名的类  
        if(clazzName.contains("com.zj.wuaipojie")){  
            Method[] mds = clazz.getDeclaredMethods();  
            for(int i =0;i<mds.length;i++){  
                final Method md = mds[i];  
                int mod = mds[i].getModifiers();  
                //去除抽象、native、接口方法  
                if(!Modifier.isAbstract(mod)  
                    && !Modifier.isNative(mod)  
                    &&!Modifier.isAbstract(mod)){  
                    XposedBridge.hookMethod(mds[i], new XC_MethodHook() {  
                        @Override  
                        protected void beforeHookedMethod(MethodHookParam param) throws Throwable {  
                            super.beforeHookedMethod(param);  
                            Log.d("zj2595",md.toString());  
                        }  
                    });  
                }  

           }  
        }  

    }  
});

Xposed的妙用

字符串赋值定位

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
XposedHelpers.findAndHookMethod("android.widget.TextView", lpparam.classLoader, "setText", CharSequence.class, new XC_MethodHook() {  
    @Override  
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {  
        super.beforeHookedMethod(param);  
        Log.d("zj2595",param.args[0].toString());  
                if(param.args[0].equals("xxx")){  
                    printStackTrace();  
                }
    }  
});
private static void printStackTrace() {  
    Throwable ex = new Throwable();  
    StackTraceElement[] stackElements = ex.getStackTrace();  
    for (int i = 0; i < stackElements.length; i++) {  
        StackTraceElement element = stackElements[i];  
        Log.d("zj2595","at " + element.getClassName() + "." + element.getMethodName() + "(" + element.getFileName() + ":" + element.getLineNumber() + ")");  
    }  
}

点击事件监听

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Class clazz = XposedHelpers.findClass("android.view.View", lpparam.classLoader);
XposedBridge.hookAllMethods(clazz, "performClick", new XC_MethodHook() {  
    @Override  
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {  
        super.afterHookedMethod(param);  
        Object listenerInfoObject = XposedHelpers.getObjectField(param.thisObject, "mListenerInfo");  
        Object mOnClickListenerObject = XposedHelpers.getObjectField(listenerInfoObject, "mOnClickListener");  
        String callbackType = mOnClickListenerObject.getClass().getName();  
        Log.d("zj2595",callbackType);  
    }  
});

改写布局

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
XposedHelpers.findAndHookMethod("com.zj.wuaipojie.ui.ChallengeSixth", lpparam.classLoader,  
        "onCreate", Bundle.class, new XC_MethodHook() {  
    @Override  
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {  
        super.afterHookedMethod(param);  
        View img = (View)XposedHelpers.callMethod(param.thisObject,  
                "findViewById", 0x7f0800de);  
        img.setVisibility(View.GONE);  

    }  
});

Xposed模块patch

LSPatch

首先要在as里面把xposed模块编译成app,编译好了在这里

image-20260619151435990

然后打开lspatch,选择集成模式就能实现免root的注入

image-20260619152718493

然后这里嵌入的模块就选择我们前面写的xposed模块

Xposed快速Hook

SimpleHook

先去mt管理器找一个普通方法,复制方法签名,然后黏贴进去,选择记录参数返回值

image-20260619154859114

然后运行程序就能在日志上看到了,也可以同理进行修改

IDA使用

ELF文件格式

ELF(Executable and Linkable Format)是一种可执行和可链接的文件格式,是linux底下二进制文件,可以理解为windows下的PE文件,在Android中可以比作SO,方便函数的移植,在常用于保护Android软件,增加逆向难度。

ELF文件的主要组成部分包括:

  • ELF Header:文件头,描述文件的基本信息
  • Program Header Table:程序头表,描述进程映像的布局
  • Section Header Table:节区头表,描述文件的各个节区
节区名描述
.text代码段,存放程序的指令
.data数据段,存放已初始化的全局变量和静态变量
.rodata只读数据段,存放只读数据
.bss未初始化数据段,存放未初始化的全局变量和静态变量
.symtab符号表,存放符号信息
.strtab字符串表,存放字符串数据
.dynsym动态符号表,存放动态链接需要的符号信息
.dynamic动态链接信息,存放动态链接器需要的信息

NDK开发

新建一个native C++的项目

image-20260714113844015

下面是cmakelist.txt和native-lib.cpp文件的作用以及简要说明:

文件名作用说明
CMakeLists.txt构建配置文件CMakeLists.txt是用于配置NDK项目的构建系统的文件。它指定了构建所需的源文件、依赖项、编译选项等。在构建过程中,CMake会根据该文件的指示生成对应的构建脚本,用于编译本地代码并生成本地库。
native-lib.cpp本地代码实现文件native-lib.cpp是包含本地代码实现的文件。它定义了通过Java和本地代码之间进行通信的本地方法。该文件中的函数实现将被编译为本地库,供Java代码调用。

我们可以更改这个cpp的内容

image-20260714121242352

然后编译成apk,放在jadx里面发现确实无法分析,因为这部分内容需要分析so文件

1.JNI的前世今生

NDK是开发套件,JNI才是调用的框架。所以与其说是NDK开发,不如说是JNI的开发。不过NDK是Android提供的开发套件。JNI可不是,JNI全称Java Native Interface,即Java本地接口,JNI是Java调用Native 语言的一种特性。通过JNI可以使得Java与C/C++机型交互。即可以在Java代码中调用C/C++等语言的代码或者在C/C++代码中调用Java代码。

2.JNI的两种注册方式

jni静态注册方式
  • 优点: 理解和使用方式简单, 属于傻瓜式操作, 使用相关工具按流程操作就行, 出错率低
  • 缺点: 当需要更改类名,包名或者方法时, 需要按照之前方法重新生成头文件, 灵活性不高
jni动态注册方式
 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
26
27
28
29
30
31
32
33
34
35
36
37
#include <jni.h>
#include <string>

extern "C" {

JNIEXPORT jstring JNICALL Java_com_example_ndkdemo_MainActivity_nativeGetStringFromJNI(JNIEnv* env, jobject obj) {
    std::string hello = "Hello wuaipojie";
    return env->NewStringUTF(hello.c_str());
}

// 定义本地方法注册函数
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    JNIEnv* env;
    if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    // 定义要注册的本地方法
    JNINativeMethod methods[] = {
        {"nativeGetStringFromJNI", "()Ljava/lang/String;", reinterpret_cast<void*>(Java_com_example_ndkdemo_MainActivity_nativeGetStringFromJNI)}
    };

    // 获取类引用
    jclass clazz = env->FindClass("com/example/ndkdemo/MainActivity");
    if (clazz == nullptr) {
        return -1;
    }

    // 注册本地方法
    if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
        return -1;
    }

    return JNI_VERSION_1_6;
}

} // extern "C"
数据类型

下面是一些常见的C++数据类型和它们在Java中的对应关系,以及它们在JNI动态注册中的数据类型签名(signature):

C++ 数据类型Java 数据类型JNI 数据类型签名
jintint“I”
jbooleanboolean“Z”
jbytebyte“B”
jcharchar“C”
jshortshort“S”
jlonglong“J”
jfloatfloat“F”
jdoubledouble“D”
jobjectObject“Ljava/lang/Object;”
jstringString“Ljava/lang/String;”
jarrayArray“[elementType”
jobjectArrayObject[]“[Ljava/lang/Object;”
jbooleanArrayboolean[]“[Z”
jbyteArraybyte[]“[B”
jcharArraychar[]“[C”
jshortArrayshort[]“[S”
jintArrayint[]“[I”
jlongArraylong[]“[J”
jfloatArrayfloat[]“[F”
jdoubleArraydouble[]“[D”

在JNI动态注册中,需要使用正确的数据类型签名来声明本地方法。例如,如果你要注册一个返回int类型的本地方法,其数据类型签名应为I

ida使用和patch

快捷键:

快捷键功能
Esc回到上一个位置
Enter跳转到当前光标处的地址
-折叠代码
+展开代码
*创建一个结构
Alt + A手动定义一个数组
Alt + F寻找直接引用的函数
Alt + G跳转到特定的地址
Alt + T显示调用树
Alt + X重命名
Ctrl + G快速跳转到指定地址
Ctrl + J显示引用列表
Ctrl + K显示 XREF 到选中的函数/数据
Ctrl + N创建一个函数
Ctrl + Q快速重命名
Ctrl + X显示从选中的函数/数据的 XREF
Ctrl + E显示结构类型
Ctrl + R手动定义一个数据结构
Ctrl + W打开函数列表
Ctrl + D以十进制显示当前值
Ctrl + B以二进制显示当前值
Ctrl + H以十六进制显示当前值
Space在图形/文本视图中切换
shift + f12打开字符串窗口
F5转伪C代码

patch方式

先分析so文件

image-20260714130525132

由于前面as里面可以看到我们静态注册的这个函数是导出函数,所以可以直接搜索到

image-20260714132214149

然后反编译出来,函数类型是int跟我们as里面的不同,按int然后按y键就可以改类型了,然后后续的方法名也能被识别出来了

image-20260714133152895

至于patch,ida下载下来都会带keypatch插件

image-20260714135617417

比如这个更改,patch的时候要把.W去掉,这个就是32位指令,代表wide的意思。

更改完数值后,需要点patch program进行apply,这个时候so就patch好了,然后覆盖之前apk里面的so就能生效了

另一种方法就是改hex,用到ARM to HEX

so和ida动调

so加载流程

image-20260714143837369

函数的基本介绍:

函数名描述
android_dlopen_ext()dlopen()do_dlopen()这三个函数主要用于加载库文件。android_dlopen_ext 是系统的一个函数,用于在运行时动态加载共享库。与标准的 dlopen() 函数相比,android_dlopen_ext 提供了更多的参数选项和扩展功能,例如支持命名空间、符号版本等特性。
find_library()find_library() 函数用于查找库,基本的用途是给定一个库的名字,然后查找并返回这个库的路径。
call_constructors()call_constructors() 是用于调用动态加载库中的构造函数的函数。
init库的构造函数,用于初始化库中的静态变量或执行其他需要在库被加载时完成的任务。如果没有定义init函数,系统将不会执行任何动作。需要注意的是,init函数不应该有任何参数,并且也没有返回值。
init_arrayinit_array是ELF(Executable and Linkable Format,可执行和可链接格式)二进制格式中的一个特殊段(section),这个段包含了一些函数的指针,这些函数将在main()函数执行前被调用,用于初始化静态局部变量和全局变量。
jni_onload这是Android JNI(Java Native Interface)中的一个函数。当一个native库被系统加载时,该函数会被自动调用。JNI_OnLoad可以做一些初始化工作,例如注册你的native方法或者初始化一些数据结构。如果你的native库没有定义这个函数,那么JNI会使用默认的行为。JNI_OnLoad的返回值应该是需要的JNI版本,一般返回JNI_VERSION_1_6

下断点时机: 应用级别的:java_com_XXX; 外壳级别的:JNI_Onload,.init,.init_array(反调试); 系统级别的:fopen,fget,dvmdexfileopen(脱壳);

ida动调

1.前置操作:

1.在IDA目录下的dbgsrv,选择跟手机架构一致的server 2.adb push as /data/local/tmp/ 3.进入手机端命令:adb shell 4.切换获取手机的root权限:su 5.跳到对应路径:cd /data/local/tmp/ 6.提权:chmod 777 as 7.XappDebug hook

image-20260715173727740

2.调试步骤

分为两种模式,一种是以debug模式启动,第二种则以普通模式启动,二者的区别在于使用场景,有时候要动态调试的参数在app一启动的时候就产生了,时机较早,所以需要以debug模式去挂起app

1
2
3
4
adb shell am start -D -n com.zj.wuaipojie/.ui.ChallengeEight (去掉-D 则表示不以debug模式启动app)
adb forward tcp:23946 tcp:23946 (端口转发)
adb forward tcp:8700 jdwp:PID (pid监听)
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700 (jdb挂起)

PS:若不是以debug启动则不需要输入后两条命令

使用 Hugo 构建
主题 StackJimmy 设计