安居客App逆向分析:从抓包到参数签名算法还原实战

安居客App逆向分析:从抓包到参数签名算法还原实战

1. 项目概述:从安居客App数据流中寻找“钥匙”

最近在分析一些房产数据时,不可避免地会接触到安居客这类头部平台。直接通过网页端获取数据,往往会遇到反爬机制越来越严格的问题,比如复杂的验证码、请求频率限制,甚至动态加载的数据难以直接抓取。这时候,很多开发者会把目光转向移动端App。App作为平台的核心客户端,其与服务器通信的API接口,往往是获取结构化数据最直接、最稳定的途径之一。因此,对安居客App进行逆向分析,理解其关键请求参数的生成逻辑,就成了一个非常实际且有价值的课题。

这个项目的核心目标,就是深入安居客App的内部,解析其网络请求,特别是那些用于身份验证和数据签名的关键参数(比如你提到的qn-mhp)。这不仅仅是“破解”一个参数那么简单,它涉及到对App整体通信安全机制的理解。我们需要搞清楚:App是如何证明“我是我”的?服务器又是如何校验请求的合法性与完整性的?这个过程,就像是在复杂的数字迷宫中,找到那把打开数据之门的“钥匙”。掌握了参数生成的原理,我们就能模拟出合法的请求,从而稳定、高效地获取所需的房产列表、房源详情等数据,为后续的数据分析、市场研究或竞品监控提供可靠的数据源。无论你是数据爱好者、市场分析师,还是对移动安全感兴趣的开发者,这个过程都能让你对现代App的通信架构有更深刻的认识。

2. 逆向工程环境与工具链搭建

工欲善其事,必先利其器。进行App逆向分析,一套趁手的工具链是成功的一半。这里我分享一套经过实战检验的、以Android平台为主的组合方案。需要注意的是,整个过程应在拥有合法授权的设备上进行,仅用于学习与研究目的。

2.1 核心抓包与调试环境

网络请求捕获是整个分析的起点。我强烈推荐使用mitmproxy作为主力抓包工具。它是一个基于Python的命令行HTTP/HTTPS代理,功能强大且可脚本化扩展。相比图形化的Fiddler或Charles,mitmproxy在处理大量请求、自动化测试和深度定制方面更具优势。你需要在一台电脑(作为代理服务器)上安装并运行它。

为了让App的流量经过我们的代理,必须在测试设备上安装mitmproxy的CA证书。对于Android设备,通常需要将证书安装到系统信任的凭据存储中,这可能需要Root权限。如果设备未Root,可以尝试将证书安装到用户凭据存储,但部分App(尤其是高版本Android上的应用)可能不会信任用户证书,这就需要更复杂的绕过方案,例如使用Magisk模块(如MagiskTrustUserCerts)或将证书打包进系统。这是逆向过程中可能遇到的第一个坎。

除了代理,一个动态调试环境也至关重要。对于Android App,Jadx是反编译APK文件、查看Java/Kotlin源码的利器。它能将Dex文件高效地转换为可读性很高的Java代码。而对于潜在的Native层(C/C++)代码分析,则需要用到IDA Pro或开源的GhidraFrida则是动态插桩的“瑞士军刀”,它允许你在App运行时注入自己的脚本,拦截函数调用、修改参数、打印调用栈等,是验证静态分析猜想、追踪加密函数执行路径的终极武器。

2.2 针对安居客App的专项配置

安居客作为一款用户量巨大的商业应用,必然会采用各种加固和混淆技术来保护其代码和通信安全。因此,我们的工具链需要针对性地进行配置。

首先,你拿到的安居客APK很可能经过了商业加固(如腾讯乐固、360加固等)。直接使用Jadx打开可能会看到大量无意义的混淆代码,或者核心逻辑被隐藏。这时,你需要先进行脱壳。对于不同的加固方案,脱壳方法也不同。有些可以通过特定版本Frida脚本在内存中Dump出解密后的Dex文件,有些则需要利用加固方案的历史漏洞。这个过程需要一定的经验和耐心,也是逆向中技术含量较高的部分。我建议先从一些公开的脱壳工具或脚本仓库(如github上的一些项目)开始尝试,并密切关注安全社区的最新动态。

