在Android Studio上搭建一套2026年最新的APP渗透环境

参考

# 参考:
https://juejin.cn/post/7371479502457782306#heading-3
https://forgo7ten.github.io/AndroidReverse/2021/Android_traffic_capture_configuration_collection/#JustTrustMe
https://blog.csdn.net/m0_38036918/article/details/130346508
https://blog.csdn.net/weixin_45965246/article/details/136287665
https://cloud.tencent.com/developer/article/2123712
https://www.cnblogs.com/easyday/p/17772924.html
https://github.com/d0ctorsec/AndroidStudioAppPentestEnvironmentSetup

0x00.前言

之前测试用的Android实体机借给朋友了,所以就打算在Android Studio上搭建一套最新的APP渗透环境,文章从去年开始共计耗时一个月(中间因为Android某些版本兼容性推翻了几次),这篇文章发出来了就是全部成功了,需要说明的是模拟器优点很多,但是APP有模拟器检测的话就不好用了。

0x01.基础环境搭建

1.1.安装Android Studio

1. 我们需要用到Android Studio里的Virtual Device Manager功能去创建我们定义的手机模拟器,先去官网下载。

# 下载链接:https://developer.android.google.cn/studio?hl=zh-cn

2. 下载后安装并打开会提示没有设置代理,并且没有安装SDK,为了下载SDK比较快我们配置一下clashx的代理,其他代理软件配置其他端口
image-20250926114014710
image-20250926114108595
SDK Platform-Tools是Android生态中“连接开发环境与设备”的基础设施,其核心工具(adb、fastboot),ADB(AndroidDebugBridge)是连接测试设备与目标Android应用的核心工具,这是我们后期渗透要用到的主要工具。

3. 配置完代理可以看到会提示你没有SDK我们点击`NEXT`,一直NEXT下去,`默认路径不要改`我们后期教程都是默认路径,改了容易混淆环境。
image-20250926113351400
image-20250926113504417
4. 这里等待安装完SDK就行,可以看到配置代理后下载的飞快不用等咯。
image-20250926114154755
我们前往`/Users/macos/Library/Android/sdk`可以看到我们的SDK工具集已经全部准备就绪了,如果这里什么都没有就是上一步下载失败了,建议检查一下代理是否配置错误重新下载。
如果还是失败根据下面的`1.2.SDK Platform-Tools安装(上一步失败选用)`步骤手动配置。
image-20250926114353480
5. 因为我们本地要调用所以写一下环境变量,这里是bash的环境变量配置文件,zsh和windows的依据自身环境变量配置即可。
5.1. 先编辑bash的环境变量文件夹
vim ~/.bash_profile 

5.2. 输入以下环境变量,注意macos替换成自己的用户名
#配置android studio

`export ANDROID_HOME=/Users/macos/Library/Android/sdk`
`export PATH=$PATH:$ANDROID_HOME/platform-tools`
`export PATH=$PATH:$ANDROID_HOME/tools`
`export PATH=$PATH:$ANDROID_HOME/tools/bin`
`export PATH=$PATH:$ANDROID_HOME/emulator`

5.3. 加载并执行 ~/.bash_profile 文件中的配置,让文件中的修改立即生效。
`source ~/.bash_profile`
image-20250926114751325
6. 配置好环境变量我们测试可以在终端执行adb,有命令输出就是成功了,如果不行自己排查一下是不是环境变量问题(自己终端是zsh还是bash)。
image-20250926115132599

1.2.安装SDK Platform-Tools(上一步失败,成功不用进行这一步)

1. 首先去官网现下载符合操作系统的SDK Platform-Tools

# 下载链接:https://developer.android.com/tools/releases/platform-tools?hl=zh-cn
image-20250926094639721
2. 下载完成以后把platform-tools文件夹放到/Users/macos/Library/Android/sdk默认文件夹下`(macos是我电脑的用户名,根据实际情况来,windows自己找个记得住的文件放起来就行)
image-20250926115755290
3. 接着配置环境变量,这里环境变量先都写上,虽然手动安装我们可能只有platform-tools的环境变量

3.1.先编辑bash的环境变量文件夹
vim ~/.bash_profile 

3.2.输入以下环境变量,注意macos替换成自己的用户名
#配置android studio
export ANDROID_HOME=/Users/macos/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/tools/bin
export PATH=$PATH:$ANDROID_HOME/emulator

3.3.source ~/.bash_profile
4. 配置好环境变量我们测试可以在终端执行adb,有命令输出就是成功了,`如果不行自己排查一下是不是环境变量问题,自己终端是zsh还是bash。
image-20250926115132599

1.3.创建Android模拟器

1. 进入Android Studio,点击`More Actions-->Virtual Device Manager`
image-20250926120316907
2. 点击+号,创建一个模拟器这里选择Pixel 9 Pro点击Next,进入下一步点击API这里的Show All,选择API 36 (Android 16)的系统,这里先不要点击Finish了

* 设备:Pixel 9 pro
* System Image:API 36 (Android 16), Google APIs, arm64-v8a
⚠️ 推荐选Google APIs版本而非Google Play版本。
image-20260608134820209
image-20260608134754736
3. 再进入Additional settings,改一些默认配置
* Default boot:ColdBoot
* Internal storage:16 GB
* Expanded storage:4 GB
* RAM:4 GB
* VM heap size:512 MB 

⚠️ 建议将RAM调至4GB以上,Internal Storage适当增大
image-20260608135952545
4. 安装完成后启动我们的模拟器
image-20260608140503089
5. 因为我们下一步要刷Root所以为了避免失败重复创建模拟器,我们利用模拟器的镜像功能把初始环境的APP先保存一下,接下来我们就要开始刷入Magisk(面具)
image-20260608141142480

0x02.框架搭建

2.1.刷入Magisk(Root)

Magisk就是给模拟器拿Root权限,有Root才能装模块、改系统文件、控制流量。

方案A:rootAVD(推荐,一键完成)

1. 下载最新版rootAVD已从GitHub迁移至GitLab,github变为只读无法下载。
git clone https://gitlab.com/newbit/rootAVD.git

2. 从下载最新 Magisk-v28.1.apk,重命名为 Magisk.zip 放到rootAVD目录下覆盖原文件。
https://github.com/topjohnwu/Magisk/releases
image-20260608144411330
image-20260608144353361
3. 一键化运行以下命令
cd rootAVD
./rootAVD.sh ListAllAVDs
./rootAVD.sh system-images/android-36.1/google_apis/arm64-v8a/ramdisk.img
image-20260608143518494
4. 重启模拟器(我们之前设置过默认冷启动,直接启动即可,没设置需要手动点击),打开Magisk APP确认版本号及Root状态。
image-20260608144954412
image-20260608145049298
5. 对Magisk进行一些初始化设置
* 开启Zygisk(临时,下一步会替换为ZygiskNext)
* 隐藏Magisk应用 → 随机包名(防止包名被检测)
* 切换语言
image-20260608145707867
image-20260608145720431

方案B:手动Magisk Patch(rootAVD失败时的备选)

1. 先让模拟器以可写系统启动(获取临时adb root权限)
emulator -avd <AVD名称> -writable-system
adb root

