第二十三课:代理与抓包工具对抗——检测并阻止中间人代理
各位同学,今天我们来聊一个攻防实战中非常核心的话题:如何检测并阻止中间人代理。
说实话,Charles、Fiddler 这些工具,做开发的同学几乎人手一个。调试接口、看网络请求,确实方便。但站在安全角度,这些工具一旦被恶意利用,就成了窃取数据的利器。我见过不少 App,上线后被反编译、挂代理抓包,用户隐私数据直接裸奔。嗯,这课就是教你怎么防。
为什么代理能抓到你的数据?
先搞清楚原理。Charles 这类工具,本质上是在你的设备和服务器之间,插入了一个“中间人”。
正常流程:
- 客户端 → 服务器(直接 TLS 握手)
- 证书由受信任的 CA 签发
代理抓包流程:
- 客户端 → Charles(Charles 伪造证书)
- Charles → 服务器(Charles 作为客户端重新发起连接)
说白了,Charles 把自己的根证书安装到系统信任区,然后动态签发伪造的服务器证书。你的 App 如果只做常规的证书校验,就会信任这个假证书。数据就被中间人一览无余了。
核心对抗思路:让 App 只信任我们自己的服务器证书,不信任任何第三方签发的证书,包括 Charles 伪造的。
方案一:证书固定(Certificate Pinning)
这是最常用、也最有效的手段。我个人习惯在项目中优先使用这种方式。
原理很简单:在客户端代码里,硬编码服务器证书的公钥或证书本身。每次 TLS 握手时,除了系统级的证书链校验,App 还会额外比对服务器返回的证书是否匹配我们预设的那个。
代码示例(OkHttp + Kotlin):
// 1. 获取服务器证书的 SHA-256 指纹
// 可以用 openssl 命令:openssl s_client -connect yourserver.com:443 | openssl x509 -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256
val certificatePinner = CertificatePinner.Builder()
.add("yourserver.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // 替换为实际指纹
.build()
val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
// 发起请求
val request = Request.Builder().url("https://yourserver.com/api/data").build()
client.newCall(request).execute()
这里要注意:指纹一定要从你自己的服务器证书里提取,不要从 Charles 的假证书里提取。我曾经见过有同事调试时顺手把 Charles 的指纹写进了代码……那等于没防。
避坑指南:证书固定有一个致命问题——证书过期或更换时,旧版本的 App 会全部无法联网。我曾经在一个金融项目里踩过这个坑,上线后才发现服务器证书续期了,但 App 里写死的指纹没更新。结果用户大面积反馈无法登录。所以,建议同时固定多个备用证书,或者使用公钥固定(公钥通常不变,证书可以换)。
方案二:检测代理环境
除了证书层面的对抗,我们还可以在运行时检测设备是否处于代理环境中。这招虽然不能完全防住,但可以增加攻击者的成本。
检测系统代理设置(Android):
fun isProxyEnabled(context: Context): Boolean {
try {
val info = context.applicationContext.packageManager
.getPackageInfo("com.android.proxy", 0)
return info != null
} catch (e: PackageManager.NameNotFoundException) {
// 没有安装代理应用
}
// 检查系统代理设置
val proxyHost = System.getProperty("http.proxyHost")
val proxyPort = System.getProperty("http.proxyPort")
return !proxyHost.isNullOrEmpty() && proxyPort?.toIntOrNull() != null
}
检测 VPN 连接:
fun isVpnConnected(context: Context): Boolean {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork ?: return false
val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
return capabilities.hasTransport(NetworkCapabilities.TRANSPORT_VPN)
}
检测代理证书是否被安装:
fun isUserInstalledCertificate(context: Context): Boolean {
// 检查用户安装的证书中是否包含可疑的 CA
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
trustManagerFactory.init(null as KeyStore?)
val trustManagers = trustManagerFactory.trustManagers
for (trustManager in trustManagers) {
if (trustManager is X509TrustManager) {
val acceptedIssuers = trustManager.acceptedIssuers
for (issuer in acceptedIssuers) {
// 检查证书的颁发者是否可疑
val issuerName = issuer.issuerX500Principal.name
if (issuerName.contains("Charles") || issuerName.contains("Fiddler")) {
return true
}
}
}
}
return false
}
个人经验:检测代理环境这招,我建议作为辅助手段,不要作为唯一防线。因为攻击者可以 Hook 掉你的检测代码,或者直接修改系统属性。真正可靠的还是证书固定。
方案三:双向 TLS 认证(Mutual TLS)
这个方案更彻底。不仅客户端验证服务器,服务器也验证客户端。Charles 就算伪造了服务器证书,也拿不到客户端的私钥,无法完成双向认证。
实现步骤:
- 服务器端生成 CA 证书,为每个客户端签发客户端证书
- 客户端安装客户端证书(通常打包在 App 资源中,或首次登录时下发)
- TLS 握手时,服务器要求客户端出示证书
- 服务器验证客户端证书的合法性
Android 端配置示例:
// 加载客户端证书
val keyStore = KeyStore.getInstance("PKCS12")
val keyStoreStream = context.resources.openRawResource(R.raw.client_cert)
keyStore.load(keyStoreStream, "password".toCharArray())
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, "password".toCharArray())
// 配置 SSLContext
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(keyManagerFactory.keyManagers, null, SecureRandom())
val client = OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})
.build()
注意:双向 TLS 虽然安全,但部署成本高。客户端证书的分发、更新、吊销都需要一套完整的管理体系。我建议只在金融、政务等高安全场景中使用。
知识体系总览
下面这张图,我把本章的核心知识点串起来了。你可以对照着理解整个对抗体系。
实战建议与避坑
最后,我结合自己的项目经验,给你几个实在的建议:
- 不要只依赖一种方案。证书固定 + 环境检测组合使用,攻击者需要同时绕过两层,难度翻倍。
- 证书固定要留后路。我建议在服务器端维护一个证书指纹列表,App 启动时拉取最新列表。这样证书更换时,不需要强制用户更新 App。
- 环境检测要隐蔽。不要在启动时立刻弹窗说“检测到代理”,这等于告诉攻击者你在检测。我习惯在正常业务流程中随机触发检测,发现异常后静默上报,或者延迟一段时间再阻断。
- 别忘了混淆。所有检测逻辑的类名、方法名都要混淆。我见过太多 App 的检测代码直接叫 ProxyDetector,反编译后一目了然。
一个小技巧:如果你用的是 OkHttp,可以自定义一个 Interceptor,在请求发起前统一做代理检测和证书校验。这样代码更整洁,也方便统一管理。
好了,这一课的内容就到这里。代理对抗是一个持续博弈的过程,没有一劳永逸的方案。但掌握了证书固定、环境检测、双向 TLS 这三板斧,你已经能挡住 90% 以上的抓包攻击了。
公众号:蓝海资料掘金营,微信deep3321