夜雨聆风学习资料网

ARTICLE · 979018

揭秘App加固漏洞:陌生人社交类App的渗透测试全过程

揭秘App加固漏洞:陌生人社交类App的渗透测试全过程
App 渗透测试实战:从签名伪造到水平越权
本文基于一次授权的移动端渗透测试项目整理,目标为新能源出行类 App(下文称"目标 A")与陌生人社交类 App(目标 B)。所有域名、包名、密钥、账号均已脱敏,仅保留技术方法与数据形态。仅用于安全研究与学习。
目录
  1. [概述与测试流程](#一概述与测试流程)
  2. [前置准备:工具链与环境]()
  3. [APK 获取与信息收集](-获取与信息收集)
  4. [静态分析:反编译与代码审计]()
  5. [动态分析基础:环境与 frida-server](-frida-server)
  6. [frida 核心:attach 与 spawn](-核心attach-与-spawn)
  7. [绕过模拟器检测]()
  8. [动态调试技巧]()
  9. [加固应用的脱壳]()
  10. [常见漏洞面测试]()
  11. [渗透测试注意事项]()
  12. [测试报告与沉淀]()
  13. [附录:frida 脚本模板合集](-脚本模板合集)
一、概述与测试流程
本次测试以两个业务形态完全不同的 App 为对象:
目标
业务形态
加固方案
核心风险点
目标 A
新能源出行(充电/换电/积分)
商业壳 A(代码虚拟化 VMP)
请求签名可伪造、水平越权(IDOR)
目标 B
陌生人社交
商业壳 B(入口加固)
512-bit RSA 弱密钥
整体测试流程:
信息收集 → 静态分析 → 动态环境搭建 → 抓包打通        → 签名机制还原 → 登录态获取 → 越权枚举 → 加密破解 → 报告输出
核心结论:
  1. 目标 A 的请求签名基于可提取的静态密钥 + 公开算法(MD5),攻击者可完全离线伪造任意请求,绕过服务端验签;
  2. 目标 A 的用户信息接口存在水平越权,任意登录用户可通过遍历 account_id 读取其他用户的账号信息;
  3. 目标 B 内嵌 512-bit RSA 公钥,存在因数分解恢复私钥的风险(可用于解密依赖该公钥的业务数据)。
二、前置准备:工具链与环境
2.1 宿主机工具链
类别
工具
用途
反编译
jadx / apktool
dex→Java 源码 / 资源与 smali
动态插桩
Frida + frida-tools
hook Java/native、脱壳
抓包
Burp Suite + PortSwigger CA
HTTP/HTTPS 中间人
二进制
Ghidra / readelf / objdump
so 逆向与符号解析
脚本
Python3 + requests/pycryptodome
签名复现、加密爆破
设备
MuMu 模拟器(Android 12, root)
动态测试环境
2.2 模拟器联动配置
模拟器到 Burp 的流量打通分三层:
# 1) 证书安装:PortSwigger CA 转 PEM 并挂载为系统证书opensslx509-informder-incacert.der-outcacert.pemHASH=$(opensslx509-informPEM-subject_hash_old-incacert.pem | head-1)adbpushcacert.pem/data/local/tmp/cacerts/$HASH.0# 2) 系统证书目录 bind mount(/system 只读时无需 remount)adbshell"mount --bind /data/local/tmp/cacerts /system/etc/security/cacerts"# 3) iptables 透明 DNAT(按 App uid 转发,App 感知不到代理)adbshell"iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner <APP_UID> \  -j DNAT --to-destination 10.0.2.2:8080"
经验:Android 7+ 用户证书默认不信任,需做系统证书;WebView 进程有证书缓存,修改后需杀 webviewzygote 重启才生效。全局 HTTP 代理会被部分 App 检测(读 settings get global httpproxy),iptables 透明转发无感知、对抗检测效果最好。
三、APK 获取与信息收集
3.1 获取方式
  • 目标 A:从应用商店/官网下载正式 APK
  • 目标 B:从已安装设备提取
# 从已安装设备提取 APKadbshellpmpathcom.target.badbpull/data/app/.../base.apk
3.2 基础信息
aaptdumpbadgingbase.apk
目标
包名
版本
minSdk/targetSdk
敏感权限
A
com.target.a
4.x
21 / 30
相机、录音、精确位置、后台定位
B
com.target.b
2.x
23 / 30
相机、录音、前后台定位、联系人
3.3 初步研判
  • 查看 lib/ 目录下的 so:目标 A 出现壳 A 特征文件(libkeys.so 等),目标 B 出现壳 B 特征文件(libsec-lib.so 等)→ 均判定为商业加固;
  • 目标 B 的 classes.dex 仅 100KB(真实业务被加密),但存在大量明文 classes2~N.dex——判断为"只加固入口,业务 dex 明文",可直接反编译。
四、静态分析:反编译与代码审计
4.1 反编译
jadx-dsrcbase.apk# 目标 B 直接全量反编译(业务 dex 明文)
目标 B 反编译产出 4.3 万+ Java 文件。目标 A 的加固 dex 在脱壳后反编译(见第九章)。
4.2 网络层审计(目标 B)
  • grep 提取主域名 api.example-social.com,接口前缀 /neapi/;
  • 定位网络加密工具类:p3/x4.java(第三方地图 SDK 内嵌加密工具):
  • 512-bit RSA 公钥(Exponent=65537,OAEPWITHSHA-1ANDMGF1PADDING);
  • AES/CBC 加密,IV 用斐波那契数列混淆生成。
4.3 网络层审计(目标 A)
  • 定位 OkHttp 拦截器链,还原请求签名机制(见第十章漏洞 1)。
4.4 代码审计要点
  1. 搜 Cipher.getInstance / SecretKeySpec → 定位加密逻辑;
  2. 搜 Interceptor 实现 → 定位请求/响应加解密与签名点;
  3. 搜 native 方法声明 → 判断加密是否下沉到 so;
  4. 扫硬编码:appKey/appSecret/token/api_key;
  5. 枚举导出组件(exported=true)与 WebView 配置。
五、动态分析基础:环境与 frida-server
5.1 frida-server 部署
# 下载与架构匹配的 frida-server (arm64)adbpushfrida-server/data/local/tmp/adbshell"chmod 755 /data/local/tmp/frida-server"adbshell"/data/local/tmp/frida-server -l 0.0.0.0:27042 &"adbforwardtcp:27042tcp:27042
5.2 基础验证
frida-ps-U# 列出进程frida-ps-Ua# 仅 App
5.3 环境排查清单
检查项
命令
root 可用性
adb shell su -c id
代理状态
adb shell settings get global http_proxy
frida-server 进程
adb shell ps -A | grep frida
证书挂载
adb shell ls /system/etc/security/cacerts/
六、frida 核心:attach 与 spawn
6.1 两种模式
模式
时机
适用
spawn
进程创建即注入,可 hook 早期初始化
需要 hook 解密/反调试逻辑早期执行时
attach
附加到已运行进程
常规 hook,简单直接
6.2 实战中的坑:attach 被反调试拒绝
目标 B(壳 B)对 attach 有反调试:
  • 直接 attach 报 unexpected early end-of-stream;
  • 使用 frida-ps 枚举时进程被隐藏/干扰。
绕过方案:
  1. 按 PID attach:壳只对 frida-ps 枚举做了隐藏,getprocesslist 拿真实 PID 后直接 attach(pid) 可绕过枚举层检测;
  2. 循环重连:反调试会杀进程(PID 漂移),用脚本"检测主进程 PID → 重新 attach + 注入脚本",实现长稳 hook。
# 循环重连核心逻辑(节选)whileTrue:    pid = get_main_pid(device, package)if pid and pid != last_pid:        session = device.attach(pid)        session.create_script(js).load()    sleep(2.5)
经验:加固 App 的 frida 对抗是"军备竞赛"。先试 attach,失败再试 spawn;spawn 可能因壳的进程模型(loader 与真实业务进程分离)而"注入错进程",需要核对注入进程的 PID 与真实主进程是否一致。
七、绕过模拟器检测
目标 B 所在网易系产品线对"模拟器 + 代理"环境有检测,直接表现为:关闭全局代理后正常,开启代理后界面"网络异常"。
7.1 检测维度
  1. 系统代理检测:读 settings get global http_proxy,非空即判定异常;
  2. VPN 状态检测:ConnectivityManager 查 VPN 接口;
  3. 模拟器特征检测:Build 指纹、/dev/qemu_pipe 等。
7.2 绕过方案(本次实测有效)
检测
绕过
系统代理检测
关闭全局代理 + iptables 透明 DNAT(按 uid 转发到 Burp)
frida 检测
按 PID attach + 循环重连(见第六章)
模拟器检测
修改 Build 指纹 / 用真机复核
# 关闭全局代理(根因修复)adbshellsettingsputglobalhttp_proxy:0
实测:关闭全局代理并改用 iptables 透明转发后,目标 B 恢复正常且流量仍到 Burp(tcpdump 确认 200+ 包进入 8080)。
八、动态调试技巧
8.1 请求/响应明文直取
hook OkHttp 响应体,绕过加密层直接读明文:
Java.perform(function () {var Res = Java.use("okhttp3.ResponseBody");    Res.create.overload("okio.BufferedSource", "java.lang.String", "long")       .implementation=function (s, t, l) {var r =this.create(s, t, l);try {var body = r.string();            console.log("[RESP] "+ body);return Java.use("okhttp3.ResponseBody")                     .create(Java.use("okio.Okio").buffer(                         Java.use("okio.ByteString").encodeUtf8(body).toByteArray()                     ), t);        } catch (e) { return r; }    };});
8.2 加密函数双向取值
目标 B 的请求加密在 native(Lua 引擎 APICryptor),hook Java 包装层拿"明文↔密文"对:
Java.perform(function () {varC= Java.use("com.target.b.crypto.caesar.APICryptor");C.encrypt.implementation=function (x) {var r =this.encrypt(x);        console.log("[ENC] "+ x +" => "+ r);   // 明文与密文成对打印return r;    };});
8.3 枚举类名辅助定位
Java.perform(function () {    Java.enumerateLoadedClasses({onMatch:function (name) {if (name.indexOf("crypto") >=0) console.log(name);        },onComplete:function () {}    });});
九、加固应用的脱壳
9.1 壳类型研判(目标 A:VMP)
目标 A 的核心 so libkeys.so 代码段为 VMP(指令虚拟化):
  • 段属性为 rwxp(可读写执行),代码被翻译为自定义字节码,静态反汇编不可行;
  • 无法用常规脱壳工具直接还原 so 逻辑。
9.2 脱壳策略:内存 dump 提取关键常量
静态不可行 → 改为运行时从内存提取解密后的敏感常量:
# 从 /proc/<pid>/mem 按映射区间 dump(dalvik 堆 + so 数据段)ddif=/proc/$PID/membs=4096skip=<start> count=<len> >> heap.bin
对 dump 产物做字符串/模式提取:
  • 签名密钥(32 位十六进制形态)命中多次;
  • AES 相关 16 字节常量命中(数据区镜像);
  • 登录态数据(token ×6、手机号 ×6、user_id ×24)均以明文存在于堆中。
9.3 脱壳结论
目标 B 业务 dex 明文,无需脱壳;目标 A 采用"VMP 保护 + 运行期明文常量"的组合,VMP 保护了 so 静态逻辑,但运行时数据面(密钥/IV/token)仍是明文可提取——这是本类加固的共性短板。
十、常见漏洞面测试
漏洞一:请求签名机制可离线伪造(目标 A)
10.1 现象
Burp 中观察目标 A 所有请求均携带统一格式的签名参数与公共参数:
GET /v1/user/summary?app_ver=4.x&region=cn&app_id=xxxxx&device_id=xxxxxxxx    &timestamp=1786888410&sign=<32位hex> HTTP/2Host: api.example-autos.com
10.2 签名还原
反编译定位两层拦截器:
  1. 公共参数拦截器:注入 appid / region / lang / appver / terminal / device_id / timestamp;
  2. 签名拦截器:对"方法 + 路径 + 排序参数 + 密钥 + token"做 MD5:
raw = METHOD + "/" + path + "?" + sorted(k=v 参数&连接, 排除sign/sig前缀) + APP_SECRET + ACCESS_TOKENsign = MD5(raw)
参数排序、拼接格式均从源码逐行还原。密钥来源:从内存 dump 中提取出 32 字符密钥(见 9.2)。
10.3 验证
写 Python 复现签名并重放:
defsign(method, path, params, access_token):    raw = method + "/" + path + "?" + "&".join(f"{k}={v}"for k, v insorted(params.items())ifnot k.startswith("sign") andnot k.startswith("sig")    ) + APP_SECRET + access_tokenreturn hashlib.md5(raw.encode()).hexdigest()
  • 使用错误密钥 → 返回 sign_failed(服务端验签,公式正确);
  • 使用提取密钥 → 返回业务数据(HTTP 200)→ 签名伪造成功。
10.4 影响
攻击者可离线构造任意签名请求:篡改业务参数、遍历资源 ID、重放订单操作,服务端无法区分伪造流量。修复方向:密钥下沉到服务端下发 + 短期轮换、增加设备绑定/行为风控、对异常频率告警。
漏洞二:用户信息接口水平越权(IDOR)(目标 A)
11.1 接口定位
在签名伪造的基础上,枚举出用户信息接口:
GET /v1/user/summary?account_id=<id>
该接口要求登录(无 token 返回未登录),登录后返回任意 account_id 的用户摘要(昵称、头像、关注数、用户关系等)。
11.2 越权验证
# 1) 用自己的 token 请求自己的 account_id → success,返回数据# 2) 将 account_id 替换为相邻值 → 返回其他用户数据(success)# 3) 步进遍历 1000 个 ID → 命中 13 个其他用户账号
请求
account_id
响应
自账号
1000x
success(本人数据)
邻账号 1
1000x+1
success(他人数据)
...
步进 1000
命中 13 个有效用户
空号
无此账号段
resource_not_found
11.3 同类延伸
  • 手机号→账号映射接口:GET /share/users?mobile=<11位>,有效手机号返回账号 ID+昵称,未注册返回 usernotfound——可用于账号枚举/撞库;
  • 昵称全局搜索接口:GET /user/v1/profile-search?key=<昵称> 可检索任意用户。
11.4 根因与修复
根因:服务端只校验了"已登录",未校验"资源归属"(对象级授权缺失)。
修复方向:
  1. 服务端强制以 token 解析的会话用户作为资源主体,忽略客户端传入的 account_id;
  2. 若必须支持查询他人,增加可见性校验(好友/授权关系);
  3. 手机号映射接口加风控(频率限制 + 验证码)。
漏洞三:512-bit RSA 弱密钥(目标 B)
目标 B 内嵌 512-bit RSA 公钥,可用于因数分解恢复私钥(GNFS/msieve/yafu)。若业务用该公钥加密敏感数据(登录/支付参数),攻击者恢复私钥后可解密任意密文。
风险评级依据:NIST 明确 512-bit RSA 早已不安全;业界共识 <1024-bit 视为可分解。修复:升级 ≥2048-bit,并检查同公钥在其他 SDK 的复用情况。
十一、渗透测试注意事项
  1. 授权边界:所有测试在授权范围内进行,不碰未授权目标;
  2. 数据脱敏:抓包与报告中的 PII 一律打码,不在报告中留存真实 token/账号;
  3. 遍历爆破节奏:IDOR 枚举、密码类爆破需控制频率,避免影响线上业务;
  4. 写入操作:涉及订单/支付/删除类接口,先确认是幂等/可逆操作,必要时只做最小验证(改查询参数,不真正提交);
  5. 环境隔离:模拟器/抓包环境与生产网络隔离,防止误伤真实用户;
  6. 加固对抗边界:frida/脱壳过程可能触发 App 风控,异常数据及时丢弃,不影响后续分析。
十二、测试报告与沉淀
12.1 报告结构
1. 项目概述(范围、时间、方法)2. 资产清单(App 版本、接口面、加固情况)3. 漏洞清单(按风险等级排序)4. 漏洞详情(位置 / 复现步骤 / 影响 / 修复建议)5. 修复验证建议6. 附录(抓包样本脱敏、脚本清单)
12.2 可复用资产
  • 签名复现脚本(一套参数适配多接口)
  • 越权枚举脚本(IDOR 步进遍历 + 结果判定)
  • 加密爆破/解密脚本(AES 密钥候选 + 密文验证)
  • frida hook 脚本模板(见附录)
  • 模拟器联动一键脚本(证书 + iptables + 代理清理)
12.3 经验沉淀
  • 先抓包再看代码:流量往往暴露最直接的攻击面;
  • 签名密钥/IV 运行时必在内存:VMP 保护静态,但数据面是突破口;
  • 代理检测 ≠ 抓不了:iptables 透明转发是对抗代理检测的标准答案;
  • 加密响应不是终点:密钥在客户端就一定能提取,关键在提取路径与验证方式。
十三、附录:frida 脚本模板合集
模板 1:通用网络请求捕获
Java.perform(function () {// 请求侧var Req = Java.use("okhttp3.Request");    Req.url.implementation=function () {var u =this.url();        console.log("[REQ] "+ u.toString());return u;    };// 响应侧var Res = Java.use("okhttp3.ResponseBody");    Res.create.overload("okio.BufferedSource", "java.lang.String", "long")       .implementation=function (s, t, l) { /* 见第八章 */ };});
模板 2:AES 密钥捕获
Java.perform(function () {varC= Java.use("javax.crypto.Cipher");C.init.overload("int", "java.security.Key", "java.security.spec.AlgorithmParameterSpec")     .implementation=function (m, k, p) {        console.log("[AES-key] "+ k.getEncoded());        console.log("[AES-iv ] "+ Java.use("javax.crypto.spec.IvParameterSpec")                                     .$new(p.getIV()).toString());returnthis.init(m, k, p);    };});
模板 3:反调试循环重连(attach)
import frida, timedefon_message(msg, data):if msg["type"] == "send":print(msg["payload"])JS = open("hook.js").read()device = frida.get_usb_device()last = 0whileTrue:try:for p in device.enumerate_processes():if p.name == "com.target.b":if p.pid != last:                    s = device.attach(p.pid)                    s.create_script(JS).on("message", on_message).load()print("[+] attached", p.pid)                    last = p.pidbreakexceptExceptionas e:print("[-]", e)    time.sleep(2.5)
模板 4:SSL Pinning 绕过(备用)
Java.perform(function () {varT= Java.use("okhttp3.CertificatePinner");T.check.overload("java.lang.String", "java.util.List")     .implementation=function (h, l) { return; };});
模板 5:native 函数 hook(JNI 导出符号)
// 先静态解析 so 导出符号(PT_DYNAMIC -> DT_SYMTAB),拿到偏移后:varbase = Module.findBaseAddress("libkeys.so");Interceptor.attach(base.add(0xB3C), {   // 0xB3C 为导出函数偏移onEnter:function (args) { console.log("[native] enter"); },onLeave:function (ret) { console.log("[native] ret="+ ret); }});
声明:本文所有技术内容仅用于授权的安全测试与学习研究,禁止用于非法用途。涉及的具体目标、密钥、账号均已完成脱敏处理。

相关学习资料

返回首页浏览学习资料