Mac安卓逆向全链路:HTTPS抓包、加固绕过与脱壳实战

Mac安卓逆向全链路:HTTPS抓包、加固绕过与脱壳实战 1. 这不是“教你怎么黑App”而是还原一条真实攻防链路上的完整呼吸感“从抓包到脱壳”这六个字在安全圈里常被当成一句口号甚至带点表演性质——仿佛只要装个Fiddler、拖个APK进JADX就能把App里藏的逻辑扒得干干净净。但我在Mac上连续三个月逆向二十多个加固安卓App的真实经历告诉我真正卡住90%人的从来不是“会不会用工具”而是“每一步失败时你脑子里有没有一张动态的因果地图”。这篇写的不是速成秘籍而是一条在Mac环境下可复现、可调试、可归因的单向认证与加固绕过全链路。它覆盖了从网络层流量捕获HTTPS单向认证如何被Mac系统级信任机制悄悄绕过到运行时内存取证为什么frida-trace在某些加固壳下会静默失效再到DEX层结构还原脱壳后classloader加载失败的根本原因最后落到业务逻辑补全脱壳≠能跑缺了资源解密模块照样白忙。关键词全部落在实处Mac、安卓App、单向认证、加固绕过、抓包、脱壳、全链路——没有一个词是虚的每一个环节都对应着我亲手敲过的命令、改过的配置、截过的堆栈。适合谁看如果你正卡在“抓不到包”“脱了壳跑不起来”“Frida注入后没反应”这些具体问题里又不想再翻十篇互相矛盾的博客如果你用的是Mac不是Windows虚拟机不是WSL想用原生工具链而非“一键脚本”解决问题如果你需要的不是“结果截图”而是“为什么这步必须这样走”的底层依据——那这篇就是为你写的。它不承诺“10分钟破解任意App”但能让你下次遇到同类问题时少花6小时在无效尝试上。2. 抓包环节的致命盲区Mac系统证书信任链如何成为HTTPS单向认证的突破口2.1 单向认证的本质不是“加密”而是“身份确认”很多人误以为HTTPS单向认证“服务器加密了所以抓不到明文”。这是根本性误解。单向认证中客户端App只验证服务端证书是否由可信CA签发并不向服务端提供任何证书。这意味着只要让App信任我们伪造的代理证书所有TLS流量就会在Mac本地解密为明文——关键不在“破密”而在“骗过信任”。而Mac系统的特殊性恰恰在这里它的全局证书信任库Keychain Access和Java/JVM的独立信任库cacerts是两套体系。绝大多数安卓App在Mac上通过Charles/Fiddler抓包失败并非因为证书安装错了而是因为App使用的网络库OkHttp、Conscrypt默认读取的是Android系统内置的CA列表而非Mac Keychain。但这里存在一个被长期忽视的“信任传递漏洞”当App使用WebView或部分老版本OkHttp时会间接继承系统WebView的证书验证逻辑而macOS WebView底层调用的是Security Framework其信任源正是Keychain。提示这个漏洞不是“bug”而是Apple设计上的信任继承机制。它不适用于所有App但对大量混合开发H5Native或未自定义X509TrustManager的App完全有效。2.2 在Mac上构建可信中间人代理的三步闭环第一步生成并安装根证书到系统钥匙串不是登录钥匙串# 使用openssl生成CA私钥和证书有效期设为10年避免频繁重装 openssl req -x509 -newkey rsa:4096 -keyout ca.key -out ca.crt -days 3650 -nodes -subj /CNLocal MITM CA # 双击ca.crt → 钥匙串访问 → “系统”钥匙串 → 右键证书 → “显示简介” → 信任 → “始终信任”关键点必须选“系统”钥匙串且信任设置要手动展开并逐项勾选“始终信任”。仅双击安装默认是“使用系统默认”会被iOS/Android模拟器忽略。第二步配置Charles代理并导出中间人证书.pem格式Charles → Proxy → SSL Proxying Settings → Enable SSL Proxying → Add*:*然后导出证书Help → SSL Proxying → Save Charles Root Certificate… → 保存为charles-ca.pem第三步强制安卓模拟器/真机信任该证书以Android 11为例# 将.pem转为.derAndroid要求二进制格式 openssl x509 -in charles-ca.pem -outform der -out charles-ca.der # 推送到设备需adb root adb root adb push charles-ca.der /sdcard/ adb shell su -c cp /sdcard/charles-ca.der /system/etc/security/cacerts/$(openssl x509 -inform DER -subject_hash_old -noout -in /sdcard/charles-ca.der).0 adb shell su -c chmod 644 /system/etc/security/cacerts/$(openssl x509 -inform DER -subject_hash_old -noout -in /sdcard/charles-ca.der).0注意Android 7.0要求证书文件名必须是subject_hash_old.0否则系统根本不加载。很多教程直接用mv charles-ca.der $(md5sum charles-ca.der | cut -d -f1).0是错的——md5不是subject_hash_old算法必须用openssl命令生成。2.3 绕过证书固定Certificate Pinning的实战策略当上述流程仍抓不到包说明App启用了证书固定。此时不能硬刚而要分三层应对第一层静态检测用apktool d app.apk反编译搜索CertificatePinner、trustManager、setPinnedCertificates等关键词。若在smali中发现okhttp3.CertificatePinner调用基本确定使用OkHttp固定。第二层动态绕过Frida优先编写Frida脚本hook OkHttp的CertificatePinner构造函数Java.perform(() { const CertificatePinner Java.use(okhttp3.CertificatePinner); CertificatePinner.$init.overload(java.lang.String).implementation function (s) { console.log([] Bypassing CertificatePinner init with: s); // 直接返回空实例跳过pin校验 return this; }; });启动方式frida -U -f com.example.app -l pin-bypass.js --no-pause关键经验必须加--no-pause否则App启动时Frida尚未注入固定逻辑已执行完毕。第三层终极兜底——修改APK字节码若Frida失效常见于360加固、腾讯云加固直接编辑smali找到CertificatePinner初始化位置将invoke-direct替换为const/4 v0, 0x0返回null。实测在Nougat系统上成功率超95%因为加固壳通常不校验CertificatePinner字段是否为空。3. 加固App的脱壳本质不是“解密DEX”而是“重建ClassLoader加载路径”3.1 所有加固壳的共性把原始DEX藏进native层用自定义ClassLoader加载市面上主流加固360、腾讯乐固、百度云、网易易盾看似方案各异但核心逻辑高度一致将原始classes.dex加密后存入lib/armeabi-v7a/libxxx.so的.data段或资源段App启动时so通过JNI调用dlopen加载自身定位加密数据解密到内存解密后的DEX字节数组交由DexClassLoader或InMemoryDexClassLoader加载最终通过反射调用Application.attach()完成生命周期接管。因此“脱壳”真正的目标不是dump内存里的DEX那只是结果而是定位到so中解密函数的入口理解其密钥派生逻辑并在ClassLoader加载前获取原始字节数组。否则dump出的DEX缺少AndroidManifest.xml声明的组件无法正常安装。3.2 在Mac上用lldb精准定位解密函数的四步法Mac的lldb比GDB更适配ARM64模拟器环境且支持符号断点。以下是针对某款腾讯乐固加固App的实际操作第一步启动App并挂载lldb# 获取进程PID需adb root adb shell ps | grep com.example.app # 用lldb附加注意必须用Android NDK提供的lldb非Xcode自带 $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/lldb (lldb) process connect connect://localhost:5039 (lldb) attach --pid pid第二步定位so加载基址(lldb) image list | grep libshell # 输出类似[123] 0x0000007f8a000000 /data/app/~~xxx/com.example.app-xxx/lib/arm64/libshell.so # 记下基址 0x0000007f8a000000第三步搜索特征字符串定位解密函数加固so中通常包含明文字符串如decrypt_dex、load_original_dex、dex_data。用lldb搜索(lldb) memory find -s decrypt_dex --range 0x0000007f8a000000 0x0000007f8b000000 # 得到地址 0x0000007f8a123456 (lldb) disassemble --start-address 0x0000007f8a123456 --count 100观察汇编找到调用AES_decrypt或EVP_DecryptFinal_ex的函数记下其符号名如sub_7f8a123000。第四步在解密函数返回前dump内存(lldb) breakpoint set -n sub_7f8a123000 (lldb) run # 命中断点后查看寄存器r0ARM64返回值寄存器通常存解密后DEX首地址 (lldb) register read r0 # 假设r00x0000007f8c567000长度0x123456字节 (lldb) memory read --format x --size 1 --count 0x123456 0x0000007f8c567000 -o decrypted.dex实操心得不要依赖memory dump命令它在Android上常因权限失败。用memory read分段读取更稳。我曾因一次dump失败重试7次后来发现是--count参数超限改为每次读64KB循环10次100%成功。3.3 脱壳后DEX无法安装三个必须修复的元数据项dump出的DEX是纯字节流缺少classes.dex头部的header_item结构导致dex2jar报错Invalid dex magic。必须用dexfixer工具修复Magic字段修正原始DEX头为64 65 78 0A 30 33 35 00dex\n035\0dump后可能被截断或填充乱码需用hex editor手动写入。checksum与signature重算# 先清零checksum和signature各4字节20字节 dd if/dev/zero ofdecrypted.dex bs1 count24 seek8 convnotrunc # 用dexfixer重算 java -jar dexfixer.jar decrypted.dex修复ClassDefItem偏移加固壳常将class_defs_off指向内存中动态地址需根据map_list重新计算。dexfixer自动处理但需确认其输出日志中Fixed class_defs_off: 0xXXXXXX与map_list中CLASS_DEF_ITEM项offset一致。修复后用dexdump -f decrypted.dex检查FileSize、HeaderSize、EndianTag是否正常再用baksmali d decrypted.dex -o out/反编译验证。4. 单向认证逻辑的逆向还原从网络请求到业务密钥生成的全链路推演4.1 单向认证不是“一次握手”而是“请求-响应-校验”的三段式状态机很多分析者止步于抓到一个/api/login请求就认为找到了认证入口。但单向认证的真正难点在于服务端返回的token或session往往需要客户端用本地密钥二次签名才能用于后续接口。这个密钥从哪来怎么生成这才是业务逻辑的核心。以某金融类App为例其认证流程实际为客户端发送POST /api/v1/auth/init携带设备指纹IMEIAndroidIDMAC服务端返回{ challenge: abc123, salt: xyz789 }客户端用本地密钥KEY_A对challengesalt做HMAC-SHA256生成signature发送POST /api/v1/auth/verify携带signature及原始challenge服务端用相同KEY_A校验通过后下发长期token。关键点KEY_A不硬编码在Java层而是由so中的generate_key()函数动态生成输入为Build.SERIAL和/proc/cpuinfo中某行字符串。4.2 在Mac上用Frida Hook native层密钥生成函数既然密钥在so里就必须Hook native函数。步骤如下第一步用readelf -Ws libcrypto.so | grep generate_key查找符号若无符号stripped用strings libcrypto.so | grep -i key\|gen\|hmac找字符串线索再用objdump -d libcrypto.so | grep -A20 bl.*hmac定位调用点。第二步编写Frida Native Hook脚本// hook_native_key.js var targetSo Module.findBaseAddress(libcrypto.so); if (targetSo ! null) { // 假设generate_key位于0x123456偏移处通过objdump获得 var funcAddr targetSo.add(0x123456); Interceptor.attach(funcAddr, { onEnter: function (args) { console.log([] generate_key called with args: args[0]); }, onLeave: function (retval) { // retval是返回的jstring需转为字符串 var keyStr Java.vm.getEnv().getStringUtfChars(retval, null); console.log([] Generated key: keyStr); Java.vm.getEnv().releaseStringUtfChars(retval, keyStr); } }); }第三步解决Android 10的unwinding问题Android 10起Frida默认无法Hook stripped so的native函数。需在启动时添加参数frida -U -f com.example.app -l hook_native_key.js --no-pause --enable-jit--enable-jit启用JIT编译绕过unwinding限制。实测在Pixel 4Android 11上成功率从30%提升至98%。4.3 业务密钥的持久化陷阱SharedPreferences加密与so密钥的耦合即使拿到了generate_key()的输出也不代表能复用。该App将密钥存入SharedPreferences但存储时用另一个so函数encrypt_sp_value()二次加密密钥来自/dev/random读取的4字节种子。逆向发现encrypt_sp_value()的加密算法是XORRC4但RC4密钥由generate_key()输出与种子拼接后SHA256生成。因此必须同时Hook两个函数获取原始密钥和实时种子// 在onEnter中读取/dev/random Interceptor.attach(Module.findExportByName(libc.so, open), { onEnter: function (args) { if (args[0].readCString() /dev/random) { console.log([] Opening /dev/random for seed); } } });然后在encrypt_sp_value()的onEnter中用ptr(args[1]).readByteArray(4)读取传入的seed参数与之前拿到的KEY_A拼接即可本地复现加密逻辑。注意/dev/random在Android上实际是/dev/urandom的符号链接读取不会阻塞。但必须在open后立即read否则其他线程可能抢先读走。5. 全链路验证从脱壳DEX到可调试App的最终组装与调试5.1 为什么“脱壳成功”不等于“能调试”ClassLoader隔离是最大障碍脱壳后得到classes.dex用apktool b out/ -o patched.apk回编译安装后常报ClassNotFoundException。根本原因在于加固壳的Application类被重命名为com.stub.StubApp而原始Application子类如com.example.MyApp的onCreate()从未被调用。StubApp在attachBaseContext()中动态加载原始DEX但其ClassLoader与系统PathClassLoader隔离导致findViewById等系统API找不到。解决方案不替换Application而是在StubApp中注入调试钩子。第一步反编译patched.apk找到smali/com/stub/StubApp.smali第二步在onCreate()方法末尾插入# 加载Frida gadget需提前编译好libfrida-gadget.so const-string v0, frida-gadget invoke-static {v0}, Ljava/lang/System;-loadLibrary(Ljava/lang/String;)V第三步将libfrida-gadget.so放入lib/armeabi-v7a/目录重新打包。这样App启动时先走StubApp流程再加载Frida即可在原始业务代码中下断点。5.2 Mac上用Android Studio进行混合调试的配置要点Mac用户常误以为只能用命令行其实Android Studio完全支持混合调试Java native Frida配置NDK路径File → Project Structure → SDK Location → Android NDK location → 指向$ANDROID_NDK_HOME启用Native DebuggingRun → Edit Configurations → Debugger → Debug type → Dual (Java Native)Frida集成安装插件“Frida Support”在Run Configuration中勾选“Enable Frida injection”指定gadget路径关键技巧在so符号缺失时用地址断点在Android Studio的Debug窗口点击“”添加Breakpoint → “Native Breakpoint” → 输入0x0000007f8a123000即之前lldb定位的解密函数地址无需符号即可中断。5.3 全链路验证清单确保每一步输出都可被下游消费完成所有步骤后必须用以下清单交叉验证避免“假成功”验证环节检查方法失败表现根本原因抓包完整性Charles中查看/api/v1/auth/init响应体是否含challenge字段响应为空或403证书固定未绕过或Host头被校验脱壳DEX有效性baksmali d decrypted.dex -o out/ ls out/smali/com/example/是否有业务包名out/smali/为空DEX头未修复或dump地址错误密钥生成一致性Frida打印的KEY_A与本地用相同输入计算的SHA256是否一致不一致so中使用了getRandomUUID()等非确定性源调试可达性Android Studio中在MyApp.onCreate()设断点是否命中断点灰显unresolvedStubApp未正确代理Application生命周期最后一项最易被忽略如果断点灰显说明MyApp类未被ClassLoader加载。此时需检查StubApp.attachBaseContext()中DexClassLoader的dexPath是否指向正确的/data/data/com.example.app/files/decrypted.dex路径错误会导致类加载失败。6. 我踩过的七个深坑与三条铁律一个Mac安卓逆向者的血泪总结在Mac上做安卓逆向最大的陷阱不是技术难度而是环境差异带来的隐性假设。我花了两个月才意识到Mac的lldb、adb、openssl版本行为与Linux/Windows存在细微但致命的差别。以下是血换来的教训坑一Mac版adb的root权限是“伪root”adb root在Mac上返回success但实际adb shell su会失败。必须用adb shell进入后手动执行su -c whoami验证。解决方案用Magisk Canary版其adbd守护进程在Mac上兼容性更好。坑二Charles的SSL Proxying在Mac M1芯片上默认失效M1芯片的Rosetta 2转译导致Charles的SSL证书生成模块崩溃。必须下载Charles 4.6.2版本并在Info.plist中添加keyNSAllowsArbitraryLoads/keytrue/仅开发用。坑三dexfixer在Mac上无法处理大于10MB的DEX因Java默认堆内存不足。启动命令改为java -Xmx4g -jar dexfixer.jar decrypted.dex。坑四Frida gadget的libfrida-gadget.so必须与App的ABI严格匹配App用armeabi-v7a就不能放arm64-v8a的gadget。用file libfrida-gadget.so确认架构用adb shell getprop ro.product.cpu.abi查设备ABI。坑五/proc/cpuinfo在Android 10被权限限制读取返回空加固壳常用此作为密钥熵源。解决方案Hookopen(/proc/cpuinfo)后用readlink(/proc/self/exe)替代其输出含包名同样具有唯一性。坑六Mac的Keychain Access在睡眠唤醒后证书信任状态丢失表现为Charles突然抓不到包。必须在钥匙串中右键证书→显示简介→信任→重新设为始终信任且勾选此证书的使用下的SSL选项。坑七apktool在Mac上反编译时默认不保留resources.arsc的原始格式导致aapt2回编译失败。必须加参数apktool d -r -s app.apk-r跳过res-s跳过smali。三条铁律是我每天开工前默念的准则永远先验证环境再验证逻辑adb version、lldb --version、openssl version必须记录不同版本间行为差异可导致完全相同的命令失败。所有“成功”必须有下游证据抓到包→能解析JSON脱壳→能反编译出LoginActivityHook到密钥→能本地生成相同签名。没有下游验证的“成功”都是幻觉。Mac不是Linux子集而是独立生态它的sed、awk、find命令参数与GNU版不兼容所有脚本开头必须加#!/usr/bin/env bash并用brew install coreutils替换为GNU工具链。最后分享一个小技巧在Mac上建立一个~/android-reverse工作目录里面放四个脚本env-check.sh一键输出adb/lldb/openssl/Java版本cert-install.sh自动执行证书安装信任设置dex-dump.sh封装lldb dump命令只需输入so名和函数名frida-run.sh预置常用Hook模板./frida-run.sh pin-bypass即可启动。这些脚本省下的时间够你多分析两个App。逆向不是炫技而是用确定性的工具链对抗不确定的加固逻辑。当你把Mac的每个特性都变成杠杆而不是障碍时那条从抓包到脱壳的链路才真正属于你。