白乐天

道阻且长,行则将至。

豆瓣frida检测绕过及搜索接口参数还原

App信息

包名:com.douban.frodo

1

frida反调试绕过

首先尝试注入frida,发现直接就挂掉了,说明frida被检测到了

1

定位检测frida的so

hook dlopen查看so加载流程,看在加载哪一个so的时候挂掉的,说明检测frida的函数在那个so里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hook_dlopen(){
//Android8.0之后加载so通过android_dlopen_ext函数
var android_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");
console.log("addr_android_dlopen_ext",android_dlopen_ext);
Interceptor.attach(android_dlopen_ext,{
onEnter:function(args){
var pathptr = args[0];
if(pathptr!=null && pathptr != undefined){
var path = ptr(pathptr).readCString();
console.log("android_dlopen_ext:",path);
}
},
onLeave:function(retvel){
console.log("leave!");
}
})
}
hook_dlopen()

根据hook的输出可以知道,在加载libmsaoaidsec.so之后,frida就挂掉了,说明是在libmsaoaidsec.so里检测的frida。

1

定位检测frida的函数地址,并进行替换

根据so的加载流程,call_construction()函数用来确保动态库中的函数得到加载并执行,在call_construction()函数里会调用一个init_array()函数,它包含了一组指向函数的指针,用于对函数进行初始化,在so加载完成之后,会调用JNI_OnLoad()函数,用于进行 JNI 环境的初始化、注册本地方法。

hook pthread_create()

检测frida的函数通常不会在主线程中,我们可以通过hook pthread_create()函数来打印线程中创建的函数的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function hook_dlopen(){
//Android8.0之后加载so通过android_dlopen_ext函数
var android_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");
console.log("addr_android_dlopen_ext",android_dlopen_ext);
Interceptor.attach(android_dlopen_ext,{
onEnter:function(args){
var pathptr = args[0];
if(pathptr!=null && pathptr != undefined){
var path = ptr(pathptr).readCString();
if(path.indexOf("libmsaoaidsec.so")!=-1){
console.log("android_dlopen_ext:",path);
hook_pthread()
}
}
},
onLeave:function(retvel){
// console.log("leave!");
}
})
}

function hook_pthread() {
var pth_create = Module.findExportByName("libc.so", "pthread_create");
console.log("[pth_create]", pth_create);
Interceptor.attach(pth_create, {
onEnter: function (args) {
var module = Process.findModuleByAddress(args[2]);
if (module != null) {
console.log("address", module.name, args[2].sub(module.base));
}

},
onLeave: function (retval) {}
});
}

function main(){
hook_dlopen()
}

main()

结果如下,在libmsaoaidsec.so里创建了三个函数

1

hook call_construction()

可以通过hook call_construction()这个函数来对pthread_create()中创建的函数进行替换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
function hook_dlopen(){
//Android8.0之后加载so通过android_dlopen_ext函数
var android_dlopen_ext = Module.findExportByName(null,"android_dlopen_ext");
console.log("addr_android_dlopen_ext",android_dlopen_ext);
Interceptor.attach(android_dlopen_ext,{
onEnter:function(args){
var pathptr = args[0];
if(pathptr!=null && pathptr != undefined){
var path = ptr(pathptr).readCString();
if(path.indexOf("libmsaoaidsec.so")!=-1){
console.log("android_dlopen_ext:",path);
hook_call_constructors()
}
}
},
onLeave:function(retvel){
//console.log("leave!");
}
})
}

function hook_call_constructors() {
var linker64_base_addr = Module.getBaseAddress("linker64")
var call_constructors_func_off = 0x2C274 //这个地址是从手机上把linker64 pull出来,然后到IDA中找到的,不同的机型,地址可能不一样
var call_constructors_func_addr = linker64_base_addr.add(call_constructors_func_off)
var listener = Interceptor.attach(call_constructors_func_addr, {
onEnter: function (args) {
console.log("hooked call_constructors")
var module = Process.findModuleByName("libmsaoaidsec.so")
if (module != null) {
Interceptor.replace(module.base.add(0x1c544), new NativeCallback(function () {
console.log("0x1c544:替换成功")
}, "void", []))
Interceptor.replace(module.base.add(0x1b924), new NativeCallback(function () {
console.log("0x1B924:替换成功")
}, "void", []))
Interceptor.replace(module.base.add(0x26e5c), new NativeCallback(function () {
console.log("0x26e5c:替换成功")
}, "void", []))
listener.detach()
}
},
})
}
function main(){
hook_dlopen()
}