其次,由于我们要分析的是网络请求,需要确保mitmproxy能成功捕获到HTTPS流量。除了安装证书,还要注意App可能启用了证书绑定(SSL Pinning)。这意味着App只信任自己内置的特定证书,而拒绝我们代理的证书。对付证书绑定,通常有两种思路:一是使用Frida脚本在运行时Hook掉证书验证的相关函数(如OkHttpCertificatePinner或Android系统的TrustManager),使其验证失效。网上有大量现成的通用脚本可供参考修改。二是如果App使用了Xposed框架可用的模块,也可以尝试使用JustTrustMe这类模块,但Frida的方案更为灵活和通用。

注意:所有工具的使用和修改都应遵守相关法律法规和服务条款。逆向分析的目的应是理解技术原理、提升安全能力,而非进行未授权的数据爬取或破坏性操作。

3. 安居客网络请求深度抓取与初步观察

配置好环境后,我们就可以开始实战了。启动mitmproxy,将手机代理设置好,然后打开安居客App,进行一些典型操作:比如搜索某个城市的二手房、翻页、查看房源详情等。mitmproxy的控制台会滚动显示所有的HTTP/HTTPS请求。

3.1 关键API接口识别

在一大堆请求中(可能包含图片、前端资源、统计上报等),我们需要快速定位到核心的数据API。通常,这类API的路径(Path)会包含明显的业务关键词,如/house/list/ershoufang/search/api/v5/property/detail等。响应内容一般是JSON格式,结构清晰,包含了房源列表、详情信息等。

找到目标API后,重点观察其请求头(Headers)请求体(Body)。除了常见的User-AgentContent-Type外,你需要特别留意那些看起来是自定义的、或者名称比较“奇怪”的头部字段。例如,你提到的qn-mhp就极有可能是这样一个关键参数。此外,还可能有类似X-SignX-TimestampAuthorizationX-Client-ID等字段。把这些可疑的参数名全部记录下来。

3.2 参数规律性分析

接下来,我们需要进行多次重复操作(如多次搜索同一条件、翻页),并对比同一个API请求的参数变化。使用mitmproxy的“Repeat”功能或者写个简单的Python脚本重放请求都很方便。对比时关注以下几点:

  1. 哪些参数是固定的?比如设备标识符、App版本号等,这些通常是一次性生成或从配置中读取的。
  2. 哪些参数是随着请求内容变化的?比如搜索关键词、分页页码、地理位置坐标等,这些是业务参数。
  3. 哪些参数是每次请求都变,且看起来无规律的?比如qn-mhpX-Sign等,这些就是我们需要重点攻破的签名或令牌参数。它们很可能由“固定部分+业务参数+时间戳+密钥”通过某种算法(如HMAC、AES、RSA或自定义哈希)生成,用于防止请求被篡改和重放。

你可以尝试将不同请求的qn-mhp值记录下来,观察其长度、字符集(是否是Base64编码?)。尝试修改业务参数(如把页码从1改成2)但不修改qn-mhp,然后重放请求,服务器很可能会返回签名错误(如HTTP 403或特定的错误码)。这证实了该参数在请求验证中的核心地位。

4. 静态代码分析与关键逻辑定位

在动态抓包获得足够线索后,我们需要深入代码内部,寻找生成这些关键参数的函数。这就是静态分析大显身手的时候。

4.1 使用Jadx进行全局搜索与入口定位

Jadx打开(已脱壳的)安居客APK。首先,利用其强大的全文搜索功能。直接搜索我们怀疑的参数名,比如“qn-mhp”。搜索结果可能会显示这个字符串常量在代码中的哪些地方被引用。通常,它可能出现在:

  • 网络请求库的拦截器(Interceptor)中:如果App使用OkHttpRetrofit,很可能会有一个自定义的Interceptor来统一添加请求头。搜索“Interceptor”、“addHeader”、“qn-mhp”等关键词。
  • 某个网络请求工具类或辅助类中:专门负责参数组装和签名的类。
  • JNI(Java Native Interface)调用附近:如果加密逻辑在Native层,Java代码中可能会有一个本地方法声明(native关键字),其方法名附近可能会有关于参数名的注释或字符串。

