Android逆向(一)
2023-05-07 20:39:37

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管理器(有些功能需要会员)

左上角三条杠,选择安装包提取

选择目标应用

image-20230405235856498

提取安装包

image-20230405235911867

成功提取并保存至相应路径

NP管理器

右上角三条杠选择安装包提取

选择目标应用程序

提取安装包

运行应用程序,进入第一关:

对该页面进行汉化:

首先尝试搜索字符串hello

在MT管理器中定位至刚刚提取安装包所在的路径

进行查看

使用搜索功能

搜索结束

点击该文件,再次点击该文件选择反编译

成功反编译(需要登陆)

修改,并保存

勾选自动签名并确定

回退至提取后的安装包所在的路径,点击安装包

发现签名不一致

选择先将应用程序卸载,再安装该汉化后的安装包

image-20230406003209246

此时hello 52pojie已被成功替换,但是其中还有两句话没有翻译,其中一句还是看不懂的语言

使用开发者助手,成功启动后将会留下一个瓢虫的浮窗,回到应用,点击浮窗,并点击开始界面分析

image-20230406003721223

点击不认识的语言,开发者工具将会提供该控件的属性

image-20230406003817777

点击该text,选择复制,并回到MT管理器中搜索该句子

成功搜索,位于arsc中,选择翻译模式

==》

image-20230406004121675

成功获得该文件中的字符串

对其进行翻译

image-20230406004419783

保存修改并退出,并且自动签名

再次选择安装该安装包

此次签名和上一次修改后的签名是一致的,所以可以直接安装成功

image-20230406004609063

再次运行程序

第三句话同样使用开发者助手复制字符串,进行搜索

位于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文件

尝试替换应用程序的图标和名字:

选择通用编辑功能进行修改

image-20230406010046239

自定义图标和应用名

安装:

由于与上文使用的MT管理器的签名不一致,所以卸载上文的应用即可(注意安装的是修改后的安装包edit)

image-20230406010908845

image-20230406010942339

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管理器中进行搜索

image-20230409192628093

上方就是关键代码

分析代码逻辑

首先判断是否拥有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管理器工具栏中

image-20230414205907621

启动该功能后即可运行目标程序,并打开该程序的广告界面,成功抓取Activity记录后停止该功能

image-20230414210238407

在MT管理器中的Activity记录中,复制获取该广告的Activity,在NP管理器中进行搜索定位

image-20230414211639872

将定位的smali代码转为JAVA代码查看

image-20230414211802806

image-20230414212412930

修改方法:

1、修改广告Activity加载时间

由JAVA代码可知,该广告将被加载3秒

image-20230414212615725

由此,将3秒改为0即可,在smali中寻找loadAd,并修改

修改后的JAVA代码

image-20230414213046006

保存,签名并安装,成功看不到广告了,但是广告所在的Acvitity依然会被加载

MT Activity记录验证:

image-20230414213441476

2、修改Intent的Activity类名(推荐使用)

定位与上个方法一样,在MT管理器中进行搜索,长按复制广告类的smali路径

image-20230414220139954

在NP中根据路径搜索,定位

排除类本身的内容,我们需要查看是在哪个外部方法中对其调用

image-20230414220523649

分析其JAVA代码

由此,只要我们将该广告Activity修改为第三关的Activity即可

一样的方式在MT管理器中定位至要修改的smali代码

按照类似的格式对其修改

保存,自动签名并安装运行,使用MT Activity记录验证==》成功跳过广告

3、实现比广告更快加载(不推荐,容易引起程序异常,崩溃等)

在MT管理器中,查看AndroidManifest.xml文件,并替换主Activity

image-20230414215214437

保存,自动签名并安装启动,将直接打开第三关的Activity

image-20230414215459306

弹窗

修改方法:

1、修改XML文件中的versiocode(用于更新弹窗,修改版本)

此处改为2即可实现

2、Hook弹窗

多出现于劫持了返回键等

例:目标程序被劫持了返回键

image-20230414222105122

使用算法助手,选择要HOOK的目标程序

开启应用总开关

开启弹窗定位(返回键可取消)

image-20230415011950118

使用右上角运行程序==》

返回第一次

image-20230415011708133

返回第二次

image-20230415011726481

成功跳过弹窗

另一种方式:通过关键词屏蔽弹窗

在刚刚的算法助手中选择屏蔽关键词弹窗,加入广告

再次运行程序,发现弹窗成功跳过

3、修改dex弹窗代码

目标

image-20230415012612067

在算法助手中,通过日志查看该弹窗

image-20230415012705242

通过该方法实现在MT管理器中定位

image-20230415012838286

成功定位

弹窗大部分是由show方法显示信息的,为此,我们寻找该弹窗的show方法,并注释即可

在前方添加#注释掉该行

上方还有一个”2号广告”,使用一样的方法注释掉show方法

image-20230415013251829

保存,安装,成功跳过弹窗

image-20230415013404974

