山姆会员商店逆向分析

默认分类 · 06-06 · 371 人浏览

1、工具准备

样本App版本:v5.0.90
注入框架:xposed、frida(hluda 16.2.1
反编译&其他:JEB、jadx、Charles

2、过程

大致分为抓包、脱壳、反编译、动态调试/加解密算法探索,构造模拟请求几个步骤,每个步骤都可能有不同的异常出现,本文主要记录在过程中的主体脉络和流程,过程中会附上关键代码。

2.1 抓包

首先尝试在手机上配置wifi代理,但Charles中无法看到相应的包记录。猜测是因为App屏蔽了网络代理,因此改用其他方式。手机上安装Drony,并开启手机全局网络代理(类型选择:socks5),代理地址指向Chares,此时就可以愉快的看到请求记录了。

2024-06-06T02:21:22.png

在抓到的报文中,可以看到每次请求中,都包含了一些奇怪的header,比如t、spv、n、st,这些字段大概率与api接口的加密与签名有关。接下来,需要结合代码进一步分析。

2.2 脱壳&反编译

直接通过frida-dump脱壳
2024-06-06T02:23:03.png
这时直接在反编译的结果中搜索关键词"spv"
2024-06-06T02:23:47.png
签名的传入参数为分别为:t - 时间戳、data_json - 按json序列化后的业务对象参数、n - 去掉"-"符号后的uuid(32位字符串)、auth_token - 登录后用户令牌,按照如下规则排列所得:

{t}{data_json}{n}{auth_token}

返回字符串即为签名结果 - st
该签名算法有使用native方法,具体算法逻辑应该需要反汇编相应的so文件了。签名规则已经基本明确了,直接调用java层方法,走RPC调用即可得到我们想要的结果。

app_inject.js

var g_instance = null;
Java.enumerateClassLoaders({
  onMatch: function (loader) {
    try {
      if (loader.findClass("cn.samsclub.app.e.c")) {
        Java.classFactory.loader = loader;
        console.log(loader);
        g_instance = Java.use("cn.samsclub.app.e.c").$new();
        console.log("target found!");
      }
    } catch (error) {}
  },
  onComplete: function () {},
});

// boolean z, String str
function sign(z, text) {
  console.log("js7 start run: sign", g_instance, text);
  var result = g_instance.a
    .overload("boolean", "java.lang.String")
    .call(g_instance, z, text);
  console.log("result = ", result);
  return result;
}

rpc.exports = {
  getsign: sign,
};
console.log("injected.");

sm.py

import frida
import time
import json
import uuid
import requests


def on_message(message, data):
    if message['type'] == 'send':
        payload = message['payload']
        print("[on_message]:", payload)
    else:
        print(message)


def start_rpc():
    # 连接到应用程序
    device = frida.get_usb_device(-1)
    pid = device.spawn('cn.samsclub.app')
    device.resume(pid)
    time.sleep(2)
    session = device.attach(pid)
    # 创建脚本
    with open('app_inject.js', 'r') as f:
        js_code = f.read()
    script = session.create_script(js_code)
    # 消息处理
    script.on('message', on_message)
    # 加载脚本
    script.load()
    # 返回脚本的导出值
    return script.exports_sync


def _headers(auth_token, device_id, t, n, signed, lon, lat):
    return {
        'system-language': 'CN',
        'device-type': 'android',
        'tpg': '1',
        'app-version': '5.0.80',
        'device-id': device_id,
        'device-os-version': '10',
        'device-name': 'blackshark_SKR-A0',
        'treq-id': '62648e345d524b42b688258947472be9.160.17166087875278032',
        'auth-token': auth_token,
        'longitude': lon,
        'latitude': lat,
        'p': '1656120205',
        't': t,
        'n': n,
        'sy': '0',
        'st': signed,
        'sny': 'c',
        'rcs': '2',
        'spv': '1.1',
        'Local-Longitude': '118.655718',
        'Local-Latitude': '31.928179',
        'Content-Type': 'application/json;charset=utf-8',
        'Host': 'api-sams.walmartmobile.cn',
        'User-Agent': 'okhttp/4.8.1'
    }


def getList(rpc):
    url = "https://api-sams.walmartmobile.cn/api/v1/sams/goods-portal/grouping/list"
    device_id = '9dfcf08859600215e2b2839c10001ea17309'
    auth_token = '740d926b981716f460078b88422fb7ecbd4e481bbd4ca3e0a87c84ab2e10b3fa7e7cfa8e7c9fd32081a1c23129bdffad0a9baaa57e7fb720'
    lon, lat = '114.011671', '22.544368'
    t = f"{int(time.time() * 1000)}"

    data = {
        "pageSize": 20,
        "useNewPage": "true",
        "addressVO": {
            "cityName": "",
            "countryName": "",
            "districtName": "",
            "provinceName": ""
        },
        "storeInfoVOList": [{
            "storeType": 256,
            "storeId": 6758,
            "storeDeliveryAttr": [2, 3, 4, 5, 6, 9, 12, 13]
        }, {
            "storeType": 2,
            "storeId": 6505,
            "storeDeliveryAttr": [7]
        }, {
            "storeType": 4,
            "storeId": 6672,
            "storeDeliveryAttr": [3, 4]
        }, {
            "storeType": 8,
            "storeId": 9992,
            "storeDeliveryAttr": [1]
        }],
        "uid": "181879357459",
        "pageNum": 1,
        "useNew": "true",
        "isReversOrder": "false",
        "isFastDelivery": "false",
        "recommendFirstCategoryId": 35145,
        "recommendSecondCategoryId": 158093,
        "frontCategoryIds": [157100]
    }
    n = str(uuid.uuid4()).replace('-', '')
    data_json = json.dumps(data, indent=None, separators=(
        ',', ':'), ensure_ascii=False)
    signed = rpc.getsign(True, f"{t}{data_json}{n}{auth_token}")
    headers = _headers(auth_token=auth_token, device_id=device_id,
                       t=t, n=n, signed=signed, lon=lon, lat=lat)
    response = requests.request(
        "POST", url, headers=headers, data=data_json.encode('utf-8'))
    print(response.text)


if __name__ == '__main__':
    rpc = start_rpc()
    for i in range(1):
        getList(rpc)
        time.sleep(1)

最终运行结果

2024-06-06T02:29:16.png

Theme Jasmine by Kent Liao