当前位置: 首页 > news >正文

Charles抓包+Frida Hook破解Android签名反爬实战

1. 这不是“抓包教学”而是移动端反爬攻防现场还原你有没有遇到过这样的情况用Charles把App的HTTPS请求全抓下来了接口地址、参数、headers一清二楚可一写Python脚本模拟请求立刻返回{code:403,msg:Invalid signature}或者更糟——连请求都发不出去直接被客户端本地校验拦在启动页这不是网络问题是典型的移动端主动式反爬体系在生效。它和网页端“看源码改User-Agent”完全不同签名算法藏在so里、时间戳由JNI生成、设备指纹实时采集、关键逻辑在Native层混淆加密。而本篇标题里的“Charles抓包Frida Hook破解”正是当前一线安全研究员与逆向工程师在真实项目中每天都在做的标准动作组合——前者定位“它在传什么”后者回答“它凭什么能传”。我做过7个金融类App、4个电商类SDK的深度分析发现92%的签名失效问题根源不在Python端构造错误而在你根本没意识到客户端在调用getSign()前已经用Frida劫持了System.currentTimeMillis()并注入了偏移量。本文不讲抽象概念只复现一个完整闭环从Charles看到一个带signxxx的POST请求开始到用Frida精准定位签名函数、动态修改输入、验证输出、最终用Python稳定复现整个签名流程。所有步骤基于Android 12真机实测工具链全部开源可验证参数命名完全贴合真实App代码风格比如v1,t2,k3这类无意义变量名拒绝理想化Demo。适合两类人一是Python后端/爬虫工程师想突破移动端数据获取瓶颈二是刚接触逆向的安全新人需要一条不绕弯、不跳步、能真正跑通的实战路径。2. Charles抓包的致命盲区为什么你看到的“明文”其实是假象2.1 HTTPS解密只是起点不是终点很多人以为在Charles里配置好SSL Proxying、手机安装证书、开启HTTPS代理就能看到“所有流量”。这是最大的认知陷阱。Charles确实能解密TLS层但它展示的是应用层协议解包后的结果——而这个“结果”可能已经被客户端主动污染。举个真实案例某银行App的登录接口Charles显示的请求体是标准JSON{ mobile: 138****1234, password: e10adc3949ba59abbe56e057f20f883e, timestamp: 1715823456, sign: a1b2c3d4e5f67890 }表面看密码是MD5时间戳是秒级Unix时间签名是固定长度字符串。但当你用Python按此结构POST服务端始终返回401 Unauthorized。问题出在哪——password字段根本不是原始密码的MD5。客户端在提交前先调用了一个叫encryptPassword()的Native方法该方法接收原始密码设备ID当前毫秒时间戳非秒级三元组经AES-CBC加密后再Base64编码。Charles看到的e10adc3949ba59abbe56e057f20f883e是这个加密结果的十六进制字符串表示而非MD5哈希值。它只是恰好长得像MD5本质是密文。这种“语义欺骗”在金融、政务类App中极为普遍目的是让分析者误判算法类型浪费数天时间暴力穷举MD5碰撞。提示在Charles中右键点击任意HTTPS请求 → “View Request in Raw” → 观察Content-Type和实际字节流。如果Content-Type是application/json但响应体包含大量不可见字符如\x00\x01\xFF基本可判定存在二进制编码或加密。2.2 时间戳陷阱客户端与服务器的“时钟战争”另一个高频踩坑点是timestamp。你以为传个int(time.time())就行错。真实场景中客户端会做三重校验本地校验检查系统时间是否在服务器允许窗口内如±300秒超时则拒绝发起网络请求服务端校验服务器收到后用自身时间戳比对要求差值120秒签名绑定校验sign字段的计算必须包含精确到毫秒的时间戳且该时间戳由JNI调用System.nanoTime()生成而非Java层System.currentTimeMillis()。我在分析某证券App时发现其签名算法核心伪代码如下// Java层调用入口你看到的 String sign SignUtil.generateSign( map.get(mobile), map.get(password), System.currentTimeMillis() / 1000 // 注意这里除以1000 ); // 实际Native层so文件实现 jstring Java_com_example_SignUtil_generateSign(JNIEnv *env, jclass clazz, jstring mobile, jstring pwd, jlong ts_sec) { // 关键ts_sec是Java层传入的秒级时间但Native层会 jlong real_ts_ms getRealTimestamp(); // 调用自定义JNI非SystemClock jlong delta real_ts_ms - (ts_sec * 1000); // 计算毫秒级偏差 // 然后将delta作为盐值参与HMAC-SHA256计算 return hmac_sha256(mobile, pwd, delta); }这意味着你在Charles里看到的timestamp: 1715823456只是签名算法的一个中间参数真正的动态因子是delta。如果你在Python里直接用int(time.time())delta恒为0签名必然失败。而Charles无法告诉你getRealTimestamp()的实现逻辑——它藏在libcrypto.so的sub_12345函数里被OLLVM混淆过。2.3 设备指纹看不见的第四个必填参数几乎所有严肃的移动端API都隐式携带设备指纹。它不会出现在Charles的Request Body里但一定存在于Header或URL Query中。常见载体有X-Device-ID: 基于Android ID IMEI MAC地址拼接后MD5X-App-Version: 不是简单的1.2.3而是1.2.3_202405151423含构建时间戳X-Signature: 与Body中sign不同这是对整个HTTP请求头不含Host的二次签名我在测试某外卖平台时发现即使Body和URL完全一致仅修改Header中的User-AgentX-Signature就会变化。进一步Hook发现其计算逻辑为# 伪代码实际在so中 def calc_header_signature(headers): # 1. 过滤掉Host、Connection等标准头 filtered {k:v for k,v in headers.items() if k not in [Host,Connection]} # 2. 按key字典序排序并拼接成字符串 sorted_str .join([f{k}{v} for k,v in sorted(filtered.items())]) # 3. 加入硬编码密钥存于so的.rodata段 key get_hardcoded_key() # Frida可dump return hmac_sha256(sorted_str, key)Charles能看到X-Signature的值但看不到get_hardcoded_key()的来源。这就是为什么单纯复制Header永远无法复现——你缺了那个藏在Native层的密钥。3. Frida Hook的精准打击从“找到函数”到“控制输入”的全流程3.1 为什么不用Xposed/JustTrustMe——环境适配性决定效率很多教程推荐用Xposed框架配合JustTrustMe模块来绕过SSL Pinning这在Android 7以下很有效。但现实是2024年主流App最低支持Android 10而Xposed在Android 10上需Magisk模块且稳定性极差更重要的是JustTrustMe只能解决证书校验对Native层签名、设备指纹毫无作用。Frida的优势在于跨Android版本一致性Frida Gadget支持Android 7~14无需Root通过frida -U -f com.xxx.app --no-pause附加已root设备或使用frida-server静默注入Native层直击能力可Hookdlopen、dlsym、__android_log_print等底层函数直接监控so加载和日志输出动态上下文捕获不仅能Hook函数入口还能在函数执行中读取寄存器、内存、调用栈精准定位参数来源我对比过三种方案在某银行App上的表现方案SSL解密成功率Native函数Hook成功率启动耗时稳定性XposedJustTrustMe100%0%so未加载1sAndroid 12崩溃率40%Objection基于Frida100%85%需手动找符号3s稳定纯Frida脚本100%100%可Hook地址2s稳定结论当目标明确指向Native层反爬时Frida是唯一可靠选择。3.2 定位签名函数的四步法不依赖符号表的实战技巧没有符号表stripped so是常态。以下是我在7个App中验证有效的定位流程第一步监控所有so加载事件// frida-script.js Java.perform(() { const Runtime Java.use(java.lang.Runtime); Runtime.exec.overload(java.lang.String).implementation function(cmd) { console.log([] exec: cmd); return this.exec(cmd); }; // 监控dlopen调用关键 Interceptor.attach(Module.findExportByName(null, dlopen), { onEnter: function(args) { const path args[0].readCString(); if (path path.includes(lib)) { console.log([] dlopen: path); // 此时so已加载可立即枚举导出函数 const module Process.findModuleByName(path.split(/).pop()); if (module) { console.log([] ${path} base: ${module.base}); } } } }); });运行后你会看到类似[] dlopen: /data/app/~~xxx/com.xxx.app/lib/arm64/libsecurity.so的日志。记下libsecurity.so和它的基址。第二步搜索可疑字符串Native层签名函数常包含关键词。用Frida搜索内存// 在dlopen回调后执行 const lib Process.findModuleByName(libsecurity.so); if (lib) { // 搜索sign, hmac, sha, md5等 const pattern sign; const matches Memory.scanSync(lib.base, lib.size, pattern); console.log([] Found ${matches.length} matches for ${pattern}); matches.forEach(match { console.log([] Match at ${match.address} - ${match.address.readUtf8String(32)}); }); }在某贷款App中我们搜到/data/data/com.xxx.app/files/sign_key_v2这直接暴露了密钥存储路径。第三步Hook JNI_OnLoad定位Java-Native桥接点所有Java调用Native函数必经JNI_OnLoad注册。Hook它可捕获所有RegisterNatives调用Interceptor.attach(Module.findExportByName(libsecurity.so, JNI_OnLoad), { onEnter: function(args) { console.log([] JNI_OnLoad called); // 枚举所有注册的Native方法 const env args[1]; const jni new JavaVM(env); // 此处可遍历JNINativeMethod数组... } });实际中我们发现SignUtil.generateSign对应Native函数地址为0x7a12345678。第四步动态参数追踪——这才是核心找到地址后不能直接Hook要先看它收什么参数Interceptor.attach(ptr(0x7a12345678), { onEnter: function(args) { console.log([] generateSign called); console.log([] arg0 (JNIEnv): args[0]); console.log([] arg1 (jclass): args[1]); console.log([] arg2 (jstring mobile): args[2].readCString()); console.log([] arg3 (jstring pwd): args[3].readCString()); console.log([] arg4 (jlong ts): args[4].toInt32()); // 关键保存参数供后续修改 this.mobile args[2].readCString(); this.pwd args[3].readCString(); this.ts args[4].toInt32(); }, onLeave: function(retval) { console.log([] generateSign returned: retval.readCString()); } });运行后你会看到真实参数值。此时发现ts参数恒为1715823456与Charles中一致证实了前述“秒级时间戳”猜想。3.3 修改参数并验证让签名函数为你打工仅仅观察不够要能控制。在onEnter中修改args[4]时间戳参数onEnter: function(args) { // ... 参数打印 ... // 强制修改时间戳为当前毫秒时间注意单位 const now_ms Date.now(); args[4] ptr(now_ms); // Frida自动处理类型转换 // 更激进修改密码参数注入调试标记 const new_pwd this.pwd _DEBUG_ now_ms; const new_pwd_ptr Memory.allocUtf8String(new_pwd); args[3] new_pwd_ptr; // 替换jstring指针 },此时再触发App请求Charles中会看到password字段末尾多了_DEBUG_1715823456789且sign值发生变化。这证明我们已获得函数控制权。注意修改jstring需分配新内存并传递指针直接args[3].writeUtf8String()会崩溃因jstring是JNI对象句柄非C字符串。4. Python端完整复现从Frida日志到稳定签名生成4.1 密钥提取从so内存dump到Python可用密钥Frida Hook只能看到运行时状态要Python复现必须提取静态密钥。方法是dump so的.rodata段只读数据段密钥多存于此# 在手机上执行需root adb shell su -c cat /data/app/~~xxx/com.xxx.app/lib/arm64/libsecurity.so /sdcard/libsecurity.so adb pull /sdcard/libsecurity.so ./libsecurity.so用readelf -S libsecurity.so查看段信息Section Headers: [Nr] Name Type Addr Off Size ES Flg Lk Inf Al [13] .rodata PROGBITS 0000000000012000 00012000 00001000 00 WA 0 0 1用Python读取该段with open(libsecurity.so, rb) as f: f.seek(0x12000) # .rodata起始偏移 rodata f.read(0x1000) # 读取4KB # 搜索ASCII密钥长度16/24/32字节 import re keys re.findall(b[A-Za-z0-9/]{16,32}, rodata) for k in keys: try: print(Found key:, k.decode()) except: pass在某教育App中我们提取到bXyZ9aBcD1eF2gH3i长度16字节正是AES-128密钥。4.2 签名算法逆向从汇编到Python的逐行翻译拿到密钥后需逆向签名逻辑。用Ghidra打开so定位generateSign函数。关键汇编片段LOAD:0000000000001234 ADRP X8, #aHmacSha256PAGE ; hmac-sha256 LOAD:0000000000001238 ADD X8, X8, #aHmacSha256PAGEOFF ; hmac-sha256 LOAD:000000000000123C MOV X0, X19 ; mobile ptr LOAD:0000000000001240 MOV X1, X20 ; pwd ptr LOAD:0000000000001244 MOV X2, X21 ; ts_sec LOAD:0000000000001248 BL hmac_sha256 ; 调用跟进hmac_sha256发现其逻辑为将mobile、pwd、ts_sec按|拼接mobile | pwd | str(ts_sec)用.rodata中密钥做HMAC-SHA256取结果前16字节转hex小写Python实现import hmac import hashlib def generate_sign(mobile: str, pwd: str, ts_sec: int, key: bytes bXyZ9aBcD1eF2gH3i) - str: 复现Native层generateSign逻辑 # 步骤1拼接字符串 data f{mobile}|{pwd}|{ts_sec} # 步骤2HMAC-SHA256 h hmac.new(key, data.encode(), hashlib.sha256) # 步骤3取前16字节hex return h.digest()[:16].hex() # 验证 print(generate_sign(138****1234, e10adc3949ba59abbe56e057f20f883e, 1715823456)) # 输出: a1b2c3d4e5f678901234567890abcdef将此结果填入Python请求的sign字段服务端返回200 OK。4.3 设备指纹同步Python端生成等效X-Device-ID设备ID通常由以下组件拼接ANDROID_IDSettings.Secure.ANDROID_IDIMEITelephonyManager.getImei()需权限MAC地址WifiManager.getConnectionInfo().getMacAddress()但App往往做变换。用Frida HookTelephonyManager.getImei()发现其返回值被截断为前8位后4位中间用*填充Interceptor.attach(telephonyClass.$new, { onEnter: function(args) { console.log([] TelephonyManager created); } }); // Hook getImei const tm Java.use(android.telephony.TelephonyManager); tm.getImei.overload().implementation function() { const imei this.getImei(); console.log([] Original IMEI: imei); // App实际使用imei.substring(0,8) **** imei.substring(12) const masked imei.substring(0,8) **** imei.substring(12); console.log([] Masked IMEI: masked); return masked; };Python端同步逻辑def generate_device_id(android_id: str, imei: str, mac: str) - str: 生成等效X-Device-ID # App逻辑android_id masked_imei mac.upper() SALT masked_imei imei[:8] **** imei[12:] raw android_id masked_imei mac.upper() SALT return hashlib.md5(raw.encode()).hexdigest() # 使用示例需从真实设备获取 android_id 9774d56d682e549c imei 861234567890123 mac 00:11:22:33:44:55 print(generate_device_id(android_id, imei, mac)) # 输出: 3a7bd3e2360a3d29eea436fcfb7e44c74.4 完整Python请求模板可直接运行的生产级代码整合所有要素形成稳定请求import requests import time import hashlib import hmac import json class MobileAPIClient: def __init__(self, android_id: str, imei: str, mac: str): self.android_id android_id self.imei imei self.mac mac self.session requests.Session() # 设置全局Headers self.session.headers.update({ User-Agent: Dalvik/2.1.0 (Linux; U; Android 12; Pixel 5 Build/SP1A.210812.016), X-App-Version: 3.2.1_202405151423, X-Device-ID: self._gen_device_id(), }) def _gen_device_id(self) - str: masked_imei self.imei[:8] **** self.imei[12:] raw self.android_id masked_imei self.mac.upper() SALT return hashlib.md5(raw.encode()).hexdigest() def _gen_sign(self, mobile: str, pwd: str, ts_sec: int) - str: data f{mobile}|{pwd}|{ts_sec} key bXyZ9aBcD1eF2gH3i h hmac.new(key, data.encode(), hashlib.sha256) return h.digest()[:16].hex() def login(self, mobile: str, password_md5: str) - dict: ts int(time.time()) sign self._gen_sign(mobile, password_md5, ts) payload { mobile: mobile, password: password_md5, timestamp: ts, sign: sign } # 计算X-SignatureHeader签名 header_str .join([ fUser-Agent{self.session.headers[User-Agent]}, fX-App-Version{self.session.headers[X-App-Version]}, fX-Device-ID{self.session.headers[X-Device-ID]} ]) header_sign hmac.new( bHEADER_KEY_2024, header_str.encode(), hashlib.sha256 ).hexdigest()[:16] self.session.headers[X-Signature] header_sign resp self.session.post( https://api.xxx.com/v1/login, jsonpayload, timeout10 ) return resp.json() # 使用 client MobileAPIClient( android_id9774d56d682e549c, imei861234567890123, mac00:11:22:33:44:55 ) result client.login(138****1234, e10adc3949ba59abbe56e057f20f883e) print(result) # {code:200, data: {...}}此代码已在3个App中稳定运行超30天日均请求2000次零失败。5. 经验总结那些文档里不会写的12条血泪教训5.1 Frida脚本的“三不原则”不依赖函数名Module.findExportByName(libxxx.so, generateSign)在stripped so中必然失败。永远用Module.findBaseAddress(libxxx.so)获取基址再用ptr(base.add(0x12345))硬编码地址地址可通过Ghidra静态分析获得。不信任日志输出console.log()在高频率Hook时会严重拖慢App甚至导致ANR。生产环境务必用send()发送到Python端处理console.log()仅用于调试。不忽略线程上下文generateSign可能在子线程如OkHttp Dispatcher中调用。Frida默认Hook主线程需显式指定Thread.backtrace()或this.context读取寄存器。5.2 Charles的“三查清单”每次抓包后强制执行查Content-Encoding若为gzip右键→“Decode gzip”再分析否则看到的是压缩乱码查Response Header的Set-Cookie很多App的session_id在首次响应头中设置后续请求必须携带Charles会自动管理但Python需手动提取查WebSocket连接金融类App常用WS推送实时行情其消息体可能是Protobuf二进制需用ws://协议单独抓取不能只看HTTP。5.3 Python复现的“四避坑”时间戳精度陷阱服务端校验的是System.currentTimeMillis()但Python的time.time()返回浮点秒。必须用int(time.time() * 1000)再除以1000确保秒级对齐字符串编码雷区Native层readCString()默认UTF-8但某些App用GBK编码中文参数。Frida中需readCString(gbk)Python端payload.encode(gbk)保持一致密钥硬编码位置.rodata段找不到密钥检查.data段可读写或.bss段未初始化。用strings libxxx.so | grep -E [A-Z0-9]{12,}快速扫描签名有效期sign通常5分钟失效。Python端需缓存ts与sign映射相同ts直接复用避免重复计算。5.4 最后一条永远验证你的假设我在某政务App上曾卡住3天因为假设sign是HMAC-SHA256但实际是SM3国密算法。破局方法很简单用Frida HookCrypto.getInstance(SM3)确认算法类名再HookMessageDigest.digest()打印输入字节数组。当看到输入是[0x31,0x32,0x33,...]ASCII码而输出是32字节立刻确定是SM3。Python端换用pysm3库一行解决from pysm3 import sm3_hash sign sm3_hash(f{mobile}|{pwd}|{ts}.encode()).lower()[:16]所有反爬分析的本质都是不断提出假设、设计实验、验证结果的过程。工具只是手脑子才是武器。我在实际操作中发现最高效的节奏是Charles抓包5分钟→ Frida Hook定位20分钟→ Ghidra逆向1小时→ Python复现15分钟→ 全流程验证5分钟。这套组合拳下来90%的移动端反爬可在2小时内突破。关键不是工具多炫酷而是每一步都带着明确的问题意识Charles看到的真的是真相吗Frida Hook到的真的是源头吗Python复现的真的覆盖了所有边界条件吗当你开始问这些问题你就已经站在了反爬攻防的正确起跑线上。
http://www.rkmt.cn/news/1369421.html

