1. 项目概述与逆向价值
最近在逆向分析圈子里,讨论某品会这类大型电商APP的声音一直没停过。这类APP集成了复杂的业务逻辑、风控策略和通信加密,对逆向工程师来说,既是挑战,也是检验技术成色的绝佳“试金石”。我花了大概两周时间,从零开始对某品会APP的某个版本进行了深度逆向分析,整个过程就像在解一个层层嵌套的密码盒,既有柳暗花明的兴奋,也有陷入泥潭的困惑。今天这篇文章,我就把这次逆向分析的完整思路、核心发现和踩过的坑,毫无保留地分享出来。无论你是想学习APP逆向的入门新手,还是想了解大型商业APP安全机制的同行,相信都能从中找到有价值的信息。我们这次的目标很明确:不搞破坏,不涉及任何灰色操作,纯粹从技术研究的角度,拆解其核心的通信加密、签名算法以及部分关键业务逻辑的实现方式,理解其安全设计的精妙之处。
2. 逆向环境与工具链搭建
工欲善其事,必先利其器。一个稳定、高效的逆向分析环境是成功的一半。这次分析我主要采用了动态分析与静态分析相结合的策略,工具链的选择也围绕这个核心展开。
2.1 核心工具选型与配置
我的主力分析机是一台运行macOS的MacBook Pro,同时通过Docker和虚拟机搭建了辅助的Windows和Linux环境,以应对不同工具的需求。以下是核心工具清单及其作用:
抓包与流量分析工具
- Charles/ Fiddler/ HTTP Toolkit: 用于拦截和查看HTTP/HTTPS流量。我最终选择了HTTP Toolkit,因为它对非标准端口和WebSocket的支持更好,且界面更现代化。关键在于配置手机代理和安装CA证书,以解密HTTPS流量。某品会大量使用HTTPS,这是观察其网络请求入口的第一步。
- Wireshark: 作为底层流量抓包的补充,用于分析Charles可能漏掉的非HTTP协议流量或验证证书握手过程。
动态调试与运行时分析工具
- Frida:本次分析的绝对核心。它是一个动态插桩工具,可以在APP运行时注入JavaScript脚本,实现函数Hook、参数打印、方法替换等。我主要用它来Hook Java层和Native层的加密函数,实时获取输入输出。
- Objection: 基于Frida的命令行工具,可以快速进行内存搜索、绕过SSL Pinning(证书绑定)等常见操作。对于快速验证和初步探索非常高效。
- Android Studio + 模拟器/真机: 用于运行目标APP。我推荐使用Google Pixel系列的官方系统镜像创建模拟器,兼容性最好。真机则需要开启USB调试并Root(对于深度逆向几乎是必须的)。
静态反编译与分析工具
- Jadx-GUI: 反编译APK中DEX文件的首选工具,可以将字节码转换为可读性极高的Java代码。它的图形化界面支持搜索、跳转、查看交叉引用,是分析Java层逻辑的利器。
- IDA Pro/Ghidra: 用于分析APP中的原生库(.so文件)。某品会的核心加密逻辑很可能放在Native层。IDA Pro的交互式反汇编和调试功能强大,而Ghidra作为免费开源工具,其反编译质量也非常高,我通常会两者结合使用。
- Apktool: 用于解包APK,获取资源文件、清单文件以及未被混淆的XML布局文件,有时能从中发现一些端倪,比如引用的第三方SDK。
注意:所有工具请从官方网站或可信的GitHub仓库下载,避免使用来历不明的破解版,以防植入后门或分析环境被污染。
2.2 关键环境配置步骤
这里重点讲一下Frida环境的搭建,因为后续很多操作都依赖它。
- 安装Frida: 在电脑端使用pip安装:
pip install frida-tools。同时,需要根据手机或模拟器的CPU架构,从Frida的GitHub Releases页面下载对应的frida-server二进制文件。 - 部署frida-server: 将下载的
frida-server通过adb推送到设备上,并赋予可执行权限。adb push frida-server /data/local/tmp/ adb shell cd /data/local/tmp chmod 755 frida-server ./frida-server & - 验证连接: 在电脑端新开一个终端,运行
frida-ps -U,如果能看到设备上的进程列表,说明连接成功。 - 绕过SSL Pinning: 某品会这类APP肯定使用了SSL Pinning来防止中间人攻击。使用Objection可以快速绕过:
objection -g com.xxx.pinhui explore,然后在Objection的REPL中运行android sslpinning disable。但这只是通用方法,对于自定义的证书验证逻辑可能无效,届时需要手动分析并Hook相关的验证函数。
环境搭好之后,建议先不要急着深入,而是用抓包工具完整地浏览一遍APP,记录下主要的API域名、请求参数结构和常见的响应格式,建立一个初步的“地图”。
3. 初步侦察与协议入口分析
逆向分析就像侦探破案,第一步永远是现场勘查。对于APP来说,就是观察它的网络行为。
3.1 网络流量抓取与特征观察
配置好HTTP Toolkit代理后,打开某品会APP,进行登录、浏览商品、加入购物车、查看个人中心等关键操作。很快,我在抓包工具中看到了大量的请求。经过筛选,我重点关注了以下几个特征明显的请求:
- 登录请求 (
/api/member/login/v2): 显然是认证入口,请求体和响应体都被加密了,看起来像是Base64编码的密文。 - 商品列表请求 (
/api/goods/list): 即使是不需要登录的公开数据,请求的URL和参数中也包含了一些类似sign、timestamp、nonce的字段,响应体同样是加密的。 - 心跳或初始化请求 (
/api/app/init): APP启动时调用,返回了包括公钥、算法标识在内的配置信息。这是一个非常重要的突破口!
我整理了一下初步观察到的规律:
- 全链路加密: 不仅敏感信息(登录密码),几乎所有业务API的请求体(body)和响应体都进行了加密传输,明文可见的只有URL和少数几个Query参数。
- 签名机制: 大部分请求的URL中都会携带
sign、timestamp等参数,这是典型的防重放和参数防篡改签名机制。 - 密钥协商: 从
/api/app/init的响应可以推测,客户端与服务器可能采用了非对称加密(如RSA)协商对称加密密钥(如AES),后续通信再用对称加密。
3.2 静态入口点定位
面对加密的流量,直接硬猜算法是不可能的。下一步就是找到执行加密和解密的代码位置。这里我采用了“由外到内”的搜索策略。
首先,用Jadx打开APK,进行全局搜索。搜索关键词的选择很重要:
- 搜索类名: 搜索“AES”、“RSA”、“DES”、“Cipher”、“Encrypt”、“Decrypt”、“Crypto”、“Security”等关键词。很快,我发现了一个名为
com.xxx.pinhui.security.EncryptUtils的类,看起来很有希望。 - 搜索初始化API的响应字段: 从抓包看到
/api/app/init返回了publicKey和algorithm。我直接在Jadx中搜索字符串publicKey和algorithm,找到了解析这个响应的代码位置,通常是在某个Callback或Model类里。顺藤摸瓜,就能找到使用这个publicKey的代码。 - 搜索网络库: 某品会很可能使用了OkHttp或Retrofit作为网络框架。搜索
Interceptor(拦截器)这个关键词,因为加密和签名逻辑通常被封装在自定义的Interceptor中,在请求发出前和响应收到后统一处理。我找到了一个SecurityInterceptor类,这几乎就是加密通信的“总闸”。
定位到SecurityInterceptor后,我看到了类似下面的伪代码结构:
public class SecurityInterceptor implements Interceptor { @Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); // 1. 对请求体进行加密,并添加签名参数 Request encryptedRequest = encryptRequest(originalRequest); // 2. 发送加密后的请求 Response response = chain.proceed(encryptedRequest); // 3. 对响应体进行解密 Response decryptedResponse = decryptResponse(response); return decryptedResponse; } }至此,我们找到了加密解密的入口函数encryptRequest和decryptResponse。接下来的任务,就是深入这两个函数,还原完整的算法流程。
4. 核心加密与签名算法逆向
这是整个逆向过程中最硬核、也最有趣的部分。我们需要像剥洋葱一样,一层层揭开加密和签名的面纱。
4.1 请求体加密流程剖析
进入encryptRequest方法,结合Jadx的反编译代码和Frida的动态Hook,我逐步理清了请求体的加密过程:
- 生成随机对称密钥: 每次请求,都会随机生成一个16字节(128位)的AES密钥,我们称之为
sessionKey。 - 加密业务数据: 将原始的JSON请求体(例如
{"phone":"13800138000", "password":"..."}),用上一步生成的sessionKey,以AES-128-CBC模式进行加密,生成密文encryptedBody。这里还涉及PKCS7填充和随机生成IV(初始化向量)。 - 加密会话密钥: 上一步的
sessionKey本身需要用服务器的公钥进行加密,以确保只有持有私钥的服务器才能解密。从/api/app/init获取的publicKey被用于RSA加密。这里注意,为了适应密钥长度,通常还会对sessionKey进行分段加密或使用RSA/ECB/PKCS1Padding模式。 - 组装最终请求体: 最终上传的请求体,是一个新的JSON结构,大致如下:
这个新的JSON会被转换成字符串,作为HTTP请求的Body(通常是{ "encryptedKey": "Base64编码的(RSA加密后的sessionKey)", "encryptedData": "Base64编码的(AES加密后的原始请求体)", "iv": "Base64编码的(AES IV)", "algorithm": "AES/CBC/PKCS7Padding_RSA/ECB/PKCS1Padding" }application/json格式)。
为了验证这个过程,我写了一个Frida脚本,Hook了javax.crypto.Cipher类的getInstance、init、doFinal方法。当APP发起登录请求时,脚本打印出了如下关键信息:
Cipher.getInstance: AES/CBC/PKCS7Padding Cipher.init: ENCRYPT_MODE, key=xxxx..., iv=xxxx... Cipher.doFinal (输入): {"phone":"138...","password":"..."} Cipher.doFinal (输出): [密文字节数组] Cipher.getInstance: RSA/ECB/PKCS1Padding Cipher.init: ENCRYPT_MODE, key=xxxx... (公钥) Cipher.doFinal (输入): [sessionKey字节数组] Cipher.doFinal (输出): [加密后的sessionKey]动态Hook的结果与静态分析的逻辑完全吻合,证实了我们的分析。
4.2 URL签名算法解析
请求体加密解决了数据保密性问题,但URL本身和Query参数还是明文的。为了防止参数被篡改或请求被重放,签名算法登场了。
在SecurityInterceptor中,encryptRequest方法里除了加密Body,还会调用一个generateSign的方法来生成sign参数。分析这个方法:
- 参数排序与拼接: 将所有待签名的参数(包括
timestamp、nonce、appVersion、deviceId以及业务参数如goodsId)按照参数名ASCII码从小到大排序(字典序),然后使用key=value的格式用&连接起来,形成一个字符串paramStr。- 例如:
appVersion=10.2.1&deviceId=abc123&nonce=xyz×tamp=1678888888
- 例如:
- 添加密钥: 在
paramStr的最后,拼接上一个固定的secretKey(这个Key通常硬编码在APP中,或由服务器在初始化时下发)。形成signStr = paramStr + "&secret=xxxxxx"。 - 计算哈希: 对
signStr计算MD5哈希值(有时也可能是SHA256),并将结果转换为小写十六进制字符串,这就是最终的sign值。 - 添加到URL: 将
sign、timestamp、nonce等参数作为Query参数附加到请求URL中。
签名算法的核心在于secretKey和排序规则。只要规则一致,任何一方都能计算出相同的sign。服务器收到请求后,会以同样的算法重新计算一遍sign,并与客户端传来的sign对比,不一致则拒绝请求。
实操心得:寻找
secretKey时,不要只搜索明文字符串。它可能被分割成几段、经过简单的位运算或Base64编码后存储。可以尝试Hook签名函数,直接打印出参与计算的完整signStr,这样secretKey就暴露无遗了。
4.3 响应体解密流程
响应体的解密是加密的逆过程。在decryptResponse方法中:
- 解析响应体: 服务器返回的响应体,其结构通常与加密请求体类似,包含
encryptedData和iv等字段。 - 获取会话密钥: 由于一个会话中可能使用同一个
sessionKey,或者服务器会用客户端的公钥加密一个新的sessionKey返回。在这个案例中,我发现它复用了请求阶段的sessionKey。这意味着,我们需要在客户端内存中缓存这个sessionKey,用于解密后续的响应。 - 解密业务数据: 使用缓存的
sessionKey和响应中的iv,以AES-128-CBC模式解密encryptedData,得到原始的JSON响应字符串。
这里有一个关键点:sessionKey的生命周期管理。是每次请求都更换?还是一个会话期内不变?通过Hook和多次请求观察,我确认在某品会的这个版本中,sessionKey在用户登录成功后的一段较长时间内(甚至整个APP生命周期)是复用的,这简化了逆向后的模拟请求过程。
5. 算法还原与本地模拟实现
分析清楚算法后,下一步就是用代码(这里以Python为例)在电脑上复现整个流程,从而能够模拟APP发送合法的请求。这是验证逆向成果的关键一步。
5.1 密钥与参数的提取
首先,我们需要从APP中提取出必要的“原料”:
- RSA公钥: 从
/api/app/init的响应中直接获取。或者,从反编译的代码中找到硬编码的公钥字符串。通常是一个Base64编码的PKCS#8格式公钥。 - 签名SecretKey: 通过Hook
generateSign函数,从拼接的字符串中提取。或者,在Jadx中搜索相关字符串或常量,可能被混淆,但最终会参与MD5计算。 - 其他固定参数: 如
appVersion、deviceId生成规则、nonce生成算法等。deviceId通常是根据手机设备信息生成的一个唯一标识,在模拟时可以随机生成一个符合规则的固定值。
5.2 Python模拟代码编写
假设我们已提取到公钥PUBLIC_KEY_BASE64和签名密钥SECRET_KEY。
import json import base64 import hashlib import time import random import string from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA from Crypto.Util.Padding import pad, unpad from urllib.parse import urlencode # 1. 准备RSA公钥 PUBLIC_KEY = RSA.import_key(base64.b64decode(PUBLIC_KEY_BASE64)) # 2. 生成随机AES会话密钥和IV def generate_aes_key_iv(): session_key = ''.join(random.choices(string.ascii_letters + string.digits, k=16)).encode() iv = ''.join(random.choices(string.ascii_letters + string.digits, k=16)).encode() return session_key, iv # 3. 加密请求体 def encrypt_request_data(original_data: dict): """原始业务数据 -> 加密后的请求体JSON字符串""" # 3.1 生成AES密钥和IV session_key, iv = generate_aes_key_iv() # 3.2 AES加密原始数据 cipher_aes = AES.new(session_key, AES.MODE_CBC, iv) original_json = json.dumps(original_data, separators=(',', ':'), ensure_ascii=False) encrypted_body = cipher_aes.encrypt(pad(original_json.encode('utf-8'), AES.block_size)) # 3.3 RSA加密AES密钥 cipher_rsa = PKCS1_v1_5.new(PUBLIC_KEY) encrypted_key = cipher_rsa.encrypt(session_key) # 3.4 组装最终请求体 encrypted_payload = { "encryptedKey": base64.b64encode(encrypted_key).decode('utf-8'), "encryptedData": base64.b64encode(encrypted_body).decode('utf-8'), "iv": base64.b64encode(iv).decode('utf-8'), "algorithm": "AES/CBC/PKCS7Padding_RSA/ECB/PKCS1Padding" } return json.dumps(encrypted_payload, separators=(',', ':')), session_key, iv # 4. 生成URL签名 def generate_sign(params: dict, secret_key: str): """生成请求签名""" # 4.1 参数排序并拼接 sorted_params = sorted(params.items(), key=lambda x: x[0]) param_str = '&'.join([f'{k}={v}' for k, v in sorted_params]) # 4.2 拼接密钥 sign_str = param_str + f'&secret={secret_key}' # 4.3 计算MD5 md5 = hashlib.md5() md5.update(sign_str.encode('utf-8')) return md5.hexdigest().lower() # 5. 解密响应体 def decrypt_response_data(encrypted_response_json: str, session_key: bytes, iv: bytes): """服务器响应 -> 解密后的业务数据字典""" resp_data = json.loads(encrypted_response_json) encrypted_data = base64.b64decode(resp_data['encryptedData']) iv = base64.b64decode(resp_data['iv']) # 通常使用响应中的新IV cipher_aes = AES.new(session_key, AES.MODE_CBC, iv) decrypted_bytes = unpad(cipher_aes.decrypt(encrypted_data), AES.block_size) return json.loads(decrypted_bytes.decode('utf-8')) # 6. 模拟一个完整的请求 def simulate_login(phone, password): # 6.1 准备业务数据 biz_data = {"phone": phone, "password": password, "loginType": 1} # 6.2 加密请求体,并保留session_key用于解密响应 encrypted_body_str, session_key, iv = encrypt_request_data(biz_data) # 6.3 准备URL参数并生成签名 timestamp = int(time.time() * 1000) nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=8)) url_params = { 'timestamp': timestamp, 'nonce': nonce, 'appVersion': '10.2.1', 'deviceId': 'simulated_device_123', } sign = generate_sign(url_params, SECRET_KEY) url_params['sign'] = sign # 6.4 构建最终请求 final_url = f'https://api.xxx.com/api/member/login/v2?{urlencode(url_params)}' headers = { 'Content-Type': 'application/json; charset=UTF-8', 'User-Agent': 'PinHui/10.2.1 (Android; ...)' } # 这里使用requests库发送POST请求,body为encrypted_body_str # response = requests.post(final_url, data=encrypted_body_str, headers=headers) # decrypted_resp = decrypt_response_data(response.text, session_key, iv) # print(decrypted_resp) print(f"请求URL: {final_url}") print(f"加密后Body: {encrypted_body_str[:100]}...") return session_key # 返回session_key供后续请求使用 # 使用示例 if __name__ == '__main__': # 这些值需要从逆向分析中获取并替换 PUBLIC_KEY_BASE64 = "你的RSA公钥Base64" SECRET_KEY = "你的签名密钥" session_key = simulate_login("13800138000", "your_encrypted_password")这段代码完整还原了从数据加密、签名生成到请求发送的客户端流程。运行它,并与抓包工具中捕获的真实请求进行对比,检查URL参数结构、签名值以及请求体格式是否一致。如果一致,恭喜你,核心通信协议已被成功攻破。
6. 深入Native层与混淆对抗
如果一切顺利,Java层的分析就完成了。但很多APP会把最核心的算法(如自定义的哈希、白盒AES)放在Native层(C/C++代码编译的.so库)中,并加以混淆,以增加逆向难度。
6.1 定位Native加密函数
在Java层的EncryptUtils或类似类中,你可能会看到native声明的方法:
public static native byte[] nativeEncrypt(byte[] data, int type); public static native String nativeGenerateSign(String paramStr);这表明实际的加密和签名计算是在Native库中完成的。你需要找到加载这个库的代码(通常是System.loadLibrary("security")),然后在解压的APK的lib目录下找到对应的libsecurity.so文件(可能有armeabi-v7a, arm64-v8a等多个版本)。
6.2 使用IDA Pro/Ghidra分析.so文件
将libsecurity.so拖入IDA Pro。分析Native层比Java层更底层,挑战更大:
- 寻找JNI函数: 在导出函数列表中,查找以
Java_开头的函数,这些是JNI接口函数。例如Java_com_xxx_pinhui_security_EncryptUtils_nativeEncrypt。定位到它们,就找到了分析的起点。 - 理解函数逻辑: Native层代码可能被严重混淆(控制流平坦化、指令替换、字符串加密)。你需要耐心地跟踪参数传递、识别关键的加密函数调用(如寻找
OPENSSL的函数符号,或识别AES的S盒、RSA的大数运算等特征)。 - 动态调试: 使用IDA Pro或Ghidra的调试功能,附加到运行中的APP进程,在JNI函数入口处下断点,直接观察传入的参数和返回的结果,与Java层的输入输出进行验证。Frida也可以Hook Native函数,但需要知道函数地址或符号。
6.3 常见的混淆与对抗技巧
- 字符串加密: 代码中的关键字符串(如算法名、密钥)是加密存储的,运行时解密。可以通过Hook内存分配函数(如
malloc)或字符串操作函数,在解密后获取明文。 - 控制流平坦化: 将正常的if-else、switch逻辑打乱,用一个大switch-case和状态变量来调度,极大地增加静态分析的难度。动态调试可以帮你理清实际执行路径。
- 符号表剥离: 发布版的.so文件通常移除了函数和变量名等调试符号,所有函数都显示为
sub_XXXX。需要通过交叉引用、特征码或动态调试来猜测其功能。 - 反调试检测: APP会检测是否被调试(如检查
ptrace、TracerPid等),一旦发现就退出或执行错误逻辑。需要使用反反调试技巧,如修改内核参数、Hook检测函数等。
面对深度混淆,我的策略是:动态分析为主,静态分析为辅。优先使用Frida Hook住JNI函数的入口和出口,打印输入输出。如果算法逻辑不复杂,甚至可以直接在Frida脚本中调用这些Native函数,让APP自己为我们计算,我们只关心结果。如果必须还原算法,再结合动态调试去理解其内部逻辑。
7. 常见问题与排查实录
在逆向过程中,我遇到了无数个坑。这里记录下几个最具代表性的问题及其解决方法,希望能帮你节省大量时间。
7.1 抓包工具看不到HTTPS流量
- 现象: 配置好代理后,APP无法联网,或只能看到乱码/证书错误。
- 原因: APP启用了SSL Pinning(证书绑定),只信任自己的证书或特定CA,不信任用户安装的抓包工具证书。
- 解决:
- 使用Objection一键绕过: 如前所述,
android sslpinning disable。这对使用标准库(如OkHttp)的Pinning有效。 - 手动Hook: 如果Objection无效,说明APP有自定义的证书验证逻辑。需要反编译找到相关的
TrustManager或X509Certificate验证代码,用Frida Hook并使其始终返回true或有效的证书链。 - 刷入系统级证书: 在已Root的设备上,将抓包工具的CA证书移动到系统证书目录(
/system/etc/security/cacerts/),并修改权限。这样证书会被系统完全信任。
- 使用Objection一键绕过: 如前所述,
7.2 Frida脚本注入失败或进程崩溃
- 现象: 执行
frida -U -f com.xxx.pinhui时进程崩溃,或脚本注入后无输出。 - 原因:
- 反Frida检测: APP检测到了Frida的存在(如检测
frida-server进程、端口、特征文件等)。 - 架构或版本不匹配:
frida-server的版本与电脑端frida-tools版本不兼容,或frida-server的CPU架构与设备不符。 - 脚本错误: JavaScript脚本语法错误或Hook了不存在的类/方法。
- 反Frida检测: APP检测到了Frida的存在(如检测
- 解决:
- 对抗检测: 使用修改版的
frida-server(如frida-server的某些变种),或使用Frida的--no-pause参数,或在APP启动后再附加(frida -U com.xxx.pinhui)。也可以写脚本先Hook掉APP的反调试检测函数。 - 检查版本: 确保
frida --version与设备上的frida-server版本一致。使用adb shell getprop ro.product.cpu.abi查看设备架构。 - 调试脚本: 先写一个简单的脚本,如
Java.perform(function() { console.log("Script loaded!"); }),测试注入是否成功。再逐步增加Hook逻辑。
- 对抗检测: 使用修改版的
7.3 算法还原后签名依然不匹配
- 现象: 自己模拟生成的
sign与APP生成的sign不同,服务器返回签名错误。 - 原因:
- 参数遗漏或多余: 参与签名的参数列表不完整,或包含了不该参与签名的参数(如
sign本身)。 - 编码问题: 参数值可能进行了URL编码或Unicode处理,拼接前需要统一。
- 排序规则不一致: 不仅是参数名排序,如果参数值本身是复杂对象(如JSON字符串),其内部的排序规则也可能有要求。
- 密钥错误: 使用的
secretKey不正确,或者密钥在拼接前经过了额外处理(如MD5哈希一次后再用)。 - 哈希算法不同: 可能不是MD5,而是SHA1、SHA256,甚至可能是自定义的哈希函数。
- 参数遗漏或多余: 参与签名的参数列表不完整,或包含了不该参与签名的参数(如
- 排查:
- 精准Hook: 用Frida Hook到签名函数的最内部,打印出即将被计算哈希的最终字符串。将这个字符串与你本地拼接的字符串进行逐字符对比(包括空格、换行、特殊字符)。
- 二分法验证: 先固定所有参数(
timestamp,nonce等),只让一个参数变化,对比APP和你本地计算的sign,看是否一致。逐步增加变量,定位到是哪个参数或环节出了问题。 - 算法验证: 将Hook得到的最终字符串,分别用MD5、SHA1、SHA256等常见算法计算,看结果是否匹配。
7.4 Native层函数地址找不到
- 现象: 知道Native函数名,但在Frida中用
Module.findExportByName(null, "函数名")返回null。 - 原因:
- 符号被剥离: 发布版的.so文件没有导出函数符号。
- 函数是静态的: 函数没有导出,只在库内部使用。
- 加载时机: 库可能还没有被加载。
- 解决:
- 通过偏移地址查找: 在IDA Pro中查看目标函数的相对虚拟地址(RVA)。在Frida中,先获取模块基地址,然后加上RVA得到绝对地址。
var base = Module.findBaseAddress('libsecurity.so'); var func_addr = base.add(0x1234); // 0x1234是函数在IDA中的偏移 Interceptor.attach(func_addr, { onEnter: function(args) { ... }, onLeave: function(retval) { ... } }); - 通过特征码搜索: 在内存中搜索函数开头的一段独特的机器码(操作码)来定位函数。
- 确保库已加载: 在
Java.perform中操作,或监听dlopen事件确保库加载后再Hook。
- 通过偏移地址查找: 在IDA Pro中查看目标函数的相对虚拟地址(RVA)。在Frida中,先获取模块基地址,然后加上RVA得到绝对地址。
整个逆向分析某品会APP的过程,是一次对耐心、细心和技术深度的综合考验。从最外层的流量观察,到Java层的逻辑梳理,再到可能存在的Native层攻防,每一步都需要严谨的推理和反复的验证。最终的目标不是破解,而是理解其设计思路。当你能够用自己写的代码完美模拟出APP的合法请求时,那种成就感是无与伦比的。这份经验不仅适用于某品会,其分析思路和方法论,对于绝大多数采用类似安全方案的Android APP,都具有很高的参考价值。记住,逆向工程的核心是学习和理解,请务必在法律和道德允许的范围内进行技术研究。