2. 安装Magisk APK到模拟器
# 从 https://github.com/topjohnwu/Magisk/releases 下载最新APK
adb install Magisk-v27.*.apk

3. 定位模拟器ramdisk并推送到模拟器
RAMDISK="$ANDROID_HOME/system-images/android-34/google_apis/arm64-v8a/ramdisk.img"
adb push "$RAMDISK" /sdcard/Download/ramdisk.img

4. 打开Magisk APP → 安装 → 选择并修补文件 → 选/sdcard/Download/ramdisk.img

5. 拉回patch后的文件
adb pull /sdcard/Download/magisk_patched_*.img ./patched_ramdisk.img

6. 替换原始ramdisk(先备份)
cp "$RAMDISK" "${RAMDISK}.bak"
cp patched_ramdisk.img "$RAMDISK"

7. 冷启动模拟器(必须Cold Boot,不能用快照)
emulator -avd <AVD名称> -no-snapshot-load

2.2.安装ZygiskNext(注入引擎)

ZygiskNext干的事就是在每个APP启动的时候往里面注入代码。后面那些隐藏Root、绕SSL的模块都得靠它才能跑起来,比Magisk自带的Zygisk更难被检测到。
1. 替代Magisk内建Zygisk,检测面更小,下载ZygiskNext拖入模拟器中,进入Magisk→模块→从本地安装,安装后重启模拟器

# 下载链接:https://github.com/Dr-TSNG/ZygiskNext/releases(当前v1.3.4)
image-20260608155911442
2. 进入Magisk设置 → 关闭内建Zygisk,重启模拟器,进入模块看到zygisk Next✅启用了,ZN Magisk Compat❌弃用了,就是成功了
image-20260608160153388
image-20260608160454638

2.3.安装Vector(Xposed Hook框架)

Vector就是Xposed模块的运行环境,装了它之后TrustMe、HideMyApplist这些模块才有地方跑,它是LSPosed的继任项目。
1. 下载Vector,进入Magisk→模块→从本地安装,然后重启模拟器
# 下载链接:https://github.com/JingMatrix/Vector/releases
image-20260608163034062
2. 从Magisk模块进入模块,或者从通知栏点击进入管理器
image-20260608163142509

2.4.部署Frida Server(动态插桩)

● 简单理解:Frida是一款跨平台的动态插桩框架,核心能力是在应用程序运行时,不修改其安装包、不依赖反编译,通过注入脚本实现对程序内部逻辑的Hook操作,无需对APP进行脱壳、反编译等预处理,仅需目标设备具备Root&ADB权限。
Frida能在APP运行时实时改它的逻辑,让证书校验直接返回通过、让加密函数把密钥吐出来。PC端发命令,手机端server执行,版本号必须完全一样。
`这里试过strongR-frida兼容性不是很好,还是换成持续更新的官方版了
1. 创建虚拟环境,执行一次就行,避免污染系统Python库
python -m venv app-venv
source app-venv/bin/activate

2. 安装Frida工具链
pip install frida frida-tools objection

3. 确认PC端Frida版本,下载会用到
frida --version
image-20260608164744184
image-20260608164919388
4. 下载与PC端`完全相同版本号`的ARM64 server,版本必须精确匹配差一个小版本都会连接失败。

# 下载链接:https://github.com/frida/frida/releases

5. 在Magisk开启Shell的Root权限,不然下面命令权限不够
image-20260608175243966
6. 推送并改名(避免进程名被检测)
adb push frida-server-17.11.0-android-arm64 /data/local/tmp/sf
adb shell "su -c 'chmod 755 /data/local/tmp/sf'"
image-20260608175847591
7. PC端端口转发,启动(改端口避免27042被扫描检测),运行后挂着就行
adb forward tcp:6789 tcp:6789
adb shell "su -c '/data/local/tmp/sf -l 0.0.0.0:6789 &'"
image-20260608180101435
image-20260608180134858
8. 列出模拟器进程验证连接
frida-ps -H 127.0.0.1:6789
image-20260608180210345

0x03.插件部署

现在的APP都会做自我保护——查Root、查代理、查证书、查模拟器。这章就是一个个把这些检测干掉,让APP觉得自己跑在一个干干净净的真机上,如果APP实体机正常,模拟器安装后直接闪退或提示环境异常基本就是检测了。

3.1.反制Root/环境完整性检测

1.Shamiko — 隐藏Root文件系统痕迹

APP检测Root会去看su文件在不在、Magisk目录在不在。Shamiko的活就是让这些东西对指定APP不可见。
1. 进入Magisk→模块安装,然后重启即可

# 下载链接:https://github.com/LSPosed/LSPosed.github.io/releases
image-20260608181120347
2. 使用方法:开启Shamiko → Magisk设置 → 配置排除列表 → 勾选目标APP(一定要展开勾选不然会漏)
`不要勾选遵循排除列表(会跟ZygiskNext冲突)
image-20260608182232554

2.Zygisk排除列表(Shamiko失效时的备选)

1. 优先尝试Shamiko,如果绕不过Root检测,在Magisk模块中关闭Shamiko→ 进入Magisk设置 → 勾选"遵循排除列表"→配置排除列表 → 勾选目标APP
image-20260608182515572
image-20260608182535065
image-20260608182551752

3.PlayIntegrityFork 伪造设备完整性

Google有个设备完整性接口,银行和支付类APP会调它来判断手机有没有被动过,这个模块就是伪造返回结果,让Google那边认为设备没问题。
1. 进入Magisk模块安装,然后重启
  # 下载链接:https://github.com/osm0sis/PlayIntegrityFork/releases

2. 让模拟器走代理,或者直接开启代理软件tun模式(推荐)
adb shell settings put global http_proxy <本机IP>:<代理端口>

3. 运行以下命令一次即可,它的作用是生成一个伪造的设备指纹配置文件(pif.json),让Play Integrity校验时返回的设备信息看起来是正常的,如果Integrity校验突然失败了,重新跑一次刷新指纹。
adb shell su -c "sh /data/adb/modules/playintegrityfix/autopif4.sh"
image-20260608184549986
image-20260608190241522

4.HideMyApplist 隐藏已安装应用列表