main()

结果如下,frida反调试绕过成功了!

1

_sign参数还原

抓包分析

通过抓包找到有关_sig参数的数据包

1

jadx反编译apk

搜索字符串"_sig"

1

intercept

定位到intercept函数,如下可以看出来sig的值为F3.first,F3的值为i0.d.F(request)

1

跟进到F方法里

F

F方法内容如下

1

返回值是E方法

跟进到E方法

E

E方法内容如下

1

分析流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1.判断str是否为空	(TextUtils.isEmpty(str))

2.获取str2 (StringBuilder l10 = android.support.v4.media.e.l(str2))

3.获取URL的路径部分,并进行url编码 (String encodedPath = HttpUrl.parse(str).encodedPath())

4.对进行过URL解码的路径部分进行URL解码 (decode = Uri.decode(encodedPath))

5.如果路径URL的路径部分的末尾有/符号,则删除它

6.在l10末尾追加一个&符号

7.对解码的URL路径部分进行url编码,然后追加到l10末尾

8.在l10末尾追加一个&符号

9.在l10末尾追加当前时间戳

10.进行HmacSHA1签名

11.进行Base64编码

hook doFinal

hook doFinal方法来查看参与加密的参数的格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
Java.perform(function () {


var ByteString = Java.use("com.android.okhttp.okio.ByteString");
function toBase64(tag, data) {
console.log(tag + " Base64: " + ByteString.of(data).base64());
}
function toHex(tag, data) {
console.log(tag + " Hex: " + ByteString.of(data).hex());
}
function toUtf8(tag, data) {
console.log(tag + " Utf8: " + ByteString.of(data).utf8());
}

var mac = Java.use("javax.crypto.Mac");
mac.init.overload('java.security.Key').implementation = function (key) {
console.log("Mac.init('java.security.Key') is called!");
var algorithm = this.getAlgorithm();
var tag = algorithm + " init Key";
var keyBytes = key.getEncoded();
toUtf8(tag, keyBytes);
toHex(tag, keyBytes);
toBase64(tag, keyBytes);

return this.init(key);
}
mac.doFinal.overload('[B').implementation = function (data) {
console.log("Mac.doFinal.overload('[B') is called!");
console.log("data:",toUtf8("data:",data))
var result = this.doFinal(data)
console.log("result:",toBase64("result",result))
console.log("=======================================================");
return result;
}
});

hook 结果

1
2
3
4
5
6
7
8
9
10
Mac.init('java.security.Key') is called!
HmacSHA1 init Key Utf8: bf7dddc7c9cfe6f7
HmacSHA1 init Key Hex: 62663764646463376339636665366637
HmacSHA1 init Key Base64: YmY3ZGRkYzdjOWNmZTZmNw==
Mac.doFinal.overload('[B') is called!
data: Utf8: GET&%2Fapi%2Fv2%2Fsearch&1736261466
data: undefined
result Base64: S6+RLmdsNP1NdjQghxJebsST7Tc=
result: undefined
=======================================================

hook E

对E方法进行hook,可以查看参数和返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hook_E(){
Java.perform(function(){
let d = Java.use("i0.d");
d.E.implementation = function(str1,str2,str3){
console.log("----E----")
console.log("str1:",str1);
console.log("str2:",str2);
console.log("str3:",str3);
let result = this.E(str1,str2,str3);
console.log("result:",result)
console.log("----over----")
return result
}
});
}
hook_E()

hook 结果

1
2
3
4
5
6
7
8
9
10
11
12
----E----
str1: https://frodo.douban.com/api/v2/search/suggestion?q=%E8%AF%AF%E6%9D%80&loc_id=108288&apikey=0dad551ec0f84ed02907ff5c42e8ec70&channel=ali_market&udid=ce0fb355c7f98012b1d6716853b6841ba7e35819&os_rom=android&timezone=Asia%2FShanghai
str2: GET
str3: null
result: Pair{idc8eWIGKhtP6hTr9piPi5OSadQ= 1736098880}
----over----
----E----
str1: https://frodo.douban.com/api/v2/search/found_words?screen_width=1080&screen_height=2028&wx_api_ver=0&opensdk_ver=638058496&webview_ua=Mozilla%2F5.0%20%28Linux%3B%20Android%209%3B%20Pixel%203%20Build%2FPD1A.180720.030%3B%20wv%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Version%2F4.0%20Chrome%2F66.0.3359.158%20Mobile%20Safari%2F537.36&sugar=0&update_mark=1709887761.853333336&network=wifi&enable_sdk_bidding=1&timezone=Asia%2FShanghai
str2: POST
str3: null
result: Pair{gYm5pJeYn7Wh/uv2QxJF6sh6sfA= 1736098863}
----over----

