
1. 项目概述与核心价值最近在分析一个主流房产信息平台“某居客”的移动端应用目标很明确完成从APP端网络请求抓包到关键接口逆向最终深入其核心的so层共享库进行算法解析的全过程。这听起来像是一个标准的移动安全分析流程但实际操作中你会发现从抓包开始就布满了“雷区”而深入到so层后更是对逆向工程基本功和耐心的一次全面考验。这个项目对于从事移动应用安全研究、风控策略对抗、数据合规审计甚至是希望理解大型APP如何构建其客户端安全体系的开发者而言都具有很高的参考价值。它不仅仅是一次技术演练更是一次对现代APP混合防护策略HTTPS证书绑定、代码混淆、Native层加密的实战拆解。简单来说我们要做三件事第一成功捕获到APP发出的所有网络请求特别是那些包含核心业务数据如房源列表、详情、用户信息的接口第二逆向分析这些接口的调用逻辑、参数构造和签名算法理解其通信协议第三也是最硬核的部分定位并逆向实现那些被转移到Android so库C/C层中的关键加密或校验函数。整个过程你会遇到证书锁定SSL Pinning、各种代码混淆、反调试以及so层的控制流平坦化等保护手段。接下来我就以一个实战者的视角带你一步步拆解这个“某居客”分享其中踩过的坑和总结出的有效技巧。2. 环境准备与工具链选型工欲善其事必先利其器。一个稳定、高效的工具链是成功的一半。对于移动端抓包与逆向工具的选择需要兼顾易用性、功能强大和对抗检测的能力。2.1 抓包环境搭建抓包是整个分析的入口。对于HTTPS流量传统的方法如设置系统代理在遇到证书绑定时会立刻失效。“某居客”这类大型APP几乎必然使用了证书绑定。我的方案是使用一部已Root的Android真机 模块化注入工具。设备选择我选择了一台闲置的Android手机刷入了Magisk获取Root权限。模拟器如Genymotion、夜神在对抗高级反调试和检测虚拟环境时容易出问题真机更稳定。核心工具Frida和JustTrustMe模块或类似功能的Xposed模块如TrustMeAlready。Frida是一个动态插桩框架我们可以编写脚本在运行时Hook住APP的证书验证逻辑使其接受我们抓包工具如Burp Suite或Charles签发的证书。抓包代理Burp Suite Professional。它的Repeater、Intruder、Decoder等功能在后续参数分析和爆破时非常有用。社区版功能受限但基础抓包足够。具体搭建步骤在电脑上安装Burp Suite配置好代理如127.0.0.1:8080并导出Burp的CA证书。在Root后的手机上安装Magisk模块管理器然后安装MagiskFrida模块或手动部署Frida-server到手机并运行。安装Xposed框架如LSPosed并安装JustTrustMe模块。这个模块的作用是禁用或绕过大量APP的证书绑定逻辑。将Burp的CA证书安装到手机的系统证书目录/system/etc/security/cacerts/这需要Root权限。这一步确保了手机系统信任Burp。配置手机Wi-Fi代理指向运行Burp的电脑IP和端口。注意仅仅安装用户证书是不够的Android 7.0以上APP默认不信任用户安装的证书必须将证书放入系统证书目录。这是第一个常见的坑。2.2 逆向分析环境搭建成功抓到包后我们需要静态和动态分析APP本身。反编译与静态分析Jadx-GUI这是我首选的Java反编译工具。它可以直接打开APK文件将Dex代码反编译成可读性相当高的Java代码。用于快速浏览APP结构、查找关键类和方法。Apktool用于反编译APK资源文件如图片、布局XML和AndroidManifest.xml对于理解APP权限和组件结构很重要。JEB或Ghidra更强大的反编译器。当Jadx遇到复杂混淆处理不佳时JEB的反编译效果往往更好。Ghidra则是免费的强大逆向工程框架对Native层so的分析支持极佳。动态调试与HookFrida不仅是绕过SSL的利器更是动态分析的瑞士军刀。我们可以编写Python脚本主动调用APP的方法、修改函数返回值、打印调用栈和参数这对于快速定位加密函数入口至关重要。Objection基于Frida的命令行工具可以快速实现一些常见操作如禁用SSL绑定、内存搜索、列出类和方法等适合快速侦查。So层分析Ghidra/IDA Pro静态分析so文件的不二之选。Ghidra免费且功能强大支持反编译C/C代码。IDA Pro是行业标准交互和插件生态更成熟。Frida同样可以用于Hook so层导出的Native函数动态获取函数的输入输出辅助静态分析。radare2命令行下的强大逆向工具适合喜欢终端操作的研究者。这套组合拳覆盖了从网络流量捕获、Java层逻辑分析到Native层算法还原的全链路需求。3. 抓包实战与证书绑定绕过环境准备好后启动“某居客”APP开始抓包。不出所料最初的尝试可能一无所获APP提示网络错误或直接闪退。这说明它检测到了代理或证书异常。3.1 初步对抗与问题定位首先检查Burp Suite是否能看到任何HTTP/HTTPS请求。如果完全看不到可能是APP使用了透明代理检测或证书绑定SSL Pinning。我们之前安装的JustTrustMe模块就是为了通用性地绕过证书绑定。如果安装了该模块后仍然抓不到包可能有以下原因APP使用了自定义的HTTP库或Socket通信有些APP会使用OkHttp的自定义TrustManager或者直接使用Socket进行非HTTP协议通信。JustTrustMe主要针对系统证书验证逻辑对高度自定义的实现可能失效。APP具备代理检测功能APP会读取系统代理设置如果发现设置了代理则拒绝发送请求或走其他通道。So层实现的绑定最棘手的情况证书验证逻辑被编译到了so文件中Java层只是一个壳子。应对策略使用Frida脚本进行精准Hook写一个Frida脚本Hook常见的证书验证类如OkHttpClient.Builder的sslSocketFactory和hostnameVerifier或者TrustManager的checkClientTrusted/checkServerTrusted方法。通过打印堆栈可以找到APP具体在哪里进行了证书验证。// 示例Frida脚本Hook OkHttp的证书验证 Java.perform(function() { var OkHttpClient_Builder Java.use(okhttp3.OkHttpClient$Builder); OkHttpClient_Builder.sslSocketFactory.implementation function(sslSocketFactory, trustManager) { console.log(\[*] OkHttpClient.Builder.sslSocketFactory() called!\); console.log(Java.use(\android.util.Log\).getStackTraceString(Java.use(\java.lang.Exception\).$new())); // 返回一个接受所有证书的Factory // ... 这里可以替换成自定义的信任所有证书的Factory return this.sslSocketFactory(sslSocketFactory, trustManager); }; });使用ProxyDroid等工具进行全局代理这类工具可以在VPN层面实现代理绕过APP层的代理检测。但可能会与抓包工具的证书安装产生冲突需要仔细配置。针对So层绑定这需要进入下一步的逆向分析。可以先通过抓包工具看到TCP连接建立但SSL握手失败或者APP直接崩溃来间接判断。3.2 成功捕获流量经过Frida脚本的辅助Hook和JustTrustMe模块的作用我最终成功在Burp Suite中看到了“某居客”的HTTPS流量。关键接口通常以.json或纯路径形式出现例如/house/list、/detail/info等。此时需要重点关注请求头Headers特别是Authorization、X-Token、User-Agent可能被自定义、X-Sign、X-Timestamp等字段这些往往是身份验证和请求签名的关键。请求参数Params/Body查看GET参数或POST的JSON/form-data数据。注意是否有看似随机的字符串参数如sign、nonce、encryptedData等。响应数据Response观察数据是明文的JSON还是经过编码或加密的。如果是加密的那么解密逻辑很可能在so层。实操心得不要一上来就分析所有接口。先进行常规的APP操作如浏览首页、搜索房源、查看详情在Burp中过滤出与这些操作相关的、数据量较大的接口。通常列表接口和详情接口是分析签名和加密算法的突破口因为它们参数相对固定易于对比分析。4. Java层接口逻辑逆向抓到关键接口例如/api/v1/house/search后下一步就是找到APP中构造这个请求的代码位置。4.1 定位关键代码常用的定位方法有几种关键词搜索法在Jadx中直接搜索接口路径的一部分如/house/search。但现代APP通常会将接口域名和路径拼接可能搜索不到。可以搜索域名或者搜索参数名如cityId、keyword。调用栈分析法这是更高效的方法。在Frida脚本中Hook住网络请求库的发送函数例如okhttp3.Call.execute()或HttpURLConnection.connect()然后在调用时打印堆栈信息Log.getStackTraceString(new Exception())。堆栈会清晰地告诉你这个请求是从哪个类、哪个方法发起的。Java.perform(function() { var OkHttpClient Java.use(okhttp3.OkHttpClient); OkHttpClient.newCall.implementation function(request) { var url request.url().toString(); if (url.indexOf(\ke.com\) ! -1) { // 过滤特定域名 console.log(\[*] Intercepting request to: \ url); console.log(\Stack Trace:\\n\ Java.use(\android.util.Log\).getStackTraceString(Java.use(\java.lang.Exception\).$new())); } return this.newCall(request); }; });参数回溯法如果某个请求有一个特征明显的参数值比如一个特定的时间戳或ID可以在Jadx中搜索这个值的字符串或数字可能直接定位到生成它的代码附近。通过上述方法我定位到了“某居客”请求构造的核心类通常命名为XXXApiService、XXXHttpClient或XXXNetworkManager。4.2 分析参数构造与签名逻辑找到发起请求的类和方法后在Jadx中查看其反编译的代码。你需要关注BaseUrl和路径拼接接口的完整URL是如何生成的。公共参数添加哪些参数如设备IDdeviceId、版本号appVersion、渠道channel是在拦截器Interceptor或方法开头统一添加的。签名Sign生成这是重中之重。寻找名为getSign、generateSignature、calculateSign的方法。签名算法通常是对所有请求参数有时包括请求体、时间戳、一个固定密钥secret按照一定规则如字典序排序拼接后再进行某种哈希如MD5、SHA256或HMAC运算。关键线索在代码中搜索MD5、SHA-256、Hmac、MessageDigest、Mac等关键词。动态验证用Frida Hook你怀疑的签名方法打印其输入参数Map和输出sign字符串。然后在Burp Repeater中用同样的参数手动计算一次签名看是否匹配。这是验证你分析是否正确的最直接方法。踩坑记录我最初发现一个generateSign方法它接收一个MapString, String参数返回一个字符串。Hook后发现它内部只是做了一次简单的MD5。但用这个算法计算出的签名和服务端验证不通过。后来通过更细致的堆栈分析发现在调用generateSign之前参数Map已经被另一个方法处理过添加了一个从so层获取的dynamicKey。这就是典型的Java层与Native层协作Java层负责收集和组装参数核心的密钥或部分计算逻辑放在so层以增加逆向难度。5. So层逆向分析与算法还原当发现关键参数如签名所需的密钥secret、加密数据的IV或key是通过System.loadLibrary加载的so库中的Native方法获取时战斗才真正开始。5.1 定位与提取So文件定位Native方法在Jadx中搜索native关键字或者查找调用System.loadLibrary(\xxx\)的代码。通常会在一个static块中加载。找到声明Native方法的类如NativeSignHelper。提取So文件APK本质是ZIP包解压后在其lib/目录下可能有armeabi-v7a、arm64-v8a等子目录找到对应的libxxx.so文件。将目标架构通常选arm64-v8a的so文件复制出来。5.2 静态分析入口点使用Ghidra或IDA Pro加载so文件。首先寻找导出函数Exported Functions。JNIJava Native Interface函数的命名有固定格式Java_包名_类名_方法名。例如Java_com_ke_app_util_NativeSignHelper_getEncryptKey。在Ghidra中可以在Symbol Tree窗口的Functions里过滤Java_来快速定位。找到目标函数后开始分析其汇编或反编译后的C代码。初始挑战so文件很可能经过了混淆。函数名可能被抹去只剩下地址控制流可能被平坦化Control Flow Flattening并穿插了大量无用指令垃圾代码。这会让反编译出来的代码逻辑支离破碎难以阅读。5.3 动态辅助与算法追踪静态分析受阻时动态调试是破局关键。Frida Hook Native函数我们可以直接Hook so层导出的JNI函数。// Hook Native方法打印输入输出 Interceptor.attach(Module.findExportByName(\libnative-sign.so\, \Java_com_ke_app_util_NativeSignHelper_getSign\), { onEnter: function(args) { // args[0]是JNIEnv*, args[1]是jclass/jobject, args[2]...是Java传入的参数 this.param1 args[2]; // 假设第一个参数是jstring var param1Str Java.vm.getEnv().getStringUtfChars(this.param1, null).readCString(); console.log(\[Native] getSign called with param: \ param1Str); }, onLeave: function(retval) { // retval是返回值可能是jstring var retStr Java.vm.getEnv().getStringUtfChars(retval, null).readCString(); console.log(\[Native] getSign returned: \ retStr); } });通过Hook我们可以确认这个Native函数的输入输出是什么验证它是否是我们寻找的签名或密钥生成函数。使用Frida Stalker追踪指令对于内部函数可以使用Frida的Stalker功能追踪CPU指令执行流虽然数据量大但有时能发现关键调用或循环。结合静态分析用动态获取到的具体输入输出值例如输入字符串“test”输出“a1b2c3d4”在静态反编译的代码中设置“断点”或进行数据流跟踪。在Ghidra中你可以搜索这些常量字符串或数值可能定位到关键的比较或计算代码块。5.4 还原算法逻辑在“某居客”的案例中我追踪到一个关键的Native函数它接收时间戳和设备ID的一部分经过一系列位运算、查表S-Box和循环后生成一个16字节的dynamicKey。这个dynamicKey会与Java层拼接好的参数字符串组合再进行一次HMAC-SHA256运算最终得到提交的sign。还原步骤理解函数原型明确输入参数的数量和类型int, string, byte array以及返回值类型。梳理主流程忽略混淆的垃圾代码和虚假分支聚焦在那些对输入参数进行实际操作的指令上。寻找常见的加密算法特征如AESAES_encrypt/AES_decrypt函数调用或者明显的SubBytes、ShiftRows、MixColumns、AddRoundKey操作序列可通过查找固定的轮常数Rcon来辅助识别。MD5/SHA初始化魔数如MD5的0x67452301等以及固定的循环位移操作。Base64编码表ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789/和填充。TEA/XXTEA简单的位运算、加法和异或配合一个魔数如0x9E3779B9。模拟实现用Python或C语言按照分析出的逻辑重写算法。这是一个反复验证的过程用相同的输入你的代码输出必须与Hook抓取的输出完全一致。验证将还原的算法用于构造新的请求参数用Burp Repeater发送看服务端是否正常响应。这是最终的验收标准。注意事项So层分析极其耗时需要耐心。有时算法是标准算法的轻微变种修改了S盒、初始向量IV或者增加了额外的置换步骤。可以尝试将so文件中的常量数组提取出来与标准算法如AES的S盒进行对比能快速识别算法类型。6. 完整请求构造与复现在成功还原了Java层的参数组装逻辑和so层的核心加密/签名算法后就可以脱离APP用任何编程语言如Python来模拟发送请求了。6.1 构建请求流程一个完整的自动化请求脚本通常包含以下步骤设备信息模拟生成或固定deviceId、imei可随机生成符合格式的、androidId等设备标识。获取动态密钥调用还原的Native算法函数已用Python实现传入必要的种子如时间戳计算出本次请求使用的dynamicKey或secret。组装业务参数根据要查询的内容如城市、页码、筛选条件构造参数字典。生成签名将业务参数、公共参数、动态密钥按照APP的规则如按Key字典序排序后拼接成k1v1k2v2...的格式拼接成待签名字符串。使用还原的签名算法如HMAC-SHA256计算签名并将签名值加入请求参数。发送请求设置好必要的请求头如User-Agent、Content-Type、Authorization(如果有Token)使用requests库发送HTTP请求。处理响应解析返回的JSON数据。如果数据被加密还需调用还原的解密算法进行解密。6.2 Python实现示例片段import hashlib import hmac import time import requests # 1. 还原的So层算法生成dynamic_key def native_get_dynamic_key(timestamp): # 这里是逆向还原的算法可能包含位运算、查表等 seed str(timestamp)[-6:] # ... 复杂的计算过程 ... dynamic_key \a1b2c3d4e5f67890\ # 示例输出 return dynamic_key # 2. 还原的签名算法 def generate_sign(params_dict, dynamic_key): # 步骤1: 参数排序并拼接 sorted_params \\.join([f\{k}{params_dict[k]}\ for k in sorted(params_dict.keys())]) # 步骤2: 拼接动态密钥 string_to_sign sorted_params \key\ dynamic_key # 步骤3: 计算HMAC-SHA256 (假设这是还原出的算法) sign hmac.new(dynamic_key.encode(\utf-8\), string_to_sign.encode(\utf-8\), hashlib.sha256).hexdigest() return sign # 3. 构造请求 def fetch_house_list(city_id, page): timestamp int(time.time() * 1000) dynamic_key native_get_dynamic_key(timestamp) base_params { \cityId\: city_id, \page\: str(page), \pageSize\: \20\, \timestamp\: str(timestamp), \deviceId\: \模拟的设备ID\, \appVersion\: \9.0.0\ } sign generate_sign(base_params, dynamic_key) base_params[\sign\] sign headers { \User-Agent\: \Dalvik/2.1.0 (Linux; U; Android 10; MI 8 Build/QQ3A.200805.001)\, \Content-Type\: \application/x-www-form-urlencoded\ } response requests.post(\https://app.api.ke.com/api/v1/house/list\, database_params, headersheaders) return response.json() # 使用 if __name__ \__main__\: data fetch_house_list(\110000\, 1) print(data)7. 常见问题排查与对抗升级在整个过程中你会遇到各种问题。以下是一些常见问题及解决思路的速查表问题现象可能原因排查与解决思路抓不到任何包1. 代理未正确设置或未生效。2. APP检测并禁用了代理。3. 使用了纯Socket或自定义协议。1. 检查手机代理设置和Burp监听端口。2. 使用Frida HookSystem.getProperty(\http.proxyHost\)等检测方法并返回空。3. 使用tcpdump或Wireshark在手机上抓取原始网络包分析。HTTPS请求显示为Tunnel to证书绑定SSL Pinning生效。1. 确认Burp CA证书已安装到系统证书目录。2. 使用JustTrustMe等Xposed模块。3. 编写Frida脚本Hook证书验证逻辑。APP启动后闪退1. 检测到Root环境。2. 检测到调试器或注入工具如Frida。3. So层有反调试。1. 使用Magisk Hide隐藏Root。2. 使用Frida的-f参数以spawn方式启动APP而非attach。3. 使用objection的android root disable或android anti-root disable命令。4. 分析so中的ptrace、fork等反调试函数并用Frida绕过。签名一直无效1. 签名算法分析错误。2. 遗漏了某个参数如请求体、Cookie。3. 密钥或盐值是动态的未正确获取。1. 用Frida Hook多个可能的签名函数对比输入输出。2. 对比多个成功请求的参数差异找出规律。3. 检查网络请求拦截器看是否有全局添加的参数。4. 确认从so层获取的动态值是否正确。So层函数无法Hook1. 函数符号被剥离stripped。2. 函数是静态的未导出。3. So文件有完整性校验。1. 使用函数地址而非符号名进行HookModule.findBaseAddress(\libxxx.so\).add(0x1234)。2. 在JNI调用的Java层方法上进行Hook间接追踪。3. 使用内存扫描找到函数特征码。反编译代码逻辑混乱代码被混淆ProGuard等。1. 结合动态调试关注实际执行的代码流。2. 根据字符串常量、资源ID等线索推测类和方法名。3. 尝试使用更强大的反编译器如JEB。对抗升级的思考作为分析方我们也要意识到我们的技术手段也在推动防护技术的升级。例如越来越多的APP开始使用白盒加密将密钥与算法深度融合、虚拟机保护VMP将关键代码转换为自定义指令集在虚拟机中执行、服务器协同关键参数由服务器下发每次不同等更高级的方案。面对这些逆向的难度和成本会指数级上升往往需要结合静态分析、动态调试、硬件调试乃至侧信道分析等多种手段。这也意味着在合规的前提下进行安全研究需要持续学习和更新知识库。