第二十三课:代理与抓包工具对抗——检测并阻止中间人代理

各位同学,今天我们来聊一个攻防实战中非常核心的话题:如何检测并阻止中间人代理

说实话,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 就算伪造了服务器证书,也拿不到客户端的私钥,无法完成双向认证。

实现步骤:

  1. 服务器端生成 CA 证书,为每个客户端签发客户端证书
  2. 客户端安装客户端证书(通常打包在 App 资源中,或首次登录时下发)
  3. TLS 握手时,服务器要求客户端出示证书
  4. 服务器验证客户端证书的合法性

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 虽然安全,但部署成本高。客户端证书的分发、更新、吊销都需要一套完整的管理体系。我建议只在金融、政务等高安全场景中使用。

知识体系总览

下面这张图,我把本章的核心知识点串起来了。你可以对照着理解整个对抗体系。

代理与抓包工具对抗体系 攻击者(Charles/Fiddler) 目标 App 服务器 防御层 方案一:证书固定 硬编码服务器证书指纹 方案二:环境检测 检测代理/VPN/可疑证书 方案三:双向 TLS 客户端+服务器双向认证 组合使用效果最佳:证书固定 + 环境检测 + 双向 TLS(高安全场景)

实战建议与避坑

最后,我结合自己的项目经验,给你几个实在的建议:

  • 不要只依赖一种方案。证书固定 + 环境检测组合使用,攻击者需要同时绕过两层,难度翻倍。
  • 证书固定要留后路。我建议在服务器端维护一个证书指纹列表,App 启动时拉取最新列表。这样证书更换时,不需要强制用户更新 App。
  • 环境检测要隐蔽。不要在启动时立刻弹窗说“检测到代理”,这等于告诉攻击者你在检测。我习惯在正常业务流程中随机触发检测,发现异常后静默上报,或者延迟一段时间再阻断。
  • 别忘了混淆。所有检测逻辑的类名、方法名都要混淆。我见过太多 App 的检测代码直接叫 ProxyDetector,反编译后一目了然。

一个小技巧:如果你用的是 OkHttp,可以自定义一个 Interceptor,在请求发起前统一做代理检测和证书校验。这样代码更整洁,也方便统一管理。

好了,这一课的内容就到这里。代理对抗是一个持续博弈的过程,没有一劳永逸的方案。但掌握了证书固定、环境检测、双向 TLS 这三板斧,你已经能挡住 90% 以上的抓包攻击了。


公众号:蓝海资料掘金营,微信deep3321