有些APP会扫你手机装了啥,看到Magisk、Frida就判定环境有问题,这个模块让目标APP查不到这些敏感应用。
1. 直接拖入模拟器安装 → 在Vector中启用 → 勾选System Framework(系统框架) → 重启
image-20260609093038217
2. 创建模板
* 打开HideMyApplist APP → 模板管理 → 创建黑名单模板→模板名随意→应用不可见及要隐藏的应用
* 已应用于x应用意思是对哪个应用隐藏这些黑名单应用
image-20260609093805407
3. 回到主界面 → 点击拦截测试下载测试工具Applist Detector并安装 → 然后回到HideMyApplist点击应用管理 → 选择Applist Detector启用隐藏 → 选择刚建的模板→看一下扫描效果→像我截图展示的全是✅就是没问题了
`后续安装的应用也要补充到模板黑名单中
image-20260609094607055
image-20260609094431724
image-20260609094717293

3.2.文件管理(MT Manager)

有Root权限的文件管理器,能直接翻APP的内部目录,方便文件的迁移。
1. 去官网下载,安装后授予Root权限即可,两个文件窗口可以互相迁移文件

# 下载链接:https://mt2.cn
image-20260609095934763
image-20260609100013825

3.3.反制代理检测

很多APP会检测代理然后拒绝工作,iptables是在模拟器出口直接劫持流量,APP属于应用反制触达不了这一层。

# 流量链路:模拟器 → iptables → Charles(:9999) → Yakit/Burp(:8083)
1. 配置iptables透明代理
1.1. 全部HTTP/HTTPS流量转发到PC代理
`<PC_IP>是你电脑的局域网IP
adb shell su -c "iptables -t nat -A OUTPUT -p tcp --dport 80 -j DNAT --to-destination <PC_IP>:9999"
adb shell su -c "iptables -t nat -A OUTPUT -p tcp --dport 443 -j DNAT --to-destination <PC_IP>:9999"

1.2. 阻断QUIC(UDP 443),强制APP回退到TCP HTTPS(否则QUIC流量不经过代理)
adb shell su -c "iptables -A OUTPUT -p udp --dport 443 -j REJECT --reject-with icmp-port-unreachable"

# 使用完了,清除全部规则
adb shell su -c "iptables -t nat -F OUTPUT"
adb shell su -c "iptables -D OUTPUT -p udp --dport 443 -j REJECT --reject-with icmp-port-unreachable"
image-20260609101321681
2. 配置Charles
2.1.进入Proxy → Proxy Settings → Port: 9999,勾选"Enable transparent HTTP proxying"、"HTTP/2"
image-20260609101517518
2.2.进入Proxy → SSL Proxying Settings → Add `*:*`(代理所有SSL流量)→记得启用SSL代理
image-20260609101602428
2.3.进入Proxy → External Proxy Settings → HTTPS → `127.0.0.1:8083`(转发给Yakit/Burp)
image-20260609101725627
3. 配置Yakit/Burp即可,端口8083、HTTP/2.0支持,启动劫持就能看到流量穿透过来了
image-20260609102043371
image-20260609101954421

3.4.反制证书检测

1.单向认证(客户端校验 SSL Pinning)

HTTPS通信时APP会验证服务器证书是不是真的,这就是SSL Pinning。你用Charles/Burp抓包时证书是伪造的,APP验证不通过就拒绝通信。
# 单向认证失败的截图,提示建立SSL连接失败,应用程序不信任Charles根证书
image-20260605094114483

前置条件:安装系统证书 — MoveCertificate

Android 7.0+以上,`APP的默认行为是只信任系统证书,而第三方APP几乎不会主动配置信任用户证书`,这会增加被抓包的风险开发者通常会限制证书信任范围。

`模拟器的HTTPS流量首先到Charles,Charles解密后再转发给Yakit/Burp,模拟器做TLS握手的对象是Charles,APP看到的证书是Charles的证书。
1. 导出Charles证书为.pem格式,推送到模拟器
adb push charles-ssl-proxying-certificate.pem /sdcard/Download/
image-20260609103804655
image-20260609103901337
2. 模拟器:设置 → 安全和隐私 → 更多安全和隐私 → 加密与凭据 → 从SD卡安装证书 → CA证书 → 选中导入的证书,可以在可信凭据看到刚安装的证书
image-20260609104333187
3. 安装MoveCertificate,将刚才安装的证书自动迁移到系统目录,下载后拖入模拟器 → 在Magisk安装,重启后模块自动将用户证书移动到`/etc/security/cacerts/`。

# 下载链接:https://github.com/ys1231/MoveCertificate/releases
image-20260609104839067
4. 进入设置 → 安全和隐私 → 更多安全和隐私 → 加密与凭据 → 可信凭据,可以看到系统级凭据已经有Charles证书了

`如果证书没有自动移动,可以用MT管理器手动将`/data/misc/user/0/cacerts-added/`下的证书复制到`/etc/security/cacerts/`。
image-20260609105249782

方法一:Vector + TrustMe模块

# TrustMe覆盖37个以上的钩子目标,几乎涵盖Android上所有SSL pinning实现。

`缺点需要Root权限,就是适用于APP无Root检测不在Zygisk排除列表、或使用Shamiko绕过了Root检测的场景。
1.直接拖入模拟器自动安装 → 进入Vector中启用 → 勾选目标APP → 重启APP 
# 下载链接:https://github.com/kirklin/TrustMe/releases
image-20260609110450482

方法二:Frida + objection(TrustMe失效时使用)

Objection工具
● 简单理解:Objection的本质是将Frida的高频逆向需求封装成交互式命令,避免新手因不会编写Frida脚本而无法上手。它相当于给Frida套了一层“图形化逻辑的命令行界面”
# 适用于TrustMe无效、或使用Zygisk排除列表反Root导致Vector模块失效的场景。
1. 确保frida-server已启动

* 启动(改端口避免27042被扫描检测),运行后挂着就行
adb shell "su -c '/data/local/tmp/sf -l 0.0.0.0:6789 &'"

* PC端端口转发,注意跟上条命令分开tab运行
adb forward tcp:6789 tcp:6789
image-20260609113924505
2. 确认已经安装的应用包名
adb shell pm list packages -3 
image-20260609145047820
3. 进入之前创建的虚拟环境,使用包名来启动APP,命令执行后会进入一个objection的shell,执行命令即可绕过APP的SSL Pinning。
`注意:objection1.10.0有bug,objection 1.12+ 要求 Python 3.10+,如果报错请用高版本python创建虚拟环境

objection version 确认版本是12+
objection -N -h 127.0.0.1 -P 6789 -n <包名> start
android sslpinning disable
image-20260609120354237
image-20260609134436096
然后我们看Charles已经可以正常拦截HTTPS的流量了
image-20251010160055444

方法三:自定义Frida脚本(objection也失效时)

1. 通用Frida-ssl绕过脚本

