网络抓包02 - Android抓包攻与防

代理抓包

上一篇我们介绍了 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代理

  1. 添加代理服务器

    手动输入[Socks] → 服务器就是 charles 主机的 ip 地址,端口是上面设置的 8889

  2. 配置规则

    设置 → 预定义规则 → 全局代理

  3. 开启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 都是可以的。

证书固定

这个可以理解为内置证书方案的谷歌官方版,道理是一样的。

有两种实现方式:

  1. 通过network_security_config.xml配置

  2. 通过代码设置

//第一种方式:配置文件
<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

公众号

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

二手的程序员

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值