4、抓包修改响应体(也可以使用路由器拦截)

布局

布局定位:1、开发者助手;2、MT管理器XML搜索定位

开发助手

image-20230415013536382

image-20230415013825715

定位广告图片

image-20230415013921240

复制id

image-20230415014015198

在MT中右上角使用XML搜索

image-20230415014148759

image-20230415014201349

成功定位

image-20230415014314152

修改方法:修改XML代码

它存在宽度和高度,我们将它都改为0即可

image-20230415014444457

保存并安装,成功消除广告图片

还有一种办法是直接隐藏布局,在刚刚的XML文件中广告图片位置插入

android:visibility="gone"

成功隐藏

动态调试

为程序添加debug权限

1、AndroidManifest,xml中添加

android:debuggable="true"

添加至application标签中

image-20230416144741669

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附加调试进程

目标

image-20230416150851015

根据关键词在JEB中搜索定位

image-20230416151101953

image-20230416151115072

右键解析将Smali转为JAVA代码

定位关键函数check中

image-20230416151553005

在check函数中最后进行base64后的比较,对此,我们将在此处下断电(ctrl + b)

image-20230416152237189

开启debug模式,注意没配置adb环境变量需要前往模拟器所在路径,使用模拟器的adb

image-20230416154446283

image-20230416154457970

JEB附加调试

注意点:若有出现附加调试时找不到模拟器和进程的情况

找不到模拟器:将该模拟器的ADB路径添加至环境变量中

找不到进程:可能是因为ADB版本的问题,我雷电模拟器自带的ADB版本是1.0.31,找不到进程(可以找到模拟器),替换为1.0.41即可

替换ADB需要替换的文件:

image-20230507202929021

关注点:

image-20230430165501878

回到模拟器,执行至输入密钥

image-20230430165554648

点击验证,成功断点

image-20230430165642306

由于我们需要看函数在最后返回了什么值与我们输入的密钥进行比较,所以我们直接进入encodeToString函数调试下

image-20230430170510075

继续步过至返回处

image-20230430170622627

得到将要返回进行比较的值(该值在此程序中由于注册的昵称不同值也不同)

中断调试,返回模拟器,输入该值,成功

image-20230430171113976

Log插桩

指的是反编译APK文件时,在对应的smali文件中添加相应的smali代码,将程序中的关键信息,以log日志的形式输出

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

例:

在上文的目标程序中,首先就要将能实现插桩的dex文件封装进APK中,并对其重命名(符合规范)

image-20230502193319100

然后在想要输出的地方插入命令,比如我们要获取v0寄存器的值

image-20230502194038020

在算法助手中勾选对应的目标程序

使用算法助手,开启Log捕获

算法助手中启动程序,触发相应功能,在算法助手中获得日志

image-20230502194607456

image-20230502200212747

验证成功

签名与签名校验

签名

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签名

image-20230506192555567

重新安装,启动,第五关,由于签名校验不通过,导致了闪退现象

使用算法助手

image-20230506192829360

启动程序,成功拦截

验证

查看拦截日志

在MT管理器中定位该闪退的方法

image-20230506193736393

在方法中发现特征

将该行代码注释掉就不会闪退了

普通签名校验方法

安装原程序,在算法助手中开启读取应用签名监听

image-20230506200546961

运行程序,触发签名验证,查看日志

image-20230506201148932

image-20230506201223950

通过Jadx定位:

image-20230506201858246

比对的值:

普通签名校验代码
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管理器中对比对的判断进行修改

原程序

image-20230506202856525

将if-nez(不等于0就跳转)修改为if-eqz(等于0就跳转)即可

2、将比对的值替换成我们修改后的签名

修改后的签名:通过再次安装修改签名后的程序,并查看日志,找到输出日志的sign

image-20230506202659355

修改

保存验证

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管理器,点击签名状态

image-20230506212736179

image-20230506212721140

全选,复制为Base64,至此,就获得了原包的签名信息(当然我这里是修改了签名后的签名信息)

调用方式:

在attachBaseContext方法中调用hookPMS==》在dex中搜索该方法,添加调用代码:

invoke-static {p1}, Lcom/Closure/Hook/ServiceManagerWraper;->hookPMS(Landroid/content/Context;)V
API签名校验

image-20230507005638704

与普通签名差不多,查看日志获取修改后的newsign

修改

image-20230507010213914

验证

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的代码

image-20230507012956132

找到后,要在调用之前添加能调用IO重定向的smali代码(直接在开头添加即可)

还需要将原包放进修改后的程序的数据目录中,在数据目录中新建files文件夹,放入原包,重命名为base.apk

image-20230507013530660

安装验证:

hash(读取整个APK)与crc(读取dex)类似,所以都将通过

image-20230507013950087

hash

image-20230507014524424

image-20230507014621203

由setApkHash设置ApkHash,查找该函数的用例

image-20230507014744075

image-20230507014919296

检测

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