Android RE
学习了下正己师傅的Android逆向,感觉还挺有意思的(开个坑~)
环境搭建
模拟器
雷电9模拟器:
https://www.ldmnq.com/
开启Root,开启System.vmdk可写入,配置ADB至环境变量(后续在进行动态调试时用到)
Android工具清单
阿里云盘:
https://www.aliyundrive.com/s/JTSiQ7fpS78
密码:3b2j
Magisk:
功能:
- MagiskSU:为应用程序提供root访问权限
- Magisk模块:通过模块修改只读分区
- MagiskHide:从根/系统完整性检查中隐藏 Magisk(Shamiko)
- MagiskBoot:最完整的安卓启动镜像解包和重新打包工具
LSPosed:一个是在Magisk中安装的模块,一个是该模块的寄生程序
开发者助手:用于定位(弹窗,字符串,图片等资源)
开发助手:用于定位(弹窗,字符串,图片等资源),更好用一点
MT管理器:对APK进行处理
NP管理器:对APK进行处理
Smali语法查询
核心破解:签名相关
XAppDebug:动态调试
Jdk:用于给Jeb提供JAVA环境
Jadx-gui:反编译等
Jeb:反编译,动态调试等
算法助手:Hook,拦截日志等
platform-tools:其他版本的ADB
日志插桩.dex
基础知识
APK
Android Package,相当于一个压缩文件,只需将apk后缀改为zip即可解压
APK文件结构:
文件 | 描述 |
---|---|
assets目录 | 存放静态资源文件,例:视频,音频,图片等 |
lib目录 | armeabi-v7a基本通用所有android设备,arm64-v8a只适用于64位的android设备,x86常见用于android模拟器,其目录下的.so文件是c或c++编译的动态链接库文件 |
META-INF目录 | 保存应用的签名信息,签名信息可以验证APK文件的完整性(验证文件是否被修改) |
res目录 | 存放资源文件,包括图片,字符串等 |
AndroidManifest.xml | APK的应用清单信息,它描述了应用的名字,版本,权限,引用的库文件等 |
classes.dex | 是java源码编译后生成的java字节码文件,APK运行的主要逻辑 |
resources.arsc | 是编译后的二进制资源文件,它是一个映射表,映射着资源和id,通过R文件中的id就可以找到对应的资源 |
APP在开发时的工程结构:
manifests:只有一个XML文件,AndroidManifest.xml,为App的运行配置文件
java:3个com.example.myapp包,第一个包存放当前模块java源码,后两个存放测试使用的java代码
res:存放当前模块的资源文件,之下又有4个子目录:
drawable:存放图形描述文件与图片文件
layout:存放App页面的布局文件
mipmap:存放App的启动图标
values:存放常量定义文件,例:字符串常量strings.xml,像素常量dimens.xml,颜色常量colors.xml
Gradle Scripts:Gradle :自动化构建工具
build.gradle:分为项目级和模块级,用于描述App工程的编译规则
proguard-rules.pro:描述java代码的混淆规则
gradle.properties:配置编译工程的命令行参数,一般无须改动
settings.gradle:配置了需要编译哪些模块,初始内容为include “.app”,表示只编译app模块
local.properties:本地配置文件,在工程编译时自动生成,用于描述开发者电脑的环境配置,包括SDK本地路径等
模块级:
android {
namespace 'com.example.closureapp'
//指定编译使用的SDK版本号
compileSdk 32
defaultConfig {
//指定该模块的应用编号==》App报名
applicationId "com.example.closureapp"
//指定App适合运行的最小SDK版本号
minSdk 28
//指定目标设备的SDK版本号
targetSdk 32
//指定App应用版本号
versionCode 1
//指定App的应用版本名称
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
//依赖
dependencies {
implementation 'androidx.appcompat:appcompat:1.4.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
双开及其原理
双开:同时运行两个或多个相同的应用
1、修改包名:生成两个数据存储路径
2、修改Framework:针对有系统修改权限的厂商,例:小米自带多开
3、通过虚拟化技术实现:使用虚拟Framework层,虚拟文件系统,模拟Android对组件的管理,虚拟应用进程管理等一整套虚拟技术,及那个APK复制一份到虚拟空间中运行
4、以插件机制运行:利用反射替换,动态代理,hook了系统的大部分与system-server进程通讯的函数,欺骗系统认为只有一个apk在运行,瞒过插件让其认为自己已经安装,例:VirtualApp
汉化APK
汉化:对外文的软件资源进行读取,翻译,修改,回写等,使软件的菜单,对话框等界面显示为中文,而程序的内核和功能保持不变
1、Arsc汉化
2、Xml汉化
3、Dex汉化
APK中基本上所有的字符串都在arsc中
汉化实操
1、提取应用安装包
MT管理器(有些功能需要会员)
左上角三条杠,选择安装包提取
选择目标应用
提取安装包
成功提取并保存至相应路径
NP管理器
右上角三条杠选择安装包提取
选择目标应用程序
提取安装包
运行应用程序,进入第一关:
对该页面进行汉化:
首先尝试搜索字符串hello
在MT管理器中定位至刚刚提取安装包所在的路径
进行查看
使用搜索功能
搜索结束
点击该文件,再次点击该文件选择反编译
成功反编译(需要登陆)
修改,并保存
勾选自动签名并确定
回退至提取后的安装包所在的路径,点击安装包
发现签名不一致
选择先将应用程序卸载,再安装该汉化后的安装包
此时hello 52pojie已被成功替换,但是其中还有两句话没有翻译,其中一句还是看不懂的语言
使用开发者助手,成功启动后将会留下一个瓢虫的浮窗,回到应用,点击浮窗,并点击开始界面分析
点击不认识的语言,开发者工具将会提供该控件的属性
点击该text,选择复制,并回到MT管理器中搜索该句子
成功搜索,位于arsc中,选择翻译模式
==》
成功获得该文件中的字符串
对其进行翻译
保存修改并退出,并且自动签名
再次选择安装该安装包
此次签名和上一次修改后的签名是一致的,所以可以直接安装成功
再次运行程序
第三句话同样使用开发者助手复制字符串,进行搜索
位于dex文件中
选择打开方式
使用搜索功能
点击搜索结果,跳转至相应位置
进行翻译修改
保存,自动修改签名,安装,运行,成功汉化
AndroidManifest.xml
是整个应用程序的信息描述文件,定义了应用程序中包含的Activity,Service,Content,provider和BroadcastReceiver组件信息;
每个应用程序在根目录下必须包含一个AndroidManifest.xml文件,且文件名不能修改,它描述了package中暴露的组件,各自的实现类,各种能被处理的数据和启动位置
属性 | 描述 |
---|---|
versionCode | 版本号,用来更新 |
versionName | 版本号,用户看 |
package | 包名 |
user-permission android:name=”” | 应用权限 |
android:label | 指定App在手机屏幕上显示的名称 |
android:icon | 指定App在手机屏幕上显示的图标 |
android:roundIcon | 指定App的圆角图标 |
android:supportsRtl | 是否支持阿拉伯语/波斯语这种从右向左的文字排序 |
android:theme | 指定App显示风格 |
android:allowBackup | 是否允许备份,允许用户备份系统应用和第三方应用的apk安装包和应用数据,以便于在刷机或数据丢失后恢复数据。用户可通过adb backup和adb restore进行应用数据的备份和恢复。 |
android:debuggable=”true” | 应用是否开启debug权限 |
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.CLosureApp"
tools:targetApi="31">
<activity
android:name=".MainActivity"//第一个运行的Activity
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>
Activity:应用程序组件,提供一个屏幕,用户可以用以交互
查看目标应用程序的AndroidManifest.xml文件
使用NP管理器,操作与MT管理器差不多,选择编辑AndroidManifest.xml文件
尝试替换应用程序的图标和名字:
选择通用编辑功能进行修改
自定义图标和应用名
安装:
由于与上文使用的MT管理器的签名不一致,所以卸载上文的应用即可(注意安装的是修改后的安装包edit)
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 methmod | 方法名的结束 |
public | 公有 |
protected | 保护 |
private | 私有 |
.parameter | 方法参数 |
.prologue | 方法开始 |
.line xxx | 位于第xxx行 |
数据类型
smali类型 | JAVA类型 | 描述 |
---|---|---|
V | void | 无返回值 |
Z | boolean | 布尔值类型,0或1 |
B | byte | 字节类型 |
S | short | 短整型 |
C | char | 字符 |
I | int | 整型 |
J | long(64位,需要两个寄存器存储) | 长整型 |
F | float | 单浮点 |
D | double(64位,需要两个寄存器存储) | 双浮点 |
string | String | 文本,返回字符串 |
Lxxx/xxx/xxx | object | 对象类型,返回对象 |
指令
指令 | 描述 |
---|---|
const | 重写整数类型 |
const-string | 重写字符串 |
const-wide | 重写长整数类型,多用于修改时间 |
return | 返回 |
if-eq | equal(a=b),比较寄存器a,b内容,相同则跳 |
if-ne | not equal(a!=b),ab内容不相同则跳转 |
if-eqz | qeual zero(a=0),z即是0的标记,a=0则跳转 |
if-nez | not equal zero(a!=0),a不等于0则跳转 |
if-ge | greater equal(a>=b),a大于等于b则跳转 |
if-le | little equal(a<=b),a小于等于b则跳转 |
goto | 强制跳转 |
switch | 分支跳转 |
iget | 获取寄存器数据 |
寄存器
在smali中的所有操作都必须经过寄存器来进行;
本地寄存器使用v开头,v0,v1,v2
参数寄存器使用p开头,p0,p1,p2;p0不一定是函数中的第一个参数==》
在非static函数中,p0代表this,p1表示函数的第一个参数……
在static函数中,p0为第一个参数(JAVA的static方法中没有this方法)
《一键三连》
目标程序:
目标:完成一键三连
定位:1、通过字符串定位;2、通过抓取按钮定位
字符串定位:
使用jadx-gui对其进行反编译,通过搜索字符串定位:
按钮定位:
使用开发者助手==》界面资源分析==》获取控件属性
在MT管理器中进行搜索
上方就是关键代码
分析代码逻辑
首先判断是否拥有10硬币,再判断是否是大会员,若是大会员,则实现一键三连;
但是我们并不是大会员,所以接下来将会修改该代码,实现一键三连的目的
==》
通过修改smali代码来实现
通过函数名在smali中定位
为了实现对smali代码的修改,则需要用到MT管理器
基本步骤与上文汉化apk类似,都是先提取安装包,再搜索字符串定位
1、修改判断
程序要求要有10个硬币,并判断,我们可以将判断改成小于10个硬币就跳转
将if-ge修改为if-le即可实现
程序又判断是否是大会员,不是大会员将会跳转,所以,我们可以将该判断跳过,直接执行接下来的是大会员的操作
保存,修改签名并安装==》成功实现
2、强制跳转
实现此操作也可以直接使用强制跳转goto实现,且goto后面需要跟上标签,而在“已经是大会员……”处并无标签,所以还需要创建一个标签(不与其他标签重复即可)
接下来再判断前+goto 标签就可以实现跳转
保存安装==》成功实现
3、修改寄存器的值
当然也可以通过修改寄存器的值来实现
在判断硬币是否有10个的时候将10改成0即可
后续还会判断是否是大会员,由于判断是通过isvip()方法的返回值,所以我们直接在方法中将返回值修改为1即可
长按isvip==》跳转
修改返回值为1
保存安装==》成功实现
四大组件
1、Activity(活动):在应用中的一个Activity可以用来表示一个界面,意思可以理解为“活动”,即一个活动开始,代表 Activity组件启动,活动结束,代表一个Activity的生命周期结束。一个Android应用必须通过Activity来运行和启动,Activity的生命周期交给系统统一管理;
2、Service(服务):Service它可以在后台执行长时间运行操作而没有用户界面的应用组件,不依赖任何用户界面,例如后台播放音乐,后台下载文件等;
3、Broadcast Receiver(广播接收器):一个用于接收广播信息,并做出对应处理的组件。比如我们常见的系统广播:通知时区改变、电量低、用户改变了语言选项等;
4、Content Provider(内容提供者):作为应用程序之间唯一的共享数据的途径,Content Provider主要的功能就是存储并检索数据以及向其他应用程序提供访问数据的接口。Android内置的许多数据都是使用Content Provider形式,供开发者调用的(如视频,音频,图片,通讯录等)
生命周期
onCreate:创建活动,将页面布局加载进内存,进入初始状态
onStart:开始活动,将活动页面显示在屏幕上,进入就绪状态
onResume:恢复活动,活动页面进入活跃状态,能够与用户正常交互,例:响应点击动作
onpause:暂停活动,页面进入暂停状态,无法与用户正常交互
onStop:停止活动,页面将不在屏幕上显示
onDestroy:销毁活动,回收活动占用的系统资源,把页面从内存中清除
onRestart:重启活动,重新加载内存中的页面数据
onNewInstent:重用已有的活动实例
活动的动态变迁:
如果一个Activity已经启动过了,并且存在于当前应用的Activety任务栈中,启动模式为singleTask;
singleInstance或者singleTop(此时已经在任务栈的顶端),那么在此启动或回到这个Activety的时候,不会创建新的实例,也就是不会执行onCreate方法,而是执行onNewIntent方法。
广告 / 弹窗 / 布局
广告
启动广告流程:
启动Activity==》广告Activity==》主页Activity
定位方法:Activity切换定位
在MT管理器工具栏中
启动该功能后即可运行目标程序,并打开该程序的广告界面,成功抓取Activity记录后停止该功能
在MT管理器中的Activity记录中,复制获取该广告的Activity,在NP管理器中进行搜索定位
将定位的smali代码转为JAVA代码查看
修改方法:
1、修改广告Activity加载时间
由JAVA代码可知,该广告将被加载3秒
由此,将3秒改为0即可,在smali中寻找loadAd,并修改
修改后的JAVA代码
保存,签名并安装,成功看不到广告了,但是广告所在的Acvitity依然会被加载
MT Activity记录验证:
2、修改Intent的Activity类名(推荐使用)
定位与上个方法一样,在MT管理器中进行搜索,长按复制广告类的smali路径
在NP中根据路径搜索,定位
排除类本身的内容,我们需要查看是在哪个外部方法中对其调用
分析其JAVA代码
由此,只要我们将该广告Activity修改为第三关的Activity即可
一样的方式在MT管理器中定位至要修改的smali代码
按照类似的格式对其修改
保存,自动签名并安装运行,使用MT Activity记录验证==》成功跳过广告
3、实现比广告更快加载(不推荐,容易引起程序异常,崩溃等)
在MT管理器中,查看AndroidManifest.xml文件,并替换主Activity
保存,自动签名并安装启动,将直接打开第三关的Activity
弹窗
修改方法:
1、修改XML文件中的versiocode(用于更新弹窗,修改版本)
此处改为2即可实现
2、Hook弹窗
多出现于劫持了返回键等
例:目标程序被劫持了返回键
使用算法助手,选择要HOOK的目标程序
开启应用总开关
开启弹窗定位(返回键可取消)
使用右上角运行程序==》
返回第一次
返回第二次
成功跳过弹窗
另一种方式:通过关键词屏蔽弹窗
在刚刚的算法助手中选择屏蔽关键词弹窗,加入广告
再次运行程序,发现弹窗成功跳过
3、修改dex弹窗代码
目标
在算法助手中,通过日志查看该弹窗
通过该方法实现在MT管理器中定位
成功定位
弹窗大部分是由show方法显示信息的,为此,我们寻找该弹窗的show方法,并注释即可
在前方添加#注释掉该行
上方还有一个”2号广告”,使用一样的方法注释掉show方法
保存,安装,成功跳过弹窗
4、抓包修改响应体(也可以使用路由器拦截)
布局
布局定位:1、开发者助手;2、MT管理器XML搜索定位
开发助手
定位广告图片
复制id
在MT中右上角使用XML搜索
成功定位
修改方法:修改XML代码
它存在宽度和高度,我们将它都改为0即可
保存并安装,成功消除广告图片
还有一种办法是直接隐藏布局,在刚刚的XML文件中广告图片位置插入
android:visibility="gone"
成功隐藏
动态调试
为程序添加debug权限
1、AndroidManifest,xml中添加
android:debuggable="true"
添加至application标签中
2、XappDebug模块Hook
3、Magisk命令
adb shell #adb进入命令行模式
su #切换至超级用户
magisk resetprop ro.debuggable 1
stop;start; #一定要通过该方式重启
4、MagiskHide Props Config模块(永久有效)
修改ro.debuggable的值为1
开启端口转发以及ADB权限
雷电模拟器自带端口转发;若是其他模拟器(测试机),请打开手机的开发者选项,开启USB调试功能
下断点
CTRL + B
debug模式启动
adb shell am start -D -n com.zj.wuaipojie/.ui.MainActivity
#参数解析
adb shell am start -D -n 包名/类名
am start -n 表示启动一个activity
am start -D 表示将应用设置为可调试模式
包名和类名获取:
Jeb附加调试进程
目标
根据关键词在JEB中搜索定位
右键解析将Smali转为JAVA代码
定位关键函数check中
在check函数中最后进行base64后的比较,对此,我们将在此处下断电(ctrl + b)
开启debug模式,注意没配置adb环境变量需要前往模拟器所在路径,使用模拟器的adb
JEB附加调试
注意点:若有出现附加调试时找不到模拟器和进程的情况
找不到模拟器:将该模拟器的ADB路径添加至环境变量中
找不到进程:可能是因为ADB版本的问题,我雷电模拟器自带的ADB版本是1.0.31,找不到进程(可以找到模拟器),替换为1.0.41即可
替换ADB需要替换的文件:
关注点:
回到模拟器,执行至输入密钥
点击验证,成功断点
由于我们需要看函数在最后返回了什么值与我们输入的密钥进行比较,所以我们直接进入encodeToString函数调试下
继续步过至返回处
得到将要返回进行比较的值(该值在此程序中由于注册的昵称不同值也不同)
中断调试,返回模拟器,输入该值,成功
Log插桩
指的是反编译APK文件时,在对应的smali文件中添加相应的smali代码,将程序中的关键信息,以log日志的形式输出
invoke-static {对应寄存器}, Lcom/mtools/LogUtils;->v(Ljava/lang/Object;)V
例:
在上文的目标程序中,首先就要将能实现插桩的dex文件封装进APK中,并对其重命名(符合规范)
然后在想要输出的地方插入命令,比如我们要获取v0寄存器的值
在算法助手中勾选对应的目标程序
使用算法助手,开启Log捕获
算法助手中启动程序,触发相应功能,在算法助手中获得日志
验证成功
签名与签名校验
签名
APK签名:通过对APK进行签名,开发者可以证明对APK的所有权和控制权,可用于安装和更新其应用;在Android设备上安装APK,如果是一个没有签名的APK,则会被拒绝安装;在安装的时候,软件管理器将会验证APK是否已经被正确签名,并且通过签名证书与数据摘要验证是否合法没有被篡改,只有在确认安全,没有被篡改的情况下,才允许被安装在设备上。
Android目前支持的签名方案:
V1:基于JAR签名
V2:APK签名方案V2(Android 7引入)
V3:APK签名方案V3(Android 9引入)
V4:APK签名方案V4(Android 11引入)
V1签名将会在META-INF目录下的三个文件:MANIFEST.MF,ANDROID.RSA,ANDROID.RSA
MANIFEST.MF:摘要文件,程序遍历APK包中的所有文件(entry),对非文件夹非签名文件的文件,逐个使用SHA1生成摘要信息,再使用Base64进行编码;若改变了APK包的文件,将在APK安装检验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,导致程序不能安装
ANDROID.RSA:对摘要文件的签名文件,对前一步生成的MANIFEST.MF使用SHA1-RSA算法,使用开发者的私钥进行签名;在安装时只能使用公钥才能解密它,解密之后,将它与未加密的摘要信息进行(MANIFEST.MF)对比,若相符则表明内容没有被异常修改
ANDROID.RSA:保存公钥,所采用的加密算法等信息
V2将会将整个APK文件视为Blob,并对整个文件进行签名检查;当对APK进行任何修改(包括对ZIP元数据进行的修改),都将使APK签名作废。
校验:开发者在数据传送时采用的一种校正数据的一种方式
常见校验:签名校验,dexcrc校验,apk完整性校验,路径文件校验
签名校验
判断特征:
kill/killprocess //杀死当前应用活动的进程,清理进程中的所有资源;由于ActivityManager时刻监听进程,所以当进程被非正常kill,它将会试图重启该进程(结束应用后又自动重新启动)
system.exit //杀死整个进程,活动占用的资源也会被释放
finish //仅针对Activity,将活动推向后台,不会立即释放内存,活动的资源不会被清理
- 三角校验:由动态加载的dex检测so(软件运行时解压释放一段dex,检测完成后删除),so检测dex,dex检测动态加载的dex。
闪退原因
对目标程序进行APK签名
重新安装,启动,第五关,由于签名校验不通过,导致了闪退现象
使用算法助手
启动程序,成功拦截
验证
查看拦截日志
在MT管理器中定位该闪退的方法
在方法中发现特征
将该行代码注释掉就不会闪退了
普通签名校验方法
安装原程序,在算法助手中开启读取应用签名监听
运行程序,触发签名验证,查看日志
通过Jadx定位:
比对的值:
普通签名校验代码
private boolean SignCheck() {
String trueSignMD5 = "d0add9987c7c84aeb7198c3ff26ca152";
String nowSignMD5 = "";
try {
// 得到签名的MD5
//系统将应用的签名信息封装在PackageInfo中,调用PackageManager的getPackageInfo即可获取指定包名的签名信息
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(),PackageManager.GET_SIGNATURES);
Signature[] signs = packageInfo.signatures;
String signBase64 = Base64Util.encodeToString(signs[0].toByteArray());
nowSignMD5 = MD5Utils.MD5(signBase64);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return trueSignMD5.equals(nowSignMD5);
}
普通签名绕过方法
1、在MT管理器中对比对的判断进行修改
原程序
将if-nez(不等于0就跳转)修改为if-eqz(等于0就跳转)即可
2、将比对的值替换成我们修改后的签名
修改后的签名:通过再次安装修改签名后的程序,并查看日志,找到输出日志的sign
修改
保存验证
PM代理
PMS:PackageManagerService,Android系统和兴服务之一,处理包管理相关的工作,例:安装,卸载等
HOOK PMS:
目的:使用动态代理的方式进行替换属性
ActivityThread的静态变量 sPackageManager
ApplicationPackageManager对象里的mPM变量
https://github.com/fourbrother/HookPmsSignature
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);
}
}
原包的签名信息:通过MT管理器,点击签名状态
全选,复制为Base64,至此,就获得了原包的签名信息(当然我这里是修改了签名后的签名信息)
调用方式:
在attachBaseContext方法中调用hookPMS==》在dex中搜索该方法,添加调用代码:
invoke-static {p1}, Lcom/Closure/Hook/ServiceManagerWraper;->hookPMS(Landroid/content/Context;)V
API签名校验
与普通签名差不多,查看日志获取修改后的newsign
修改
验证
IO重定向(CRC,Hash)
实现在读取A文件时指向B文件
目的:
1、过签名检测(读取原包)
2、风控对抗(记录App启动次数)
3、过Root检测,Xposed检测(文件不可取)
https://github.com/virjarRatel/ratel-core
本质上是对open,openat等几个函数进行hook
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) { //调用hook
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);
}
调用:
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
CRC
寻找比对的值
是一个地址,不是一个固定的值==》
由于每次修改程序文件都将导致crc不一致==》使用IO重定向使得在进行校验时直接使用原包的crc,从而绕过校验(hash读取整个APK,也一样)
为此,将在调用crc校验之前就使用IO重定向==》在MT管理器中找到调用校验crc的代码
找到后,要在调用之前添加能调用IO重定向的smali代码(直接在开头添加即可)
还需要将原包放进修改后的程序的数据目录中,在数据目录中新建files文件夹,放入原包,重命名为base.apk
安装验证:
hash(读取整个APK)与crc(读取dex)类似,所以都将通过
hash
由setApkHash设置ApkHash,查找该函数的用例
检测
Root检测
fun isDeviceRooted(): Boolean {
return checkRootMethod1() || checkRootMethod2() || checkRootMethod3()
}
//检查设备的build tags是否包含test-key
fun checkRootMethod1(): Boolean {
val buildTags = android.os.Build.TAGS
return buildTags != null && buildTags.contains("test-keys")
}
//检查设备是否存在一些特定文件(指纹)
//这些文件常用于执行root操作
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
}
//使用Runtime.exec()执行which su命令,检查命令的输出是否不为空;若输出不为空,则认为设备已被root
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()
}
}
模拟器检测
//通过检测系统的Build对象来判断当前设备是否为模拟器
//检测Build.FINGERPRINT属性是否包含字符串"generic"
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"
}
反调试检测
//安装系统自带调试检测函数
fun checkForDebugger() {
if (Debug.isDebuggerConnected()) {
// 如果调试器已连接,则终止应用程序
System.exit(0)
}
}
//debuggable属性检查
public boolean getAppCanDebug(Context context)//上下文对象为xxActivity.this
{
boolean isDebug = context.getApplicationInfo() != null &&
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
return isDebug;
}
//ptrace检测
int ptrace_protect()
//ptrace附加自身线程(追踪进程) 会导致此进程TracerPid(追踪进程ID) 变为父进程的TracerPid 即zygote
//Zygote是Android中的一个重要进程,和init进程,SystemServer进程是Android中最重要的三大进程
{
return ptrace(PTRACE_TRACEME,0,0,0);;//返回-1即为已经被调试
}
//因为每个进程之恶能被一个调试进程ptrace,自己主动ptrace进程可以使其他调试器无法调试
//调试进程名检测
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;
}
//Fride检测
//https://github.com/xxr0ss/AntiFrida