Featured image of post 安卓逆向

安卓逆向

Windows虚拟机环境配置

版本windows10 20H2

地址D:\Linux\ISO\cn_windows_10_consumer_editions_version_20h2_updated_feb_2021_x64_dvd_8ddab99d.iso

参考vmware虚拟机windows10系统安装_windows10虚拟机-CSDN博客

jdk、jadx、gda、jeb

AndroidStudio配置

地址https://developer.android.google.cn/studio/

A1

A2

A3

A4

A5

不导入配置文件

A6

Dont send

A7

A8

SDK路径

A9

next-finish

SDK配置

SDK 版本29

S1

Build-Tools 版本30.0.3

S2

编辑注册表:'win+r'->regedit->修改安卓默认配置路径为D:\Android\.android

S3

创建模拟器

AndroidStudio配置–>AVD Manager–>Google Pixel 1 代–>Android 10.0

  1. 创建新虚拟设备

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    1. 点击 "Create Virtual Device"
    2. 选择硬件:
       - Category: Phone
       - 选择 "Pixel"(5.0英寸)或 "Pixel XL"(5.5英寸)
       - 点击 "Next"
    3. 选择系统镜像:
       - 选择 "Pie" API Level 29
       - 在 "Recommended" 标签页选择:
         Google APIs Intel x86 Atom_64 System Image
       - 点击 "Next"
    4. 配置 AVD:
       - AVD Name: Pixel_API_29(建议命名)
       - Startup orientation: Portrait(竖屏)
       - Performance:
         * Graphics: Hardware - GLES 2.0(性能最好)
         * Device frame: 可选
       - 点击 "Show Advanced Settings"
    
  2. 高级配置

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    # 在 Advanced Settings 中设置:
    Camera:
      - Front: None
      - Back: None
    Network:
      - Speed: Full
      - Latency: None
    Memory and Storage:
      - RAM: 2048 MB(如果主机内存充足,可设4096)
      - VM Heap: 256 MB
      - Internal Storage: 2048 MB
      - SD Card: 512 MB(可选)
    Emulated Performance:
      - Graphics: Hardware - GLES 2.0
      - Boot option: Cold boot(默认)
    Device Frame:
      - 可选,为节省资源可不勾选
    
  3. 完成创建

    1
    2
    3
    
    1. 点击 "Finish"
    2. 在 AVD Manager 列表中会看到新设备
    3. 点击 "Play" 按钮启动测试
    

模拟器网络连接

1
2
# 查看所有网络接口
ip addr show

根据输出结果

radio0 是当前分配了 IP 地址(10.0.2.15)的接口,但状态是 DOWN

1
2
3
4
5
# 启动 radio0 接口
ip link set radio0 up

# 再次检查接口状态
ip addr show radio0

应该看到 state UP 而不是 state DOWN

1
2
3
4
5
# 添加默认网关(模拟器的网关通常是 10.0.2.2)
ip route add default via 10.0.2.2 dev radio0

# 检查路由表
ip route

输出应该包含:default via 10.0.2.2 dev radio0

1
2
3
4
5
6
# 设置 DNS 服务器
setprop net.dns1 8.8.8.8
setprop net.dns2 8.8.4.4

# 验证 DNS 设置
getprop | grep dns

问题确认了! 模拟器启动后 radio0 接口仍然是 DOWN 状态,而且没有 eth0 接口。这表明模拟器的网络初始化完全失败

最终方案

现在需要从主机端修复这个问题,因为模拟器内部的网络栈已经损坏

完全关闭当前模拟器

1
2
# 在主机命令提示符中执行
adb emu kill

修复模拟器网络配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
cd /d "D:\CyberSec\Reverse\AndroidRe\AndroidSDK\emulator"

# 尝试1:使用 virtio 网络设备
emulator -avd Pixel_API_29 -no-snapshot -wipe-data -gpu host -qemu -netdev user,id=mynet0 -device virtio-net-pci,netdev=mynet0

# 尝试2:使用 e1000 网络设备
emulator -avd Pixel_API_29 -no-snapshot -wipe-data -gpu host -qemu -netdev user,id=mynet0 -device e1000,netdev=mynet0

# 尝试3:使用默认网络但强制初始化
emulator -avd Pixel_API_29 -no-snapshot -wipe-data -no-boot-anim -netdelay none -netspeed full -dns-server 8.8.8.8,8.8.4.4

验证修复

模拟器启动后,在主机命令提示符中执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 等待模拟器完全启动
timeout /t 60

# 检查 adb 连接
adb devices

# 进入 shell 并检查网络
adb shell
>>
su
ip link show
ip addr show
ping -c 2 10.0.2.2

adb:在电脑端操控手机端

真机配置

线刷包组成

adb常用命令

ADBAndroid 调试桥 的命令行工具,它是 Android SDK 的一部分。它允许你的电脑与 Android 设备(真实手机或模拟器)进行通信

语法 功能
adb/adb help/adb –help
adb version 显示adb版本和路径
adb start-server 启动server
adb kill-server 停止server
adb devices 显示连接的设备列表
adb install xxx.apk 通过adb安装app
adb install -r xxx.apk 覆盖安装
adb uninstall 包名 通过adb卸载app
adb push xxx xxx 推送电脑的文件到手机
adb pull xxx xxx 拉取手机的文件到电脑
adb pull xxx
adb shell 进入到手机的Linux控制台
adb -s 设备名 shell 多设备时,指定设备

logcat的使用

Logcat 是 Android 系统的日志记录工具,它会捕获系统和应用的运行日志。你可以通过 ADB 来访问这些日志:adb logcat

  1. logcat常用选项

    adb logcat -help 查看帮助

    adb logcat 常规显示

    adb logcat -c 清除日志

    adb logcat -g 显示缓冲区大小

    adb logcat -G 256M 修改缓冲区大小

    adb logcat -v time 设置不同的显示格式

    adb logcat -v color 带颜色的显示

    ctrl+c 强制中断程序的执行

  2. 根据tag过滤日志

    adb logcat -s xiaojianbang

  3. 根据pid过滤日志

    ps -A |grep com.xiaojianbang.app 先获取进程pid

    adb logcat |findstr 5568

  4. 在AndroidStudio的logcat中查看,更加方便

日志级别(从低到高):

  1. V - Verbose(详细)- 所有信息
  2. D - Debug(调试)- 调试信息
  3. I - Info(信息)- 一般信息
  4. W - Warning(警告)- 警告信息
  5. E - Error(错误)- 错误信息
  6. F - Fatal(致命)- 严重错误

安卓系统

apk基本结构

APK(Android Package Kit)是 Android 应用程序的安装包文件,本质上是一个 ZIP 格式的压缩包。以下是 APK 的基本结构和各部分的详细说明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
APK文件(.apk)
├── AndroidManifest.xml          # 应用清单文件(二进制)
├── classes.dex                  # 可执行字节码文件
├── resources.arsc               # 编译后的资源文件
├── META-INF/                    # 签名和验证信息
│   ├── MANIFEST.MF
│   ├── CERT.SF
│   └── CERT.RSA
├── res/                         # 未编译的资源文件
│   ├── drawable/
│   ├── layout/
│   ├── values/
│   └── ...
├── assets/                      # 原始资源文件
├── lib/                         # 原生库文件
│   ├── armeabi-v7a/
│   ├── arm64-v8a/
│   └── x86/
└── kotlin/                      # Kotlin 相关类(可选)

Linux常用命令

命令 功能 常用选项
ls 列出文件 -l, -a, -h
cd 切换目录 .., ~, -
pwd 显示当前目录
cp 复制文件 -r, -v
mv 移动/重命名
rm 删除文件 -r, -f
cat 显示文件内容
grep 搜索文本 -r, -i, -n
find 查找文件 -name, -type
chmod 修改权限 755, u+x
ps 查看进程 aux, -ef
top 动态查看进程
kill 结束进程 -9
df 磁盘使用 -h
du 文件大小 -sh
tar 压缩解压 -xzvf, -czvf
ssh 远程连接 user@host
scp 远程复制 -r
wget 下载文件 -c, -O
curl 网络请求 -O, -L
history 命令历史
man 查看手册
sudo 提权执行
file 查看文件类型
touch/mkdir 创建 -p
rmdir/rm 删除 -f, -rf
echo 输出

Android常用目录

目录 用途 是否需要 root
/sdcard/ 内部存储
/data/data/<包名>/ 应用私有数据
/data/local/tmp/ 临时文件 否(adb)
/system/app/ 系统应用
/proc/ 系统信息 部分需要
/sys/ 设备信息 部分需要
/data/anr/ ANR 日志
/data/tombstones/ Native 崩溃
/data/misc/wifi/ WiFi 配置
/data/system/packages.xml 包管理信息
  1. data/data目录 存放用户apk数据的目录,每个apk都有自己的目录,以包名命名 就是在data/data/目录下,会产生一个跟Package一样的目录 这是一个私有目录,app只能访问各自的目录,除非root权限

  2. data/app目录用户安装的app存放在这个目录下

  3. data/local/tmp临时目录,权限比较大

  4. system/app目录存放系统自带的app

  5. system/lib目录、system/lib64存放app用到so文件

  6. system/bin目录存放shell命令

  7. system/framework目录Android系统所用到框架,如一些jar文件,XposedBridge.jar

  8. sd卡目录 不管手机有没有存储卡都会有这个目录,app操作sd卡目录需要申请权限

    /sdcard -> /storage/self/primary

    /mnt/sdcard

    /storage/emulated/0

Linux权限

权限值 字符表示 适用场景
777 rwxrwxrwx 测试、临时文件(不安全)
755 rwxr-xr-x 可执行文件、脚本、目录
750 rwxr-x— 程序文件(组内共享)
644 rw-r–r– 配置文件、网页、文档
640 rw-r—– 配置文件(组可读)
600 rw——- 私密文件(SSH密钥、密码)
400 r——– 只读配置文件
1777 rwxrwxrwt 公共临时目录(/tmp)
2755 rwxr-sr-x 共享目录(SGID)
4755 rwsr-xr-x 特权程序(SUID)