// universal_ssl_bypass.js
Java.perform(function() {
    // OkHttp CertificatePinner
    try {
        var CertificatePinner = Java.use("okhttp3.CertificatePinner");
        CertificatePinner.check.overload('java.lang.String', 'java.util.List').implementation = function() {};
        console.log("[+] OkHttp CertificatePinner bypassed");
    } catch(e) {}

    // TrustManagerImpl (Conscrypt)
    try {
        var TrustManagerImpl = Java.use("com.android.org.conscrypt.TrustManagerImpl");
        TrustManagerImpl.verifyChain.implementation = function(untrustedChain) {
            return untrustedChain;
        };
        console.log("[+] TrustManagerImpl bypassed");
    } catch(e) {}

    // X509TrustManager
    try {
        var X509TrustManager = Java.use("javax.net.ssl.X509TrustManager");
        var TrustManager = Java.registerClass({
            name: "com.bypass.TrustManager",
            implements: [X509TrustManager],
            methods: {
                checkClientTrusted: function() {},
                checkServerTrusted: function() {},
                getAcceptedIssuers: function() { return []; }
            }
        });
        console.log("[+] X509TrustManager bypassed");
    } catch(e) {}
});
2. 运行HOOK脚本
`这里演示的是成功的APP,但是有些APP加固了没有通用绕过脚本,每个APP的加固技术都有区别,需要自己去网上收集,可以在MT管理器看到加固厂家
frida -H 127.0.0.1:6789 -f <包名> -l SSL_Pinning.js
image-20260609144749078
image-20260609145013693

2.双向认证(mTLS)

■ 双向认证是服务端与客户端互相发送证书,客户端校验服务端证书+服务端校验客户端证书,双方均确认对方身份。

■ HTTPS双向证书校验在实际中几乎很少用到,因为服务器端需要维护所有客户端的证书,这无疑增加了很多消耗,因此大部分厂商选择使用单向证书绑定。对抗双向认证需要完成两个环节:
    ● 让客户端认为burp 是服务端,这一步其实就是破解SSL pinning,这一步客户端校验已经完成。
    ● 让服务端认为burp是客户端,这一步需要导入客户端的证书到burp,客户端的证书一定会存在本地代码中,而且还可能会有密码,这种情况下需要逆向客户端 app,找到证书和密码,并转为pkcs12格式导入到 burp。 User options -> SSL -> Client SSL Certificate。

■ 客户端校验证书时Charles抓包报错的特征,我们抓包时可以通过此特征确认
    400 No required SSL certificate was sent

方法一:Frida hook KeyStore提取

很难受的是我找了十几款金融APP都没有采用双向证书校验,我自己项目上测了几十款金融APP了也没遇到过,可能是双向证书校验确实不太实用APP会变的很卡,不利于推广APP。不过还是演示一下绕过双向证书校验的流程。
1. mtls.js - 通用mTLS客户端证书提取
Java.perform(function() {
    var KeyStore = Java.use("java.security.KeyStore");

    // Hook InputStream版本的load(最常见的证书加载方式)
    KeyStore.load.overload('java.io.InputStream', '[C').implementation = function(stream, password) {
        console.log("[*] KeyStore.load called");
        console.log("[*] Password: " + (password ? Java.use("java.lang.String").$new(password) : "null"));

        if (stream != null) {
            var ByteArrayOutputStream = Java.use("java.io.ByteArrayOutputStream");
            var baos = ByteArrayOutputStream.$new();
            var buf = Java.array('byte', new Array(1024).fill(0));
            var len;
            while ((len = stream.read(buf)) !== -1) {
                baos.write(buf, 0, len);
            }
            var data = baos.toByteArray();

            var FileOutputStream = Java.use("java.io.FileOutputStream");
            var fos = FileOutputStream.$new("/sdcard/Download/client_cert.p12");
            fos.write(data);
            fos.close();
            console.log("[*] Saved " + data.length + " bytes to /sdcard/Download/client_cert.p12");

            // 重建InputStream供原函数使用
            var ByteArrayInputStream = Java.use("java.io.ByteArrayInputStream");
            var newStream = ByteArrayInputStream.$new(data);
            return this.load(newStream, password);
        }
        return this.load(stream, password);
    };
});
2. 命令
frida -H 127.0.0.1:6789 -f <包名> -l mtls.js
image-20260609145736354

方法二:r0capture通杀提取

我们需要下载“r0capture”,这款工具无视所有证书校验或绑定,缺点就是没法做中间人,但是我们可以用到它的提取证书功能,首先先走安装流程这里要配合frida去跑。

1. 确保frida-server已启动
image-20260609150145865
2. 下载并进入虚拟环境,安装依赖后查看正在运行应用的包名,运行脚本调用应用

source app-venv/bin/activate
pip install loguru
pip install click
frida-ps -U -a
python r0capture.py -U -f -v <包名>
image-20260609152301670
image-20260609152315533
3. 下面引用作者的原话,如果有双向校验在输出日志中搜索一下`dump`如果有后面会跟上路径
* 运行脚本之前必须手动给App加上存储卡读写权限;
* 并不是所有App都部署了服务器验证客户端的机制,只有配置了的才会在Apk中包含客户端证书
* 导出后的证书位于`/sdcard/Download/包名xxx.p12路径`,导出多次,每一份均可用,`密码默认为:r0ysue`,推荐使用keystore-explorer打开查看证书。
image-20260604190823610
4. 这里不一样了,谁跟目标服务器握手,谁就要带客户端证书,所以让我们链路中的出口节点(Burp/Yakit)装上导出的p12证书,这样服务端会认为yakit就是客户端。
image-20260605082556771

3.5.反制特殊协议

有些APP不走标准HTTPS,自己带了加密库(Flutter用BoringSSL、WebView有自己的验证)。前面的方案对它们没用,得针对性处理。

1.WebView独立证书验证

APP里经常嵌WebView加载H5页面。WebView有自己的SSL错误处理回调——`onReceivedSslError`。正常情况下证书不对会弹错误页,APP开发者如果重写了这个回调,比如只处理了特定域名,你的代理证书就过不去。

`判断方法:APP原生页面能正常抓包,但点进某个H5页面就白屏或加载失败。
1. 这个hook的是Android标准类
但很多APP不直接用 `WebViewClient`,而是写了个子类比如 `com.xxx.MyWebViewClient`,你用jadx打开APK搜 `onReceivedSslError`,看看实际是哪个类实现的,把下面面代码里的类名换掉。
比如你在 jadx 里搜到:

  public class com.niuguwang.utils.MySSLWebViewClient extends WebViewClient {
      @Override
      public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
          ...
      }
  }
// webview_ssl.js 那脚本就改成

  Java.perform(function() {
      var WebViewClient = Java.use("com.niuguwang.utils.MySSLWebViewClient");
      //只改这里,换成jadx里找到的完整类名
      WebViewClient.onReceivedSslError.implementation = function(view, handler, error) {
          console.log("[+] WebView SSL error bypassed for: " + view.getUrl());
          handler.proceed();
      };
  });

2.QUIC/HTTP3协议

QUIC是Google搞的基于UDP的传输协议,跑在UDP 443端口上。Charles和Burp都是TCP代理,UDP流量根本不经过它们。所以你在代理工具里什么都看不到,但APP用着完全正常。

