代理抓包
上一篇我们介绍了 charles 是使用代理来进行抓包。
一种防止抓包的方式是,检测代理:
String proxyHost = System.getProperty("http.proxyHost");
String proxyPort = System.getProperty("http.proxyPort");
一般情况下,这俩返回的都是空。
另外一个简单的防护方式就是让App不使用代理:
OkHttpClient.Builder()
.proxy(Proxy.NO_PROXY)
.build()
因为是直连,不走代理,所以charles等工具就抓不到包了,这样一定程度上保证了数据的安全,这种方式只是通过代理抓不到包,但是无法防住使用 VPN 导流进行的抓包
使用VPN抓包的原理是,先将手机请求导到VPN,再对VPN的网络进行Charles的代理,绕过了对App的代理。Charles 也可以配置VPN的方式来抓包。
charles vpn 代理配置
然后在手机端安装 vpn 应用,例如,v2rayNG,因为我们的思路是让手机的流量都走 vpn,然后 vpn 连接 charles,这样,charles 也能拦截所有的请求。
配置v2rayNG
https://github.com/2dust/v2rayNG/releases
首先记得关闭你的wifi代理
-
添加代理服务器
手动输入[Socks] → 服务器就是 charles 主机的 ip 地址,端口是上面设置的 8889
-
配置规则
设置 → 预定义规则 → 全局代理
-
开启VPN即可连接 charles
这种VPN抓包的方式也可以检测出来的,我们看一下开启 VPN 前后手机网卡对比,开启后的网卡多出来一个叫做 tun0 的:
sailfish:/ # ifconfig
tun0 Link encap:UNSPEC
inet addr:26.26.26.1 P-t-P:26.26.26.1 Mask:255.255.255.252
UP POINTOPOINT RUNNING MTU:1500 Metric:1
RX packets:10571 errors:0 dropped:0 overruns:0 frame:0
TX packets:7914 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:500
RX bytes:13436215 TX bytes:810038
这个是可以通过代码获取到的,可以利用这个 api 来进行检测:
java.net.NetworkInterface.getName()
还有一个API也可以拿到VPN相关信息:
android.net.ConnectivityManager.getNetworkCapabilities()
所以,有的时候需要hook这些vpn相关的api才能绕过检测。
证书校验
将公钥证书编译到Android应用中,一般在assets文件夹保存,由应用在交互过程中去验证证书的合法性。
想要绕过的话,需要通过hook各网络框架的证书校验方法,替换原有逻辑,使校验失效。
比如,这样的校验逻辑:
public static SSLSocketFactory getSSlFactory(Context context) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
//把证书打包在asset文件夹中 BuildConfig.AUTH_CERT:证书名称
InputStream caInput = new BufferedInputStream(context.getAssets().open(BuildConfig.AUTH_CERT));
Certificate ca;
try {
ca = cf.generateCertificate(caInput);
LogManager.getLogger().d("Longer", "ca=" + ((X509Certificate) ca).getSubjectDN());
LogManager.getLogger().d("Longer", "key=" + ((X509Certificate) ca).getPublicKey());
} finally {
caInput.close();
}
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext s = SSLContext.getInstance("TLSv1", "AndroidOpenSSL");
s.init(null, tmf.getTrustManagers(), null);
return s.getSocketFactory();
} catch (CertificateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (KeyStoreException e) {
e.printStackTrace();
} catch (KeyManagementException e) {
e.printStackTrace();
} catch (NoSuchProviderException e) {
e.printStackTrace();
}
return null;
}
可以看到,这里是强制使用了内置的证书去做 SSL 通信,如果有中间人的话,SSL 握手必定会失败。Frida 需要hook这些方法来绕过校验。比如我们可以使用 frida 返回一个信任所有证书的实例:
public static SSLSocketFactory createTrustAllSSLSocketFactory() {
SSLSocketFactory sSLSocketFactory = null;
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new TrustManager[]{new TrustAllManager()}, new SecureRandom());
sSLSocketFactory = sc.getSocketFactory();
} catch (Exception ignored) {
}
return sSLSocketFactory;
}
public static class TrustAllManager implements X509TrustManager {
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@SuppressLint("TrustAllX509TrustManager")
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
}
这种校验方式还有一种加强版,就是双向校验,不仅使用内置公钥,还内置了一套私钥与密码,它用来做双向校验,服务端校验客户端是否合法。
由于私钥与密码是内置的,所以还是有办法拿到的,由于加载私钥需要使用到 KeyStore 相关的 api,所以我们hook这个类就能拿到私钥与密码。
拿到私钥与密码就能在 charles 里面设置客户端证书,让它伪造成 app 与服务端通信。
私钥的格式是可以互转的,只要知道了密钥,p12 与 pem 都是可以的。
证书固定
这个可以理解为内置证书方案的谷歌官方版,道理是一样的。
有两种实现方式:
-
通过network_security_config.xml配置
-
通过代码设置
//第一种方式:配置文件
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.zuoyebang.cn</domain>
<pin-set expiration="2025-01-01">
<pin digest="SHA-256">38JpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhK90=</pin>
<!-- 备用证书信息,一般为域名证书的二级证书 -->
<pin digest="SHA-256">9k1a0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM90K=</pin>
</pin-set>
</domain-config>
</network-security-config>
//第二种方式:代码设置
fun sslPinning(): OkHttpClient{
val builder = OkHttpClient.Builder()
val pinners = CertificatePinner.Builder()
.add("api.zuoyebang.cn", "sha256//89KpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRh00L=")
.add("api.zuoyebang.com", "sha256//a8za0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1o=09")
.build()
builder.apply {
certificatePinner(pinners)
}
return builder.build()
}
可以看到它是储存了公钥证书的的 sha256 值,与内置证书本质上差不多。
我们看一下 CertificatePinner
的源码,就知道该如何绕过检验了:
public void check(String hostname, List<Certificate> peerCertificates)
throws SSLPeerUnverifiedException {
Set<ByteString> pins = findMatchingPins(hostname);
if (pins == null) return;
for (int i = 0, size = peerCertificates.size(); i < size; i++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i);
if (pins.contains(sha1(x509Certificate))) return; // Success!
}
// If we couldn't find a matching pin, format a nice exception.
StringBuilder message = new StringBuilder()
.append("Certificate pinning failure!")
.append("\n Peer certificate chain:");
for (int i = 0, size = peerCertificates.size(); i < size; i++) {
X509Certificate x509Certificate = (X509Certificate) peerCertificates.get(i);
message.append("\n ").append(pin(x509Certificate))
.append(": ").append(x509Certificate.getSubjectDN().getName());
}
message.append("\n Pinned certificates for ").append(hostname).append(":");
for (ByteString pin : pins) {
message.append("\n sha1/").append(pin.base64());
}
throw new SSLPeerUnverifiedException(message.toString());
}
如果证书校验不通过,这个方法就会抛出异常,我们可以简单的让这个方法不执行直接返回就好了。
二手的程序员
欢迎关注二手的程序员,这里主要分享逆向相关的知识。专注于完整系列,让知识不再碎片化。不定时更新,也欢迎关注我的博客:lyldalek.top
公众号