相关文章:

  • 苏州生产型外贸商家建站纠结?5家跨境电商建站服务公司测评,WaiMaoYa(外贸鸭)适配全场景出海 - 外贸营销工具
  • 如何在Windows电脑上高效安装安卓应用?APK-Installer完整指南
  • QKeyMapper:终极Windows按键映射工具完全指南 - 免费开源游戏手柄映射神器
  • OBS Advanced Timer:7种计时模式让你的直播时间管理精准无忧
  • Windows上安装安卓应用的终极方案:APK Installer深度体验指南
  • VSCode-R扩展:构建企业级R语言开发环境的技术方案
  • 2026年Hermes Agent/OpenClaw怎么部署?阿里云高性能部署及Token Plan配置
  • 初次使用taotoken模型广场进行模型选型与测试的流程感受
  • C# PriorityQueue优先队列方法详解
  • 如何打造个性化AI工作台:Chatbox界面定制终极指南
  • 终极QMC音频解密方案:3步解锁你的加密音乐库
  • WechatDecrypt终极指南:3步快速解密微信聊天记录
  • PvZ Toolkit深度解析:植物大战僵尸PC版内存修改器的架构设计与实现机制
  • AI换脸终极指南:5分钟掌握roop-unleashed完整教程
  • 如何快速掌握开源无人机数据处理工具:5步生成专业级三维模型与正射影像
  • 算法日记 | C++ 结构体
  • 2025年StreamFX完整教程:OBS直播效果提升终极指南
  • JMeter HTML报告Charts模块深度解析与避坑指南
  • 计算机组成原理 | 无符号整数除法原理
  • 解锁Mac新玩法:用Whisky轻松运行Windows应用与游戏终极指南
  • Gifsicle:命令行中的GIF魔术师,让你的动画图片更轻更快
  • 深度换脸技术革新:roop-unleashed如何重新定义AI视频编辑
  • 智能电视上网难题?TV Bro浏览器:用遥控器轻松浏览网页的终极方案
  • EnKF(集合卡尔曼滤波)在气象预测与金融时间序列分析中的应用对比
  • 2026货运配送行业获客新玩法!推广营销靠谱的GEO优化系统公司,依托大模型流量稳稳接单 - 一点学习库
  • 零标注情感分析:GPT筛选+领域BERT+传统分类器实战
  • 具身智能的发展趋势对社会伦理道德有哪些挑战?
  • 如何用roop-unleashed突破数字创作极限:从技术黑盒到创意白盒的深度解析
  • 3个实战场景揭秘LogExpert:Windows日志分析的终极解决方案
  • 基于BERT与K-Means的法律文本智能分析:GDPR与CCPA合规自动化实践