安卓逆向入门

  1. 创建Native C++工程

  2. AndroidStudio界面介绍

  3. Project Structure

  4. app的编译

    Build -> Make Project

    Make Module ‘HashMapTest.app’

    Build -> Build Bundle -> Build APK

    Clean Project

    Rebuild Project

    Run app

AndroidStudio工程目录 作用
gradle->wrapper->gradle-wrapper.properties 配置项目gradle版本
build.gradle 描述工程整体的编译规则
gradle.properties gradle配置文件,一般无须改动
local.properties 本机环境配置,SDK、NDK路径等,一般无须改动
settings.gradle 配置哪些模块在一起编译 include ‘:app’ 只编译app
app -> build.gradle 描述当前模块的编译规则
app -> build -> outputs -> apk -> debug/release 生成的apk的存放位置
app -> build -> intermediates -> cmake -> debug/release -> obj 生成的so存放位置
libs 模块中使用了第三方jar的时候,会放这里
src -> main -> cpp C/C++代码
src -> main -> java Java代码
src -> proguard-rules.pro Java代码混淆规则
res -> drawable 用来放图片
res -> layout 用来放布局文件
res -> mipmap-hdpi 用来放应用图片,不同屏幕的适配图标
res -> values strings.xml、public.xml
AndroidManifest.xml 清单文件,app的icon图标、四大组件的注册、权限申请、指明程序入口

环境变量

目录 变量名 变量值
Android配置文件 ANDROID_SDK_HOME D:\Android
Gradle GRADLE_USER_HOME D:\Android.gradle

配置文件

build.gradle

 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
plugins {
    id 'com.android.application' // 声明这是一个Android应用项目
}

android {
    compileSdkVersion 29 // 编译SDK版本
    buildToolsVersion "30.0.3" // 构建工具版本

    defaultConfig {
        applicationId "com.example.myapplication" // 应用包名
        minSdkVersion 16 // 最低支持的Android版本
        targetSdkVersion 29 // 目标SDK版本
        versionCode 1 // 内部版本号
        versionName "1.0" // 显示给用户的版本号

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" // 测试运行器
        externalNativeBuild {
            cmake {
                cppFlags '-std=c++11' // C++编译参数
            }
        }
    }

    buildTypes {
        release {
            minifyEnabled false // 是否启用代码混淆
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' // 混淆规则文件
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8 // Java源代码兼容性
        targetCompatibility JavaVersion.VERSION_1_8 // 生成的字节码兼容性
    }
    externalNativeBuild {
        cmake {
            path file('src/main/cpp/CMakeLists.txt') // CMake构建脚本路径
            version '3.10.2' // CMake版本
        }
    }
    buildFeatures {
        viewBinding true // 启用视图绑定
    }
}

dependencies {
    // AndroidX支持库
    implementation 'androidx.appcompat:appcompat:1.2.0'
    // Material Design组件
    implementation 'com.google.android.material:material:1.2.1'
    // 约束布局
    implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
    // 单元测试
    testImplementation 'junit:junit:4.+'
    // Android测试支持
    androidTestImplementation 'androidx.test.ext:junit:1.1.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
}

清单文件

 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
<?xml version="1.0" encoding="utf-8"?>
<!-- 
    AndroidManifest.xml - Android应用的清单文件
    这是Android应用的核心配置文件,定义了应用的基本信息、组件和权限
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication"> <!-- 应用的包名,在整个Android系统中唯一标识你的应用 -->
    
    <!-- 
        application 节点 - 应用的整体配置
        这里定义了应用的基本属性和包含的组件
    -->
    <application
        android:allowBackup="true"          <!-- 是否允许应用数据备份到云端 -->
        android:icon="@mipmap/ic_launcher"  <!-- 应用图标,显示在手机桌面 -->
        android:label="@string/app_name"    <!-- 应用名称,显示在手机桌面 -->
        android:roundIcon="@mipmap/ic_launcher_round"  <!-- 圆形应用图标(部分系统使用) -->
        android:supportsRtl="true"          <!-- 是否支持从右到左的布局(如阿拉伯语) -->
        android:theme="@style/Theme.MyApplication">  <!-- 应用的主题样式 -->
        
        <!-- 
            activity 节点 - 定义一个活动(Activity)
            每个活动代表应用中的一个屏幕或界面
        -->
        <activity android:name=".MainActivity">  <!-- 活动类名,.MainActivity 表示 com.example.myapplication.MainActivity -->
            <!-- 
                intent-filter 节点 - 意图过滤器
                定义了此活动可以响应的系统或应用内的操作
            -->
            <intent-filter>
                <!-- 
                    action 节点 - 指定此活动能响应的操作
                    MAIN 表示这是应用的主入口点
                -->
                <action android:name="android.intent.action.MAIN" />
                
                <!-- 
                    category 节点 - 指定活动的类别
                    LAUNCHER 表示这个活动应该显示在应用启动器中(手机桌面)
                -->
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <!-- 
            注意:这里只有一个活动,实际应用中可能会有多个活动
            每个活动都需要在这里声明才能使用
        -->
        
    </application>
    
    <!-- 
        注意:这里没有声明任何权限
        如果应用需要访问网络、存储、摄像头等,需要在此处添加权限声明
        例如:<uses-permission android:name="android.permission.INTERNET" />
    -->
    
</manifest>

文件结构

1
2
3
4
5
6
manifest (整个应用)
└── application (应用配置)
    └── activity (一个界面)
        └── intent-filter (这个界面的作用)
            ├── action: MAIN (主要入口)
            └── category: LAUNCHER (显示在桌面)

程序的执行入口

一句话总结:

安卓程序有两个入口:后台的 Application.onCreate() 和前台的 MainActivity.onCreate()

📋 详细说明:

1. 后台入口(先执行)

  • 位置Application 类的 onCreate() 方法
  • 作用:整个应用启动时最早执行的代码
  • 用途:全局初始化(如数据库、网络库、统计分析等)

2. 前台入口(后执行)

  • 位置MainActivity 类的 onCreate() 方法
  • 作用:用户看到的第一个界面的初始化
  • 标志:清单文件中配置了 MAINLAUNCHER

🔄 启动流程:

1
用户点击图标 → Application.onCreate() → MainActivity.onCreate() → 显示界面

💡 简单比喻:

  • Application.onCreate() = 公司后台准备(用户看不到)
  • MainActivity.onCreate() = 门店开门营业(用户看得到)

记住:

应用启动时,先执行后台初始化,再显示第一个界面

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapplication">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApplication"
        android:name=".myapplication">//注意:这里的android:name=".myapplication"表示:系统会先加载并执行com.example.myapplication.myapplication类
        <activity android:name=".MainActivity">//再加载com.example.myapplication.MainActivity类
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

如果.myapplication类(自定义Application类)不存在,系统会:

  • 使用默认的 android.app.Application
  • 不会崩溃,但你的自定义初始化代码不会执行

类加载时机

静态代码块在类加载时执行,不一定是应用启动时:

  • Application 类:应用启动时立即加载
  • Activity 类:启动该 Activity 时才加载

在Application类中创建的变量为全局变量

Log

Log(日志)是 Android 开发中的调试工具,就像程序的"日记本",用来记录程序运行过程中的各种信息。

主要用途:

  1. 调试程序:找出代码中的错误
  2. 跟踪流程:了解程序执行到哪一步
  3. 监控数据:查看变量的值
  4. 记录事件:保存用户操作、异常信息等

6种Log级别(从低到高)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 1. VERBOSE(详细) - 黑色
Log.v(TAG, "最详细的日志信息");  // 通常用于大量调试信息

// 2. DEBUG(调试) - 蓝色 ★最常用★
Log.d(TAG, "调试信息");  // 开发时最常用的级别

// 3. INFO(信息) - 绿色
Log.i(TAG, "一般信息");  // 正常运行的重要信息

// 4. WARN(警告) - 黄色
Log.w(TAG, "警告信息");  // 有问题但不影响程序运行

// 5. ERROR(错误) - 红色
Log.e(TAG, "错误信息");  // 程序出错,需要修复

// 6. ASSERT(断言) - 红色
Log.wtf(TAG, "严重错误");  // "What a Terrible Failure" 非常严重的错误

Toast

Toast(吐司)是 Android 中的一个轻量级消息提示组件,就像手机屏幕底部弹出的"小纸条",几秒后自动消失

Toast 的特点

特性 说明
显示位置 默认屏幕底部(可自定义)
显示时间 短时间(2秒)或长时间(3.5秒)
交互性 非模态,不打断用户操作
自动消失 无需用户关闭
使用场景 提示、确认、轻量反馈

基本使用方法

1. 最简单的 Toast

1
2
3
4
5
6
7
8
// 显示一个简单的 Toast
Toast.makeText(this, "这是一个提示", Toast.LENGTH_SHORT).show();

// 分解说明:
// 1. this: 当前 Context(上下文)
// 2. "这是一个提示": 要显示的文字
// 3. Toast.LENGTH_SHORT: 显示时长(短时间)
// 4. .show(): 显示 Toast

2. 两种显示时长

1
2
3
4
5
// 短时间(约 2 秒)
Toast.makeText(this, "短提示", Toast.LENGTH_SHORT).show();

// 长时间(约 3.5 秒)
Toast.makeText(this, "长提示", Toast.LENGTH_LONG).show();

3. 完整示例

 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
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        Button btnShort = findViewById(R.id.btn_short);
        Button btnLong = findViewById(R.id.btn_long);
        
        // 短时间 Toast
        btnShort.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, 
                    "操作成功", 
                    Toast.LENGTH_SHORT).show();
            }
        });
        
        // 长时间 Toast
        btnLong.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this, 
                    "文件已保存到相册,请注意查看", 
                    Toast.LENGTH_LONG).show();
            }
        });
    }
}

