安卓API自动化安全扫描
背景
解决的问题
在日常的移动端安全审计,自动化审计一直停留在应用客户端,对于安卓应用中的网络API接口长期处于空白阶段,该方案主要想解决实际工作中移动安审自动化覆盖范围不全遗漏掉API相关内容的问题,同时对公司APP端的资产进行梳理,进一步完善公司移动应用SDL流程缺失的环节。
API自动化扫描
扫描流程
先说整体的扫描流程:
一、业务测试人员提交APK到检测平台;
二、检测平台对APK进行静态分析结合动态监控的方式完成API资产的收集;
- 静态分析获取APK中静态的API信息;
- 动态监控通过模拟点击+VPN代理的方式捕获应用运行时与服务端的交互API信息;
三、检测平台完成API资产收集后开始进行请求数据的清洗,包括去除重复、无效的API信息;
四、将处理过的API资产信息发送给WEB扫描器,进行应用API的自动化扫描。
项目框架
框架图:
在整个过程中,主要需要解决的有以下两个问题:
如何实现API资产的自动化收集;
如何对应用内业务逻辑自动化触发。
API资产收集
首先来看第一个问题,对于应用内API资产的收集有两个思路,一个是静态解析应用内的字符串,通过正则表达式的方式来识别出应用内的API资产,另一个思路是对应用的网络通信进行捕获来获取。鉴于一般应用的url链接都是动态拼接出来的,并且纯静态分析很难解析出API请求对应的数据格式,所以这里主要对第二种思路进行实践。
如何实现APP网络通信的捕获?
聊个老生常谈的话题:抓包,相信做过移动安全审计的小伙伴应该知道,App应用抓包不管是在协议分析还是渗透测试中都属于比较重要的一环,在拿到需要审计的应用后,首先会对应用是否加固进行检测,除了加固检测第二步就是对应用与服务端的请求进行抓包然后再根据请求数据包的内容来展开更深层次的审计。根据目的不同分析方向也有些差别,像协议分析主要是去apk中定位相应的处理函数或者其算法逻辑,对于渗透测试更多的则是对数据包中关键字段进行修改来检测服务端是否存在鉴权、越权、SQL注入等问题,但多数情况下渗透测试也会涉及到协议分析相关的工作。
HTTP介绍
经常听到http协议、https协议,那它们有什么区别?http即超文本传输协议,采用明文的方式去传输数据,而https是http的升级,https在http协议的基础上加上了SSL/TLS功能,http协议负责建立客户端与服务端的网络通信,而SSL/TLS则负责通信的安全,包括传输数据的加密与身份的认证。
SSL与TLS的区别:TLS是SSL迭代的版本,因为HTTP协议传输的数据都是未加密的,所以为了保证这些明文数据能够进行安全传输,网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,SSL目前的版本是3.0。互联网标准化组织ISOC接替网景公司对SSL 3.0进行了升级,衍生出了TLS1.0(Transport Layer Security),因此可以理解为TLS1.0=SSL3.1,目前TLS版本支持1.3。
安卓通信框架
对于安卓应用,有以下几种较常见的http通信方式,Apache的HttpClient类、Java提供的HttpsURLConnection类、Android提供的WebView以及第三方库比如OkHttp、Retrofit2。
其中HttpClinet在安卓6.0的时候就已经被废弃,安卓官方推荐使用HttpsURLConnection,但OkHttp和Retrofit2使用起来更方便、功能更多,所以大部分应用都采用OkHttp来实现网络通信。
常见抓包方案
抓包原理
即中间人攻击,就是在通信双方的中间建立一个代理服务器。让客户端以为这个代理服务器就是真正的服务端,这样客户端一切的请求都会先发给中间服务器并由中间服务器代理转发给真实的服务端,而真实服务端的响应也都会被中间服务器接收,再由中间服务器转发给客户端。这样这个中间服务器就可对客户端与真实服务端通信的数据进行拦截、监听、篡改、重放等操作。
抓包工具
对于HTTP的抓包工具在PC端的主要有BurpSuitePro、Fiddler4、Charles等工具,客户端主要有HttpCanary(小黄鸟),对于一些不使用HTTP/HTTPS协议传输数据的app,还可以使用tcpdump来进行抓包,抓到的包可以使用wireshark工具进行解析,最后就是基于frida的r0capture也比较好用。
WIFI代理
1.首先将安装了客户端应用的手机与安装了抓包工具的PC处于同一网段,也就是连上同一wifi;
2.其次将手机的wifi设置为手动代理,代理主机IP设置为PC的IP,端口设置为8888(随便设置,只要和抓包工具一致);
4.然后打开PC上的抓包工具并配置监听端口开始进行代理抓包工作;
5.最后还需要通过手机浏览器访问以下抓包工具的证书下载地址,进行证书安装,安卓完成后即可进行抓包。
注意:当应用的targetSdkVersion到28后会发现在Android7.0及以上机型上抓不到包,主要原因是应用不再信任客户端用户自己安装的证书,除非App应用自身明确开启用户证书信任的功能。
来看下7.0之前App的配置与7.0及之后的配置有什么不一样。
6.0的"res/xml/network_security_config.xml"文件
1
2
3
4
5
|
<base
-
config cleartextTrafficPermitted
=
"true"
>
<trust
-
anchors>
<certificates src
=
"system"
/
>
<
/
trust
-
anchors>
<
/
base
-
config>
|
7.0的"res/xml/network_security_config.xml"文件
1
2
3
4
5
6
|
<base
-
config cleartextTrafficPermitted
=
"true"
>
<trust
-
anchors>
<certificates src
=
"system"
/
>
<certificates src
=
"user"
/
>
<
/
trust
-
anchors>
<
/
base
-
config>
|
因为我们自己安装的证书属于user域,可以看到配置取消了对user域证书的信任。
想要在7.0及7.0以上的手机上进行抓包,可以从以下几个点入手。
1.最简单的方法,就是重新配置,修改配置文件支持对user域证书的信任。
- 具体配置详情请看官方介绍:https://developer.android.com/training/articles/security-config
2.将我们的证书添加到系统证书的目录,这样我们的证书就是系统证书,也就会被信任了。
- 方法一:/system/etc/security/cacerts目录包含每个已安装根证书的问题,在有root的情况下重新挂载/system目录,并将我们的证书拷贝到该目录。
- 方法二:使用Magisk自定义模块,将任何用户证书识别为系统证书。
VPN隧道
除了可以通过WIFI设置代理进行抓包还有一种方式就是在安卓设备上创建VPN隧道来配合抓包工具进行抓包,VPN隧道属于七层协议的网络层,设备在开启VPN后会多出一个网络接口,相当于多加了个虚拟网卡,所有流量都会走这个新增的虚拟网卡,这样应用层和传输层的请求数据就都能捕获。
开启VPN可以使用postern应用来完成,然后配置下代理服务器地址以及规则,即可在PC端使用抓包软件进行抓包。
如果觉得上面的设置流程较麻烦,那么直接选择HttpCanary(小黄鸟)吧,该应用的抓包原理也是基于VPN实现的,安装完即配置完,比较方便,配合投屏工具用起来更丝滑。需要注意的点:有些应用会检测是否存在HttpCanary,所以可以先将目标软件打开后再启动HttpCanary应用进行抓包。
透明代理
最后一种叫透明代理(路由重定向),顾名思义就是可以让客户端感觉不到代理的存在。该方法主要依赖linux上的iptables命令行工具的流量转发功能,用户不需要设置代理服务器,设置下默认网关即可。当设备访问外部网络时,客户端的数据包会被转发到设置的默认网关上,通过默认网关的路由,最终到达目标服务器。该方案可以适用在设备不支持WIFI代理设置并且不能安装第三方应用的情况下,比如对机车进行抓包时。
首先需要在手机上使用su权限设置将设备所有tcp流量转发到指定IP(172.20.10.9)的电脑上。
1
2
3
|
iptables
-
t nat
-
A OUTPUT
-
d
0.0
.
0.0
/
0
-
p tcp
-
j DNAT
-
-
to
172.20
.
10.9
iptables
-
t nat
-
D OUTPUT
-
d
0.0
.
0.0
/
0
-
p tcp
-
j DNAT
-
-
to
172.20
.
10.9
|
然后再重定向的电脑上开启BurpSuite并配置对HTTP/HTTPS的默认端口80和443进行监听。
抓包防护
根据前面的内容我们可以看到,攻击者想要抓包其实是很容易的,那作为开发者如何去增加应用的防护能力,来避免应用的业务数据被中间人抓包获取呢?这里整理了以下几种方法供大家参考;
- 方法一:使用系统API进行常规检测,包括是否设置了WIFI代理,是否开启VPN等;
- 方法二:通过设置让APP请求时不用系统代理来绕过WIFI代理抓包;
- 方法三:自定义Sooket实现HTTP/HTTPS;
- 方法四:使用证书校验的方式来验证服务端是否为信任的服务端(双向认证和证书锁定);
实际情况中遇到的比较多的有上面的第一种,第二种和第四种,第三种目前遇到的较少。
WIFI代理检测
最基础的抓包就是设置WIFI代理进行抓包,所以就可以从WIFI代理入手,检测当前的WIFI环境是否安全,是否被设置了代理服务器和代理端口,当检测到存在代理时就判定为用户正在使用抓包,这样我们就能采取一些措施,比如退出应用;
WIFI代理的检测代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public static boolean isWifiProxy(Context context) {
final boolean IS_ICS_OR_LATER
=
Build.VERSION.SDK_INT >
=
Build.VERSION_CODES.ICE_CREAM_SANDWICH;
/
/
判断安卓版本
String proxyAddress;
int
proxyPort;
if
(IS_ICS_OR_LATER) {
proxyAddress
=
System.getProperty(
"http.proxyHost"
);
/
/
获取代理主机
String portStr
=
System.getProperty(
"http.proxyPort"
);
/
/
获取代理端口
proxyPort
=
Integer.parseInt((portStr !
=
null ? portStr :
"-1"
));
}
else
{
proxyAddress
=
android.net.Proxy.getHost(context);
proxyPort
=
android.net.Proxy.getPort(context);
}
Log.i(
"代理信息"
,
"proxyAddress :"
+
proxyAddress
+
"prot :"
+
proxyPort);
return
(!TextUtils.isEmpty(proxyAddress)) && (proxyPort !
=
-
1
);
}
|
设置不走系统代理
对于WIFI代理抓包,除了可以通过检测进行防护,还可以通过网络请求时设置不走系统代理来绕过,检测代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public void run() {
Looper.prepare();
OkHttpClient okHttpClient
=
new OkHttpClient.Builder().
proxy(Proxy.NO_PROXY).
/
/
使用此参数,可绕过系统代理直接发包
build();
Request request
=
new Request.Builder()
.url(
"http://www.baidu.com"
)
.build();
Response response
=
null;
try
{
response
=
okHttpClient.newCall(request).execute();
Toast.makeText(this, Objects.requireNonNull(response.body()).string(), Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
}
Looper.loop();
}
|
VPN检测
VPN检测的检测有两种方式,因为创建VPN隧道会新增一个网络接口,一般是"tun0"或"ppp0",所以VPN的第一种检测原理是对设备网络接口进行检测,当发现存在"tun0"或"ppp0"的接口时,则判定存在VPN抓包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public boolean isInVPN() {
try
{
Enumeration<NetworkInterface> networkInterfaces
=
NetworkInterface.getNetworkInterfaces();
while
(networkInterfaces.hasMoreElements()){
String name
=
networkInterfaces.nextElement().getName();
if
(name.equals(
"tun0"
) || name.equals(
"ppp0"
)) {
return
true;
}
}
} catch(SocketException e) {
e.printStackTrace();
}
return
false;
}
|
第二种检测则比较简单,直接使用系统服务,获取当前网络的状态来判定是否使用了VPN。
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
|
@RequiresApi
(api
=
Build.VERSION_CODES.LOLLIPOP)
private final boolean hasVpnTransport(Network network, ConnectivityManager connectivityManager) {
NetworkCapabilities networkCapabilities
=
connectivityManager.getNetworkCapabilities(network);
return
networkCapabilities !
=
null && networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
}
@RequiresApi
(api
=
Build.VERSION_CODES.LOLLIPOP)
private final boolean isInVPN(Context context){
Object
systemService
=
context.getSystemService(Context.CONNECTIVITY_SERVICE);
if
(systemService !
=
null) {
ConnectivityManager connectivityManager
=
(ConnectivityManager) systemService;
boolean isInVpn
=
false;
if
(Build.VERSION.SDK_INT >
=
23
) {
Network activeNetwork
=
connectivityManager.getActiveNetwork();
isInVpn
=
hasVpnTransport(activeNetwork, connectivityManager);
}
else
{
Network[] allNetworks
=
connectivityManager.getAllNetworks();
if
(allNetworks !
=
null) {
for
(Network network: allNetworks) {
isInVpn
=
hasVpnTransport(network, connectivityManager);
}
}
}
return
isInVpn;
}
throw new NullPointerException();
}
|
SSL pinning(证书锁定)
SSL pinning就是在应用发布时将服务端的安全证书的证书指纹内置在apk中,当客户端与服务器交互时将当前服务端的证书与内置的证书指纹进行对比校验,如果验证通过则判定安全,否则为不安全,强行断开连接。SSL pinning在带来较高安全性的同时也牺牲了灵活性,因为证书锁定只信任内置的证书指纹,一旦服务端证书发生变化那么客户端也必须随着升级,除此之外,服务端不得不为了兼容以前的客户端而做出一些妥协或直接停用以前的客户端。不过一般是很少变动证书的,所以如果产品安全性要求较高的还是启动证书锁定比较好,实现方式多种多样,一般通过预埋证书的方式。
首先创建一个带证书锁定的SSLContext对象,大概流程:使用KeyStore加载应用内置的安全证书,然后用这个KeyStore去初始化生成TrustManager,并用生成的TrustManager初始化SSLContext对象,最后将SSLContext对象提供给各网络框架使用。
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
|
private static SSLContext getSSLContext(boolean needVerifyCa, Context context, String cAalias)
throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException {
X509TrustManager x509TrustManager;
/
/
https请求,需要校验证书
if
(needVerifyCa) {
/
/
第一步:使用安全证书创建一个包含可信CA的密钥库(KeyStore对象)
InputStream caInputStream
=
context.getAssets().
open
(
"CA.crt"
);
CertificateFactory certificateFactory
=
CertificateFactory.getInstance(
"X.509"
);
KeyStore keyStore
=
KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
/
/
可在这里加载带密码的CA证书
keyStore.setCertificateEntry(cAalias, certificateFactory.generateCertificate(caInputStream));
/
/
第二步:使用包含信任CA的密钥库获取TrustManager对象
TrustManagerFactory trustManagerFactory
=
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
/
/
第三步:使用TrustManager数组初始化SSLContext对象
SSLContext sslContext
=
SSLContext.getInstance(
"TLS"
);
sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
return
sslContext;
}
/
/
https请求,不作证书校验
/
/
第一步:自定义一个无证书校验的TrustManager对象
x509TrustManager
=
new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
}
@Override
public void checkServerTrusted(X509Certificate[] arg0, String arg1) {
/
/
不对服务端证书进行验证
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return
new X509Certificate[
0
];
}
};
/
/
第二步:使用无证书校验的TrustManager数组初始化SSLContext对象
SSLContext sslContext
=
SSLContext.getInstance(
"TLS"
);
sslContext.init(null, new TrustManager[]{x509TrustManager}, new SecureRandom());
return
sslContext;
}
|
各网络框架证书校验实现如下:
一、HttpClient 实现证书锁定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public static CloseableHttpClient getHttpClient(Context context) {
CloseableHttpClient httpClient;
SSLConnectionSocketFactory sslSocketFactory;
try
{
/
/
证书的别名, 注:cAalias只需要保证唯一即可,不过推荐使用生成keystore时使用的别名。
String cAalias
=
System.currentTimeMillis()
+
""
+
new SecureRandom().nextInt(
1000
);
/
/
创建带证书锁定的sslSocketFactory
sslSocketFactory
=
new SSLConnectionSocketFactory(getSSLContext(true, context, cAalias));
} catch (Exception e) {
throw new RuntimeException(e);
}
httpClient
=
HttpClientBuilder.create().setSSLSocketFactory(sslSocketFactory).build();
return
httpClient;
}
|
二、HttpsURLConnection 实现证书锁定
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
|
public static void doHttpsGet(final Context context, final String urlPath){
new Thread(){
@Override
public void run() {
Looper.prepare();
try
{
/
/
证书的别名, 注:cAalias只需要保证唯一即可,不过推荐使用生成keystore时使用的别名。
String cAalias
=
System.currentTimeMillis()
+
""
+
new SecureRandom().nextInt(
1000
);
URL realUrl
=
new URL(urlPath);
HttpsURLConnection conn
=
(HttpsURLConnection) realUrl.openConnection();
/
/
创建带证书锁定的sslSocketFactory
conn.setSSLSocketFactory(getSSLContext(true, context, cAalias).getSocketFactory());
conn.setRequestMethod(
"GET"
);
conn.connect();
int
code
=
conn.getResponseCode();
if
(code
=
=
200
){
Log.i(
"https"
,
"connection success"
);
}
else
{
Log.i(
"https"
,
"connection failed code:"
+
code);
}
} catch (Exception e) {
e.printStackTrace();
}
Looper.loop();
}
}.start();
}
|
三、Okhttp 实现证书锁定
公钥锁定
1
2
3
4
5
6
7
8
9
|
private static final String CA_DOMAIN
=
"*.xxx.com"
;
private static final String CA_PUBLIC_KEY
=
"sha256/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
;
.....
CertificatePinner pinner
=
new CertificatePinner.Builder()
.add(CA_DOMAIN, CA_PUBLIC_KEY)
.build();
.....
OkHttpClient.Builder clientBuilder
=
new OkHttpClient.Builder()
.certificatePinner(pinner);
|
证书锁定
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
|
private SSLSocketFactory getSSLSocketFactory()
throws CertificateException, KeyStoreException, IOException,
NoSuchAlgorithmException, KeyManagementException {
CertificateFactory cf
=
CertificateFactory.getInstance(
"X.509"
);
InputStream caInput
=
getResources().openRawResource(R.raw.cert);
Certificate ca
=
cf.generateCertificate(caInput);
caInput.close();
KeyStore keyStore
=
KeyStore.getInstance(
"BKS"
);
keyStore.load(null, null);
keyStore.setCertificateEntry(
"ca"
, ca);
String tmfAlgorithm
=
TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory trustManagerFactory
=
TrustManagerFactory.getInstance(tmfAlgorithm);
trustManagerFactory.init(keyStore);
SSLContext sslContext
=
SSLContext.getInstance(
"TLS"
);
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
return
sslContext.getSocketFactory();
}
private HostnameVerifier getHostnameVerifier() {
return
new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
HostnameVerifier hv
=
HttpsURLConnection.getDefaultHostnameVerifier();
return
hv.verify(
"BNK-PC.LOCALHOST.COM"
, session);
}
};
}
OkHttpClient client
=
new OkHttpClient.Builder()
.sslSocketFactory(getSSLSocketFactory())
.hostnameVerifier(getHostnameVerifier())
.build();
|
四、WebView实现证书锁定
WebView没有自定义SSL Pinning的实现方法,只能通过network_security_config.xml配置证书锁定;
分别在应用的\res\xml\network_security_config.xml
文件和\AndroidManifest.xml
文件中添加证书相关信息,系统自动对访问的域名进行校验;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<network
-
security
-
config xmlns:tools
=
"http://schemas.android.com/tools"
>
<!
-
-
证书校验
-
-
>
<domain
-
config>
<domain includeSubdomains
=
"true"
>xxxx.com<
/
domain>
<trust
-
anchors>
<certificates src
=
"@raw/cert"
/
>
<
/
trust
-
anchors>
<
/
domain
-
config>
<!
-
-
公钥校验
-
-
>
<domain
-
config>
<domain includeSubdomains
=
"true"
>xxxx.com<
/
domain>
<pin
-
set
expiration
=
"2099-01-01"
tools:ignore
=
"MissingBackupPin"
>
<pin digest
=
"SHA-256"
>vzXV96
/
gpZMyyNNhyTdjtX0
/
NUVYTtmYqWcVVaUtTdQ
=
<
/
pin>
<
/
pin
-
set
>
<
/
domain
-
config>
<
/
network
-
security
-
config>
|
AndroidManifest.xml:
1
|
<application android:networkSecurityConfig
=
"@xml/network_security_config"
>
|
使用webview请求上面配置的域名时,系统会自动校验证书信息;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
private
class
MyWebViewClient extends WebViewClient {
private String checkflag
=
"checkCerts"
;
/
/
是否忽略证书校验
public void setCheckflag(String checkflag) {
this.checkflag
=
checkflag;
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if
(
"trustAllCerts"
.equals(checkflag)){
/
/
忽略证书校验
handler.proceed();
}
else
{
handler.cancel();
Toast.makeText(MainActivity.this,
"证书异常,停止访问"
, Toast.LENGTH_SHORT).show();
}
}
}
|
双向认证
双向认证是基于单向认证(客户端只认证服务端)的基础上,添加了服务端对客户端证书的认证。除了客户端会对服务端证书进行认证外,服务端还会去认证客户端的证书是否有效。
证书认证与证书锁定(SSL pinning)的区别
这里需要区分下证书认证和证书锁定的不同,android系统默认帮我们预置了150多个可信证书,这些证书可以在设置->安全->信任的凭据中看到。https特点就是证书认证,因为服务端用的证书是从android认可的证书颁发机构购买的证书,而在android中也已经内置了这些机构的信任证书,所以默认情况下是可以直接访问服务器而无需在客户端设置证书信息,证书认证是对android预置的这些证书进行认证,所以通常情况下抓包时需要导入抓包工具的证书到系统中,让系统信任抓包软件的证书。而证书锁定(SSL pinning)是对开发者自己的私有证书进行安全校验。
SSL证书大概分为三类。
1.由可信证书颁发机构颁发的证书,比如Symantec,Go Daddy等机构,约150多个。在手机"设置->安全->信任的凭据"中可以查看。
2.不是可信证书机构颁发的证书,比如抓包软件的证书,12306网站的证书等,都是非可信证书机构颁发的证书
3.公司自己颁发的证书。这三类证书中,只有第一种在使用时不会出现安全提示,不会抛出异常。
反抓包防护
所有的防护都不是一劳永逸的,防护只能提高逆向的门槛,而不能完全解决风险,经过多年的对抗,攻击者早已研究出如何绕过前面的抓包防护了,看下攻击者用的比较多的一些绕过方法。
一、透明代理(路由重定向),可以对抗WIFI代理检测、设置不走系统代理、VPN检测。
原理:HTTP/HTTPS的默认端口是80和443,我们在BurpSuite上设置透明代理并监听这两个端口。在透明代理中应用会认为我们用BurpSuite模拟服务器开放端口就是真实服务器,实际上是将手机的TCP协议的路由重定向到了我们电脑IP地址中进而BurpSuite会进行代理服务器转发。
详情请看常见抓包方案中的透明代理+抓包内容;
二、对于证书锁定的绕过可以采用hook的方式。
在手机已经root的情况下,可以通过xposed框架下的JustTustMe/SSLUnping或者分析代码定制hook来对抗证书锁定,至于双向认证的话还需要分析应用中客户端证书的位置以及使用密码,并将找到的证书导入到抓包应用中。
证书锁定绕过
1.利用Xposed+JustTrustMe插件直接绕过证书锁定
原理:JustTrustMe的原理是将前面提到的,常见的HTTP请求库中用于设置校验证书的相关API进行了Hook,创建一个自定义的未校验证书的X509TrustManager对象并替换掉应用安全有证书校验的X509TrustManager对象,达到绕过的效果。
2.通过Frida绕过SSL单向验证
使用Frida绕过的思路也是对客户端的验证函数进行hook,因为SSLPinning的原理就是内置一个安全的证书,当客户端与服务端通信时验证服务端的证书是否安全,这样我们就可以通过hook将这个验证函数一直返回通过,那么就达到了绕过验证的目的。
双向证书校验绕过
因为服务端会验证客户端的证书,所以应用一般会把证书和证书秘钥存放在app中,一般在app/assert目录或raw目录下,后缀名一般为.p12或.pks。找到客户端的证书并导入到抓包工具中再进行抓包就行了。有些情况证书可能会有加密,这就需要手动去逆向解密或逆向密码了。
下面利用soul来实际操作下,这里使用的抓包工具是Charles-proxy,别的工具思路是一样的,只是最后导入证书的位置有所差异。
在没有绕过SSL pinning的情况下,我们发现利用抓包工具对应用的数据包进行抓取,什么数据也看不到。在使用Xposed+JustTrustMe绕过单向验证。
可以看到,开启JustTrustMe后可以抓取到一些数据,但服务端返回400报错,提示"No required SSL certificate was sent"。根据这个提示说明服务端开启了双向认证机制,要求客户端发送所需的SSL证书,所以还需要去绕过服务端的双向认证机制。
既然服务端会对客户端的证书进行验证,那么客户端证书肯定在客户端会保留一份,那么能否直接在apk中找到呢。实际可以在apk的assets目录下发现确实存在我们需要的证书。
现在有了客户端的证书,但这还不够,因为使用该证书的时候会提示我们输入密码,还需要找到证书的密码,思路和刚才一样,在应用中找。
把应用apk丢到jadx中解析,然后利用该工具的搜索功能直接搜索关键字"client.p12",很快就能找到证书相关的逻辑。
在v4_1.load函数的第二个参数v1就是密码,通过向上回溯分析,发现v1来源于getStorePassword函数,该函数是一个jni函数,通过名字大概可以猜到。
接着分析该函数所在的类可以知道该类会加载一个soul-netsdk动态库,那么就知道了getStorePassword的实现应该就在这个动态库中。
从apk中找到这个动态库并丢到IDA中解析可以找到下面这个函数,可以看到该函数直接返回了一个字符串,到此为止得到这个字符串就是该证书的密码"soulapp123!@#1"。
最后把我们前面获取到的证书导入到抓包工具中,让工具使用我们提供的证书。通过以下的选项打开我们的导入窗口。
Proxy -> SSL Proxying Settings
一切顺利之后点击OK,然后再次进行抓包。
可以看到我们成功抓到了数据包,并且也可以看到里面的内容。
技术选型
看了前面几种抓包方案,那么如何选择适合我们的方案呢?我们的需求在于如何自动化,所以交互越少的方案是我们的优选方案;这里对前面几种方案进行了简单对比:
方案一:WIFI代理/透明代理,在手机设备上设置代理服务器,将设备上的数据转发至代理服务器(PC)端,以此完成对应用的网络请求捕获;
优点:数据包捕获的很完整,PC端可以配合一些扫描工具,完成对应用接口进行扫描;(xary+burp)
缺点:存在对抗问题,需要增加绕过抓包防护的功能;
方案二:VPN隧道,基于NetBare框架自实现一个VPN代理应用安装在手机设备上,通过拦截虚拟网卡对应用网络请求进行拦截;
优点:数据捕获的很完整,并且不依赖PC端抓包工具,因为框架开源所以更灵活,对单个应用的数据包进行自定义格式保存,用作后续的各种分析,比如漏洞扫描,合规检测;
缺点:存在对抗问题,需要添加抓包防护绕过的功能;
方案三:HOOK获取,基于Hook,对目标应用中的一些常见通信函数进行hook,直接抓取原始数据;
优点:基本不存在抓包对抗问题,可以将捕获的数据保存下来,用作后续的各种分析,比如漏洞扫描、合规检测;
缺点:捕获数据的完整度依赖hook点,存在请求覆盖不全,数据包不完整的问题;
从数据捕获的完整性来看,方案一和方案二都符合我们的要求,但考虑到方案一需要依赖代理服务器,并且需要配置抓包工具,部署起来比较复杂,而方案二只用在手机设备上安装一个应用即可,所以最终没考虑方案一直接选择的是方案二,同时结合方案三的方式来实现对API资产进行收集。
自动化测试
在确定了应用API资产收集方案后,再来看第二个问题,如何对应用内业务逻辑自动化触发?
即在不人工介入的情况下自动对应用内的业务逻辑进行触发,使客户端与服务端进行网络请求,再结合前面的捕获方案,完成应用内API接口的收集工作。在一番调研后得知,如果想实现该需求可以使用自动化测试相关的框架,所以对目前比较流行的自动化测试框架进行简单的了解学习。
自动化测试框架
自动化测试就是通过机器代替人工来对目标进行测试,测试人员通过编写测试脚本并在脚本的实际运行中添加对业务逻辑的判断,实现测试自动化。一般QA团队用的较多,软件安全有时也会用到,像一些做游戏外挂、抢红包、视频刷赞、刷阅读量、爬虫的也都会用到这类技术。常见自动化测试框架:Monkey、MonkeyRunner、UIAutomator、UIAutomator2、Appium、AirTest+Poco。
在对上面几款测试框架进行检测使用后,发现其实这些测试框架操作控件实现自动化测试的方式都大同小异。主要分为以下两种:
- 一种是通过随机生成测试事件流并发送给目标系统来完成对目标的测试,这种方式因为是随机生成的事件流,事件流不可控,所以用的比较少,一般用来做稳定性测试。代表框架有Monkey。
- 另一种则是通过脚本的方式生成可控的事件流,之后测试框架按一定顺序发送给目标系统来完成对目标的测试,相对第一种因为事件是测试人员自己生成的所以测试的更精准。
对于第二种脚本的生成又分为两种方式:
最常见的就是脚本录制,主要思想是记录人为操作时控件的坐标位置和发生的事件,之后通过回放录制的脚本来完成测试事件流,现在很多测试框架都提供了比较方便的录制回放功能;
另一种是通过工具(比如源码、UIAutomatorviewer等)分析UI布局来获取测试界面的控件布局、找到目标控件的ID、名称、描述或位置等信息。测试人员再编写脚本来让测试框架得到控件对象,并对控件对象执行一系列事件操作,像Robotium、UIAutomator2等,这里以UIAutomator2举例,下图为一个应用的控件布局。
控件遍历
在实际情况中,我们需要测试的应用是未知的,如何在不介入人工的情况下对未知的应用进行自动化测试?
这种情况下,随机事件的自动化方案不适合,又因为目标应用不是固定的,录制脚本的自动化方案也不适用,能选的就只有通过分析UI布局来实现自动化测试的方案。
最终决定采用UIAutomator2来通过控件遍历的方式进行自动化测试,遍历逻辑如下图。
1.安装测试应用,通过adb启动应用;
2.解析UI布局,获取界面中具备点击属性的控件对象;
3.计算当前页面Hash值,对可点击控件进行模拟点击;
4.根据点击前后页面的Hash值,判断是否进入新页面,进行深度优先遍历;
- 相同进入下一控件模拟点击;
- 不同进入结束条件判断,不结束则跳到第二步;
5.结束遍历;
其中的一个难点就在于对页面的判断,判断点击后是否进入到新的页面,这里列出几个判断依据:
- 点击前后页面的Activity名
- 点击前后页面的可点击控件数
- 点击前后页面截图的相似度
还有个需要规避的问题:业务层级遍历的越深整体测试的时间越长,因此需在全面与效率之间取出一个较能接受的中间值用作遍历深度,同时设置超时时间,在满足一定时长后自动终止测试。
效果如下图:
优化点
(1) 对于一些应用的抓包防护需要处理;
(2) 自动化遍历对于一些复杂页面,比如登录时处理还待优化;
(3) 对于做了API接口防护,比如具备签名、数据加密的接口还不支持;
参考