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流量。选择理由相比FiddlerCharles对JSON等格式的展示更友好相比mitmproxy它提供了图形化界面对初学者更友好。首先用它来判断App是否使用HTTP协议。模拟器在电脑上运行安卓环境方便配合Charles进行抓包避免对真机进行复杂的证书安装和网络配置。静态分析工具Jadx-GUI Android KillerJadx-GUI当前最优秀的Java反编译工具之一能将DEX文件反编译成可读性相当高的Java代码。它支持全局搜索、跳转引用、查看资源文件是静态分析的起点。Android Killer一个集成了Apktool、签名、编译等功能的工具箱。我主要用它来解包APK得到Smali代码和资源文件以及进行简单的字符串搜索和修改尝试。它的资源查看功能有时比Jadx更直接。选择理由Jadx用于快速浏览和搜索Java层逻辑Android Killer用于处理资源文件和进行底层的Smali修改。两者互补。动态调试工具IDA Pro 调试服务器android_serverIDA Pro逆向分析的“瑞士军刀”尤其擅长分析原生库.so文件。如果应用的关键逻辑如加密算法用C/C实现并编译进了so库就必须用它来调试。android_serverIDA Pro附带的调试服务器程序运行在安卓设备或模拟器上与电脑端的IDA Pro通信。选择理由当静态分析遇到高度混淆或遇到Native层代码时动态调试是唯一能看清执行流程和数据变化的方法。辅助工具adbAndroid 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/并修改其权限为644rw-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等。定位网络请求封装类通过搜索okhttp3.OkHttpClient或retrofit2.Retrofit我很快找到了一个名为NetworkClient或ApiService的类。这类通常负责创建OkHttpClient实例配置拦截器Interceptor并定义所有的API接口。分析拦截器InterceptorOkHttp的拦截器是修改请求和响应的绝佳位置加密、签名、添加公共头等操作常在这里进行。在找到的NetworkClient类中我发现了几个自定义的拦截器比如SignInterceptor和EncryptInterceptor。这简直是宝藏SignInterceptor在intercept方法里它从请求中获取参数进行某种运算可能是MD5或HMAC生成一个sign字段添加到请求头或参数中。EncryptInterceptor它可能会对请求体RequestBody进行加密或者对响应体进行解密。4.2 深入加密与签名逻辑找到拦截器后就需要深入分析其内部的算法。签名算法分析打开SignInterceptor的代码。发现它做了一下几步获取所有请求参数包括URL中的query参数和Body中的参数并按照参数名的字典序排序。将排序后的参数拼接成key1value1key2value2...格式的字符串。在这个字符串后面拼接一个固定的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文件。这就是我们的下一个主攻目标。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会检测是否被调试。我们可以使用修改过的模拟器如“雷电模拟器调试版”或者使用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.poolcom.xxx.pool是包名。触发一个网络请求比如登录。在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指定一个端口。端口转发与附加进程在电脑上执行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下断点。触发断点与分析回到模拟器操作App触发一个网络请求如登录。IDA的调试器会中断在断点处。此时我们可以使用F7单步步入、F8单步步过来跟踪程序执行。关键观察点函数的参数通常R0寄存器指向JNIEnvR1寄存器指向jobjectR2寄存器指向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编码为了在网络中安全传输作为请求体如dataBase64Encode(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. 拼接 keyvalue 形式的字符串 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, datarequest_params, headersheaders) return resp.json() # 使用示例 if __name__ __main__: # 以下密钥均为示例真实密钥需从反编译的代码或SO中提取 client PoolGameClient( base_urlhttps://api.poolgame.example.com, secret_keyyour_secret_key_from_code, # 从Java代码中找到的 aes_key16byte_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 PinningApp只信任自己的证书不信任用户安装的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进行代码混淆。排查与解决寻找未混淆的线索字符串常量、资源IDR.string.xxx、系统API调用、第三方库的类名如OkHttp的类通常不会被混淆是重要的锚点。通过这些锚点去定位关键代码。关注“脉络”而非“名字”忽略无意义的变量名关注代码的结构和控制流。比如看到一个方法里调用了MessageDigest.getInstance(MD5)那它很可能就是计算签名的函数不管它叫a()还是calculateSign()。动态调试辅助在关键位置如网络请求发起处下断点通过动态运行来看具体的对象和值可以反向推断出代码逻辑。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或模拟器有些为逆向定制的安卓镜像或模拟器默认关闭了反调试。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库的所有导出函数或者HookRegisterNatives来获取动态注册的函数地址。7.5 模拟请求时服务器返回“签名错误”问题现象自己编写的Python脚本发送请求服务器返回签名校验失败的提示。可能原因参数顺序错误签名要求按字典序排序你的排序逻辑可能和服务器不一致比如大小写敏感度。参数缺失或多余你可能漏掉了某些隐式参数如timestamp,nonce或者多加了参数。仔细对比抓包到的原始请求和你构造的请求确保参数列表完全一致。拼接格式错误拼接字符串时是keyvalue还是keyvalue末尾是否有secretKey是直接拼接还是需要加什么前缀编码问题参数值是否需要URL编码MD5计算前字符串的编码是UTF-8还是GBK排查与解决逐字节对比用抓包工具如Charles抓取一次成功的请求记录下所有参数和值。然后在你生成签名的代码中打印出每一步生成的字符串与成功请求的签名生成过程进行逐字节对比。这是最笨但最有效的方法。Hook验证在App运行时用Frida Hook签名生成函数打印出它接收的参数和生成的签名结果与你本地计算的结果进行比对能快速定位差异所在。