1、工具准备
样本App版本:v5.0.90
注入框架:xposed、frida(hluda 16.2.1)
反编译&其他:JEB、jadx、Charles
2、过程
大致分为抓包、脱壳、反编译、动态调试/加解密算法探索,构造模拟请求几个步骤,每个步骤都可能有不同的异常出现,本文主要记录在过程中的主体脉络和流程,过程中会附上关键代码。
2.1 抓包
首先尝试在手机上配置wifi代理,但Charles中无法看到相应的包记录。猜测是因为App屏蔽了网络代理,因此改用其他方式。手机上安装Drony,并开启手机全局网络代理(类型选择:socks5),代理地址指向Chares,此时就可以愉快的看到请求记录了。
在抓到的报文中,可以看到每次请求中,都包含了一些奇怪的header,比如t、spv、n、st,这些字段大概率与api接口的加密与签名有关。接下来,需要结合代码进一步分析。
2.2 脱壳&反编译
直接通过frida-dump脱壳
这时直接在反编译的结果中搜索关键词"spv"
签名的传入参数为分别为: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)