`判断方法:
    • Charles/Burp里完全没有目标域名的请求
    • APP功能却正常工作
    • 用Wireshark抓模拟器网卡,能看到大量UDP 443流量
1. 方法很简单跟上面抓包一样,把UDP协议通信干掉,APP发现QUIC不通会自动fallback到TCP HTTPS

adb shell su -c "iptables -A OUTPUT -p udp --dport 443 -j REJECT --reject-with icmp-port-unreachable"
# 使用完清除规则
adb shell su -c "iptables -D OUTPUT -p udp --dport 443 -j REJECT --reject-with icmp-port-unreachable"

3.6.模拟器检测绕过

因为本文章使用的是模拟器,虽然对于真机方便但是多了一道模拟器检测的坎。APP的检测SDK“梆梆、同盾、数美这些”会查系统属性、硬件特征、运行时行为,检测逻辑通常是打分制——命中越多越可疑,所以不用做到完美,把得分压下去就行。

1.系统属性伪装(Pixel Props 模块)

Android系统有几百个 `ro.xxx` 属性,模拟器的值和真机差别很大。APP调用 `SystemProperties.get()` 或者直接读 `/system/build.prop` 就能拿到。绝大多数检测SDK第一步就查这个。
优点:一键刷入,覆盖所有分区属性,还会跟随月度安全补丁更新
1. 去 Releases 页面下载目标机型的 zip(文件名格式:`<代号>_<Build ID>.zip`),我下载的是Pixel 9 Pro → `Caiman_CP1A.260505.005.zip`
image-20260609181231435
2. 进入Magisk → 模块 → 从本地安装 → 选择 zip,重启后模块会自动覆盖属性
`注意这里比较特殊,安装时要不停的按音量键同意
image-20260609182730363
image-20260609183356114
3. 安装后还需要补的属性,可以在Magisk post-fs-data 脚本里写死
# 路径:/data/adb/post-fs-data.d/props.sh

#!/system/bin/sh
resetprop ro.hardware qcom
resetprop ro.boot.hardware qcom
resetprop ro.hardware.chipname Tensor

3.1.然后加上执行权限
adb shell su -c 'chmod 755 /data/adb/post-fs-data.d/props.sh'
image-20260609184859101
4. 验证成功了吗,可以看到指纹已经变了

adb shell getprop ro.product.model
adb shell getprop ro.build.fingerprint
adb shell getprop ro.build.version.security_patch
image-20260609185025792

2.硬件特征伪装

模拟器里有 `/dev/qemu_pipe`、`/dev/goldfish_pipe`、`/sys/qemu_trace` 这些设备节点,真机上不存在。APP调用 `File.exists()` 就能查到。

Shamiko + HideMyApplist正常配置的话已经能把这些路径对目标APP隐藏。如果还是被检测到(有些检测走native层直接syscall),用Frida兜底:
// hide_emulator.js

Java.perform(function() {
    var File = Java.use("java.io.File");
    // 这些关键词出现在路径里就返回"不存在"
    var blacklist = [
        "qemu_pipe", "goldfish_pipe", "qemu_trace",
        "nox", "vbox", "genymotion", "andy", "bluestacks"
    ];

    File.exists.implementation = function() {
        var path = this.getAbsolutePath();
        for (var i = 0; i < blacklist.length; i++) {
            if (path.indexOf(blacklist[i]) !== -1) {
                return false;
            }
        }
        return this.exists();
    };
});
2. 目前frida脚本已经很多了,所以启动时同时加载多个,-l 可以重复使用
frida -H 127.0.0.1:6789 -f <包名> -l hide_emulator.js -l xxx.js

3.行为特征伪装

到这一步基本只有重度检测SDK才会查了。原理是:模拟器的传感器、电池、电话功能都是假的或者残缺的,和真机用户的手机表现不一样。
1. 还是用frida hook
// fake_sensor_battery.js

Java.perform(function() {
    // 让电池看起来像正常使用中
    var BatteryManager = Java.use("android.os.BatteryManager");
    BatteryManager.getIntProperty.implementation = function(id) {
        if (id === 4) return 2;   // STATUS → 放电中(不是充电)
        if (id === 1) return 73;  // 电量73%
        return this.getIntProperty(id);
    };

    // 让电话信息看起来像正常手机
    var TelephonyManager = Java.use("android.telephony.TelephonyManager");
    TelephonyManager.getSimOperatorName.implementation = function() {
        return "CMCC";  // 中国移动
    };
    TelephonyManager.getNetworkOperatorName.implementation = function() {
        return "CMCC";
    };
    TelephonyManager.getSimState.implementation = function() {
        return 5; // SIM_STATE_READY,表示有SIM卡且正常
    };
});
2. 目前frida脚本已经很多了,所以启动时同时加载多个,-l 可以重复使用
frida -H 127.0.0.1:6789 -f <包名> -l fake_sensor_battery.js -l xxx.js

0x04.逆向分析

`逆向章节对于有壳无壳APP是有不同链路的,所以把章节分为了审计、修改、打包便于区分
`先了解一下APK原理:APK 本质就是一个 zip 包,改后缀名为 `.zip` 就能直接解压。里面的核心文件:

target.apk (实际是个zip)
├── AndroidManifest.xml    ← APP的"身份证":包名、权限、四大组件声明(二进制格式,直接看是乱码)
├── classes.dex            ← Java/Kotlin 代码编译后的字节码(核心逻辑都在这里)
├── classes2.dex           ← 方法数超过65535时会分成多个dex
├── resources.arsc         ← 编译后的资源索引表
├── res/                   ← 布局XML、图片、字符串等资源
├── assets/                ← 原始资源文件(不会被编译,原样打包)
├── lib/                   ← Native库(.so文件,C/C++编译的)
│   ├── arm64-v8a/         ← 64位ARM架构
│   └── armeabi-v7a/       ← 32位ARM架构
└── META-INF/              ← 签名信息

`核心概念:
* DEX(Dalvik Executable):Android的字节码格式。`Java/Kotlin源码 → 编译 → .class → 打包 → .dex。逆向就是把 dex 还原回接近源码的样子。
* Smali:dex字节码的人类可读文本形式。类比关系:Java源码 → 高级语言,Smali → 汇编语言。jadx 还原成 Java 方便你看,apktool 还原成 Smali 方便你改。
* Native层(.so):C/C++编译的动态库,比 Java 层更底层。一些加固和检测逻辑放在 so 里,用 IDA/Ghidra 分析。

4.1.查壳脱壳

1.查壳-APKiD

查壳就是先看APP有没有被加固过,用了什么壳。知道壳的类型才能选对应的脱壳方式。常见的有梆梆、360、腾讯乐固。
1. 首次使用需安装(进入虚拟环境)
source app-venv/bin/activate
pip install apkid

2. 查壳
apkid target.apk 
情况一:可以看到没有加壳,只是加固了看到是阿里聚安全加固的
image-20260610093335567
情况二:加壳了,并且加固了
image-20260610134314213
字段含义关键信息
packer加壳了必须先脱壳才能看到代码
protector有保护SDK可能有检测+加固双重功能
anti_hook: syscallsnative层反hookFrida Java层hook可能被绕过
anti_debug: ptrace反调试需要绕过ptrace检测才能attach
compiler: dexlibdex被重编译正常,加固的副产品
compiler: r8/d8正常编译器说明这部分没加壳
manipulator: Resources Confusion资源混淆资源名被打乱,不影响代码分析

2.脱壳-frida-dexdump

加固壳会在APP启动时把真正的代码解密到内存里。frida-dexdump就是趁这时候从内存中把解密后的dex抓出来,然后才能拿去反编译。
APP启动 → 壳解密真实dex → 加载到内存 → [这个时机dump] → 正常运行
1. 首次使用需安装
source app-venv/bin/activate
pip install frida-dexdump

2. 记得先启动frida server端见“2.4”,spawn模式启动(绕过早期检测),输出在当前目录,多个dex文件
frida-dexdump -H 127.0.0.1:6789 -f <包名>
image-20260610135657296