找到引用点后,点击进入,Jadx会尝试进行反编译。你可能会看到类似这样的代码片段(此为示例,非真实代码):

public class SignUtils { public static String generateMhp(Map<String, String> params, String timestamp, String deviceId) { // ... 参数排序、拼接 ... String signStr = sortAndConcat(params) + timestamp + deviceId + SECRET_KEY; // ... 调用某个加密方法 ... return encrypt(signStr); } }

或者,在OkHttp的拦截器中:

@Override public Response intercept(Chain chain) throws IOException { Request originalRequest = chain.request(); String mhp = calculateMhp(originalRequest); Request newRequest = originalRequest.newBuilder() .header("qn-mhp", mhp) .build(); return chain.proceed(newRequest); }

4.2 追踪加密函数与算法识别

一旦定位到生成qn-mhp的函数(比如calculateMhpgenerateMhp),就顺着调用链往下分析。这个函数内部可能直接包含了加密逻辑,也可能调用了其他工具方法。

你需要关注:

  • 字符串操作:参数是如何拼接的?是否按照键(Key)的字母顺序排序?是否在拼接的字符串前后加了特定的前缀或后缀(如"prefix" + data + "suffix")?
  • 加密API调用:代码中是否出现了MessageDigest(MD5, SHA-1, SHA-256)、Mac(HmacSHA256)、Cipher(AES, RSA)等Java加密类?这些是明确的算法指示。
  • 第三方库:是否引入了像CryptoJS的Java移植版、或某些商业SDK?这可能需要额外分析库的用法。
  • Native调用:如果看到native方法声明,如public static native String encryptNative(String data);,那么核心逻辑就在so动态链接库文件中。你需要记下这个JNI函数名(如Java_com_anjuke_util_SignHelper_encryptNative),然后转到IDA ProGhidra中分析对应的so文件。

在静态分析中,代码混淆会增加难度。类名、方法名、变量名可能变成a,b,c,a1,b2这种无意义的字符。这时候,你需要结合上下文逻辑(比如它处理了哪些数据、调用了哪些已知的API)和动态调试来推断其真实功能。

5. 动态调试与算法验证

静态分析给出了“地图”,动态调试则是我们行走在程序实际执行路径上的“导航”。Frida在这里扮演了无可替代的角色。

5.1 使用Frida进行函数Hook

假设我们通过静态分析,怀疑com.anjuke.security.SignGenerator类下的makeSignature方法是生成qn-mhp的关键。我们可以编写一个Frida脚本:

Java.perform(function() { var SignGenerator = Java.use('com.anjuke.security.SignGenerator'); // Hook makeSignature方法 SignGenerator.makeSignature.overload('java.lang.String', 'java.util.Map').implementation = function(param1, paramMap) { console.log('[+] makeSignature called!'); console.log(' param1: ' + param1); // 可能是时间戳或固定字符串 console.log(' paramMap: ' + JSON.stringify(mapToStringObject(paramMap))); // 打印Map内容 // 调用原方法获取结果 var result = this.makeSignature(param1, paramMap); console.log(' result (qn-mhp): ' + result); // 打印调用栈,帮助定位调用来源 console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); return result; }; // 辅助函数,将Java Map转换为JS可JSON化的对象 function mapToStringObject(map) { var obj = {}; var iterator = map.keySet().iterator(); while (iterator.hasNext()) { var key = iterator.next(); obj[key] = map.get(key).toString(); } return obj; } });

将这个脚本通过Frida注入到正在运行的安居客App进程中。然后,在App里触发一次网络请求。如果Hook成功,你将在Frida的控制台看到详细的输入参数和输出结果。这直接验证了我们的猜想,并拿到了清晰的输入输出对应关系。

5.2 参数构造过程还原与算法复现

通过多次Hook,收集不同请求下的输入和输出。例如:

  • 请求1:输入param1=“1648793600”,paramMap={“city”: “sh”, “page”: “1”},输出mhp=“abc123..."
  • 请求2:输入param1=“1648793605”,paramMap={“city”: “sh”, “page”: “2”},输出mhp=“def456...”

对比这些数据,结合之前静态分析看到的可能逻辑(如排序、拼接),我们可以尝试在本地用Python还原这个算法。过程可能如下:

  1. 参数排序与拼接:将paramMap中的所有键值对,按照键的字典序排序,然后拼接成key1=value1&key2=value2...的字符串。
  2. 拼接其他要素:将排序后的参数字符串、param1(时间戳)、可能还有一个固定的设备ID或Token、以及一个密钥(SECRET)拼接起来。这个密钥是静态分析中难以直接获取的,可能硬编码在代码里(经过混淆),也可能从服务器动态获取。
  3. 执行哈希/加密:对拼接后的整个字符串,执行某种哈希(如HMAC-SHA256)或加密操作。
  4. 编码输出:将二进制结果进行Base64或Hex编码,得到最终的qn-mhp值。

如何获取密钥?这是最难的一步。密钥可能:

  • 以字符串常量形式存在,但被分割、混淆或简单加密。
  • 通过多个字符串拼接、或经过简单变换(如异或)后得到。
  • 在App启动时从服务器获取,并缓存到本地。这时需要Hook网络请求或特定的初始化函数。

你可以尝试在Jadx中搜索与“secret”、“key”、“salt”、“auth”相关的字符串常量。或者,在Frida中Hook所有String的初始化或相关工具类的初始化方法,观察运行时产生的字符串。

5.3 本地Python算法复现示例

假设我们最终推断出算法是:qn-mhp = HMAC-SHA256(排序后的参数字符串 + timestamp, SECRET_KEY).hexdigest()。那么Python复现代码可能如下:

import hashlib import hmac import time from urllib.parse import urlencode def generate_qn_mhp(params: dict, timestamp: str, secret_key: str) -> str: """ 模拟生成安居客 qn-mhp 参数 params: 业务参数字典,如 {'city': 'sh', 'page': '1'} timestamp: 时间戳字符串 secret_key: 密钥(需要从逆向中获取) """ # 1. 对业务参数按key进行字典序排序并拼接 sorted_params = sorted(params.items(), key=lambda x: x[0]) param_str = urlencode(sorted_params) # 输出如 'city=sh&page=1' # 2. 拼接时间戳(假设拼接在参数后面) sign_string = param_str + timestamp # 3. 使用HMAC-SHA256计算签名 # 注意:secret_key需要转为bytes,sign_string也需要encode hmac_obj = hmac.new(secret_key.encode('utf-8'), sign_string.encode('utf-8'), hashlib.sha256) signature = hmac_obj.hexdigest() # 或者可能是base64编码 hmac_obj.digest().b64encode() return signature # 示例使用(密钥是假设的,需要逆向获取真实值) my_params = {'city': 'sh', 'page': '1'} current_timestamp = str(int(time.time())) my_secret = "your_reversed_secret_key_here" # 此处需替换为真实密钥 mhp = generate_qn_mhp(my_params, current_timestamp, my_secret) print(f"生成的 qn-mhp: {mhp}")

6. 完整请求模拟与数据抓取实践

在成功复现了qn-mhp或其他关键参数的生成算法后,我们就可以用Python的requests库来模拟完整的App请求了。

6.1 构建请求头与参数

你需要完整地复制App请求中的Headers。除了我们逆向出来的签名头,以下头部也至关重要,服务器可能会校验:

  • User-Agent: 需要使用移动端的UA,例如Dalvik/2.1.0 (Linux; U; Android 10; ...)或App自定义的UA。
  • Content-Type: 通常是application/jsonapplication/x-www-form-urlencoded
  • X-Client-ID/Device-ID: 设备标识符,可能是一个固定的UUID,需要从App的首次请求或本地存储中提取。
  • Authorization: 如果涉及用户登录,可能是Bearer Token。
  • X-Timestamp: 时间戳,需要和签名中使用的时间戳一致。

业务参数(Body或Query Params)则根据API文档(我们逆向出来的规律)来构造。

6.2 请求流程编排与错误处理

编写一个完整的请求函数,其流程如下:

  1. 准备业务参数。
  2. 获取当前时间戳。
  3. 调用我们复现的签名函数,生成qn-mhp
  4. 组装完整的请求头和请求体。
  5. 发送HTTP请求。
  6. 处理响应:检查状态码。如果成功(200),解析JSON数据。如果失败(如403签名错误、429频率限制),根据错误信息进行调试(检查时间戳同步、参数顺序、密钥是否正确、是否缺少必要头部)。

时间戳同步问题:服务器的时间可能和本地时间有微小偏差。如果签名包含时间戳且服务器校验时间窗口很窄(如±30秒),就需要确保本地时间准确,或者从服务器响应中获取时间(有些API会在响应头里返回服务器时间)。

频率限制与IP封禁:模拟请求时,务必加入合理的延时(如time.sleep(random.uniform(1, 3))),避免请求过快触发服务器的反爬机制,导致IP被暂时封禁。可以考虑使用代理IP池来分散请求。

6.3 数据解析与存储

成功获取到的数据通常是JSON格式。使用Python的json库解析后,按需提取字段。例如,对于房源列表,你可能需要:房源ID、标题、总价、单价、面积、户型、朝向、楼层、小区名、区域、发布时间、图片URL等。

存储可以选择多种方式:

  • JSON文件:简单直接,适合小批量数据。
  • CSV文件:便于用Excel打开和分析。
  • 数据库(SQLite/MySQL/PostgreSQL):适合大规模、结构化存储和复杂查询。使用sqlite3SQLAlchemy等库。
  • MongoDB:如果JSON结构非常复杂或多变,NoSQL数据库更灵活。

7. 逆向过程中的常见陷阱与应对策略

在整个逆向安居客参数的过程中,我踩过不少坑,这里总结几个最常见的难题和解决思路。

7.1 加固与混淆的对抗

问题:代码被严重混淆,类名方法名全是a.b.c,逻辑被隐藏或调用链被打乱。策略

  • 脱壳是第一要务:确保拿到的是可分析的Dex。多尝试不同的脱壳工具和Frida内存Dump脚本。
  • 动态分析优先:当静态分析无从下手时,直接用Frida去Hook那些被频繁调用的、或参数/返回值是字符串的函数(如StringBuilder.toString(),JSONObject.toString()),从运行时数据倒推逻辑。
  • 搜索特征字符串:即使类名混淆,代码中出现的API域名、路径、固定的错误信息字符串是不会变的。以这些字符串为锚点,定位关键代码区域。

7.2 密钥的隐藏与变换

问题:找不到明显的密钥字符串,或者找到的字符串直接用于计算签名不对。策略

  • Hook 加密函数入口:用FridaHookjavax.crypto.Mac.getInstance()MessageDigest.getInstance()等,打印传入的密钥(SecretKeySpec的密钥字节数组)。这是获取运行时密钥最直接的方法。
  • 追踪字符串生成:密钥可能是由多个片段拼接而成。HookStringBuilder.append()或字符串的+操作,观察相关代码段。
  • 算法识别:如果密钥被编码(如Base64),先解码再使用。有时密钥会与某个固定值进行异或(XOR)运算,需要分析其变换逻辑。

7.3 请求链依赖与上下文

问题:单独调用某个API失败,提示“缺少上下文”或“会话失效”。策略

  • 模拟完整会话:App的请求往往不是孤立的。可能需要先模拟一个“启动”或“初始化”请求,获取一个全局的tokensession_id,后续请求都要带上。
  • 顺序请求:有些API需要前一个API的返回结果作为参数。仔细分析App的正常操作流程,用抓包工具查看请求序列,并完整模拟这个序列。
  • 状态保持:使用requests.Session()来维持Cookie和部分Header,模拟App的状态保持机制。

7.4 算法更新与风控升级

问题:今天还能用的脚本,明天就失效了,返回签名错误。策略

  • 关注App更新:算法的改变通常伴随着App版本的更新。保留不同版本的APK,以便回滚分析。
  • 设计可配置的签名模块:将密钥、算法步骤、拼接顺序等写成可配置项,便于快速调整。
  • 监控与告警:自动化脚本应包含对响应状态的监控,一旦大量失败,能及时通知人工检查。

逆向工程是一场与开发者的“猫鼠游戏”。安居客这样的平台会持续升级其安全策略。因此,理解原理比掌握某一个固定算法更重要。掌握了静态分析、动态调试、算法还原这一套方法论,你就能适应不断变化的技术挑战。最后再次强调,所有技术都应在法律和道德框架内使用,尊重数据所有权和平台规则,将重点放在技术原理的学习和钻研上。