1. 项目概述:一次针对某台球App的逆向工程实战
最近在技术社区里,关于移动应用安全与逆向分析的讨论热度不减,尤其是涉及到一些特定领域的应用。今天我想分享一个我近期完成的实战案例:对一款名为“某台球”的移动应用进行逆向分析。这个项目并非为了破解或非法用途,而是作为一名安全研究员,出于技术研究和学习的目的,去深入理解其客户端与服务器端的交互逻辑、数据加密方式以及潜在的协议实现。对于移动开发、安全测试以及对网络协议感兴趣的同行来说,这个过程能让你对App的“内里”有更直观的认识,了解数据是如何被封装、传输和解密的。如果你正想学习如何安全、合规地分析一个Android应用,拆解其网络请求,那么这个案例的完整思路和实操细节,或许能给你提供一个清晰的参考路径。
“某台球”这类应用通常涉及实时对战、用户数据同步、虚拟物品消费等核心功能,其通信安全是开发者重点防护的环节。我们的目标就是像解谜一样,层层剥开它的外衣,看看它是如何“说话”的。整个过程会涉及APK拆包、静态分析、动态调试、抓包分析以及协议模拟等多个环节。我会尽量用通俗的语言,结合具体的操作步骤和踩过的坑,把这次逆向之旅讲清楚。无论你是刚入门的新手,还是有一定经验的开发者,都能从中找到值得借鉴的点。
2. 逆向分析的整体思路与工具选型
逆向工程就像侦探破案,需要一个清晰的调查思路和一套顺手的工具。对于移动应用,尤其是像“某台球”这样可能做了加固和混淆的应用,盲目动手只会事倍功半。我的核心思路是“由外而内,动静结合”。
2.1 核心分析路径设计
首先,我们需要明确分析的目标。对于这个案例,我主要关注以下几点:
- 通信协议识别:App使用HTTP/HTTPS还是自定义的TCP/UDP协议?如果是HTTP,接口地址和参数格式是怎样的?
- 数据加密/编码分析:请求参数和响应数据是明文的吗?如果被加密或编码,使用了何种算法(如AES、RSA、Base64变种)?密钥是如何生成或存储的?
- 核心业务逻辑定位:例如,用户登录、开始游戏、击球动作同步、积分结算等关键功能对应的代码位置在哪里?
- 签名与校验机制:请求是否带有防篡改的签名?客户端和服务器之间如何进行合法性校验?
基于这些目标,我设计的路径是:先通过抓包了解网络通信概况(外),再通过静态分析定位关键代码(内),最后用动态调试验证猜想(动静结合)。
2.2 工具链的组建与考量
工欲善其事,必先利其器。以下是本次分析用到的主要工具及选择理由:
抓包工具:Charles + 安卓模拟器(如夜神)
- Charles:老牌且功能强大的HTTP/HTTPS抓包代理。它的优势在于界面直观,能方便地查看、修改、重发请求,对于分析HTTP(S)协议至关重要。配置SSL证书后可以解密HTTPS流量。
- 选择理由:相比Fiddler,Charles对JSON等格式的展示更友好;相比mitmproxy,它提供了图形化界面,对初学者更友好。首先用它来判断App是否使用HTTP协议。
- 模拟器:在电脑上运行安卓环境,方便配合Charles进行抓包,避免对真机进行复杂的证书安装和网络配置。
静态分析工具:Jadx-GUI + Android Killer
- Jadx-GUI:当前最优秀的Java反编译工具之一,能将DEX文件反编译成可读性相当高的Java代码。它支持全局搜索、跳转引用、查看资源文件,是静态分析的起点。
- Android Killer:一个集成了Apktool、签名、编译等功能的工具箱。我主要用它来解包APK(得到Smali代码和资源文件),以及进行简单的字符串搜索和修改尝试。它的资源查看功能有时比Jadx更直接。
- 选择理由:Jadx用于快速浏览和搜索Java层逻辑,Android Killer用于处理资源文件和进行底层的Smali修改。两者互补。
动态调试工具:IDA Pro + 调试服务器(android_server)
- IDA Pro:逆向分析的“瑞士军刀”,尤其擅长分析原生库(.so文件)。如果应用的关键逻辑(如加密算法)用C/C++实现并编译进了so库,就必须用它来调试。
- android_server:IDA Pro附带的调试服务器程序,运行在安卓设备(或模拟器)上,与电脑端的IDA Pro通信。
- 选择理由:当静态分析遇到高度混淆或遇到Native层代码时,动态调试是唯一能看清执行流程和数据变化的方法。
辅助工具
- adb(Android Debug Bridge):必备命令行工具,用于安装应用、推送文件、获取日志等。
- Frida:一个动态插桩工具,可以注入JavaScript脚本来Hook Java和Native函数,实时获取参数和返回值。在本次分析中用于快速验证加密函数。
- Postman/Insomnia:用于模拟和测试分析出来的API接口。
注意:所有分析活动必须在你自己拥有完全控制权的设备和应用副本上进行,并且仅用于学习与研究目的。尊重知识产权和法律法规是技术人员的底线。
3. 环境准备与初步侦查
在开始深入分析之前,搭建一个稳定、隔离的分析环境至关重要。这一步做得好,能避免很多后续的麻烦。
3.1 抓包环境搭建与证书信任
我的操作是在Windows系统上,使用夜神模拟器(Android 7)和Charles进行的。
Charles配置:
- 启动Charles,设置代理端口(默认为8888)。
- 在
Proxy -> SSL Proxying Settings中,添加一个通配符规则,如*:*,以便捕获所有HTTPS站点的流量。 - 在
Help -> SSL Proxying中,选择Install Charles Root Certificate on a Mobile Device or Remote Browser,会弹出一个提示,告诉你如何在设备上设置代理并安装证书。
模拟器网络配置:
- 启动夜神模拟器,进入系统设置 -> WLAN -> 长按当前网络 -> 修改网络。
- 设置代理为“手动”,代理主机名填写你电脑的IP地址(在Charles的
Help -> Local IP Address中可以查看),端口填写Charles的代理端口(8888)。 - 保存后,在模拟器的浏览器中访问
chls.pro/ssl,下载并安装Charles的根证书。关键一步:在Android 7及以上版本,用户安装的证书默认不被应用信任。你需要将证书移动到系统证书目录。这通常需要root权限。在已root的模拟器中,可以使用RE文件管理器将下载的证书(通常位于/data/misc/user/0/cacerts-added/或/sdcard/Download/)复制到/system/etc/security/cacerts/,并修改其权限为644(rw-r--r--)。然后重启模拟器。
初步抓包测试:
- 配置完成后,在Charles中确保“代理”是开启状态。
- 在模拟器中打开浏览器访问一个HTTPS网站(如百度),如果Charles能成功捕获并解密流量(显示为明文),说明抓包环境搭建成功。如果遇到“unknown”或证书错误,通常是证书未正确安装或未被系统信任。
3.2 APK获取与基础信息探查
获取到“某台球”的APK文件后,不要急着反编译,先看看它的“身份证”。
使用
apktool或Android Killer解包:apktool d your_app.apk -o output_dir。这个命令会将APK解压到一个目录,里面包含:AndroidManifest.xml:应用的清单文件,包含权限、组件、版本等信息。res/:资源文件目录。smali/:所有的Smali代码(相当于Java字节码的汇编语言)。lib/:包含不同CPU架构(如armeabi-v7a, arm64-v8a)的Native库(.so文件)。assets/和unknown/:可能存放配置文件、游戏资源等。
查看AndroidManifest.xml:重点关注以下信息:
package:应用的包名,是代码的唯一标识。uses-permission:申请了哪些权限?网络、存储、电话状态等。application标签下的属性:特别是android:debuggable(是否可调试,发布版通常为false),以及是否有android:networkSecurityConfig指向一个网络安全配置文件,这可能意味着它使用了证书锁定(Certificate Pinning),会阻碍Charles抓HTTPS包。- 主Activity:找到应用启动的第一个界面。
检查加固情况:用解压软件直接打开APK,查看根目录下是否有
lib/文件夹以及里面so文件的命名。如果看到libshella-.so,libprotect.so,libexec.so等,或者classes.dex文件大小异常小,主逻辑都在so库里,那很可能使用了腾讯乐固、梆梆、爱加密等第三方加固。对于“某台球”,初步查看发现classes.dex文件大小正常,且lib目录下so文件命名较为常规(如libcocos2dcpp.so,说明它可能基于Cocos游戏引擎),初步判断没有使用强力的第三方商业加固,这降低了分析门槛。
4. 静态分析与关键代码定位
环境准备好后,就可以开始“阅读”应用的源代码了。静态分析的目标是从海量代码中找到与我们目标(网络协议、加密)相关的蛛丝马迹。
4.1 使用Jadx进行全局搜索与入口定位
将APK文件直接拖入Jadx-GUI,它会自动进行反编译。
搜索网络相关关键字:这是最直接的方法。在Jadx的搜索框(通常按两下Shift键)中搜索以下关键词:
- 域名/URL片段:如果在抓包中看到了任何域名或接口路径(如
/api/login),直接搜索它们。 - 网络库特征:搜索
okhttp3,retrofit2,HttpURLConnection,Socket等。我发现“某台球”大量使用了okhttp3,这是一个明确的线索。 - 加密相关:搜索
AES,DES,RSA,MD5,SHA,Base64,encrypt,decrypt,encode,decode等。 - 关键业务:搜索
login,auth,token,startGame,shot,score等。
- 域名/URL片段:如果在抓包中看到了任何域名或接口路径(如
定位网络请求封装类:通过搜索
okhttp3.OkHttpClient或retrofit2.Retrofit,我很快找到了一个名为NetworkClient或ApiService的类。这类通常负责创建OkHttpClient实例,配置拦截器(Interceptor),并定义所有的API接口。分析拦截器(Interceptor):OkHttp的拦截器是修改请求和响应的绝佳位置,加密、签名、添加公共头等操作常在这里进行。在找到的
NetworkClient类中,我发现了几个自定义的拦截器,比如SignInterceptor和EncryptInterceptor。这简直是宝藏!SignInterceptor:在intercept方法里,它从请求中获取参数,进行某种运算(可能是MD5或HMAC),生成一个sign字段添加到请求头或参数中。EncryptInterceptor:它可能会对请求体(RequestBody)进行加密,或者对响应体进行解密。
4.2 深入加密与签名逻辑
找到拦截器后,就需要深入分析其内部的算法。
签名算法分析:打开
SignInterceptor的代码。发现它做了一下几步:- 获取所有请求参数(包括URL中的query参数和Body中的参数),并按照参数名的字典序排序。
- 将排序后的参数拼接成
key1=value1&key2=value2...格式的字符串。 - 在这个字符串后面拼接一个固定的
secretKey(这个key在代码中以字符串常量形式存在,通过搜索secret或key找到)。 - 对拼接后的整个字符串进行MD5运算(调用
MessageDigest.getInstance("MD5")),得到的结果就是sign。 - 将
sign作为一个新的参数添加到请求中。
实操心得:这种“参数排序+拼接密钥+MD5”的签名方式非常常见,目的是防止参数被篡改。服务器端会用同样的逻辑计算一遍,如果签名对不上就拒绝请求。找到那个
secretKey是破解签名的关键。加密算法分析:
EncryptInterceptor的代码相对复杂一些。它没有直接使用AES等标准算法,而是调用了一个本地Native方法。代码类似这样:public native byte[] encryptData(byte[] data); public native byte[] decryptData(byte[] data);这说明核心的加解密逻辑被编译到了
.so原生库里。这是开发者提高逆向难度的一种常用手段。我们需要把分析重点转向lib文件夹下的.so文件。定位关键Native库与函数:
- 在Jadx中搜索
System.loadLibrary或encryptData/decryptData这些Native方法声明所在的类。找到了一个NativeHelper类。 - 查看这个类,发现它加载了一个名为
gamecore的库:System.loadLibrary("gamecore")。 - 于是,我们去解包后的
lib/armeabi-v7a/(或对应架构)目录下,找到了libgamecore.so文件。这就是我们的下一个主攻目标。
- 在Jadx中搜索
5. 动态调试与协议还原
静态分析给了我们地图,但有些路必须亲自走一遍才能看清。动态调试就是我们的“实地勘探”。
5.1 绕过可能的反调试与证书锁定
在启动动态调试前,需要扫清障碍。
应对证书锁定(SSL Pinning):如果配置好Charles后,App打开就网络错误或抓不到包,很可能触发了证书锁定。有两种思路:
- 修改App:使用Android Killer或MT管理器,找到
res/xml/network_security_config.xml文件(在AndroidManifest中指定的),删除其中的<pin-set>标签内容,或者直接删除整个配置文件引用。然后重打包并签名。这需要一定的Smali修改基础。 - 使用Frida脚本绕过:编写Frida脚本,Hook证书验证的相关方法(如
OkHttpClient.Builder的sslSocketFactory和hostnameVerifier),使其总是返回信任。这对于新手来说更友好。我在本次分析中采用了Frida方案,因为我不想修改APK文件。
- 修改App:使用Android Killer或MT管理器,找到
应对反调试:一些App会检测是否被调试。我们可以使用修改过的模拟器(如“雷电模拟器调试版”),或者使用Frida来Hook反调试检测函数(如
android.os.Debug.isDebuggerConnected()),让其返回false。
5.2 使用Frida快速验证加密函数
Frida在验证猜想时效率极高。我们假设NativeHelper.encryptData是加密入口。
编写Frida脚本:
// hook_encrypt.js Java.perform(function() { var NativeHelper = Java.use("com.xxx.pool.NativeHelper"); // 替换为实际的类名 NativeHelper.encryptData.overload('[B').implementation = function(data) { console.log("[*] encryptData called!"); console.log("[+] Input data (hex): " + bytesToHex(data)); var result = this.encryptData(data); // 调用原方法 console.log("[+] Output data (hex): " + bytesToHex(result)); return result; }; function bytesToHex(bytes) { return Array.from(bytes, function(byte) { return ('0' + (byte & 0xFF).toString(16)).slice(-2); }).join(''); } });注入脚本:
- 在电脑上启动Frida服务:
frida-server推送到模拟器并运行。 - 在电脑上执行:
frida -U -l hook_encrypt.js -f com.xxx.pool(com.xxx.pool是包名)。 - 触发一个网络请求(比如登录)。在Frida控制台,你就能看到加密函数的输入和输出数据了。这能立刻验证我们的定位是否正确,以及加密前后数据的变化。
- 在电脑上启动Frida服务:
5.3 使用IDA Pro动态调试SO库
为了彻底弄清encryptData在libgamecore.so里做了什么,需要用IDA Pro进行调试。
准备调试环境:
- 将IDA Pro安装目录下的
android_server(或android_server64)推送到模拟器的/data/local/tmp/目录。 - 在模拟器的adb shell中,给该文件执行权限:
chmod 755 /data/local/tmp/android_server。 - 运行调试服务器:
./data/local/tmp/android_server -p 23946(指定一个端口)。
- 将IDA Pro安装目录下的
端口转发与附加进程:
- 在电脑上执行:
adb forward tcp:23946 tcp:23946。 - 启动IDA Pro,选择
Debugger -> Attach -> Remote ARM Linux/Android debugger。 - 主机填写
localhost,端口填写23946。 - 在弹出的进程列表中,找到“某台球”的进程(通过包名识别),并附加(Attach)。
- 在电脑上执行:
定位与下断点:
- 附加成功后,IDA会加载进程的模块。在
Modules窗口中找到libgamecore.so并双击。 - 由于我们有函数名
encryptData,可以直接在函数窗口(Functions window)搜索encryptData。但通常so库中的函数名会被混淆或以JNI接口形式存在(如Java_com_xxx_pool_NativeHelper_encryptData)。我们需要搜索这个完整的JNI函数名。 - 找到函数后,在其入口处按F2下断点。
- 附加成功后,IDA会加载进程的模块。在
触发断点与分析:
- 回到模拟器,操作App触发一个网络请求(如登录)。
- IDA的调试器会中断在断点处。此时,我们可以使用
F7(单步步入)、F8(单步步过)来跟踪程序执行。 - 关键观察点:
- 函数的参数(通常R0寄存器指向JNIEnv,R1寄存器指向jobject,R2寄存器指向jbyteArray参数)。我们需要查看传入的字节数组内容。
- 函数内部的调用逻辑。它可能调用了其他函数,或者内联了加密算法。注意观察是否有明显的加密算法特征,如循环移位、查表(S-Box)、密钥扩展等。
- 最终返回值(存放在R0寄存器中)的内容。
- 通过单步跟踪和观察内存、寄存器变化,我最终发现这个
encryptData函数内部实际上是调用了一个标准的AES加密函数(可能是OpenSSL库中的AES_encrypt),并使用了一个硬编码在so文件数据段中的密钥。
踩坑实录:动态调试so时,最大的困难是代码混淆和反调试。这个so文件虽然没加很强的壳,但函数内部逻辑跳转很多。我的经验是,不要纠结于每一行汇编,重点关注函数调用(BL指令)和内存访问(LDR指令)。当看到调用了一个参数为(数据指针,密钥指针,输出指针)的函数时,就要高度怀疑是标准加密函数。可以结合静态分析,用IDA的“Hex-Rays”插件生成伪C代码,能极大提高分析效率。
6. 协议模拟与功能验证
分析清楚了签名和加密的算法,逆向的最终目的——模拟协议——就可以实现了。
6.1 还原完整请求流程
根据前面的分析,一个完整的请求构建流程如下:
- 组装业务参数(JSON格式)。
- 将JSON字符串作为参数,调用
NativeHelper.encryptData进行加密,得到密文encryptedData。 - 将
encryptedData进行Base64编码(为了在网络中安全传输),作为请求体(如data=Base64Encode(encryptedData))。 - 将所有明文参数(包括这个
data参数和其他如timestamp,version等)按照字典序排序,拼接成字符串,末尾加上secretKey,计算MD5值,得到sign。 - 将
sign作为参数,和data等其他参数一起,以application/x-www-form-urlencoded格式POST到目标服务器接口。
6.2 使用Python实现协议客户端
我们可以用Python来模拟这个流程,验证我们的分析是否正确。
import hashlib import time import base64 import requests from Crypto.Cipher import AES from Crypto.Util.Padding import pad class PoolGameClient: def __init__(self, base_url, secret_key, aes_key): self.base_url = base_url self.secret_key = secret_key # AES密钥,从SO中分析得到的16字节密钥 self.aes_key = aes_key.encode('utf-8') if isinstance(aes_key, str) else aes_key # 假设是AES-128-ECB模式,根据SO分析确定 self.cipher = AES.new(self.aes_key, AES.MODE_ECB) def _encrypt_data(self, data_str): """模拟NativeHelper.encryptData""" # 1. 字符串转字节 data_bytes = data_str.encode('utf-8') # 2. PKCS7填充 padded_data = pad(data_bytes, AES.block_size) # 3. AES加密 encrypted_bytes = self.cipher.encrypt(padded_data) return encrypted_bytes def _generate_sign(self, params): """生成签名""" # 1. 参数名按字典序排序 sorted_keys = sorted(params.keys()) # 2. 拼接 key=value& 形式的字符串 sign_str = '' for key in sorted_keys: sign_str += f"{key}={params[key]}&" # 3. 去掉最后一个&,拼接secret_key sign_str = sign_str.rstrip('&') + self.secret_key # 4. 计算MD5 m = hashlib.md5() m.update(sign_str.encode('utf-8')) return m.hexdigest() def make_request(self, api_path, business_params): """构造一个完整的请求""" # 1. 业务参数转JSON字符串 import json data_json = json.dumps(business_params, separators=(',', ':')) # 2. 加密业务参数 encrypted_data = self._encrypt_data(data_json) # 3. Base64编码 encoded_data = base64.b64encode(encrypted_data).decode('utf-8') # 4. 组装请求参数 request_params = { 'data': encoded_data, 'timestamp': int(time.time() * 1000), # 毫秒时间戳 'version': '1.0.0', 'deviceId': 'test_device_123', } # 5. 生成签名 request_params['sign'] = self._generate_sign(request_params) # 6. 发送请求 url = self.base_url + api_path headers = {'Content-Type': 'application/x-www-form-urlencoded'} # 注意:参数要以表单形式发送 resp = requests.post(url, data=request_params, headers=headers) return resp.json() # 使用示例 if __name__ == '__main__': # 以下密钥均为示例,真实密钥需从反编译的代码或SO中提取 client = PoolGameClient( base_url='https://api.poolgame.example.com', secret_key='your_secret_key_from_code', # 从Java代码中找到的 aes_key='16byte_aes_key!!!' # 从SO文件数据段中找到的 ) # 模拟登录 login_resp = client.make_request('/user/login', {'username': 'test', 'password': '123456'}) print(login_resp)运行这个脚本,如果能够成功收到服务器的正常响应(而不是签名错误或解密失败),那就证明我们的逆向分析是完全正确的,成功还原了客户端的通信协议。
7. 常见问题与排查技巧实录
在整个逆向过程中,会遇到各种各样的问题。这里记录几个最典型的问题和我的解决思路,希望能帮你少走弯路。
7.1 抓包抓不到HTTPS流量或显示Tunnel to
- 问题现象:Charles中只看到
CONNECT请求,显示为Tunnel to ...,没有具体的请求和响应内容。 - 可能原因:
- 证书未正确安装或信任:这是最常见的原因。确保Charles根证书已安装到模拟器的系统证书目录,并重启了模拟器。
- App使用了证书锁定(SSL Pinning):App只信任自己的证书,不信任用户安装的Charles证书。
- Android 7+ 的网络安全配置:即使安装了用户证书,App也可能通过
networkSecurityConfig禁用了用户证书。
- 排查与解决:
- 先用模拟器浏览器访问
https://www.baidu.com,看Charles能否解密。如果不能,是证书问题。 - 如果能解密百度,但不能解密目标App,则是App自身的问题。使用
apktool反编译后,检查AndroidManifest.xml和res/xml/下的网络安全配置文件。 - 使用Frida脚本绕过证书锁定。搜索“Frida disable SSL pinning”可以找到针对不同网络库(OkHttp, Retrofit, X509TrustManager)的通用脚本。
- 先用模拟器浏览器访问
7.2 反编译后代码混淆严重,无法阅读
- 问题现象:Jadx打开的代码中,类名、方法名、变量名都变成了
a,b,c,aa,ab等无意义字符。 - 可能原因:开发者使用了ProGuard或R8进行代码混淆。
- 排查与解决:
- 寻找未混淆的线索:字符串常量、资源ID(
R.string.xxx)、系统API调用、第三方库的类名(如OkHttp的类通常不会被混淆)是重要的锚点。通过这些锚点去定位关键代码。 - 关注“脉络”而非“名字”:忽略无意义的变量名,关注代码的结构和控制流。比如,看到一个方法里调用了
MessageDigest.getInstance("MD5"),那它很可能就是计算签名的函数,不管它叫a()还是calculateSign()。 - 动态调试辅助:在关键位置(如网络请求发起处)下断点,通过动态运行来看具体的对象和值,可以反向推断出代码逻辑。
- 寻找未混淆的线索:字符串常量、资源ID(
7.3 动态调试时IDA无法附加进程或一附加就崩溃
- 问题现象:在IDA中选择进程后附加,App立刻闪退或IDA失去连接。
- 可能原因:App内置了反调试检测,检测到调试器附着后主动退出。
- 排查与解决:
- 使用Frida过反调试:在启动App前,先注入一个反反调试的Frida脚本。这类脚本会Hook常见的反调试检测函数,如
ptrace,fork,/proc/self/status读取等,使其返回无害的结果。 - 修改内核参数(需root):在adb shell中,执行
echo 0 > /proc/sys/kernel/yama/ptrace_scope和echo 0 > /proc/sys/kernel/kptr_restrict,降低调试限制。(重启后失效) - 使用定制ROM或模拟器:有些为逆向定制的安卓镜像或模拟器默认关闭了反调试。
- 使用Frida过反调试:在启动App前,先注入一个反反调试的Frida脚本。这类脚本会Hook常见的反调试检测函数,如
7.4 分析SO库时找不到关键函数
- 问题现象:在IDA的Functions窗口搜索
encryptData或Java_开头的函数名,找不到。 - 可能原因:
- 函数名被混淆或剥离。
- 函数是通过动态注册(
RegisterNatives)的,而不是传统的JNI命名规则。
- 排查与解决:
- 查找JNI_OnLoad:这是SO库加载时第一个被调用的函数,动态注册通常在这里完成。在IDA中查看
JNI_OnLoad函数的代码,寻找对RegisterNatives的调用,其参数会包含Java类名、方法名和本地函数指针的映射关系。 - 在导出表中查找:查看IDA的Exports窗口,有时关键函数会在这里。
- 字符串交叉引用:在SO库中搜索可能出现的密钥字符串、算法名称(如“AES”),然后查看哪些代码引用了这些字符串,再顺藤摸瓜找到函数。
- 使用Frida枚举所有Native函数:可以写一个Frida脚本,枚举某个Native库的所有导出函数,或者Hook
RegisterNatives来获取动态注册的函数地址。
- 查找JNI_OnLoad:这是SO库加载时第一个被调用的函数,动态注册通常在这里完成。在IDA中查看
7.5 模拟请求时,服务器返回“签名错误”
- 问题现象:自己编写的Python脚本发送请求,服务器返回签名校验失败的提示。
- 可能原因:
- 参数顺序错误:签名要求按字典序排序,你的排序逻辑可能和服务器不一致(比如大小写敏感度)。
- 参数缺失或多余:你可能漏掉了某些隐式参数(如
timestamp,nonce),或者多加了参数。仔细对比抓包到的原始请求和你构造的请求,确保参数列表完全一致。 - 拼接格式错误:拼接字符串时是
key=value&还是key=value?末尾是否有&?secretKey是直接拼接还是需要加什么前缀? - 编码问题:参数值是否需要URL编码?MD5计算前字符串的编码是UTF-8还是GBK?
- 排查与解决:
- 逐字节对比:用抓包工具(如Charles)抓取一次成功的请求,记录下所有参数和值。然后在你生成签名的代码中,打印出每一步生成的字符串,与成功请求的签名生成过程进行逐字节对比。这是最笨但最有效的方法。
- Hook验证:在App运行时,用Frida Hook签名生成函数,打印出它接收的参数和生成的签名结果,与你本地计算的结果进行比对,能快速定位差异所在。