4.2.审计修改

1.只读审计-jadx

* jadx = 看代码。把 dex 还原成接近 Java 源码的样子,方便你读逻辑、找关键函数。
`只读代码,不能改。
1. 下载jadx解压后运行bin下的jadx-gui,我是mac电脑直接brew安装了

# MAC:brew install jadx
# win下载链接:https://github.com/skylot/jadx/releases
image-20260610112331758
# 无壳:直接打开APK就能看
# 有壳:需要先脱壳,打开脱壳dex看

2. APP无壳的情况,运行jadx-gui图形化程序直接打开apk即可审计
jadx-gui

* 左侧目录树浏览所有类
* `Ctrl+Shift+F` 全局搜索字符串(搜 "root"、"emulator"、"ssl" 等关键词定位检测逻辑)
* 点击类名/方法名跳转引用
* 右键 → Find Usage 查看谁调用了这个方法
image-20260610112646755
APP有壳的情况,需要先用上面的frida-dexdump脱壳,然后jadx直接读取输出的文件夹即可
image-20260610140018396
image-20260610140600186

2.解包修改-apktool

* apktool = 改代码。把 APK 解成 Smali(字节码的文本形式)+ 资源文件,改完能再打包回去。`不好读代码,能改。
1. 先安装apktool,这是mac的
brew install apktool

# windows手动下载 jar:https://github.com/iBotPeaches/Apktool/releases
image-20260610144523771
2. 不管有没有壳,apktool 都是解包原始 APK,命令一样。但是有一定区别
# ✅无壳:解包 → 改smali → 重打包
# ⚠️有壳:解包 → smali里是壳,改不了业务逻辑,只能改Manifest、资源、网络配置
mac:apktool d target.apk -o apk_output/
win:java -jar apktool.jar d target.apk -o apk_output/
image-20260610155403808
# 解包后的目录结构对比
apk_output/
├── AndroidManifest.xml    明文XML,可以改权限、组件
├── apktool.yml            apktool元数据,不要动
├── res/                   资源(布局XML、字符串、图片)
│   ├── layout/            界面布局
│   ├── values/strings.xml 字符串资源
│   └── drawable/          图片
├── assets/                原始资源文件
├── lib/                   so文件
│   └── arm64-v8a/
└── smali/                 Smali代码(可能有多个目录)
    ├── smali/             对应classes.dex
    ├── smali_classes2/    对应classes2.dex
    └── smali_classes3/    对应classes3.dex

4.3.绕过(Frida Hook)

# 现在APP基本都有壳、加固措施,Smali重打包会遇到以下阻力,所以本文废弃这个方法,以Frida Hook为主要方法

❌Smali重打包:
⚠️1:apktool解包 → smali里是壳代码,找不到业务逻辑
⚠️2:第三方SDK代码可能在其他dex里,smali目录下找不到
⚠️3:核心检测逻辑在native层(JNI),Java层只是调用入口
⚠️4:重打包后签名变化 → 触发签名校验 → 闪退/锁账号

✅Frida:
运行时壳已解密 → 类正常加载在内存里 → 直接hook Java方法返回false不管native层做了什么检测,只要Java入口拿到false,APP就认为没问题
1.第一步是使用jadx审计代码,但是前面说过有壳审计到的是壳代码,我们先查壳如果有壳需要脱壳才能审到源码,可以看到应用是加壳了的
apkid <APK>
image-20260610134314213
2. 给APP脱个壳,记得先启动frida server端见“2.4”,spawn模式启动(绕过早期检测),输出在当前目录,多个dex文件

frida-ps -U -a 看一下包名
frida-dexdump -H 127.0.0.1:6789 -f <包名>
image-20260611165343900

1.Hook改定位

我们改一下APP默认定位,这里默认定位是上海。以前能不能抠出链路运气和不停尝试缺一不可,现在模型十几分钟就出来,接着就可以用jadx读取输出的dex文件了,这部分以前要跟踪方法写hook代码去篡改getter的值,hook错了还要回去重跟链路。
image-20260611185541153
# 定位数据流
百度定位SDK (native层)
    ↓ 定位结果
UPLocationManagerExNew (定位管理器)
    ↓ 解析/转换
UPLocateCityRespParamNew (城市结果模型) ← Hook点
    ↓
APP UI / 优惠券筛选 / 商户推荐等业务逻辑

# 关键类分析

通过jadx反编译定位到核心数据模型
com.unionpay.network.model.resp.UPLocateCityRespParamNew:

public class UPLocateCityRespParamNew {
    @SerializedName("divisionCd")    private String mCityCode;     // 
城市代码 "330100"
    @SerializedName("divisionCNNm")  private String mCityName;     // 
城市名 "杭州"
    @SerializedName("divisionENNm")  private String mCityENName;   // 
英文名
    @SerializedName("adcode")        private String mAdcode;       // 
区县代码
    @SerializedName("isOut")         private String mIsOut;        // 
境外标识
    @SerializedName("divisionAliasNm") private String mCityAliasName; // 
别名
}

# 同时发现 UPLocationManagerEx.java 中的fallback逻辑:

return (uPLocateCityRespParam == null ||
TextUtils.isEmpty(uPLocateCityRespParam.getCityCode()))
    ? "310000"   // 默认上海
    : uPLocateCityRespParam.getCityCode();

说明APP定位失败时会fallback到上海,这就是为什么hook
SDK层不生效的原因——APP认为定位无效,直接用了硬编码默认值。但只要hook了getCityCode() 的返回值,无论走哪条分支都会拿到我们的假数据。

# Hook实现

核心只需hook业务层几个模型的getter:

Java.perform(function() {
    var fakeCityCode = "330100";
    var fakeCity = "Hangzhou";
    var fakeAdCode = "330106";

    // 主模型 - APP所有城市相关逻辑的数据源
    var CityResp =
Java.use("com.unionpay.network.model.resp.UPLocateCityRespParamNew");
    CityResp.getCityCode.implementation = function() { return
fakeCityCode; };
    CityResp.getCityName.implementation = function() { return fakeCity; };
    CityResp.getAdcode.implementation = function() { return fakeAdCode; };
    CityResp.getIsOut.implementation = function() { return "0"; };

    // 旧版兼容模型
    var CityRespOld =
Java.use("com.unionpay.network.model.resp.UPLocateCityRespParam");
    CityRespOld.getCityCode.implementation = function() { return
fakeCityCode; };
    CityRespOld.getCityName.implementation = function() { return fakeCity;
};

    // 风控SDK城市实体
    var CityInfo = Java.use("com.unionpay.ttsdk.entity.CityInfo");
    CityInfo.getCityCode.implementation = function() { return
fakeCityCode; };
    CityInfo.getCityName.implementation = function() { return fakeCity; };
});
我们Hook调试一下,可以看到定位被强制hook到杭州了,这个自行扩展改积分、改抽奖结果、改VIP都行,有些是临时生效但是也够你访问平常访问不到的内容了,这个没危害代码放下面了仅做研究。

frida -H 127.0.0.1:6789 -f com.unionpay -l fake_location_v3.js
image-20260611185856971
image-20260611185926229
# fake_location_v3.js

