欢迎光临
我们一直在努力

踩了双向证书+Frida检测+签名校验三重坑!我用Python+Frida把某生鲜APP的接口扒得明明白白

大家好,我是威哥。前阵子帮楼下开社区团购的张哥做个竞品价格监控,选了本地最大的那家连锁生鲜APP,本来以为用之前的Magisk+JustTrustMe就能搞定,结果一上来就被三重防护按在地上摩擦——双向证书绑定绕不过,刚开Frida APP就闪退,好不容易绕过闪退,接口又说“客户端签名无效”。

前前后后折腾了快两周,换了三台测试机,刷了N个Magisk模块,终于把这三重坑全踩平了。今天把完整的实战流程分享出来,没有网上抄烂的理论,全是我跑通了、能长期稳定用的硬货,每个步骤都有具体的避坑点,看完你就能直接套用到类似的APP上。


第一步:先搭个能抓包的基础环境,别一开始就碰Frida

很多新手一上来就开Frida,结果APP直接闪退,连抓包的机会都没有。我建议先从最基础的双向证书绑定绕起,搭个能看到接口的环境,再一步步往上加防护绕过。

1. 双向证书绑定的终极绕法:Magisk+LSPosed+TrustMeAlready

JustTrustMe模块虽然经典,但现在很多大厂的双向证书绑定已经能检测到它的Hook点了,我这次用的是TrustMeAlready,它是基于LSPosed的,Hook点更隐蔽,能绕过99%的双向证书绑定和SSL Pinning。

