本文尝试对安卓上见到的抓包方法及其检测做一个总结。大体上来看,抓包就是将手机app上的流量导向到代理程序,代理程序会使用MITM中间人攻击技术来充当一个假的服务器,以监听客户端与服务端的通信,当然此处需要强制客户端信任代理程序生成的假证书。而检测可以去检查是否存在非标准的信道,例如代理,VPN等;或者针对MITM的弱点,即限制证书,采用ssl pinning;又可以采取双向验证,让服务器来验证客户端的证书,增加一个逆向证书的门槛。

当然,也有另外一套方法,即通过Hook TLS相关函数的方式来监听通信。提到Hook自然能够想到Android Hook的集大成者frida;不过也出现了新的eBPF技术,其uprobe的模块可以跟踪用户态函数,相比于frida下日渐激烈的对抗,还是一个比较新的领域。

Certificate

安卓对证书的配置相对iOS比较麻烦,主要原因是高版本上用户添加的证书app默认不会信任。 而系统证书本身的目录是只读的,只能通过一些hack(bind mount)去将用户证书挂在到系统证书目录上。

目前高版本的可用方法:

mkdir -p -m 700 /data/local/tmp/tmp-ca-copy
 
cp /apex/com.android.conscrypt/cacerts/* /data/local/tmp/tmp-ca-copy/
 
mount -t tmpfs tmpfs /system/etc/security/cacerts
 
mv /data/local/tmp/tmp-ca-copy/* /system/etc/security/cacerts/
 
cp /data/local/tmp/269953fb.0 /system/etc/security/cacerts/
cp /data/local/tmp/9a5ba575.0 /system/etc/security/cacerts/
cp /data/local/tmp/6e39a726.0 /system/etc/security/cacerts/
 
chown root:root /system/etc/security/cacerts/*
chmod 644 /system/etc/security/cacerts/*
chcon u:object_r:system_file:s0 /system/etc/security/cacerts/*
 
ZYGOTE_PID=$(pidof zygote || true)
ZYGOTE64_PID=$(pidof zygote64 || true)
 
for Z_PID in "$ZYGOTE_PID" "$ZYGOTE64_PID"; do
    if [ -n "$Z_PID" ]; then
        nsenter --mount=/proc/$Z_PID/ns/mnt -- \
            /bin/mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts
    fi
done
 
APP_PIDS=$(
    echo "$ZYGOTE_PID $ZYGOTE64_PID" | \
    xargs -n1 ps -o 'PID' -P | \
    grep -v PID
)
 
for PID in $APP_PIDS; do
    nsenter --mount=/proc/$PID/ns/mnt -- \
        /bin/mount --bind /system/etc/security/cacerts /apex/com.android.conscrypt/cacerts &
done
wait # Launched in parallel - wait for completion here
 
echo "System certificate injected"

其他方法(尚未尝试)

Proxy

在安卓的网络设置界面设置即可,或者也有更自动化的通过adb设置的方法。

Adb Set Proxy

adb shell settings put global http_proxy 192.168.225.100:3128
adb shell settings put global global_http_proxy_host 192.168.225.100
adb shell settings put global global_http_proxy_port 3128

VPN Based

该方法会创建一个虚拟的tun网卡,和科学上网类似,可以将app的流量强制定向到代理工具。

VPN Detection

这部分是网络环境检测。例如,常见的银行和12306的app都会检测vpn和代理,检测到后可能会发出网络状况异常的通知。

ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
Network[] networks = cm.getAllNetworks();
 
Log.i(TAG, "Network count: " + networks.length);
for(int i = 0; i < networks.length; i++) {
 
  NetworkCapabilities caps = cm.getNetworkCapabilities(networks[i]);
 
  Log.i(TAG, "Network " + i + ": " + networks[i].toString());
  Log.i(TAG, "VPN transport is: " + caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN));
  Log.i(TAG, "NOT_VPN capability is: " + caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN));
 
}
 
Network activeNetwork = connectivityManager.getActiveNetwork();
NetworkCapabilities caps = connectivityManager.getNetworkCapabilities(activeNetwork);
boolean vpnInUse = caps.hasTransport(NetworkCapabilities.TRANSPORT_VPN);
 

Iptables

这种方法也可以实现按uid来过滤。安卓会为每一个app分配一个uid。可以通过下面的命令查找:

pm list packages -U | grep package_name

iptables -t nat -A OUTPUT -p tcp ! -d 127.0.0.1 -m owner --uid-owner 10291 -m multiport --dports 80,443 -j DNAT --to-destination 192.168.1.106:8080

clash based

算是比较新奇的思路。如果应用同时做了对VPN,代理的检测,可以尝试,但缺点就是配置麻烦。

Proxy Tools

Proxyman

Charles

支持protobuf

Burp Suite

Hooking Based

Frida

思路比较简单,也就是利用fridahook库里的SSL_read以及SSL_write函数,以及java层的一些TLS相关函数。

eBPF

可以很方便地跟踪libssl层的函数,但对于java还没有完备的支持。

目前这方面的对抗还比较少,但也有可行的方法:

Dection

Mutual Verification

简单讲就是不仅客户端验证服务器的证书,服务器也会去验证客户端的证书。

server {
        listen       443 ssl;
        server_name  www.yourdomain.com;
        ssl                  on;  
        ssl_certificate      /data/sslKey/server.crt;  #server公钥证书
        ssl_certificate_key  /data/sslKey/server.key;  #server私钥
        ssl_client_certificate /data/sslKey/root.crt;  #根证书,可以验证所有它颁发的客户端证书
        ssl_verify_client on;  #开启客户端证书验证  
 
        location / {
            root   html;
            index  index.html index.htm;
        }
    }
 

ssl_verify_clientssl_verify_client是用来验证客户端的。

可以通过hook一些常见的私钥相关方法来获取私钥和证书。例如,这里的java.security.KeyStore$PrivateKeyEntry.getPrivateKey方法和java.security.KeyStore$PrivateKeyEntry.getCertificateChain方法。

获取了私钥和证书后,可以指定对应参数来发包,例如这里的requests库。

import requests
 
url = 'https://example.com'
cert = ('/path/to/your/client_cert.pem', '/path/to/your/client_key.pem')
 
response = requests.get(url, cert=cert)
print(response.text)
 

SSL Pinning

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">example.com</domain>
        <pin-set expiration="2018-01-01">
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
            <!-- backup pin -->
            <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

SSL Library

对于用到了boringssl库的应用,可以通过SSL_CTX_set_custom_verify函数来实现ssl pining

OpenSSL提供类似的函数:

代码示例可以在下面地址找到:

Java Level

OkHttpClient client = new OkHttpClient.Builder()
        //完成证书绑定
        .certificatePinner(new CertificatePinner.Builder()
                .add("test.com", "sha512/14cf3DRF...")
                .build())
        .build();
var CertificatePinner = Java.use('okhttp3.CertificatePinner');

App Specific

Tiktok

Reference