Java.perform(function() {
    var fakeLat = 30.2741;
    var fakeLng = 120.1551;
    var fakeCity = "Hangzhou";
    var fakeCityCode = "330100";
    var fakeAdCode = "330106";
    var fakeCityEN = "hangzhou";
    var fakeProvince = "Zhejiang";
    var fakeDistrict = "Xihu";

    // ============================================================
    // CORE: Hook UPLocateCityRespParamNew getters
    // This is what the APP actually reads for city display/logic
    // ============================================================
    try {
        var CityResp = Java.use("com.unionpay.network.model.resp.UPLocateCityRespParamNew");

        CityResp.getCityCode.implementation = function() {
            console.log("[CITY] getCityCode -> " + fakeCityCode);
            return fakeCityCode;
        };
        CityResp.getCityName.implementation = function() {
            console.log("[CITY] getCityName -> " + fakeCity);
            return fakeCity;
        };
        CityResp.getCityENName.implementation = function() {
            return fakeCityEN;
        };
        CityResp.getCityAliasName.implementation = function() {
            return fakeCity;
        };
        CityResp.getAdcode.implementation = function() {
            return fakeAdCode;
        };
        CityResp.getIsOut.implementation = function() {
            return "0";
        };

        console.log("[+] UPLocateCityRespParamNew hooked");
    } catch(e) { console.log("[-] UPLocateCityRespParamNew: " + e); }

    // ============================================================
    // Hook UPLocateCityRespParam (old version, still used in some paths)
    // ============================================================
    try {
        var CityRespOld = Java.use("com.unionpay.network.model.resp.UPLocateCityRespParam");

        CityRespOld.getCityCode.implementation = function() {
            return fakeCityCode;
        };
        CityRespOld.getCityName.implementation = function() {
            return fakeCity;
        };
        CityRespOld.getCityENName.implementation = function() {
            return fakeCityEN;
        };

        console.log("[+] UPLocateCityRespParam hooked");
    } catch(e) { console.log("[-] UPLocateCityRespParam: " + e); }

    // ============================================================
    // Hook UPCityInfo model
    // ============================================================
    try {
        var UPCityInfo = Java.use("com.unionpay.network.model.UPCityInfo");

        UPCityInfo.getCityName.implementation = function() {
            return fakeCity;
        };
        UPCityInfo.getCityCode.implementation = function() {
            return fakeCityCode;
        };
        try {
            UPCityInfo.getCityEnName.implementation = function() {
                return fakeCityEN;
            };
        } catch(e2) {}

        console.log("[+] UPCityInfo hooked");
    } catch(e) { console.log("[-] UPCityInfo: " + e); }

    // ============================================================
    // Hook CityInfo (ttsdk entity)
    // ============================================================
    try {
        var CityInfo = Java.use("com.unionpay.ttsdk.entity.CityInfo");

        CityInfo.getCityCode.implementation = function() {
            return fakeCityCode;
        };
        CityInfo.getCityName.implementation = function() {
            return fakeCity;
        };
        CityInfo.getProviceName.implementation = function() {
            return fakeProvince;
        };
        CityInfo.getProviceCode.implementation = function() {
            return "330000";
        };
        CityInfo.getCountryName.implementation = function() {
            return "China";
        };

        console.log("[+] CityInfo (ttsdk) hooked");
    } catch(e) { console.log("[-] CityInfo ttsdk: " + e); }

    // ============================================================
    // Hook UPGeoInfo - careful, only hook safe getters
    // ============================================================
    try {
        var UPGeoInfo = Java.use("com.unionpay.location.model.UPGeoInfo");
        UPGeoInfo.getCity.implementation = function() { return fakeCity; };
        UPGeoInfo.getIsMock.implementation = function() { return "0"; };
        UPGeoInfo.getIsMockReal.implementation = function() { return "0"; };
        try { UPGeoInfo.getAdCode.implementation = function() { return fakeAdCode; }; } catch(e2) {}
        try { UPGeoInfo.getCityCode.implementation = function() { return fakeCityCode; }; } catch(e2) {}
        try { UPGeoInfo.getProvince.implementation = function() { return fakeProvince; }; } catch(e2) {}
        try { UPGeoInfo.getDistrict.implementation = function() { return fakeDistrict; }; } catch(e2) {}
        console.log("[+] UPGeoInfo hooked");
    } catch(e) { console.log("[-] UPGeoInfo: " + e); }

    // ============================================================
    // BDLocation getters (SDK layer backup)
    // ============================================================
    try {
        var BDLoc = Java.use("com.baidu.location.BDLocation");
        BDLoc.getLatitude.implementation = function() { return fakeLat; };
        BDLoc.getLongitude.implementation = function() { return fakeLng; };
        BDLoc.getCity.implementation = function() { return fakeCity; };
        BDLoc.getDistrict.implementation = function() { return fakeDistrict; };
        BDLoc.getProvince.implementation = function() { return fakeProvince; };
        BDLoc.getAddrStr.implementation = function() { return fakeDistrict + ", " + fakeCity; };
        BDLoc.getCountry.implementation = function() { return "China"; };
        BDLoc.getLocType.implementation = function() { return 161; };
        BDLoc.getAdCode.implementation = function() { return fakeAdCode; };
        BDLoc.getCityCode.implementation = function() { return fakeCityCode; };
        BDLoc.getLocationWhere.implementation = function() { return 0; };
        try { BDLoc.isMockGpsLocation.implementation = function() { return false; }; } catch(e2) {}
        try { BDLoc.getMockGpsLocation.implementation = function() { return false; }; } catch(e2) {}
        console.log("[+] BDLocation getters hooked");
    } catch(e) { console.log("[-] BDLocation: " + e); }

    // ============================================================
    // Android system layer
    // ============================================================
    try {
        var Location = Java.use("android.location.Location");
        Location.getLatitude.implementation = function() { return fakeLat; };
        Location.getLongitude.implementation = function() { return fakeLng; };
        Location.isFromMockProvider.implementation = function() { return false; };
        console.log("[+] android.location.Location hooked");
    } catch(e) {}
    try {
        var Location2 = Java.use("android.location.Location");
        Location2.isMock.implementation = function() { return false; };
    } catch(e) {}

    console.log("==============================================");
    console.log("[*] fake_location_v3.js - City Level Spoof");
    console.log("[*] Target: " + fakeCity + " (" + fakeCityCode + ")");
    console.log("[*] Strategy: hook APP business models directly");
    console.log("==============================================");
});

2.Hook改登录跳转

第二个场景:再Hook个登录页面,我们未登录是不能未授权使用这些模块的,我们尝试把跳转登录页面弹窗直接hook掉,直接强制进入页面。
image-20260611190637151
1. 打开jadx加载脱壳完的dex,JADX全局搜索needLogin、Login等关键词,命中一下需要登录相关的方法
image-20260612110723088
2. 定位到这个方法有boolean判断,基本可以确认到路由跳转时用 Postcard.withBoolean("needLoginCheck", true/false)的布尔值,标记是否需要登录检查。
image-20260612110941510
3. 有了这些信息,我们让AI阅读这些关键类代码,当然你也可以直接开始我这样定位到目的性更明确一些需要调试的会少一些,让开始调试就行了,最后输出了js文件,我们Hook一下。

