探索学习app接口的加解密机制,并通过api模拟调用的方式,发起业务请求。仅供学习。
样本App版本:v5.0.80,v5.0.90
设备:Oppo R9s(Android7.1.1)+ MacOS Big Sur(Intel)
注入框架:xposed、frida(hluda 15.2.2)
反编译&其他:JEB、jadx、Charles
大致分为抓包、脱壳、反编译、动态调试/加解密算法探索,构造模拟请求几个步骤,每个步骤都可能有不同的异常出现,本文主要记录在过程中的主体脉络和流程,过程中会附上关键代码。
首先尝试在手机上配置wifi代理,但Charles中无法看到相应的包记录。猜测是因为App屏蔽了网络代理,因此改用其他方式。手机上安装Drony,并开启手机全局网络代理(类型选择:socks5),代理地址指向Chares,此时就可以愉快的看到请求记录了。
如果是通过iOS抓包,直接通过小火箭抓包也是灰常方便。另外下载Drony App可能需要TZ,解决无法访问的问题。
在抓到的报文中,可以看到每次请求中,都包含了一些奇怪的header,比如t、spv、n、st,这些字段大概率与api接口的加密与签名有关。接下来,需要结合代码进一步分析。
直接通过Xposed + 反射大师App,即可做到轻松脱壳,App未针对Xposed做检测。脱壳后得到7个dex文件,使用python脚本合并,将7个dex文件利用jadx全部反编译成Java文件到同一目录,即可直接翻阅App反编译后的源码。
这时直接在反编译的结果中搜索关键词"spv",却发现找不到。难道这些字段都隐藏到so中了,那就麻烦了。这时使用JEB再次反编译试试看,再次搜索"spv",找到了。
这里,要提醒一下:针对反编译,同样的dex文件,用不同的反编译工具,结果也会不一样,可读性差异很多,因此当使用一种工具反编译失败的话,可以尝试用不同的工具,比如,通用一段代码的反编译结果,使用jadx时,提示反编译失败,如下:
但是使用JEB时,结果则基本可用,如下:
拿到反编译源码后,接下来就需要结合frida动态分析代码调用链,找到api调用的核心算法逻辑并加以验证。
在App最新版本v5.0.90上,连接frida客户端。frida注入失败。随后换了hluda、xcube等方案均以失败告终,看了下app的加固方案,使用的腾讯的加固方案,对应的壳文件是libshell-super.cn.xxxxclub.app.so
,尝试绕过壳的反注入逻辑,也没有效果。
这时偶然看到旧版本的app使用的壳文件是libshell-super.2019.so
,灵光一闪,感觉旧版本的app上应该有机会,于是下载安装v5.0.80,frida注入成功了。app上开启了强制更新,于是在Charles上hook重写了app检查更新接口的返回结果,让app检查不到新版本,app仍然可以继续使用(后续有风险,历史接口可能下线)。
旧版本app上也可以使用frida工具集:Objection,通过调试和代码比对,基本确认了核心的算法签名逻辑位置:
签名的传入参数为分别为:t
- 时间戳、data_json
- 按json序列化后的业务对象参数、n
- 去掉"-"符号后的uuid(32位字符串)、auth_token
- 登录后用户令牌,按照如下规则排列所得:
返回字符串即为签名结果 - st
该签名算法有使用native方法,具体算法逻辑应该需要反汇编相应的so文件了。签名规则已经基本明确了,直接调用java层方法,走RPC调用即可得到我们想要的结果。偷懒了,就不去深挖汇编代码了,笔者也不确认一定能找到结果-_-||
1)创建js文件app_inject.js
,声明rpc接口
2)创建frida客户端,声明rpc调用。文件名:frida_client.py
3)构造参数,发起RPC调用。文件名:demo.py
再看看结果,已经成功得到响应数据了。大功告成!
在执行frida js注入时,Java.enumerateClassLoaders()
仅支持Android 7.0及以上系统,若使用低版本的Android系统,如Android 6.1,则需要使用send(),进行消息异步通知。当采用异步通知时,在Python客户端的编码中,需要定义消息回调函数,同时将异步调用封装成同步调用,方便上游调用使用。对应的js代码和python代码如下:
app_inject_for_android_6.0.js
:
frida_client_for_android_6.0.js
:
通过测试验证,可以发现两个版本v5.0.80,v5.0.90的签名算法是一致的。因此可以直接利用v5.0.80做签名即可。
打完收工!
原文链接:https://bbs.kanxue.com/thread-281158.htm
未经授权,禁止转载!!
import
os, sys
if
__name__
=
=
"__main__"
:
if
len
(sys.argv) <
3
:
print
(
"start error"
)
sys.exit()
source_dir
=
sys.argv[
1
]
output_dir
=
sys.argv[
2
]
print
(source_dir, output_dir)
files
=
os.listdir(source_dir)
for
file
in
files:
if
file
.find(
".dex"
) >
0
:
sh
=
'{your_path}/bin/jadx -Pdex-input.verify-checksum=no -j 1 -r -d '
+
output_dir
+
" "
+
source_dir
+
file
print
(sh)
os.system(sh)
import
os, sys
if
__name__
=
=
"__main__"
:
if
len
(sys.argv) <
3
:
print
(
"start error"
)
sys.exit()
source_dir
=
sys.argv[
1
]
output_dir
=
sys.argv[
2
]
print
(source_dir, output_dir)
files
=
os.listdir(source_dir)
for
file
in
files:
if
file
.find(
".dex"
) >
0
:
sh
=
'{your_path}/bin/jadx -Pdex-input.verify-checksum=no -j 1 -r -d '
+
output_dir
+
" "
+
source_dir
+
file
print
(sh)
os.system(sh)
throw
new
UnsupportedOperationException(
"Method not decompiled: cn.xxxxclub.app.e.c.intercept(okhttp3.w$a):okhttp3.ad"
);
}
throw
new
UnsupportedOperationException(
"Method not decompiled: cn.xxxxclub.app.e.c.intercept(okhttp3.w$a):okhttp3.ad"
);
}
public
ad intercept(w.a arg19) {
....(略)
String v8_1 = String.valueOf(z.b());
v4_1.b(
"t"
, v8_1);
l.b(
"dc1ad18e-3e2d-4d49-a303-f637c6a5a3fb"
,
"randomUUID().toString()"
);
String v9_1 = b.m.g.a(
"dc1ad18e-3e2d-4d49-a303-f637c6a5a3fb"
,
"-"
,
""
,
false
,
4
,
null
);
v4_1.b(
"n"
, v9_1);
v4_1.b(
"sy"
,
"0"
);
int
v10_1 = v10 ==
0
|| !cn.xxxxclub.app.base.manager.d.a.i() ?
0
:
1
;
String v5_4 =
this
.a(((
boolean
)v10_1), v8_1 + v5_3 + v9_1 + g.a.b());
if
(((CharSequence)v5_4).length() >
0
) {
v4_1.b(
"st"
, v5_4);
}
v4_1.b(
"sny"
, (v10_1 ==
0
?
"j"
:
"c"
));
v4_1.b(
"rcs"
,
"1"
);
v4_1.b(
"spv"
,
"1.1"
);
if
(v11) {
String v5_5 = URLEncoder.encode(String.valueOf(cn.xxxxclub.app.base.manager.f.a.b()),
"utf-8"
);
l.b(v5_5,
"encode(LocationManager.g…de().toString(), \"utf-8\")"
);
v4_1.b(
"Local-Longitude"
, v5_5);
String v5_6 = URLEncoder.encode(String.valueOf(cn.xxxxclub.app.base.manager.f.a.a()),
"utf-8"
);
l.b(v5_6,
"encode(LocationManager.g…de().toString(), \"utf-8\")"
);
v4_1.b(
"Local-Latitude"
, v5_6);
}
....
return
v5_7;
}
public
ad intercept(w.a arg19) {
....(略)
String v8_1 = String.valueOf(z.b());
v4_1.b(
"t"
, v8_1);
l.b(
"dc1ad18e-3e2d-4d49-a303-f637c6a5a3fb"
,
"randomUUID().toString()"
);
String v9_1 = b.m.g.a(
"dc1ad18e-3e2d-4d49-a303-f637c6a5a3fb"
,
"-"
,
""
,
false
,
4
,
null
);
v4_1.b(
"n"
, v9_1);
v4_1.b(
"sy"
,
"0"
);
int
v10_1 = v10 ==
0
|| !cn.xxxxclub.app.base.manager.d.a.i() ?
0
:
1
;
String v5_4 =
this
.a(((
boolean
)v10_1), v8_1 + v5_3 + v9_1 + g.a.b());
if
(((CharSequence)v5_4).length() >
0
) {
v4_1.b(
"st"
, v5_4);
}
v4_1.b(
"sny"
, (v10_1 ==
0
?
"j"
:
"c"
));
v4_1.b(
"rcs"
,
"1"
);
v4_1.b(
"spv"
,
"1.1"
);
if
(v11) {
String v5_5 = URLEncoder.encode(String.valueOf(cn.xxxxclub.app.base.manager.f.a.b()),
"utf-8"
);
l.b(v5_5,
"encode(LocationManager.g…de().toString(), \"utf-8\")"
);
v4_1.b(
"Local-Longitude"
, v5_5);
String v5_6 = URLEncoder.encode(String.valueOf(cn.xxxxclub.app.base.manager.f.a.a()),
"utf-8"
);
l.b(v5_6,
"encode(LocationManager.g…de().toString(), \"utf-8\")"
);
v4_1.b(
"Local-Latitude"
, v5_6);
}
....
return
v5_7;
}
"{t}{data_json}{n}{auth_token}"
"{t}{data_json}{n}{auth_token}"
var
g_instance =
null
;
Java.enumerateClassLoaders({
onMatch:
function
(loader) {
try
{
if
(loader.findClass(
"cn.xxxxclub.app.e.c"
)) {
Java.classFactory.loader = loader;
g_instance = Java.use(
"cn.xxxxclub.app.e.c"
).$
new
();
console.log(
"target found!"
)
}
}
catch
(error) {}
}, onComplete:
function
() {
}
});
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,
hello:
function
() {
return
'hello'
;
}
}
console.log(
"injected."
)
var
g_instance =
null
;
Java.enumerateClassLoaders({
onMatch:
function
(loader) {
try
{
if
(loader.findClass(
"cn.xxxxclub.app.e.c"
)) {
Java.classFactory.loader = loader;
g_instance = Java.use(
"cn.xxxxclub.app.e.c"
).$
new
();
console.log(
"target found!"
)
}
}
catch
(error) {}
}, onComplete:
function
() {
}
});
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,
hello:
function
() {
return
'hello'
;
}
}
console.log(
"injected."
)
import
frida
import
time
class
FridaClient:
class
StartMode:
attach
=
'attach'
spawn
=
'spawn'
def
__init__(
self
, package_name, js_file, mode
=
StartMode.attach, delay_sec_4_spawn
=
2
):
self
.results
=
{}
self
.script
=
None
self
.package_name
=
package_name
self
.delay_sec_4_spawn
=
delay_sec_4_spawn
self
.mode
=
mode
self
.js_file
=
js_file
def
on_message(
self
, message, data):
if
message[
'type'
]
=
=
'send'
:
payload
=
message[
'payload'
]
print
(
"[on_message]:"
, payload)
else
:
print
(message)
def
start(
self
):
print
(f
"starting frida client with mode: {self.mode} ... pkg = [{self.package_name}]"
)
if
self
.mode
=
=
FridaClient.StartMode.attach:
session
=
frida.get_device_manager().add_remote_device(
"127.0.0.1:1234"
).attach(
self
.package_name)
elif
self
.mode
=
=
FridaClient.StartMode.spawn:
device
=
frida.get_device_manager().add_remote_device(
"127.0.0.1:1234"
)
pid
=
device.spawn([
self
.package_name])
device.resume(pid)
time.sleep(
self
.delay_sec_4_spawn)
session
=
device.attach(pid)
with
open
(
self
.js_file,
'r'
) as f:
js_code
=
f.read()
script
=
session.create_script(js_code)
script.on(
'message'
,
self
.on_message)
self
.script
=
script
script.load()
print
(
"load ready"
)
def
stop(
self
):
if
self
.script:
self
.script.unload()
self
.script
=
None
def
get_sign(
self
, text:
str
):
return
self
.script.exports.getsign(
True
, text)
import
frida
import
time
class
FridaClient:
class
StartMode:
attach
=
'attach'
spawn
=
'spawn'
def
__init__(
self
, package_name, js_file, mode
=
StartMode.attach, delay_sec_4_spawn
=
2
):
self
.results
=
{}
self
.script
=
None
self
.package_name
=
package_name
self
.delay_sec_4_spawn
=
delay_sec_4_spawn
self
.mode
=
mode
self
.js_file
=
js_file
def
on_message(
self
, message, data):
if
message[
'type'
]
=
=
'send'
:
payload
=
message[
'payload'
]
print
(
"[on_message]:"
, payload)
else
:
print
(message)
def
start(
self
):
print
(f
"starting frida client with mode: {self.mode} ... pkg = [{self.package_name}]"
)
if
self
.mode
=
=
FridaClient.StartMode.attach:
session
=
frida.get_device_manager().add_remote_device(
"127.0.0.1:1234"
).attach(
self
.package_name)
elif
self
.mode
=
=
FridaClient.StartMode.spawn:
device
=
frida.get_device_manager().add_remote_device(
"127.0.0.1:1234"
)
pid
=
device.spawn([
self
.package_name])
device.resume(pid)
time.sleep(
self
.delay_sec_4_spawn)
session
=
device.attach(pid)
with
open
(
self
.js_file,
'r'
) as f:
js_code
=
f.read()
script
=
session.create_script(js_code)
script.on(
'message'
,
self
.on_message)
self
.script
=
script
script.load()
print
(
"load ready"
)
def
stop(
self
):
if
self
.script:
self
.script.unload()
self
.script
=
None
def
get_sign(
self
, text:
str
):
return
self
.script.exports.getsign(
True
, text)
import
json
import
time
import
uuid
import
requests
from
frida_client
import
FridaClient
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'
:
'7.1.1'
,
'device-name'
:
'OPPO_OPPO+R9s'
,
'treq-id'
:
'1540d0ec530741abbab593af41966110.313.17103985647343144'
,
'auth-token'
: auth_token,
'longitude'
: lon,
'latitude'
: lat,
'p'
:
'1656120205'
,
't'
: t,
'n'
: n,
'sy'
:
'0'
,
'st'
: signed,
'sny'
:
'c'
,
'rcs'
:
'1'
,
'spv'
:
'1.1'
,
'Local-Longitude'
:
'0.0'
,
'Local-Latitude'
:
'0.0'
,
'Content-Type'
:
'application/json;charset=utf-8'
,
'Host'
:
'api-xxxx.walmartmobile.cn'
,
'User-Agent'
:
'okhttp/4.8.1'
}
def
work():
frida_client
=
FridaClient(package_name
=
'cn.xxxxclub.app'
, js_file
=
'app_inject.js'
, mode
=
FridaClient.StartMode.spawn)
frida_client.start()
url
=
"https://api-xxxx.walmartmobile.cn/api/v1/xxxx/goods-portal/spu/search"
device_id
=
'b9fb859f7cfeb98ef39a31c410001f716c04'
user_uid
=
'181864991321'
auth_token
=
'740d926b981716f45de7a402b7b6761a46d9af48f752262b77a2cb0701d482f20c60e6345685b46681a1c23129bdffad022e2e75f60ac763'
lon, lat
=
'114.151608'
,
'22.554734'
t
=
'1711440481379'
goods_name
=
'蛋糕'
data
=
{
"userUid"
: user_uid,
"pageNum"
:
1
,
"pageSize"
:
20
,
"keyword"
: goods_name,
"rewriteWord"
: goods_name,
"filter"
: [],
"storeInfoVOList"
: [
{
"storeId"
:
9991
,
"storeType"
:
32
,
"storeDeliveryAttr"
: [
10
]
},
{
"storeId"
:
6758
,
"storeType"
:
256
,
"storeDeliveryAttr"
: [
2
,
3
,
4
,
5
,
6
,
9
,
12
,
13
]
},
{
"storeId"
:
6580
,
"storeType"
:
2
,
"storeDeliveryAttr"
: [
7
,
13
]
},
{
"storeId"
:
9992
,
"storeType"
:
8
,
"storeDeliveryAttr"
: [
1
]
}
],
"addressVO"
: {
"cityName"
: "",
"countryName"
: "",
"detailAddress"
: "",
"districtName"
: "",
"provinceName"
: ""
},
"uid"
: device_id,
"uidType"
:
3
,
"sort"
:
"0"
}
n
=
str
(uuid.uuid4()).replace(
'-'
, '')
data_json
=
json.dumps(data, indent
=
None
, separators
=
(
','
,
':'
), ensure_ascii
=
False
)
signed
=
frida_client.get_sign(text
=
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)
work()
import
json
import
time
import
uuid
import
requests
from
frida_client
import
FridaClient
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'
:
'7.1.1'
,
'device-name'
:
'OPPO_OPPO+R9s'
,
'treq-id'
:
'1540d0ec530741abbab593af41966110.313.17103985647343144'
,
'auth-token'
: auth_token,
'longitude'
: lon,
'latitude'
: lat,
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2024-6-13 09:38
被Dalin1编辑
,原因: 添加原文链接,防无脑搬运