hook SecretKeySpec

对于hmacsha1算法,我们得知道密钥才行,可以hook SecretKeySpec得到密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function hook_SecretKeySpec(){
Java.perform(function(){
let secretkeyspec = Java.use("javax.crypto.spec.SecretKeySpec");
secretkeyspec.$init.overload('[B', 'java.lang.String').implementation = function(str5,sha1){
let stringdata = Java.use("java.lang.String").$new(str5);
console.log("str5:",stringdata);
console.log("sha1:",sha1);
console.log("====+++====")
}
})
}
hook_SecretKeySpec()

>>>
str5: bf7dddc7c9cfe6f7
sha1: HmacSHA1

得到hmacsha1的密钥为bf7dddc7c9cfe6f7

参数还原

这里拿一份hook到的数据进行分析

1
2
3
4
5
6
----E----
str1: https://frodo.douban.com/api/v2/search/suggestion?q=%E8%AF%AF%E6%9D%80&loc_id=108288&apikey=0dad551ec0f84ed02907ff5c42e8ec70&channel=ali_market&udid=ce0fb355c7f98012b1d6716853b6841ba7e35819&os_rom=android&timezone=Asia%2FShanghai
str2: GET
str3: null
result: Pair{idc8eWIGKhtP6hTr9piPi5OSadQ= 1736098880}
----over----

首先需要的是请求方式,这里是GET

其次是URL的路径部分,还要进行url编码,这里拿到的是%2Fapi%2Fv2%2Fsearch%2Fsuggestion

然后是时间戳1736098880

进行HmacSHA1签名,在进行Base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import hashlib
import hmac
import base64

def hmacsha1(key,data):
key_bytes = key.encode()
data_bytes = data.encode()
mac = hmac.new(key_bytes,data_bytes,hashlib.sha1)
sha1_result = mac.digest()
return base64.b64encode(sha1_result).decode("utf-8")

key = "bf7dddc7c9cfe6f7"
data = "GET&%2Fapi%2Fv2%2Fsearch%2Fsuggestion&1736098880"
print("sig:"+hmacsha1(key,data))

>>>
sig:idc8eWIGKhtP6hTr9piPi5OSadQ=

搜索接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import requests
import json
import time
from urllib.parse import quote
import hashlib
import hmac
import base64

def hmacsha1(key,data):
key_bytes = key.encode()
data_bytes = data.encode()
mac = hmac.new(key_bytes,data_bytes,hashlib.sha1)
sha1_result = mac.digest()
return base64.b64encode(sha1_result).decode("utf-8")


url = "https://frodo.douban.com/api/v2/search"
headers = {
"user-agent": "api-client/1 com.douban.frodo/7.89.0(307) Android/28 product/blueline vendor/Google model/Pixel 3 brand/google rom/android network/wifi udid/ce0fb355c7f98012b1d6716853b6841ba7e35819 platform/mobile",
"accept-encoding": "br,gzip"
}

timestamp = int(time.time())
encode_data = "GET"+"&"+"%2Fapi%2Fv2%2Fsearch"+"&"+str(timestamp)
key = "bf7dddc7c9cfe6f7"
sig = hmacsha1(key,encode_data)
print("sig:",sig)

query = input("请输入:")

params = {
"q": query,
"sort": "relevance",
"count": "20",
"screen_width": "1080",
"screen_height": "2028",
"wx_api_ver": "0",
"opensdk_ver": "638058496",
"webview_ua": "Mozilla/5.0 (Linux; Android 9; Pixel 3 Build/PD1A.180720.030; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/66.0.3359.158 Mobile Safari/537.36",
"sugar": "0",
"update_mark": "1709887761.853333336",
"network": "wifi",
"enable_sdk_bidding": "1",
"loc_id": "108288",
"apikey": "0dad551ec0f84ed02907ff5c42e8ec70",
"channel": "ali_market",
"udid": "ce0fb355c7f98012b1d6716853b6841ba7e35819",
"os_rom": "android",
"oaid": "EdGi3zYQCRzmwwB1YR7WKg==\n",
"timezone": "Asia/Shanghai",
"_sig":sig,
"_ts": timestamp
}

response = requests.get(url=url,headers=headers,params=params)
print(response.status_code)
# print(response.text)
text = json.loads(response.text)
print(text)