这个app的本地证书做了处理,对证书名加密以及去掉证书后缀,导致用常规全局搜索.p12等文件失败。
该app可以抓到包,但是存在以下问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
HTTP
/
1.1
400
Bad Request
Server: openresty
/
1.17
.
8.1
Date: Wed,
14
Oct
2020
10
:
00
:
30
GMT
Content
-
Type
: text
/
html; charset
=
utf8
Content
-
Length:
645
Connection: close
<html>
<head><title>
400
No required SSL certificate was sent<
/
title><
/
head>
<body>
<center><h1>
400
Bad Request<
/
h1><
/
center>
<center>No required SSL certificate was sent<
/
center>
<hr><center>openresty
/
1.17
.
8.1
<
/
center>
<
/
body>
<
/
html>
|
一般是由于做了双向认证校验,服务器校验客户端证书导致的。
对app进行脱壳,全局搜索".cer"、".crt"、".pfx"、"PKCS12"、"keyStore"、".p12"等关键字。在
跳转到目标代码上
1
2
3
4
5
6
|
if
(!hashMap.containsKey(host2)) {
hashMap.put(host2, host2
+
".p12"
);
}
}
Client(hashMap);
}
|
找到调用上面Client的方法
1
2
3
4
|
private void Client(
Map
<String, String>
map
) {
...
OkHttpClient.Builder createClientBuilder
=
createClientBuilder();
createClientBuilder.sslSocketFactory(surea.a(App.b().getAssets().
open
(E.i(value)),EncryptNDK.getInstance().gainValue(value)), a.a());
|
从代码中可以看到
1
|
createClientBuilder.sslSocketFactory(surea.a(App.b().getAssets().
open
(E.i(value)),EncryptNDK.getInstance().gainValue(value)), a.a());
|
sslSocketFactory中参数open(E.i(value))打开本地的证书文件,参数EncryptNDK.getInstance().gainValue(value)为获取证书的key。
跟踪获取证书的key函数
1
2
3
4
5
6
7
8
|
public
class
EncryptNDK {
static {
System.loadLibrary(
"EncryptLib"
);
}
...
public native String gainValue(String
str
);
....
}
|
调用了so文件,而gainValue中的str即是证书的key。利用frida去hook EncryptNDK.gainValue以及E.i(value)。
利用hook到的值去全局搜索即可得到被调用的证书以及证书的key。在burpsuit中的User options 》 TLS中导入相对于的证书与域名即可。
在证书存在于本地的资源文件中,对证书名进行加密和去掉证书后缀。导致常规的全局搜索找不到证书的所在,若是证书通过远程进行下载,以及存在这样的情况下如何去解决。
1、可以通过本地文件的更新时间去判断,然而可能需要去甄别那些是证书。若是存在上述的加密以及去掉证书后缀,则可能要花费很大功夫甚至是无法达成目的。
2、如果证书是以java.security.KeyStore来进行双向认证校验,那么通过hook该函数的load方法可获取证书文件和证书密码,即
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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
|
#!/usr/bin/python3
import
frida, sys, time
app_name
=
'包名'
i
=
0
ext
=
''
def
on_message(message, data):
global
i, ext
if
(message[
'type'
]
=
=
'send'
and
'event'
in
message[
'payload'
]):
if
(message[
'payload'
][
'event'
]
=
=
'+found'
):
i
+
=
1
print
(
"\n[+] Hooked keystore"
+
str
(i)
+
"..."
)
elif
(message[
'payload'
][
'event'
]
=
=
'+type'
):
print
(
" [+] Cert Type: "
+
'
'.join(message['
payload
']['
certType']))
if
(message[
'payload'
][
'certType'
]
=
=
'PKCS12'
):
ext
=
'.jks'
elif
(message[
'payload'
][
'event'
]
=
=
'+pass'
):
print
(
" [+] Password: "
+
'
'.join(message['
payload
']['
password']))
elif
(message[
'payload'
][
'event'
]
=
=
'+write'
):
print
(
" [+] Writing to file: keystore"
+
str
(i)
+
ext)
f
=
open
(
'keystore'
+
str
(i)
+
ext,
'wb'
)
f.write(bytes.fromhex(message[
'payload'
][
'cert'
]))
f.close()
else
:
print
(message)
jscode
=
"""
setTimeout(function() {
Java.perform(function () {
var keyStoreLoadStream = Java.use('java.security.KeyStore')['load'].overload('java.io.InputStream', '[C');
/* following function hooks to a Keystore.load(InputStream stream, char[] password) */
keyStoreLoadStream.implementation = function(stream, charArray) {
/* sometimes this happen, I have no idea why, tho... */
if (stream == null) {
/* just to avoid interfering with app's flow */
this.load(stream, charArray);
return;
}
/* just to notice the client we've hooked a KeyStore.load */
send({event: '+found'});
/* read the buffer stream to a variable */
var hexString = readStreamToHex (stream);
/* send KeyStore type to client shell */
send({event: '+type', certType: this.getType()});
/* send KeyStore password to client shell */
send({event: '+pass', password: charArray});
/* send the string representation to client shell */
send({event: '+write', cert: hexString});
/* call the original implementation of 'load' */
this.load(stream, charArray);
/* no need to return anything */
}
});
},0);
/* following function reads an InputStream and returns an ASCII char representation of it */
function readStreamToHex (stream) {
var data = [];
var byteRead = stream.read();
while (byteRead != -1)
{
data.push( ('0' + (byteRead & 0xFF).toString(16)).slice(-2) );
/* <---------------- binary to hex ---------------> */
byteRead = stream.read();
}
stream.close();
return data.join('');
}
"""
print
(
"[.] Attaching to device..."
)
try
:
device
=
frida.get_remote_device()
except
:
print
(
"[-] Can't attach. Is the device connected?"
)
sys.exit()
print
(
"[.] Spawning the app..."
)
try
:
pid
=
device.spawn(app_name)
device.resume(pid)
time.sleep(
1
)
except
:
print
(
"[-] Can't spawn the App. Is filename correct?"
)
sys.exit()
print
(
"[.] Attaching to process..."
)
try
:
process
=
device.attach(pid)
except
:
print
(
"[-] Can't connect to App."
)
sys.exit()
print
(
"[.] Launching js code..."
)
print
(
" (run the app until needed, close it and then kill this script)"
)
script
=
process.create_script(jscode)
script.on(
'message'
, on_message)
script.load()
try
:
sys.stdin.read()
except
KeyboardInterrupt:
print
(
"\nExiting now"
)
exit(
0
)
|
针对于该脚本的实践中,发现应用于本次的app是需要改动的。本次的app存在spwan的检测,无法使用device.spawn去启动app,因此需要修改。去掉
1
2
3
4
5
6
7
8
|
print
(
"[.] Spawning the app..."
)
try
:
pid
=
device.spawn(app_name)
device.resume(pid)
time.sleep(
1
)
except
:
print
(
"[-] Can't spawn the App. Is filename correct?"
)
sys.exit()
|
同时将pid改为app_name
1
2
|
try
:
process
=
device.attach(app_name)
|
经过实践该脚本是可以成立的,但是好像只生成有带密码的证书文件,而且证书后缀为.jks。
3、如果app本身不存在检测的话,可以利用objection,去hook java.io.File
1
|
android hooking watch class_method java.io.
File
.$
|
或使用spawn方式
1
|
objection
-
g xxx explore
-
-
startup
-
command "android hooking watch class_method java.io.
File
.$init
-
-
dump
-
args
|
更多【遇见非常规双向证书绕过】相关视频教程:www.yxfzedu.com