基本控件

1.Button

C1

2.TextView

C2

3.EditText

C3

布局文件

(activity_main.xml)

 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
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">
	<!-- vertical:垂直布局 -->
    <!-- TextView -->
    <TextView
        android:id="@+id/myTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="显示文本"
        android:textSize="18sp" />

    <!-- EditText -->
    <EditText
        android:id="@+id/myEditText"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="输入内容"
        android:layout_marginTop="10dp" />

    <!-- Button -->
    <Button
        android:id="@+id/myButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="点击我"
        android:layout_marginTop="10dp"
        android:onClick="testButton"/>

</LinearLayout>
<!-- LinearLayout标签:线性布局 -->

Activity中的绑定

(MainActivity.java)

 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
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.example.myapplication.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 1. 初始化绑定
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        
        // 2. 绑定并设置控件
        
        // TextView 绑定:设置文本
        binding.myTextView.setText("欢迎使用App");
        
        // EditText 绑定:设置事件监听
        binding.myEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (!hasFocus) {
                    // 失去焦点时处理
                    String input = binding.myEditText.getText().toString();
                    Toast.makeText(MainActivity.this, "输入了: " + input, Toast.LENGTH_SHORT).show();
                }
            }
        });
        
        // Button 绑定:设置点击事件
        binding.myButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 获取 EditText 的内容
                String input = binding.myEditText.getText().toString();
                
                // 更新 TextView 显示
                binding.myTextView.setText("你输入了: " + input);
                
                // 清空 EditText
                binding.myEditText.setText("");
                
                // 显示提示
                Toast.makeText(MainActivity.this, "按钮被点击", Toast.LENGTH_SHORT).show();
            }
        });	//匿名类
    }
}

例:

 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
package com.example.myapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import com.example.myapplication.databinding.ActivityMainBinding;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    // Used to load the 'native-lib' library on application startup.
    static {
        Log.d("example","MainAcivity static");
        System.loadLibrary("native-lib");
    }

    private ActivityMainBinding binding;
    private TextView tv;
    private EditText et;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d("example","MainAcivity onCreate");
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        Button button = binding.button;
        tv = binding.sampleText;
        et = binding.editTextTextPersonName;

        button.setOnClickListener(this);    //method 1
        //button.setOnClickListener(new MainActivity());
        //button.setOnClickListener(MainActivity.this);

        //button.setOnClickListener(new MyClick()); //method 2
//        button.setOnClickListener(new View.OnClickListener(){ //method 3
//            @Override
//            public void onClick(View v) {
//                Log.d("example","onClick");
//            }
//        });

        // Example of a call to a native method
    //    TextView tv = binding.sampleText;
    //    tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

    public void testButton(View view) {
        Log.d("example","This is a button");
    }   //method 4

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.button:
                Log.d("example","onClick");
                Toast.makeText(MainActivity.this,"szh",Toast.LENGTH_SHORT).show();
                tv.setText(R.string.lhq);
                Log.d("example",et.getText().toString());
                et.setText(R.string.lhq);

                break;
        }
    }
}

class MyClick implements View.OnClickListener {
    @Override
    public void onClick(View v) {
        Log.d("example","onClick");
    }
}

res->layout->activity_main.xml中查看控件

strings.xml

在代码中按住Ctrl,鼠标点击字符串跟踪R$string 类的字段定义

R.string.app_name, xml文件中用@string/xiao引用

res->values->string.xml

string

public.xml

Resources->values->public.xml

Ctrl+F搜索值

p1

p2

抓包

Charles

下载 https://www.charlesproxy.com/download/ 注册 https://www.zzzmode.com/mytools/charles

代码main.go生成注册码

B1

注册Register

B2

需关闭防火墙,Windows Proxy可关闭(接下来抓取手机上的流量)

代理端口设置Proxy -> Proxy Settings

B3

socksdroid

抓取手机上的流量

安装adb install socksdroid-1.0.3.apk

设置主机ip以及charles设置的代理port,右上角打开VPN

B4

验证抓取http流量(https已加密)

配置证书

https的抓包,ssl代理设置Proxy -> SSL Proxying Settings

B5

save证书

B6

推送证书adb push 刚刚保存的证书名.pem /sdcard

手机端安装证书

设置->安全->加密与凭据->从SD卡安装

B7

B8

安装成功,验证https流量(未加密)

B9

安卓7以后系统只信任系统证书,需要把用户证书移动到系统证书目录

系统证书路径/etc/security/cacerts

用户证书路径/data/misc/user/0/cacerts-added

利用Magisk的**Move Certificates模块**,来移动证书

magisk装不上啊!本虚拟机x86架构

于是使用 mount --bind 命令

它用 /data/local/cacerts 目录替换了原来的 /system/etc/security/cacerts 系统目录

现在系统看到的证书目录实际上是你新创建的目录

查看原来的系统证书

1
2
# 查看原来的系统证书目录
ls -la /system/etc/security/cacerts/