具体步骤:

  • 找一台能解锁Bootloader的测试机(我用的是小米10青春版,MIUI12.5,解锁Bootloader比较容易),刷入Magisk 24.3(别刷太新的,太新的模块兼容性不好);
  • 在Magisk里刷入LSPosed模块(注意要刷Zygisk版本的,Riru版本的已经停更了),安装后重启手机;
  • 下载TrustMeAlready模块的APK(GitHub上搜就能找到,要下载最新的适配LSPosed Zygisk的版本),安装后在LSPosed的“模块”里启用它,勾选你要抓包的生鲜APP,重启手机;
  • 在手机上安装Charles/Fiddler的证书,然后设置代理(注意是在系统设置里设置,不是在LSPosed里);
  • 打开生鲜APP,你就能在Charles/Fiddler里看到所有的HTTPS请求了。
  • 这里踩了个巨坑:一开始我刷的是最新的Magisk 26.4,结果LSPosed Zygisk版本的模块根本装不上,换了Magisk 24.3才成功;还有,TrustMeAlready一定要勾选“系统框架”和“目标APP”两个选项,只勾选目标APP有时候绕不过系统级的SSL Pinning。


    第二步:绕过Frida检测,让APP在Hook下正常运行

    能抓包之后,我发现接口里有个32位的client_sign参数,复制到Postman里改个时间戳,直接返回403,说“客户端签名无效”。这个参数肯定是在APP里生成的,我准备用Frida Hook看看生成逻辑,结果刚开Frida,APP就直接闪退了——这是遇到了Frida检测。

    1. 常见的Frida检测手段

    我先查了一下这个生鲜APP的反编译代码(用jadx-gui),搜索“frida”、“xposed”、“magisk”这些关键词,找到了几个常见的检测点:

    • 检测进程名里有没有“frida”、“frida-helper”这些关键词;
    • 检测/proc/self/maps里有没有“frida”、“libfrida”这些库;
    • 检测系统里有没有安装Frida的APK;
    • 检测端口27042(Frida默认的监听端口)有没有被占用;
    • 检测Zygisk的特征,比如/proc/self/status里有没有“TracerPid”不为0的情况。

    2. 终极绕过方案:Frida-Gadget+重打包+Magisk Hide

    普通的Frida Spawn模式(frida -U -f 包名 -l hook.js)很容易被检测到,我这次用的是Frida-Gadget+重打包+Magisk Hide的组合拳,能绕过几乎所有的Frida检测:

    具体步骤:
  • 下载对应架构的Frida-Gadget:GitHub上搜Frida,找到最新的Release,下载对应你手机架构的frida-gadget-xxx-android-xxx.so文件(我手机是arm64-v8a的,所以下载的是frida-gadget-16.4.10-android-arm64.so);
  • 反编译APK并注入Frida-Gadget:
    • 用apktool反编译生鲜APP的APK:apktool d xxx.apk -o xxx;
    • 把下载好的Frida-Gadget重命名为libfrida-gadget.so,放到反编译后的lib/arm64-v8a/目录下(如果是32位手机,放到lib/armeabi-v7a/目录下);
    • 找到反编译后的AndroidManifest.xml文件,在<application>标签里添加android:debuggable="true"属性(如果已经有了,就不用加了);
    • 找到反编译后的smali/com/xxx/xxx/MainActivity.smali文件(或者APP启动的第一个Activity的smali文件),在onCreate方法的第一行添加加载Frida-Gadget的代码:const-string v0, "frida-gadget"
      invoke-static {v0}, Ljava/lang/System;->loadLibrary(Ljava/lang/String;)V

  • 重打包并签名APK:
    • 用apktool重打包反编译后的文件:apktool b xxx -o xxx_mod.apk;
    • 用jarsigner或者apksigner给重打包后的APK签名(注意要用自己的签名文件,不要用系统默认的):apksigner sign –ks mykey.keystore –ks-key-alias mykey xxx_mod.apk
  • 安装重打包后的APK并配置Magisk Hide:
    • 卸载原来的生鲜APP,安装重打包后的APK;
    • 打开Magisk Manager,点“设置”→“Magisk Hide”,勾选重打包后的生鲜APP;
    • 打开LSPosed,取消勾选原来的生鲜APP,勾选重打包后的生鲜APP;
  • 启动APP并连接Frida:
    • 打开重打包后的生鲜APP,APP会自动加载Frida-Gadget,然后停在启动页(这是正常的,因为Frida-Gadget在等待连接);
    • 用adb连接手机,运行frida-ps -U,你会看到重打包后的生鲜APP的进程名;
    • 运行frida -U 进程名 -l hook.js,APP会自动继续运行,你就能在终端里看到Hook的结果了。
  • 这里踩了好几个坑:

    • 一开始我用的是Frida Spawn模式,结果APP直接闪退,换了Frida-Gadget+重打包才成功;
    • 重打包后的APK一定要用自己的签名文件签名,不然安装不上;
    • 一定要在Magisk Hide里勾选重打包后的APP,不然APP会检测到Root,直接闪退;
    • 加载Frida-Gadget的代码一定要放在Activity的onCreate方法的第一行,不然APP会先启动,然后再加载Frida-Gadget,Hook不到启动时的代码。

    第三步:Hook签名生成逻辑,用Python复现

    绕过Frida检测之后,我终于能Hook签名生成逻辑了。我先查了一下反编译代码,搜索“client_sign”关键词,找到了签名生成类com.xxx.common.utils.SignUtils,里面有个getClientSign方法,核心逻辑是调用native层的getNativeSign函数,实现放在libsign.so文件里。

    1. Hook Java层和Native层的函数

    我写了一个Frida脚本,同时Hook Java层的getClientSign方法和Native层的getNativeSign函数,看看入参和出参:

    Java.perform(function() {
    // Hook Java层的getClientSign方法
    var SignUtils = Java.use("com.xxx.common.utils.SignUtils");
    SignUtils.getClientSign.overload('java.lang.String', 'java.lang.String', 'long').implementation = function(path, body, timestamp) {
    console.log("=== Java层入参 ===");
    console.log("请求路径:" + path);
    console.log("请求体:" + body);
    console.log("时间戳:" + timestamp);
    var result = this.getClientSign(path, body, timestamp);
    console.log("生成的client_sign:" + result);
    return result;
    };

    // Hook Native层的getNativeSign函数
    var nativeAddr = Module.findExportByName("libsign.so", "Java_com_xxx_common_utils_SignUtils_getNativeSign");
    if (nativeAddr) {
    Interceptor.attach(nativeAddr, {
    onEnter: function(args) {
    console.log("=== Native层入参 ===");
    console.log("入参1(请求路径+时间戳+排序后的请求体JSON):" + Java.vm.getEnv().getStringUtfChars(args[2], null).readCString());
    console.log("入参2(固定盐值):" + Java.vm.getEnv().getStringUtfChars(args[3], null).readCString());
    },
    onLeave: function(retval) {
    console.log("Native层输出(MD5后的client_sign):" + Java.vm.getEnv().getStringUtfChars(retval, null).readCString());
    }
    });
    }
    });

    运行脚本之后,我直接傻了——native层根本没有做复杂加密,只是把Java层传进来的拼接好的字符串做了个MD5!所谓的native加密就是个障眼法,真正的拼接规则在Java层就已经完成了:client_sign = MD5(请求路径 + 时间戳 + 按字典序排序后的请求体JSON + 固定盐值),固定盐值是从libsign.so里的一个全局变量里拿的,我直接在Frida脚本里打印出来了。

    2. 用Python复现签名生成逻辑

    拿到拼接规则和固定盐值之后,复现代码非常简单,而且可以批量生成不同设备的client_sign,完全不用依赖手机:

    import hashlib
    import time
    import json
    import uuid
    import requests

    # 从Frida脚本里打印出来的固定盐值
    SALT = "xxx1234567890abcdefghijklmnopqrst"
    # 生鲜APP的接口地址
    BASE_URL = "https://api.xxx.com"

    def generate_client_sign(path: str, body: dict, timestamp: int) > str:
    """生成client_sign参数"""
    # 按字典序排序请求体
    sorted_body = dict(sorted(body.items()))
    body_json = json.dumps(sorted_body, separators=(',', ':'))
    # 按规则拼接字符串
    sign_str = f"{path}{timestamp}{body_json}{SALT}"
    # MD5加密,转小写
    client_sign = hashlib.md5(sign_str.encode()).hexdigest().lower()
    return client_sign

    def get_merchant_list(city_id: str, page: int, page_size: int) > dict:
    """获取商家列表"""
    # 随机生成设备ID,模拟不同设备
    device_id = str(uuid.uuid4()).replace('-', '')
    # 生成时间戳(毫秒级)
    timestamp = int(time.time() * 1000)
    # 请求路径
    path = "/api/v1/merchant/list"
    # 请求参数
    body = {
    "cityId": city_id,
    "page": page,
    "pageSize": page_size,
    "deviceId": device_id
    }
    # 生成client_sign
    client_sign = generate_client_sign(path, body, timestamp)
    # 完整的请求参数
    params = {
    **body,
    "timestamp": timestamp,
    "clientSign": client_sign
    }
    # 请求头(完全复制Charles里的请求头)
    headers = {
    "Host": "api.xxx.com",
    "Connection": "keep-alive",
    "User-Agent": "Mozilla/5.0 (Linux; Android 11; Mi 10 Lite Build/RKQ1.200826.002; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/120.0.6099.230 Mobile Safari/537.36 xxx/1.0.0",
    "Accept": "application/json",
    "Accept-Language": "zh-CN,zh;q=0.9",
    "Accept-Encoding": "gzip, deflate, br",
    "X-Requested-With": "com.xxx.xxx"
    }
    # 发起请求
    resp = requests.get(BASE_URL + path, params=params, headers=headers, timeout=15)
    resp.raise_for_status()
    return resp.json()

    # 测试调用
    if __name__ == "__main__":
    # 获取北京地区的商家列表,第1页,每页20条
    result = get_merchant_list("110000", 1, 20)
    print(result)

    这里踩了个小坑:请求头里的User-Agent一定要完全复制Charles里的,包括APP的版本号、手机的型号和系统版本,不然接口会返回“客户端版本过低”或者“设备不兼容”。


    最后说几句

    做了这么多年APP反爬,我最大的感受就是:企业级APP反爬的核心从来都不是加密算法有多复杂,而是检测手段有多隐蔽。不管是双向证书绑定、Frida检测还是签名校验,只要你能找到对应的检测点,然后用合适的工具绕过它,就能拿到你想要的数据。

    当然,还是要提醒大家:一定要严格遵守《网络安全法》《数据安全法》,遵守APP的用户协议,只爬取公开的、合规的数据,不要爬取用户隐私、商业机密等敏感信息,更不要给对方服务器造成过大的访问压力。

    赞(0)
    未经允许不得转载:171主机测评 » 踩了双向证书+Frida检测+签名校验三重坑!我用Python+Frida把某生鲜APP的接口扒得明明白白
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址