frida -H 127.0.0.1:6789 -f com.unionpay -l bypass_login_intercept.js
image-20260611190808835
4. 进入任意一个页面点击重新登录,可以Hook掉弹出的登录页面,后续弹出登录页面都能hook掉,然后就能以未授权用户进入各种页面了
image-20260612104228060
image-20260612104458872
image-20260612104647113
# bypass_login_intercept.js

'use strict';

Java.perform(function () {
    console.log("[*] === 云闪付登录绕过 v3(稳定版) ===");

    // ========== 第一部分:路由拦截器 hook(立即生效,安全) ==========

    try {
        var UPLoginInterceptor = Java.use("com.unionpay.interceptors.UPLoginInterceptor");
        UPLoginInterceptor.process.implementation = function (postcard, callback) {
            var path = postcard.getPath();
            // 拦截登录页面本身的路由,不让它跳转
            if (path.indexOf("/up_login/login") !== -1 || path.indexOf("/login") !== -1) {
                console.log("[+] UPLoginInterceptor 拦截登录页路由: " + path);
                callback.onInterrupt(null);
                return;
            }
            console.log("[+] UPLoginInterceptor 放行: " + path);
            callback.onContinue(postcard);
        };
        console.log("[+] UPLoginInterceptor hooked");
    } catch (e) { console.log("[-] " + e); }

    try {
        var UPAuthInterceptor = Java.use("com.unionpay.interceptors.UPAuthInterceptor");
        UPAuthInterceptor.process.implementation = function (postcard, callback) {
            var path = postcard.getPath();
            if (path.indexOf("/up_login/login") !== -1 || path.indexOf("/login") !== -1) {
                console.log("[+] UPAuthInterceptor 拦截登录页路由: " + path);
                callback.onInterrupt(null);
                return;
            }
            console.log("[+] UPAuthInterceptor 放行: " + path);
            callback.onContinue(postcard);
        };
        console.log("[+] UPAuthInterceptor hooked");
    } catch (e) { console.log("[-] " + e); }

    try {
        var UPBindCardInterceptor = Java.use("com.unionpay.interceptors.UPBindCardInterceptor");
        UPBindCardInterceptor.process.implementation = function (postcard, callback) {
            callback.onContinue(postcard);
        };
        console.log("[+] UPBindCardInterceptor hooked");
    } catch (e) { console.log("[-] " + e); }

    try {
        var UPAppInfo = Java.use("com.unionpay.network.model.UPAppInfo");
        UPAppInfo.needLogin.implementation = function () { return false; };
        UPAppInfo.needAuth.implementation = function () { return false; };
        console.log("[+] UPAppInfo.needLogin/needAuth -> false");
    } catch (e) { console.log("[-] " + e); }

    try {
        var Postcard = Java.use("com.alibaba.android.arouter.facade.Postcard");
        Postcard.withBoolean.implementation = function (key, value) {
            if ((key === "needLoginCheck" || key === "needAuthcheck") && value === true) {
                return this.withBoolean(key, false);
            }
            return this.withBoolean(key, value);
        };
        console.log("[+] Postcard.withBoolean hooked");
    } catch (e) { console.log("[-] " + e); }

    try {
        var Activity = Java.use("android.app.Activity");
        Activity.startActivity.overload("android.content.Intent").implementation = function (intent) {
            var comp = intent.getComponent();
            if (comp != null && comp.getClassName().indexOf("Login") !== -1) {
                console.log("[+] 拦截登录页跳转: " + comp.getClassName());
                return;
            }
            this.startActivity(intent);
        };
        console.log("[+] startActivity hooked");
    } catch (e) { console.log("[-] " + e); }

    console.log("[*] 路由拦截 Hook 完成");

    // ========== 第二部分:WebView H5 弹窗拦截(延迟 10 秒) ==========

    setTimeout(function () {
        Java.perform(function () {
            console.log("[*] 激活 WebView 弹窗拦截...");

            // Hook WebChromeClient.onJsAlert
            try {
                var WebChromeClient = Java.use("android.webkit.WebChromeClient");
                WebChromeClient.onJsAlert.implementation = function (view, url, message, result) {
                    if (message && (message.indexOf("4007") !== -1 || message.indexOf("未检测到用户") !== -1 || message.indexOf("请登录后再试") !== -1)) {
                        console.log("[+] 拦截 H5 alert: " + message);
                        result.confirm();
                        return true;
                    }
                    return this.onJsAlert(view, url, message, result);
                };
                console.log("[+] WebChromeClient.onJsAlert hooked");
            } catch (e) { console.log("[-] " + e); }

            // Hook WebViewClient.onPageFinished 注入 JS
            try {
                var WebViewClient = Java.use("android.webkit.WebViewClient");
                WebViewClient.onPageFinished.implementation = function (view, url) {
                    this.onPageFinished(view, url);
                    var js = "javascript:void(function(){" +
                        "var _a=window.alert;window.alert=function(m){if(m&&(m.indexOf('4007')>-1||m.indexOf('未检测到用户')>-1)){return;}_a.call(window,m);};" +
                        "new MutationObserver(function(ms){ms.forEach(function(m){m.addedNodes.forEach(function(n){if(n.nodeType===1&&n.innerText&&(n.innerText.indexOf('4007')>-1||n.innerText.indexOf('未检测到用户')>-1)){n.style.display='none';try{n.remove();}catch(e){}}});});}).observe(document.body||document.documentElement,{childList:true,subtree:true});" +
                        "}())";
                    view.loadUrl(js);
                    console.log("[+] JS 注入完成: " + url.substring(0, 60));
                };
                console.log("[+] WebViewClient.onPageFinished hooked");
            } catch (e) { console.log("[-] " + e); }

            // 尝试找到已加载的 WebView 实例直接注入
            try {
                Java.choose("android.webkit.WebView", {
                    onMatch: function (instance) {
                        var js = "javascript:void(function(){" +
                            "var _a=window.alert;window.alert=function(m){if(m&&(m.indexOf('4007')>-1||m.indexOf('未检测到用户')>-1)){return;}_a.call(window,m);};" +
                            "new MutationObserver(function(ms){ms.forEach(function(m){m.addedNodes.forEach(function(n){if(n.nodeType===1&&n.innerText&&(n.innerText.indexOf('4007')>-1||n.innerText.indexOf('未检测到用户')>-1)){n.style.display='none';try{n.remove();}catch(e){}}});});}).observe(document.body||document.documentElement,{childList:true,subtree:true});" +
                            "}())";
                        instance.loadUrl(js);
                        console.log("[+] 已向现有 WebView 注入 JS");
                    },
                    onComplete: function () {}
                });
            } catch (e) { console.log("[-] WebView.choose: " + e); }

            console.log("[*] WebView 弹窗拦截已激活!");
        });
    }, 10000);

    console.log("[*] 弹窗拦截将在 10 秒后激活(等待 App 启动完成)");
});
© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容