创建包含所有证书的新目录

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 1. 清空我们之前创建的目录
rm -rf /data/local/cacerts/*
# 或者创建一个新目录
mkdir -p /data/local/full_cacerts

# 2. 复制原来的系统证书(如果有的话)
# 注意:原来的系统证书目录可能为空或只有少数几个证书
cp /system/etc/security/cacerts/* /data/local/full_cacerts/ 2>/dev/null

# 3. 复制HttpCanary证书(需要移动的用户证书)
cp /data/media/0/HttpCanary/certs/87bc3517.0 /data/local/full_cacerts/

# 4. 设置正确权限
chmod 644 /data/local/full_cacerts/*

bind mount包含所有证书的目录

1
2
3
4
5
# 使用新的包含所有证书的目录进行bind mount
mount --bind /data/local/full_cacerts /system/etc/security/cacerts

# 验证
ls -la /system/etc/security/cacerts/ | wc -l

移动新的用户证书到系统目录

可以直接在 /data/local/full_cacerts 目录中添加证书文件。因为使用了 mount --bind,对这个目录的任何修改都会实时反映到 /system/etc/security/cacerts/

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
### 直接复制证书文件到目录 ###
# 1. 确保在挂载状态下
mount | grep cacerts

# 2. 将用户证书复制到目录
# 假设用户证书在 /sdcard/user_cert.0
cp /sdcard/user_cert.0 /data/local/full_cacerts/

# 3. 设置正确的权限
chmod 644 /data/local/full_cacerts/user_cert.0

# 4. 验证证书已添加
ls -la /system/etc/security/cacerts/ | grep user_cert

### 添加多个证书 ###
# 批量添加证书
for cert in /sdcard/certs/*.0; do
    cp "$cert" /data/local/full_cacerts/
    chmod 644 "/data/local/full_cacerts/$(basename "$cert")"
done

恢复原来的系统证书

1
2
3
4
5
# 解除bind mount
umount /system/etc/security/cacerts

# 验证是否已解除
ls -la /system/etc/security/cacerts/

HttpCanary

安装apk,下载证书,配置证书到系统目录即可

r0capture

安卓应用层抓包通杀脚本

抓包原理

https通信:对称加密aes+rsa

B10

代理抓包:客户端和服务端中间通过抓包工具内置代理服务端,代理服务器伪造证书(用根证书签发),客户端识别证书

检验证书:证书路径/证书链,判断根证书是否在系统证书目录

APP端和服务端都是同一团队开发的,APP端可以识别真实服务端

数字证书:使用者、颁发者、有效期、公钥、指纹、签名信息

数字签名:用自己的私钥加密明文得到密文和明文,用本身的公钥校验

B11

app界面控件查看

使用uiautomatorviewer.bat查看Android SDK根目录\tools\bin

我的sdk没有上述工具

使用Androidstudio自带tools->Layout Inspector,选择当前正在前台运行、想要分析的应用程序的进程

B12

app算法分析流程

  1. 抓包分析是否有需要逆向的加密字段

  2. 查壳分析是否有加固

  3. 查看界面元素,看是否是原生开发的app,因为不同形式app分析方法不一样

  4. 传统关键代码定位方法

    通过控件绑定的事件代码,来一步步定位控件id、setOnClickListener

    人肉手工搜索字符串

    搜索链接 搜索加密的参数名:当有多个可疑关键处,如何确定是哪一个?动态调试、Hook 搜索同一个数据包中,没有加密的参数名

  5. 关键代码快速定位

    跑一下自吐算法插件 Hook常用系统函数,如果app有调用就打印函数栈 在自制的沙盒中运行,打印app运行过程中的指令、函数调用关系等

  6. 逆向分析不是完全静态分析明白了才去hook,实际上是边分析边hook、不断怀疑、不断验证、不断推翻、不断找新关键函数的过程

Fridahook

frida的安装

pip install frida pip install frida-tools(装frida-tools时会自动安装frida)

frida-server的版本与frida的版本要匹配

Hook可以用来做什么

可以用来判断app执行某个操作的时候,是否经过我们的怀疑的这个函数

可以用来修改被hook函数的运行逻辑

可以用来在运行过程中,获取被hook的函数传入的具体的参数和返回值

可以用来主动调用app中的某些函数

辅助算法分析

  1. 找到一些疑似关键函数,可以通过hook来确认app执行某个操作的时候,是否调用了它们

  2. 如果没有触发这些函数,考虑以下问题

    a) app在执行这个操作的时候,真的没有调用这个函数,换一个其他的关键函数

    b) 代码写错了,导致hook函数没执行

    c) 一般可以通过主动调用上层函数,来触发这些hook函数

  3. 如果触发了这些函数,可以通过hook来打印执行过程中传入函数的参数和返回值

  4. frida -U -F -l HookDemo.js

    -U 代表远程USB设备

    -F 代表附加到最前的这个app

    -l 后面指明需要加载的JS脚本

  5. 写好的js脚本要注入手机端,并不是在Node.js中使用,所以只能用v8和fridaAPI支持的代码

代码的编写

 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
// 如果是java hook代码 都放到 Java.perform 中
Java.perform(function () { });

Java.perform(function () {
    // 普通方法
    var jsonRequest = Java.use("com.dodonew.online.http.JsonRequest");
    console.log("jsonRequest:", jsonRequest);
    jsonRequest.paraMap.implementation = function(a){
        console.log("params1: ", a);
        this.paraMap(a);
    }
    // overload函数重载
    jsonRequest.addRequestMap.overload('java.util.Map', 'int').implementation = function (a, b) {
        //console.log("addRequestMap params: ", a, b);
        //console.log("addRequestMap params1: ", a.get("username"));
        //console.log("addRequestMap params2: ", a.get("userPwd"));
        // 获取参数值_Java.cast向下转型,使用HashMap重写的toString方法
        var bb = Java.cast(a, Java.use("java.util.HashMap"));
        console.log("addRequestMap params: ", bb.toString());
        this.addRequestMap(a, b);
    }
    var utils = Java.use("com.dodonew.online.util.Utils");
    utils.md5.implementation = function (a) {
        console.log("md5 params: ", a);
        var retval = this.md5(a);
        console.log("md5 retval: ", retval);
        return retval;
    }
    var requestUtil = Java.use("com.dodonew.online.http.RequestUtil");
    requestUtil.encodeDesMap.overload('java.lang.String', 'java.lang.String', 'java.lang.String').implementation = function (a, b, c) {
        console.log("encodeDesMap params: ", a);
        console.log("encodeDesMap key: ", b);
        console.log("encodeDesMap iv: ", c);
        var retval = this.encodeDesMap(a, b, c);
        console.log("encodeDesMap retval:", retval);
        return retval;
    }
    // 主动调用(自动判断重载函数)
    var base64 = Java.use("android.util.Base64");
    // 构造函数->$init
    var dESKeySpec = Java.use("javax.crypto.spec.DESKeySpec");
    dESKeySpec.$init.overload('[B').implementation = function (a) {
        console.log("DESKeySpec params: ", base64.encodeToString(a, 0));
        this.$init(a);
    }
});

主动调用的作用

可以用来测试Hook代码的正确性

调用加密函数观察算法输出结果特征

调用加密函数测试算法复现正确性

比较复杂的算法,需要借助主动调用实现算法转发

算法复现

node.js环境配置

官网下载node.js镜像安装,vscode下载插件code runner、ESLint、Prettier

D1

协议复现

Python为例:

1
2
pip install requests
pip install PyExecJS

模拟发包服务器返回404,模拟器本身环境小程序抓包也是

关键代码快速定位

  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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
1. HashMap的put方法
    var hashMap = Java.use("java.util.HashMap");
    hashMap.put.implementation = function (a, b) {
        if(a.equals("username")){
            showStacks();
            console.log("hashMap.put: ", a, b);
        }
        return this.put(a, b);
    }

HashMapLinkedHashMap(HashSetLinkedHashSet)
java.util.concurrent.ConcurrentHashMap put

2. Java和Frida代码的互相翻译
    function showStacks() {
        console.log(
            Java.use("android.util.Log")
                .getStackTraceString(
                    Java.use("java.lang.Throwable").$new()
                )
        );
    }

3. ArrayList的addaddAllset方法等
var arrayList = Java.use("java.util.ArrayList");
arrayList.add.overload('java.lang.Object').implementation = function (a) {
   if(a.equals("username=15968079477")){
        showStacks();
        console.log("arrayList.add: ", a);
    }
    //console.log("arrayList.add: ", a);
    return this.add(a);
}
arrayList.add.overload('int', 'java.lang.Object').implementation = function (a, b) {
    console.log("arrayList.add: ", a, b);
    return this.add(a, b);
}

Vector
LinkedList

4. TextUtils的isEmpty方法
    var textUtils = Java.use("android.text.TextUtils");
    textUtils.isEmpty.implementation = function (a) {
        if(a == "2v+DC2gq7RuAC8PE5GZz5wH3/y9ZVcWhFwhDY9L19g9iEd075+Q7xwewvfIN0g0ec/NaaF43/S0="){
            showStacks();
            console.log("textUtils.isEmpty: ", a);
        }
        //console.log("textUtils.isEmpty: ", a);
        return this.isEmpty(a);
    }

5. Log
    var log = Java.use("android.util.Log");
    log.w.overload('java.lang.String', 'java.lang.String')
.implementation = function (tag, message) {
        showStacks();
        console.log("log.w: ", tag, message);
        return this.w(tag, message);
    }

6. Collections的sort方法
    var collections = Java.use("java.util.Collections");
    collections.sort.overload('java.util.List').implementation = function (a) {
        showStacks();
        var result = Java.cast(a, Java.use("java.util.ArrayList"));
        console.log("collections.sort List: ", result.toString());
        return this.sort(a);
    }
    collections.sort.overload('java.util.List', 'java.util.Comparator')
.implementation = function (a, b) {
        showStacks();
        var result = Java.cast(a, Java.use("java.util.ArrayList"));
        console.log("collections.sort List Comparator: ", result.toString());
        return this.sort(a, b);
    }

java.util.Arrays sort toString

7. JSONObject的putgetString方法等
    var jSONObject = Java.use("org.json.JSONObject");
    jSONObject.put.overload('java.lang.String', 'java.lang.Object')
.implementation = function (a, b) {
        showStacks();
        //var result = Java.cast(a, Java.use("java.util.ArrayList"));
        console.log("jSONObject.put: ", a, b);
        return this.put(a, b);
    }
    jSONObject.getString.implementation = function (a) {
        //showStacks();
        //var result = Java.cast(a, Java.use("java.util.ArrayList"));
        console.log("jSONObject.getString: ", a);
        var result = this.getString(a);
        console.log("jSONObject.getString result: ", result);
        return result;
    }

8. Toast的show方法
    var toast = Java.use("android.widget.Toast");
    toast.show.implementation = function () {
        showStacks();
        console.log("toast.show: ");
        return this.show();
    }

9. Base64
    var base64 = Java.use("android.util.Base64");
    base64.encodeToString.overload('[B', 'int').implementation = function (a, b) {
        showStacks();
        console.log("base64.encodeToString: ", JSON.stringify(a));
        var result = this.encodeToString(a, b);
        console.log("base64.encodeToString result: ", result)
        return result;
    }

java.net.URLEncoder
java.util.Base64
okio.Base64
okio.ByteString

10. String的getBytesisEmpty方法
    var str = Java.use("java.lang.String");
    str.getBytes.overload().implementation = function () {
        showStacks();
        var result = this.getBytes();
        var newStr = str.$new(result);
        console.log("str.getBytes result: ", newStr);
        return result;
    }
    str.getBytes.overload('java.lang.String').implementation = function (a) {
        showStacks();
        var result = this.getBytes(a);
        var newStr = str.$new(result, a);
        console.log("str.getBytes result: ", newStr);
        return result;
    }

11. String构造函数的Hook实例化字符串
    var stringFactory = Java.use("java.lang.StringFactory");
    stringFactory.newStringFromString.implementation = function (a) {
        showStacks();
        var retval = this.newStringFromString(a);
        console.log("stringFactory.newStringFromString: ", retval);
        return retval;
    }
    stringFactory.newStringFromChars.overload('[C').implementation = function (a) {
        showStacks();
        var retval = this.newStringFromChars(a);
        console.log("stringFactory.newStringFromChars: ", retval);
        return retval;
    }

 newStringFromBytesnewStringFromChars
 newStringFromStringnewStringFromStringBuffer  newStringFromStringBuilder

12. StringBuilderStringBuffer的Hook拼接字符串
    var sb = Java.use("java.lang.StringBuilder");
    sb.toString.implementation = function () {
        var retval = this.toString();
        if (retval.indexOf("Encrypt") != -1) {
            showStacks();
        }
        console.log("StringBuilder.toString: ", retval);
        return retval;
    }
    var sb = Java.use("java.lang.StringBuffer");
    sb.toString.implementation = function () {
        var retval = this.toString();
        if (retval.indexOf("username") != -1) {
            showStacks();
        }
        console.log("StringBuffer.toString: ", retval);
        return retval;
    }

13. findViewById 找控件id(打印R$id的属性)
a) Java.enumerateLoadedClassesSync枚举所有已加载的类
    如果不知道类路径可以用这个方法然后过滤一下类名
b) frida -U -f com.dodonew.online -l HookDemo.js -o log.txt --no-pause
启动注入findViewById在界面创建时已调用
-f 代码让frida帮我们重新启动app一开始就注入js
--no-pause 直接运行主线程中途不暂停
c) R$id 内部类的访问
d) R$id.btn_login.value 类的属性的访问

var btn_login_id = Java.use("com.dodonew.online.R$id").btn_login.value;
console.log("btn_login_id", btn_login_id);
var appCompatActivity = Java.use("android.support.v7.app.AppCompatActivity");
appCompatActivity.findViewById.implementation = function (a) {
    if(a == btn_login_id){
        showStacks();
        console.log("appCompatActivity.findViewById: ", a);
    }
    return this.findViewById(a);
}

14. setOnClickListener
    hook这个函数比对控件id打印函数栈
var btn_login_id = Java.use("com.dodonew.online.R$id").btn_login.value;
console.log("btn_login_id", btn_login_id);

var view = Java.use("android.view.View");
view.setOnClickListener.implementation = function (a) {
    if(this.getId() == btn_login_id){
        showStacks();
        console.log("view.id: " + this.getId());
        console.log("view.setOnClickListener is called");
    }
    return this.setOnClickListener(a);
}

加密库相关的hook(自吐算法)

SSL相关的hook

socket相关的hook

SocketOutputStream

SocketInputStream

读写文件相关的 java.io.File

证书双向验证 Keystore.load 通常有证书和密码

安卓退出进程的方式

//快速定位协议头加密okhttp3的addHeader方法

var okhttp_Builder = Java.use(‘okhttp3.Request$Builder’);

okhttp_Builder.addHeader.implementation = function (a, b) { showStacks(); return this.addHeader(a, b); }

安卓系统沙盒

Frida API

  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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
1. 静态方法和实例方法的hook
    //不需要区分修饰符,也不需要区分静态和实例方法,hook代码的写法都是一样的
    //得到Money类对象
    var money = Java.use("com.xiaojianbang.hook.Money");
    //hook实例方法
    money.getInfo.implementation = function () {
        var result = this.getInfo();
        console.log("money.getInfo result: ", result)
        return result;
    }
    //hook静态方法
    money.setFlag.implementation = function (a) {
        console.log("money.setFlag param: ", a);
        return this.setFlag(a);
    }

2. 函数参数和返回值的修改
    var money = Java.use("com.xiaojianbang.hook.Money");
    var str = Java.use("java.lang.String");
    money.getInfo.implementation = function () {
        var result = this.getInfo();
        console.log("money.getInfo result: ", result);
        return str.$new("xiaojianbang");
        //上述字符串"xiaojianbang"是JS的string,而被hook的Java方法返回值是Java的String
        //因此,可以主动调用Java方法转成Java的String
        //但是为了方便起见,通常会直接直接返回JS的string,这时frida会自动处理,代码类似如下
        //return "xiaojianbang";
        //Java的类型可以调用Java的方法,JS的类型可以调用JS的方法
        //区分清楚何时是Java的类型,何时是JS的类型,有助于代码的编写
        //frida在参数传递的处理上也类似
    }
    money.setFlag.implementation = function (a) {
        console.log("money.setFlag param: ", a);
        return this.setFlag("xiaojianbang");
    }

3. 构造方法的hook $init
    var money = Java.use("com.xiaojianbang.hook.Money");
    money.$init.implementation = function (a, b) {
        console.log("money.$init param: ", a, b);
        return this.$init("美元", 200);
    }

4. 对象参数的构造与修改 $new
    var wallet = Java.use("com.xiaojianbang.hook.Wallet");
    var money = Java.use("com.xiaojianbang.hook.Money");
    wallet.deposit.implementation = function (a) {
        console.log("wallet.deposit param: ", a.getInfo());
        return this.deposit(money.$new("美元", 200));
    }

    var wallet = Java.use("com.xiaojianbang.hook.Wallet");
    wallet.deposit.implementation = function (a) {
        a.setAmount(2000);
        console.log("wallet.deposit param: ", a.getInfo());
        return this.deposit(a);
    }

5. HashMap的打印
    var utils = Java.use("com.xiaojianbang.hook.Utils");
    var stringBuilder = Java.use("java.lang.StringBuilder");
    utils.shufferMap.implementation = function (a) {
        var key = a.keySet();
        var it = key.iterator();
        var result = stringBuilder.$new();
        while(it.hasNext()){
            var keystr = it.next();
            var valuestr = a.get(keystr);
            result.append(valuestr);
        }
        console.log("utils.shufferMap param: ", result.toString());
        var result = this.shufferMap(a);
        console.log("utils.shufferMap result: ", result);
        return result;
    }

6. 重载方法的hook
    var utils = Java.use("com.xiaojianbang.hook.Utils");
    utils.getCalc.overload('int', 'int').implementation = function (a, b) {
        console.log("utils.getCalc param: ", a, b);
        return this.getCalc(a, b);
    }
    utils.getCalc.overload('int', 'int', 'int').implementation = function (a, b, c) {
        console.log("utils.getCalc param: ", a, b, c);
        return this.getCalc(a, b, c);
    }
    utils.getCalc.overload('int', 'int', 'int', 'int').implementation = function (a, b, c, d) {
        console.log("utils.getCalc param: ", a, b, c, d);
        return this.getCalc(a, b, c, d);
    }

7. hook方法的所有重载
    var utils = Java.use("com.xiaojianbang.hook.Utils");
    var overloadsArr = utils.getCalc.overloads;
    for (var i = 0; i < overloadsArr.length; i++) {
        overloadsArr[i].implementation = function () {
            showStacks();
            var params = "";
            for (var j = 0; j < arguments.length; j++) {
                params += arguments[j] + " ";
            }
            console.log("utils.getCalc is called! params is: ", params);
            // if(arguments.length == 2){
            //     return this.getCalc(arguments[0], arguments[1]);
            // }else if(arguments.length == 3){
            //     return this.getCalc(arguments[0], arguments[1], arguments[2]);
            // }else if(arguments.length == 4){
            //     return this.getCalc(arguments[0], arguments[1], arguments[2], arguments[3]);
            // }
            console.log(this);
            return this.getCalc.apply(this, arguments);
        }
    }

8. 主动调用
    a) 静态方法
    var money = Java.use("com.xiaojianbang.hook.Money");
    money.setFlag("xiaojianbang");
    b) 实例方法 创建新对象
    var moneyObj = money.$new("卢布", 1000);
    console.log(moneyObj.getInfo());
    c) 实例方法 获取已有对象(Java.choose)
    Java.choose("com.xiaojianbang.hook.Money", {
        onMatch: function (obj){
            console.log(obj.getInfo());
        },
        onComplete: function (){
            console.log("内存中的Money对象搜索完毕");
        }
    });

	d) android Context的获取
    var current_application = Java.use('android.app.ActivityThread').currentApplication();
	var context = current_application.getApplicationContext();

9. 获取和修改类的字段
        // a) 静态字段
        var money = Java.use("com.xiaojianbang.hook.Money");
        console.log(money.flag.value);
        money.flag.value = "VX: xiaojianbang8888";
        console.log(money.flag.value);
        // b) 实例字段 创建新对象
        var moneyObj = money.$new("欧元", 2000);
        console.log(moneyObj.currency.value);
        moneyObj.currency.value = "xiaojianbang currency";
        console.log(moneyObj.currency.value);
        // c) 实例字段(获取已有对象)
        Java.choose("com.xiaojianbang.hook.Money", {
            onMatch: function (obj) {
                console.log("Java.choose Money: ", obj.currency.value);
            }, onComplete: function () {

            }
        });
        // 如果字段名和方法名一样 需要加下划线前缀
        Java.choose("com.xiaojianbang.hook.BankCard", {
            onMatch: function (obj) {
                console.log("Java.choose BankCard: ", obj._accountName.value);
            }, onComplete: function () {

            }
        });

10. Hook内部类与匿名类
        Java.choose("com.xiaojianbang.hook.Wallet$InnerStructure", {
            onMatch: function (obj) {
				console.log(
                    "Java.choose Wallet$InnerStructure: ", obj.bankCardsList.value
                );
            }, onComplete: function () {

            }
        });
        var money$1 = Java.use("com.xiaojianbang.app.MainActivity$1");
        money$1.getInfo.implementation = function () {
            var result = this.getInfo();
            console.log("money.getInfo result: ", result);
            return result;
        }

11. 枚举所有已加载的类与枚举类的所有方法
        // 1. 枚举的是已经加载的类,没有加载的类不会出现
        // 2. 出现的类,你也不一定能够hook到,原因是类加载器的关系
        //console.log(Java.enumerateLoadedClassesSync().join("\n"));
        var wallet = Java.use("com.xiaojianbang.hook.Wallet");
        var methods = wallet.class.getDeclaredMethods();
        var constructors = wallet.class.getDeclaredConstructors();
        var fields = wallet.class.getDeclaredFields();
        var classes = wallet.class.getDeclaredClasses();

        for (let i = 0; i < methods.length; i++) {
            console.log(methods[i].getName());
        }
        console.log("============================");
        for (let i = 0; i < constructors.length; i++) {
            console.log(constructors[i].getName());
        }
        console.log("============================");
        for (let i = 0; i < fields.length; i++) {
            console.log(fields[i].getName());
        }
        console.log("============================");
        for (let i = 0; i < classes.length; i++) {
            console.log(classes[i].getName());
            //classes[i] 这里得到的已经是类的字节码,不需要再.calss
            var Wallet$InnerStructure = classes[i].getDeclaredFields();
            for (let j = 0; j < Wallet$InnerStructure.length; j++) {
                console.log(Wallet$InnerStructure[j].getName());
            }
        }

12. hook类的所有方法
    function hookFunc(methodName) {
        console.log(methodName);
        var overloadsArr = utils[methodName].overloads;
        for (var j = 0; j < overloadsArr.length; j++) {
            overloadsArr[j].implementation = function () {
                var params = "";
                for (var k = 0; k < arguments.length; k++) {
                    params += arguments[k] + " ";
                }
                console.log("utils." + methodName + " is called! params is: ", params);
                return this[methodName].apply(this, arguments);
            }
        }
    }

    var utils = Java.use("com.xiaojianbang.hook.Utils");
    var methods = utils.class.getDeclaredMethods();
    for (var i = 0; i < methods.length; i++) {
        var methodName = methods[i].getName();
        hookFunc(methodName);
    }

13. Java.registerClass
    frida的官方JavaScriptAPI
https://frida.re/docs/javascript-api/

    const MyWeirdTrustManager = Java.registerClass({
        name: 'com.xiaojianbang.app.MyRegisterClass',
        implements: [Java.use("com.xiaojianbang.app.TestRegisterClass")],
        fields: {
            description: 'java.lang.String',
            limit: 'int',
        },
        methods: {
            $init() {
                console.log('Constructor called');
            },
            test1: [{
                returnType: 'void',
                argumentTypes: [],
                implementation() {
                    console.log('test1 called');
                }
            }, {
                returnType: 'void',
                argumentTypes: ['java.lang.String', 'int'],
                implementation(str, num) {
                    console.log('test1(str, num) called', str, num);
                }
            }],
            test2(str, num) {
                console.log('test2(str, num) called', str, num);
                return null;
            },
        }
    });
    var myObj = MyWeirdTrustManager.$new();
    myObj.test1();
    myObj.test1("xiaojianbang1", 100);
    myObj.test2("xiaojianbang2", 200);
    myObj.limit.value = 10000;
    console.log(myObj.limit.value);

14. Frida注入dex
    Java.openClassFile("/data/local/tmp/patch.dex").load();
    var test = Java.use("com.xiaojianbang.myapplication.Test");
    var utils = Java.use("com.xiaojianbang.hook.Utils");
    utils.shufferMap.implementation = function (map) {
        var result = test.print(map);
        console.log(result);
        return result;
    }

15. hook枚举类(Java.choose)
    Java.choose("com.xiaojianbang.app.Season", {
        onMatch: function (obj) {
            console.log(obj.ordinal());
        }, onComplete: function () {

        }
    })
    console.log(Java.use("com.xiaojianbang.app.Season").values());

16. Frida写文件
var ios = new File("/sdcard/xiaojianbang.txt", "w");
ios.write("xiaojianbang is very good!!!\n");
ios.flush();
ios.close();

17. Java.cast
    向上转型的不能用toString直接得到结果比如MapList类型的打印
    var utils = Java.use("com.xiaojianbang.hook.Utils");
    utils.shufferMap2.implementation = function (map) {
        console.log("map: ", map);
        var result = Java.cast(map, Java.use("java.util.HashMap"));
        console.log("map: ", result);
        return this.shufferMap2(result);
    }

18. 数组的构建
//Java.array("Ljava.lang.Object;", ...)
var utils = Java.use("com.xiaojianbang.hook.Utils");
//console.log(
//	utils.myPrint(["xiaojianbang", "QQ:24358757", "VX:xiaojianbang8888", "公众号:非攻code"])
//);
var strarr = Java.array(
	"Ljava.lang.String;", 
	["xiaojianbang", "QQ:24358757", "VX:xiaojianbang8888", "公众号:非攻code"]
);
console.log(utils.myPrint(strarr));

19. Object数组的构建
    可变参数本质上就是数组按数组处理即可
    只需要处理基本数据类型的包装其他的Frida会处理
var utils = Java.use("com.xiaojianbang.hook.Utils");
var bankCard = Java.use("com.xiaojianbang.hook.BankCard");
var bankCardObj = bankCard.$new("xiaojianbang", "123456789", "CBDA", 1, "15900000000");
var integer = Java.use("java.lang.Integer");
var boolean = Java.use("java.lang.Boolean");
//var objarr = Java.array(
//	"Ljava.lang.Object;", 
//	["xiaojianbang", integer.$new(30), boolean.$new(true), bankCardObj]
//);
console.log(
utils.myPrint(["xiaojianbang", integer.$new(30), boolean.$new(true), bankCardObj])
);

20. ArrayList的主动调用
    var arrayList = Java.use("java.util.ArrayList").$new();
    var integer = Java.use("java.lang.Integer");
    var boolean = Java.use("java.lang.Boolean");
    var bankCard = Java.use("com.xiaojianbang.hook.BankCard");
    var bankCardObj = bankCard.$new("xiaojianbang", "123456789", "CBDA", 1, "15900000000");
    arrayList.add("xiaojianbang");
    arrayList.add(integer.$new(30));
    arrayList.add(boolean.$new(true));
    arrayList.add(bankCardObj);
    var utils = Java.use("com.xiaojianbang.hook.Utils");
    console.log(utils.myPrint(arrayList));

21. hook动态加载的dex(Java.enumerateClassLoaders)
Java.enumerateLoadedClassesSync()枚举已加载类的结果中有但是hook报错找不到类可以尝试枚举ClassLoader切换ClassLoader来hook
可以查看加载的dex所在路径
Java.enumerateClassLoaders({
    onMatch: function (loader) {
        try {
            Java.classFactory.loader = loader;
            var dynamic = Java.use("com.xiaojianbang.app.Dynamic");
            console.log("dynamic: ", dynamic);
            //console.log(dynamic.$new().sayHello());
            dynamic.sayHello.implementation = function () {
                console.log("hook dynamic.sayHello is run!");
                return "xiaojianbang";
            }
        } catch (e) {
            console.log(loader);
        }
    },
    onComplete: function () {}
});

22. 让hook只在指定函数内生效
    var mainActivity = Java.use("com.xiaojianbang.app.MainActivity");
    var stringBuilder = Java.use('java.lang.StringBuilder');
    mainActivity.generateAESKey.implementation = function () {
        console.log("mainActivity.generateAESKey is called!");
        stringBuilder.toString.implementation = function () {
            var result = this.toString();
            console.log(result);
            return result;
        };
        var result = this.generateAESKey.apply(this, arguments);
        stringBuilder.toString.implementation = null;  //取消hook
        return result;
    };

23. hook定位接口的实现类
    var classes = Java.enumerateLoadedClassesSync();
    for (const index in classes) {
        let className = classes[index];
        if(className.indexOf("com.xiaojianbang") === -1) continue;
        try {
            let clazz = Java.use(className);
            let resultArr = clazz.class.getInterfaces();
            console.log("resultArr: ", resultArr);
            if(resultArr.length === 0) continue;
            for (let i = 0; i < resultArr.length; i++) {
                if(resultArr[i].toString().indexOf("com.xiaojianbang.app.TestRegisterClass") !== -1){
                    console.log(className, resultArr);
                }
            }
        }catch (e) {console.log("Didn't find class: " + className);}
    }

24. hook定位抽象类的实现类
    var classes = Java.enumerateLoadedClassesSync();
    for (const index in classes) {
        let className = classes[index];
        if(className.indexOf("com.xiaojianbang") === -1) continue;
        try {
            let clazz = Java.use(className);
            let resultClass = clazz.class.getSuperclass();
            console.log("resultClass: ", className, resultClass);
            if(resultClass == null) continue;
            if(resultClass.toString().indexOf("com.xiaojianbang.app.TestAbstract") !== -1){
                console.log(className, resultClass);
            }
        } catch (e) {}
    }

jadx动态调试(了解)

混淆函数的hook方法

1.用dexlib2修改dex混淆函数名(用base64编码)

2.hook代码中用base64解码函数名得到

frida.exe的使用

  1. frida.exe的选项介绍 frida attach 包名、pid、前端进程注入 frida spawn frida.exe的的选项介绍 -U 连接远程USB设备(-U和-H只能选一个) -H 通过ip和端口连接,可以连接多台设备(netstat -nltp查看端口) -F 附加到最前的这个app -l 后面指明需要加载的JS脚本 -o 把信息输出到指定文件中

  2. frida-server的选项介绍 -l 可以修改监听的地址端口,与frida.exe的-H选项配合使用 电脑和手机的ip需要互通

objection

1.objection的安装

a) objection对frida做了进一步的封装,通过输入一系列的命令即可完成hook。忘记命令时还可以按空格弹出对应提示信息,大大降低了hook框架的使用门槛

b) 安装objection之前,先安装frida和frida-tools

c) 为了有更好的兼容性,objection的版本,最好选择当前frida版本之后更新 objection更新时间查看地址 https://pypi.org/project/objection/1.1.3/#history frida更新时间查看地址

d) 教程使用的各版本号 pip install frida==14.2.18 pip install frida-tools==9.2.5 pip install objection==1.11.0

常见报错: pkg_resources.ContextualVersionConflict: (Pygments 2.11.2 (d:\soft\python386\lib\site-packages), Requirement.parse(‘Pygments<=2.11.1,>=1.6’), {’litecli’}) 降级Pygments库到2.11.1即可

2.objection的使用

注入进程,如果objection没有找到进程,会以spwan方式启动进程 objection –help objection -g <进程名> explore objetion log 文件位置 C:\Users\Administrator.objection

3.内存漫游

 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
列出所有已加载的类
android hooking list classes
在所有已加载的类中搜索包含特定关键字的类
android hooking search classes <pattern>
列出类的所有方法
android hooking list class_methods <路径.类名>
hook类的所有方法(不包括构造方法)
android hooking watch class <路径.类名>
hook类的构造方法
android hooking watch class_method <路径.类名.$init>
默认是hook方法的所有重载
android hooking watch class_method <路径.类名.方法名>
hook方法的参数、返回值和调用栈
android hooking watch class_method <路径.类名.方法名> --dump-args --dump-return --dump-backtrace
hook单个重载函数,需要指定参数类型,多个参数用逗号分隔
android hooking watch class_method <路径.类名.方法名> "<参数类型>" 

查看与取消hook
jobs list
jobs kill <jobId>

指定ip和端口连接
objection -N -h <ip> -p <port> -g <进程名> explore

启动前就hook
objection -N -h <ip> -p <port> -g <进程名> explore --startup-command "android hooking watch class <路径.类名>"

启动前就hook打印参数、返回值、函数调用栈
objection -N -h <ip> -p <port> -g <进程名> explore -s "android hooking watch class_method <路径.类名.方法名>  --dump-args --dump-return --dump-backtrace"

如果启动前需要运行多条命令,可以写到一个文件中,使用-c选项
objection -g <进程名> explore -c "路径"

关闭ssl校验	android sslpinning disable
关闭root检测	android root disable

搜索堆中的实例
android heap search instances <类名>
通过实例调用静态和实例方法
调用 android heap execute <handle> <方法名>
调用打印返回值 android heap execute <handle> <方法名> --return-string
调用带参数方法,进入编辑器环境
android heap evaluate <handle>
console.log(clazz.getCalc(100, 200));

查看当前app的activity
android hooking list activities
尝试跳转到对应activiy
android intent launch_activity <activiyName>
枚举内存中所有模块
memory list modules
枚举模块中所有导出函数
memory list exports <so库名>
当结果太多,可以将结果导出到本地文件中
memory list exports <so库名> --json <路径.文件名>

4.Wallbreaker

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
1. objection的插件加载
plugin load 插件路径 name
name指的是自己定义的插件名字,将来使用这个名字访问插件,区分大小写

2. 搜索类
plugin Wallbreaker classsearch <pattern>

3. 搜索对象
plugin Wallbreaker objectsearch <classname>

4. classdump输出类结构,若加了--fullname参数,打印数据中的类名会带完整包名
plugin Wallbreaker classdump <classname> [--fullname]

5. objectdump在classdump的基础上,输出指定对象中的每个字段的数据
plugin Wallbreaker objectdump <handle> [--fullname]

脱离pc使用Frida

  1. 脱离PC使用Frida的几种方式 a) 在手机上使用Termux终端 b) frida-inject c) frida-gadget.so 优点:可以免root使用frida、frida-gadget比较稳定 缺点:需要重打包app,局限性较大。但是我们可以通过魔改系统,让系统帮我们注入so,免去重打包的繁琐
  2. frida-inject的配置、选项和使用 -f、-p、-n、-s、-e
  3. 脱离pc后,如何判断hook是否生效 a) 写文件,然后查看文件内容 b) 主动调用Log的方法输出信息,然后使用logcat查看 c) 修改某些方法逻辑

免root使用Frida

 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
1. objection的选项-N-h-p-g

2. objection explore的选项-s-c

3. objection patchapk的介绍
   a) https://github.com/sensepost/objection/wiki
   b) 环境的准备
    aaptjarsignerapktooladb
   c) objection patchapk的选项介绍
      -s:指明目标文件
      -a:指明so平台
      -V:指明gadget.so的版本
      -D:反编译是跳过资源
      -c:指定gadget的config文件路径

4. Android系统对应so平台的选择
   Android系统是32位,那么app的so都走32位的
   Android系统是64位,那么app里面如果有64位的so,就只走64位的so
   Android系统是64位,那么app里面如果只有32位的so,就走32位的so

5. objection patchapk的gadget默认存放路径
C:\Users\Administrator\.objection\android\arm64-v8a\libfrida-gadget.so

6. 重打包后的app,安装运行,会在启动界面等待frida连接,之后的与frida-server一致
frida -U <packageName> -l <scriptFileName>

7. 重打包存在的缺陷
有些app反编译和回编译可能会出错
有些app存在签名校验、文件校验

8. root使用frida的改进方法
魔改系统,启动app时,自动加载gadget.so

免root+脱离pc使用Frida

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
1. 官方文档地址
https://frida.re/docs/gadget/

2. objection patchapk -V 14.2.18 -c gadget_config.txt -s HookDemo.apk
-c 指定gadget的config文件路径

3. gadget_config
{
  "interaction": {
    "type": "script",
    "path": "/data/local/tmp/hook_encrypt.js"
  }
}

Frida自吐算法演示

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
1. Frida自吐算法演示
HookDemo、dodonew
vsf2f、m1905: 加固的app
z酷:标准算法加密后的数据,又进行了别的处理
0715quan:登录不一定一条数据包完成,注意登录之前的数据包
  指宝玩:提交数据乱码、非标准算法

2. Frida自吐算法的应用场景
加密在Java层,并且调用了标准加密库的
加密在Java层,但是使用反射调用标准加密库的
加密在so层,但是用jni调用了Java标准加密库的
app被加固,但是调用了标准加密库的
通过打印函数栈辅助定位加密代码所在类

3. Frida自吐算法的局限性
加密在Java层,自写的标准算法或非标准算法
加密在so层,C/C++写的算法
加密在Java层,并且调用了标准加密库,但是加密前和加密后的数据都做了其他处理

4. 掌握好最基本的逆向手段,还是很重要的

H5的app逆向

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
1. H5的核心代码通常在JS文件中
解决方法:远程调试、修改JS代码注入代码

2. WebView远程调试的要求
2.1 手机端的WebView版本要比电脑端的chrome版本低
2.2 手机端的WebView需要开启可调试
2.3 需要有一个科学上网的vpn,因为点击inspect时要去下载一些东西,不然打开是白屏
2.4 通常app中的WebView是不开启可调试的,需要通过Hook来开启
Hook WebView

3. H5的app也分很多种
3.1 纯JS发包,这时可以在远程调试工具上抓到包,也有相应JS代码
3.2 部分JS发包,部分Java发包,这时有些包可以在调试工具上抓到,有些不行,需要做额外的分析
   比如:JS和Java如何相互调用,从这个角度入手,找Java里面的接口
   加密可能部分在JS文件中,部分在Java中,说白了有些代码可以在调试工具中看到,有些代码是Java只能逆向app去找
3.3 纯Java发包,典型是uni-app。但奇怪的是uni-app核心代码都在JS中

so入门

  1. so中通常会接触到的东西 jni调用 系统库函数 加密算法 魔改算法 系统调用 自定义算法

  2. so加固、so混淆 so的dump so的修复 so的文件结构 自定义linker

NDK

 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
1. app为什么会把代码放到so中
a) C语言历史悠久,有很多现成的代码可用
b) C代码执行效率比Java高
c) Java代码很容易被反编译,而且反编译以后的逻辑很清晰
2. 为什么要学习NDK开发
在安卓的so开发中,其他基本与C/C++开发一致,而与Java交互需要用到jni
在本部分的NDK开发讲解中,主要就是介绍jni相关内容
so中会接触的:系统库函数、jni调用、加密算法、魔改算法、系统调用、自定义算法
3. 什么是JNI
jni是Java Native Interface的缩写。从Java1.1开始,jni标准成为Java平台的一部分,允许Java代码和其他语言写的代码进行交互
4. 什么是NDK
交叉编译工具链
NDK的配置
NDK、CMake、LLDB的作用 https://developer.android.com/ndk/guides
5. ABI与指令集
https://developer.android.com/ndk/guides/abis

NDK与Java工程的区别

1. Java代码中加载so和声明所需使用的so中的函数
2. 编写CMakeLists.txt和C文件
3. build.gradle中添加一些代码
defaultConfig {
......
externalNativeBuild {
	cmake {
	    cppFlags "-std=c++11"
	}
}
ndk {
	abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
}
}
externalNativeBuild {
cmake {
	path "src/main/cpp/CMakeLists.txt"
	version "3.10.2"
}
}

第一个NDK工程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
1. CMakeLists介绍
2. so的加载
3. native函数的声明
4. JNI函数的静态注册规则
5. JNIEnv、jobject/jclass
6. NewstringUTF
7. 在NDK开发中,一定要注意哪些是Java的类型,哪些是C/C++的类型,在适当的时候需要转换
8. extern "C" JNIEXPORT jstring JNICALL
9. 指定只编译arm64的so
10. 指定编译后的so名字

so中常用的Log输出

1
2
3
4
5
6
7
8
#include <android/log.h>

#define TAG "xiaojianbang"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__);
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO , TAG, __VA_ARGS__);
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__);

#define中的...和__VA_ARGS__

NDK多线程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
JavaVM每个进程中只有一份
JNIEnv每个线程中都有一份
为了更好的演示,所以先简单介绍一下多线程

//线程id,其实就是long
pthread_t thread;
//线程id 线程属性 函数 传给函数的参数
pthread_create(&thread, nullptr, myThread, nullptr);
//等待线程执行完毕
//默认的线程属性是joinable 随着主线程结束而结束
//线程属性是dettach,可以分离执行 
pthread_join(thread, nullptr);
//子线程中使用它来退出线程
pthread_exit(0);

传递int参数
传递多个参数 结构体 数组
接收返回值

JNI_OnLoad

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
1. so中各种函数的执行时机
  initinit_arrayJNI_OnLoad

2. JNI_OnLoad的定义
JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = nullptr;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        LOGD("GetEnv failed");
        return -1;
    }
    return JNI_VERSION_1_6;
}

3. 注意事项
一个so中可以不定义JNI_OnLoad
一旦定义了JNI_OnLoad,在so被加载的时候会自动执行
必须返回JNI版本 JNI_VERSION_1_6

JavaVM

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
1. JavaVM是什么
JavaVM结构体介绍
C和C++中的区别
JavaVM中的常用方法
GetEnv
AttachCurrentThread

2. JavaVM的获取方式
JNI_OnLoad的第一个参数
JNI_OnUnload的第一个参数
env->GetJavaVM
对比各种方式获取的JavaVM指针是否一致
%p打印地址

JNIEnv

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1. JNIEnv是什么
JNIEnv结构体介绍
C和C++中的区别
JNIEnv中的常用方法后续详细介绍

2. JNIEnv的获取方式
函数静态/动态注册,传的第一个参数
vm->GetEnv
globalVM->AttachCurrentThread
对比各种方式获取的JNIEnv指针是否一致
%p打印地址

so相关的几个概念

1
2
3
4
5
6
7
8
1. 导出表、导入表是什么

2. 出现在导出表、导入表里面的函数,一般可以通过frida相关API直接得到函数地址,也可以自己计算函数地址

3. 没有出现在导出表、导入表、符号表里面的函数,都需要自己计算函数地址

4. 要完成so层的hook,都需要得到一个地址
5. Java层中的native函数,被调用后会找到so中对应的函数。简单的说,就是Java调用C需要先完成函数注册,函数注册分为静态注册、动态注册

so函数注册

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
1. JNI函数的静态注册
必须遵循一定的命名规则,一般是Java_包名_类名_方法名
系统会通过dlopen加载对应的so,通过dlsym来获取指定名字的函数地址,然后调用
静态注册的jni函数,必然在导出表里

2. JNI函数的动态注册
通过env->RegisterNatives注册函数,通常在JNI_OnLoad中注册
JNINativeMethod
函数签名
可以给同一个Java函数注册多个native函数,以最后一次为准

jclass MainActivityClazz = env->FindClass("com/xiaojianbang/ndk/NDKMain");
JNINativeMethod methods[] = {
    //public native String encode(int i, String str, byte[] byt);
    {"encode", "(ILjava/lang/String;[B)Ljava/lang/String;", (void *)encodeFromC},
};
env->RegisterNatives(MainActivityClazz, methods, sizeof(methods)/sizeof(JNINativeMethod));

so路径的动态获取

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
1. 3264so存放路径不一样,为了更加通用,可以用代码动态获取so路径
    public String getPath(Context cxt){
        PackageManager pm = cxt.getPackageManager();
        List<PackageInfo> pkgList = pm.getInstalledPackages(0);
        if (pkgList == null || pkgList.size() == 0) return null;
        for (PackageInfo pi : pkgList) {
            if (pi.applicationInfo.nativeLibraryDir.startsWith("/data/app/")
                    && pi.packageName.startsWith("com.xiaojianbang.demo")) {
                //Log.e("xiaojianbang", pi.applicationInfo.nativeLibraryDir);
                return pi.applicationInfo.nativeLibraryDir;
            }
        }
        return null;
    }

so之间的相互调用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
1. 多个cpp文件编译成一个so

2. 编译多个so
编写多个cpp文件
修改CMakeLists.txt
Java静态代码块加载多个so

3. so之间的相互调用
    2.1 使用dlopendlsymdlclose获取函数地址,然后调用。需要导入dlfcn.h
    void *soinfo = dlopen(nativePath, RTLD_NOW);
    void (*def)(char* str) = nullptr;
    def = reinterpret_cast<void (*)(char *)>(dlsym(soinfo, "_Z7fromSoBPc"));
    def("xiaojianbang");
2.2 extern 函数声明

JNI

通过jni创建Java对象

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
1. NewObject创建对象
jclass clazz = env->FindClass("com/xiaojianbang/ndk/NDKDemo");
jmethodID methodID = env->GetMethodID(clazz, "<init>", "()V");
jobject ReflectDemoObj = env->NewObject(clazz, methodID);
LOGD("ReflectDemoObj %p", ReflectDemoObj);

2. AllocObject创建对象
jclass clazz = env->FindClass("com/xiaojianbang/ndk/NDKDemo");
jmethodID methodID2 = env->GetMethodID(clazz, "<init>", "(Ljava/lang/String;I)V");
jobject ReflectDemoObj2 = env->AllocObject(clazz);
jstring jstr = env->NewStringUTF("from jni str");
env->CallNonvirtualVoidMethod(ReflectDemoObj2, clazz, methodID2, jstr, 100);

通过jni访问Java属性

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
1. 获取静态字段
jfieldID privateStaticStringField = 
	env->GetStaticFieldID(clazz, "privateStaticStringField", "Ljava/lang/String;");
jstring privateStaticString = 
	static_cast<jstring>(env->GetStaticObjectField(clazz, privateStaticStringField));
const char* privatecstr = 
	env->GetStringUTFChars(privateStaticString, nullptr);
LOGD("privateStaticString: %s", privatecstr);
env->ReleaseStringUTFChars(privateStaticString, privatecstr);

2. 获取对象字段
jfieldID publicStringField = 
	env->GetFieldID(clazz, "publicStringField", "Ljava/lang/String;");
jstring publicString = 
	static_cast<jstring>(env->GetObjectField(ReflectDemoObj, publicStringField));
const char* publiccstr = 
	env->GetStringUTFChars(publicString, nullptr);
LOGD("publicStringField: %s", publiccstr);
env->ReleaseStringUTFChars(publicString, publiccstr);

3. 设置字段
env->SetObjectField(ndkobj, privateStringFieldID, env->NewStringUTF("xiaojianbang"));

通过jni访问Java数组

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
1. 获取数组字段ID
jfieldID byteArrayID = env->GetFieldID(clazz, "byteArray", "[B");
jbyteArray byteArray = 
	static_cast<jbyteArray>(env->GetObjectField(ReflectDemoObj, byteArrayID));
int _byteArrayLength = env->GetArrayLength(byteArray);

2. 修改数组字段
char javaByte[10];
for(int i = 0; i < 10; i++){
	javaByte[i] = static_cast<char>(100 - i);
}
const jbyte *java_array = reinterpret_cast<const jbyte *>(javaByte);
env->SetByteArrayRegion(byteArray, 0, _byteArrayLength, java_array);

3. 获取数组字段
byteArray = static_cast<jbyteArray>(env->GetObjectField(ReflectDemoObj, byteArrayID));
_byteArrayLength = env->GetArrayLength(byteArray);
char* str = reinterpret_cast<char *>(env->GetByteArrayElements(byteArray, nullptr));
for(int i = 0; i< _byteArrayLength; i++){
	LOGD("str[%d]=%d", i, str[i]);
}
env->ReleaseByteArrayElements(jbyteArray, reinterpret_cast<jbyte *>(cbyteArray), 0);

通过jni访问Java方法

 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
1. 调用静态函数
jclass ReflectDemoClazz = env->FindClass("com/xiaojianbang/ndk/NDKDemo");
jmethodID publicStaticFuncID = 
	env->GetStaticMethodID(ReflectDemoClazz, "publicStaticFunc", "()V");
env->CallStaticVoidMethod(ReflectDemoClazz, publicStaticFuncID);

2. 调用对象函数
jmethodID privateFuncID = 
	env->GetMethodID(ReflectDemoClazz, "privateFunc", "(Ljava/lang/String;I)Ljava/lang/String;");
jmethodID ReflectDemoInit = 
	env->GetMethodID(ReflectDemoClazz, "<init>", "(Ljava/lang/String;)V");
jstring str1 = env->NewStringUTF("this is from NDK");
jobject ReflectDemoObj = env->NewObject(ReflectDemoClazz, ReflectDemoInit, str1);
jstring str2 = env->NewStringUTF("this is from JNI");
jstring retval = 
	static_cast<jstring>(env->CallObjectMethod(ReflectDemoObj, privateFuncID, str2, 1000));

3. CallObjectMethodA的使用
jvalue args[2];
args[0].l = str2;
args[1].i = 1000;
jstring retval = 
	static_cast<jstring>(env->CallObjectMethodA(ReflectDemoObj, privateFuncID, args));
const char* cpp_retval = env->GetStringUTFChars(retval, nullptr);
LOGD("cpp_retval: %s", cpp_retval);
env->ReleaseStringUTFChars(retval, cpp_retval);

4. 参数是数组,返回值是数组的函数
jclass StringClazz = env->FindClass("java/lang/String");
jobjectArray StringArr = env->NewObjectArray(3, StringClazz, nullptr);
for(int i = 0; i < 3; i++){
	jstring str3 = env->NewStringUTF("NDK");
	env->SetObjectArrayElement(StringArr, i, str3);
	//env->DeleteLocalRef(str3);
}
jmethodID privateStaticFuncID = 
	env->GetStaticMethodID(ReflectDemoClazz, "privateStaticFunc", "([Ljava/lang/String;)[I");
jintArray intArr = 
	static_cast<jintArray>(env->CallStaticObjectMethod(
		ReflectDemoClazz, privateStaticFuncID, StringArr));
int *cintArr = env->GetIntArrayElements(intArr, nullptr);
LOGD("cintArr[0]=%d", cintArr[0]);
env->ReleaseIntArrayElements(intArr, cintArr, JNI_ABORT);

通过jni访问Java父类方法

1
2
3
4
5
6
1. super.onCreate(savedInstanceState);

jclass AppCompatActivityClazz = env->FindClass("androidx/appcompat/app/AppCompatActivity");
jmethodID onCreateID = 
env->GetMethodID(AppCompatActivityClazz, "onCreate", "(Landroid/os/Bundle;)V");
env->CallNonvirtualVoidMethod(thiz, AppCompatActivityClazz, onCreateID, saved_instance_state);

内存管理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1. 局部引用
大多数的jni函数,调用以后返回的结果都是局部引用
因此,env->NewLocalRef 基本不用
一个函数内的局部引用数量是有限制的,在早期的安卓系统中,体现的更为明显
当函数体内需要大量使用局部引用时,比如大循环中,最好及时删除不用的局部引用
可以使用 env->DeleteLocalRef 来删除局部引用

2. 局部引用相关的其他函数
env->EnsureLocalCapacity(num)	判断是否有足够的局部引用可以使用,足够则返回0
需大量使用局部引用时,手动删除太过麻烦,可使用以下两个函数来批量管理局部引用
env->PushLocalFrame(num)
env->PopLocalFrame(nullptr)

3. 全局引用
在jni开发中,需要跨函数使用变量时,直接定义全局变量是没用的
需要使用以下两个方法,来创建和删除全局引用
env->NewGlobalRef
env->DeleteGlobalRef

4. 弱全局引用
与全局引用基本相同,区别是弱全局引用有可能会被回收
env->NewWeakGlobalRef
env->DeleteWeakGlobalRef 

子线程中获取Java类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
1. 在子线程中,findClass可以直接获取系统类

2. 在主线程中获取类,使用全局引用来传递到子线程中

3. 在主线程中获取正确的ClassLoader,在子线程中去加载类
3.1 Java中,可以先获取类字节码,然后使用getClassLoader()来获取
Demo.class.getClassLoader()
new Demo().getClass().getClassLoader()
Class.forName(...).getClassLoader()
3.2 jni的主线程中获取ClassLoader
3.3 jni的子线程中loadClass

init与initarray

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
1. so在执行JNI_Onload之前,还会执行两个构造函数initinitarray

2. so加固so中字符串加密等等,一般会把相关代码放到这里

3. init的使用
extern "C" void _init(){	//函数名必须为_init
	......
}

4. initarray的使用
__attribute__ ((constructor)) void initArrayTest1(){ ... }
__attribute__ ((constructor(200))) void initArrayTest2(){ ... }
__attribute__ ((constructor(101))) void initArrayTest3(){ ... }
__attribute__ ((constructor, visibility("hidden"))) void initArrayTest4(){ ... }
constructor后面的值,较小的先执行,最好从100以后开始用
如果constructor后面没有跟值,那么按定义的顺序,从上往下执行 
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
人生若只如初见
使用 Hugo 构建
主题 StackJimmy 设计