Just do it.

4 抓包对抗

Posted on By qfzwy

1.基本概念

【胖虎的逆向之路】Android自制Https证书实现双向认证-腾讯云开发者社区-腾讯云

1.1 HTTP

HTTP是一种应用层协议,用于传输超文本,如HTML文档,以及其他资源,如图像和视频。它是一种无状态的协议,这意味着每个请求都是独立的,服务器不会保存关于客户端的任何信息。HTTP工作在TCP/IP协议栈的应用层,使用TCP端口80进行通信。

  • 通信使用明文(不加密),内容可能会被窃听
  • 不验证通信方的身份,因此有可能遭遇伪装
  • 无法证明报文的完整性,所以有可能已遭篡改

1.2 HTTPS

通过SSL/TLS(安全套接层/传输层安全)协议对HTTP进行加密。

HTTP + 通信加密 + 认证 + 完整性保护 = HTTPS

其中验证身份问题是通过验证服务器的证书来实现的,证书是第三方组织(CA 证书签发机构)使用数字签名技术管理的,包括创建证书、存储证书、更新证书、撤销证书。

1.3 SSL功能

客户对服务器的身份认证

服务器允许客户的浏览器使用标准的公钥加密技术和一些可靠的认证中心(CA)的证书,来确认服务器的合法性

服务器对客户的身份认证

也可通过公钥技术和证书进行认证,也可通过用户名,password 来认证

建立服务器与客户之间安全的数据通道

SSL 要求客户与服务器之间的所有发送的数据都被发送端加密、接收端解密,同时还检查数据的完整性。 SSL 协议位于 TCP/IP 协议与各种应用层协议之间,为数据通讯提供安全支持~

1.4 中间人攻击

当攻击者进行抓包时,也就是中间人攻击,其要点是伪造了一个假的服务器证书,让客户端信以为真。而SSL pingning在客户端内置了服务端的证书,如果发现服务端的证书不对,就检测到了攻击。

中间人攻击:举个例子就是攻击者给你呈现了一个伪造的银行页面,您正常地输入您的登录详细信息,这些详细信息被发送到中间人服务器,但他仍然会将您登录到银行,并正常显示页面。但攻击者的中间人服务器已捕获您的登录凭据,可供攻击。

2.代理检测

定义

代理检测是用于检测设备是否设置了网络代理。这种检测的目的是识别出设备是否尝试通过代理服务器(如抓包工具)来转发网络流量,从而可能截获和分析App的网络通信。

原理

App会检查系统设置或网络配置,以确定是否有代理服务器被设置为转发流量。例如,它可能会检查系统属性或调用特定的网络信息API来获取当前的网络代理状态。

检测方法

return System.getProperty("http.proxyHost") == null && System.getProperty("http.proxyPort")  == null

Port跟设置有关,例如Charles默认是8888

或者强制不走代理

connection = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);

OkHttpClient.Builder()  
    .proxy(Proxy.NO_PROXY)  
    .build()

对抗方法

function hook(){
    let GetProperty = Java.use("java.lang.System");
    GetProperty.getProperty.overload("java.lang.String").implementation = function (getprop) {
        if (getprop.indexOf("http.proxyHost") >= 0 || getprop.indexOf("http.proxyPort") >= 0) {
            return null
        }
        return this.getProperty(getprop);
    };
}

2.1 透明代理

安卓上基于透明代理对特定APP抓包 - SeeFlowerX

3.VPN检测

定义 VPN检测是指应用程序或系统检查用户是否正在使用虚拟专用网络(Virtual Private Network, VPN)的一种技术。当用户使用VPN时,他们的网络流量会被加密并通过一个远程服务器路由,这可以隐藏用户的实际IP地址和位置信息,同时保护数据的安全性和隐私。 原理 当客户端运行VPN虚拟隧道协议时,会在当前节点创建基于eth之上的tun0接口或ppp0接口。这些接口是用于建立虚拟网络连接的特殊网络接口。

检测方法

public final boolean Check_Vpn1() {
        try {
            //NetworkInterface.getNetworkInterfaces() 获取当前设备的所有网络接口。返回值是一个 Enumeration<NetworkInterface> 对象,表示网络接口的枚举集合。
            Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
            if (networkInterfaces == null) {
                return false;
            }
            Iterator it = Collections.list(networkInterfaces).iterator();
            while (it.hasNext()) {
                NetworkInterface networkInterface = (NetworkInterface) it.next();
                if (networkInterface.isUp() && !networkInterface.getInterfaceAddresses().isEmpty()) {
                    Log.d("zj595", "isVpn NetworkInterface Name: " + networkInterface.getName());
                    if (Intrinsics.areEqual(networkInterface.getName(), "tun0") || Intrinsics.areEqual(networkInterface.getName(), "ppp0") || Intrinsics.areEqual(networkInterface.getName(), "p2p0") || Intrinsics.areEqual(networkInterface.getName(), "ccmni0")) {
                        return true;
                    }
                }
            }
            return false;
        } catch (Throwable th) {
            th.printStackTrace();
            return false;
        }
    }

 public final boolean Check_Vpn2() {
        boolean z;
        String networkCapabilities;
        try {
            //调用 getApplicationContext() 获取应用的上下文,然后通过 getSystemService("connectivity") 获取系统的连接服务(ConnectivityManager)
            Object systemService = getApplicationContext().getSystemService("connectivity");
            Intrinsics.checkNotNull(systemService, "null cannot be cast to non-null type android.net.ConnectivityManager");
            //ConnectivityManager 是 Android 用于管理网络连接的服务类。
            ConnectivityManager connectivityManager = (ConnectivityManager) systemService;
            //获取当前活跃网络的 NetworkCapabilities,它表示网络的能力和状态,比如是否支持 VPN、Wi-Fi 等。
            NetworkCapabilities networkCapabilities2 = connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork());
            Log.i("zj595", "networkCapabilities -> " + networkCapabilities2);
            boolean z2 = networkCapabilities2 != null && networkCapabilities2.hasTransport(4);
            // 检查网络能力是否包含 "WIFI|VPN" 
            //检查当前网络是否支持 VPN(编号为 4 表示 VPN)。如果 networkCapabilities2 不为 null 且支持 VPN(hasTransport(4) 返回 true),则 z2 为 true,否则为 false。
            if (networkCapabilities2 != null && (networkCapabilities = networkCapabilities2.toString()) != null) {
                if (StringsKt.contains$default((CharSequence) networkCapabilities, (CharSequence) "WIFI|VPN", false, 2, (Object) null)) {
                    z = true;
                    return !z || z2;
                }
            }
            z = false;
            if (z) {
            }
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

对抗方法

function hookVpn(){
    let NetworkInterface = Java.use("java.net.NetworkInterface");
    NetworkInterface.getName.implementation = function () {
        let name = this.getName();
        console.log("name: " + name);
        if (name === "tun0" || name === "ppp0") {
            return "rmnet_data0";
        } else {
            return name;
        }
    };
    let NetworkCapabilities = Java.use("android.net.NetworkCapabilities");
    NetworkCapabilities.hasTransport.implementation = function () {
        return false;
    };

    //添加wifi字段
    NetworkCapabilities.appendStringRepresentationOfBitMaskToStringBuilder.implementation = function (sb, bitMask, nameFetcher, separator) {
            if (bitMask === 18) {
                console.log("bitMask", bitMask);
                sb.append("WIFI");
            } else {
                console.log(sb, bitMask);
                this.appendStringRepresentationOfBitMaskToStringBuilder(sb, bitMask, nameFetcher, separator);
            }
        }
}

4.SSL Pinning

SSL Pinning 也称为证书锁定,是Google官方推荐的检验方式,意思是将服务器提供的SSL/TLS证书内置到移动客户端,当客户端发起请求的时候,通过对比内置的证书与服务器的证书是否一致,来确认这个连接的合法性。

1.客户端向服务端发送SSL协议版本号、加密算法种类、随机数等信息。
2.服务端给客户端返回SSL协议版本号、加密算法种类、随机数等信息,同时也返回服务器端的证书,即公钥证书
3.客户端使用服务端返回的信息验证服务器的合法性,包括:
    (1)证书是否过期
    (2)发型服务器证书的CA是否可靠
    (3)返回的公钥是否能正确解开返回证书中的数字签名
    (4)服务器证书上的域名是否和服务器的实际域名相匹配、验证通过后,将继续进行通信,否则,终止通信
4.客户端向服务端发送自己所能支持的对称加密方案,供服务器端进行选择
5.服务器端在客户端提供的加密方案中选择加密程度最高的加密方式。
6.服务器将选择好的加密方案通过明文方式返回给客户端
7.客户端接收服务端返回的加密方式后,使用该加密方式生成产生随机码,用作通信过程中对称加密的密钥,使用服务端返回的公钥进行加密,将加密后的随机码发送至服务器
8.服务器收到客户端返回的加密信息后,使用自己的私钥进行解密,获取对称加密密钥。在接下来的会话中,服务器和客户端将会使用该密码进行对称加密,保证通信过程中信息的安全

图片

简单来说就是

  1. 客户端发起请求,服务端收到后把自己的一些列信息以及公钥发给客户端
  2. 客户端使用服务端返回的信息验证服务器的合法性
  3. 如果合法,客户端告诉服务端自己有哪些加密方式
  4. 服务端告诉客户端使用哪种加密方式(明文)
  5. 客户端使用服务端提供的公钥将该种加密方式的密钥加密,然后发给服务端
  6. 服务端用自己的私钥解密,获取加密方式的密钥,然后进行通信。

4.1 okhttp

[OkHttp 证书绑定流程 ssl pinning分析 CTF导航](https://www.ctfiot.com/222806.html)

4.1.1 正向开发

4.1.1.1 基本配置

在build.gradle.kts-dependencies加入

implementation("com.squareup.okhttp3:okhttp:4.9.1")

在 AndroidManifest.xml 文件中的 <application> 标签,并在其中添加 android:usesCleartextTraffic="true"

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Test"
        tools:targetApi="31"
        android:usesCleartextTraffic="true">

配置网络权限

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
<!--    配置网络权限-->
    <uses-permission android:name="android.permission.INTERNET" />

4.1.1.2 get请求

  1. 构建客户端对象OkhttpClient
  2. 构建请求Request
  3. 生成call对象
  4. Call发起请求(同步/异步)
public void NetTest(){
    //1.构建客户端对象OkhttpClient
    OkHttpClient httpClient = new OkHttpClient();
    //2.构建请求Request
    String url = "https://www.baidu.com";
    Request request = new Request.Builder()
        .url(url)
        .get()
        .build();
    //3.生成call对象
    // 每一个Call只能执行一次。
    // 如果想要取消正在执行的请求,可以使用call.cancel(),通常在离开页面时都要取消执行的请求的。
    Call call = httpClient.newCall(request);
    //4. Call发起请求
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Response response = call.execute();
                Log.i("qfzwy","okHttpGet run: response:"+ response.body().string());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }).start();
}

4.1.1.3 post请求

在构造Request对象时,需要多构造一个RequestBody对象

OkHttpClient httpClient = new OkHttpClient();

MediaType contentType = MediaType.parse("text/x-markdown; charset=utf-8");
String content = "hello!";
RequestBody body = RequestBody.create(contentType, content);

Request getRequest = new Request.Builder()
    .url("https://api.github.com/markdown/raw")
    .post(body)
    .build();

Call call = httpClient.newCall(getRequest);

call.enqueue(new Callback() {
    @Override
    public void onFailure(Call call, IOException e) {
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        Log.i(TAG, "okHttpPost enqueue: \n onResponse:"+ response.toString() +"\n body:" +response.body().string());
    }
});

传入RequestBody的 MediaType 还可以是其他类型,如客户端要给后台发送json字符串、发送一张图片,那么可以定义为:

// RequestBody:jsonBody,json字符串
String json = "jsonString";
RequestBody jsonBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);

//RequestBody:fileBody, 上传文件
File file = new File(Environment.getExternalStorageDirectory(), "1.png");
RequestBody fileBody = RequestBody.create(MediaType.parse("image/png"), file);

post请求提交表单

RequestBody formBody = new FormBody.Builder()
        .add("username", "hfy")
        .add("password", "qaz")
        .build();

复杂请求体

//RequestBody:fileBody,上传文件
File file = drawableToFile(this, R.mipmap.bigpic, new File("00.jpg"));
RequestBody fileBody = RequestBody.create(MediaType.parse("image/jpg"), file);


//RequestBody:multipartBody, 多类型 (用户名、密码、头像)
MultipartBody multipartBody = new MultipartBody.Builder()
    .setType(MultipartBody.FORM)
    .addFormDataPart("username", "hufeiyang")
    .addFormDataPart("phone", "123456")
    .addFormDataPart("touxiang", "00.png", fileBody)
    .build();


Request getRequest = new Request.Builder()
    .url("http://yun918.cn/study/public/file_upload.php")
    .post(multipartBody)
    .build();

上述请求中,okhttp未做任何配置,默认信任系统证书

4.2 指纹校验

使用openssl获取证书,手动生成hash,再进行base64编码

openssl s_client -connect www.52pojie.cn:443 -servername www.52pojie.cn | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

4.2.1 客户端实现

public void NetTest(){
    OkHttpClient httpClient = new OkHttpClient();
    //1.使用CertificatePinner.Builder构建一个证书固定器,添加服务器证书的指纹
    CertificatePinner certificatePinner = new CertificatePinner.Builder()
        .add("www.52pojie.cn","sha256/WnsD5UGdP5/a65xO1rpH8ru2EjyxkmPEaiNtKixhJLU=")
        .build();
    //2.构建新的客户端对象Client
    OkHttpClient client = httpClient.newBuilder()
        .certificatePinner(certificatePinner)
        .build();

    //3.构建请求Request
    String url = "https://www.52pojie.cn/?q=SSLPinningCode";
    Request request = new Request.Builder()
        .url(url)
        .get()
        .build();
    //4.生成call对象
    // 每一个Call只能执行一次。
    // 如果想要取消正在执行的请求,可以使用call.cancel(),通常在离开页面时都要取消执行的请求的。
    Call call = client.newCall(request);
    //4. Call发起请求
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                Response response = call.execute();
                Log.i("qfzwy", "指纹检测通过");
            } catch (IOException e) {
                Log.d("qfzwy", "指纹检测不通过");
                throw new RuntimeException(e);
            }
        }
    }).start();
}

与普通请求不同的是,为客户端对象配置了证书固定器并添加了服务器证书的指纹,在请求时,okhttp会进行一系列调用进行验证,见源码分析

4.2.2 anti脚本

function anti_ssl_key() {
        //check方法置空即可
    var okhttp3_Activity_1 = Java.use('okhttp3.CertificatePinner');   
    okhttp3_Activity_1.check.overload('java.lang.String', 'java.util.List').implementation = function(a, b) {                             
    console.log('[+] Bypassing SSL key pinning: ' + a);
    return;
}}

4.3 证书校验

通过trustManager 类实现的checkServerTrusted接口,核心在于验证服务器证书的公钥。具体步骤包括:获取服务器返回的证书,将其公钥编码为 Base64 字符串;同时从本地资源加载预存的可信客户端证书,并将其公钥也编码为 Base64 字符串。然后,比较这两个公钥是否匹配,以此确认服务器的身份是否合法。最后,使用自定义的 SSLSocketFactory 发起 HTTPS 请求,确保通信过程中只信任预定义的服务器证书,从而有效抵御中间人攻击。

获取网站证书

openssl s_client -connect 52pojie.cn:443 -servername 52pojie.cn | openssl x509 -out wuai.pem

image-20250209115927273

4.3.1 客户端实现

private void check_SSL_PINNING_CA(){
    X509TrustManager trustManager = new X509TrustManager() {

        // 检查客户端证书(在双向认证时使用)
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException {

        }

        @Override
        public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException {
            //获取服务器返回的证书
            X509Certificate cf = chain[0];
            //提取服务器证书的公钥。将公钥转换为 Base64 编码,以便与本地证书进行比对。
            String serverPubkey = Base64.encodeToString(cf.getPublicKey().getEncoded(), Base64.DEFAULT);
            Log.e("qfzwy", "服务器返回的证书:" + serverPubkey);
            //获取本地证书
            InputStream clientInput = getResources().openRawResource(R.raw.wuai);
            //创建 X.509 证书工厂
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            //将本地证书转换为 X509Certificate 对象
            X509Certificate clientCertificate = (X509Certificate) certificateFactory.generateCertificate(clientInput);
            //提取本地证书的公钥。将公钥转换为 Base64 编码,以便与服务器证书进行比对。
            String clientPubkey = Base64.encodeToString(clientCertificate.getPublicKey().getEncoded(), Base64.DEFAULT);
            Log.e("qfzwy", "客户端的证书:" + clientPubkey);
            //检查服务器证书的有效期,若证书过期则抛出 CertificateException。
            cf.checkValidity();

            boolean expected = clientPubkey.equals(serverPubkey);
            if (expected) {
                Log.e("qfzwy", "证书校验通过");
            } else {
                Log.e("qfzwy", "证书校验不通过");
                throw new CertificateException("证书校验不通过");
            }

        }
        //返回信任的 CA 证书列表(这里返回空数组,表示不信任任何 CA,只信任指定的服务器证书)。
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return new X509Certificate[0];
        }
    };

    SSLSocketFactory factory = null;
    try{
        //获取 SSLContext 实例(用于管理 SSL 连接)。
        SSLContext sslContext = SSLContext.getInstance("SSL");
        /*
            初始化 SSLContext 实例。
            第一个参数:密钥管理器(null 表示默认)。
            第二个参数:信任管理器数组(此处传入 trustManager,即我们的自定义证书校验逻辑)。
            第三个参数:随机数生成器(SecureRandom())。
            */
        sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom());
        //获取 SSLSocketFactory,用于 HTTPS 请求。
        factory = sslContext.getSocketFactory();
    } catch (Exception e) {
        e.printStackTrace();
    }

    final SSLSocketFactory finalFactory = factory;
    new Thread(new Runnable() {
        @Override
        public void run() {
            OkHttpClient client = new OkHttpClient.Builder()
                .sslSocketFactory(finalFactory, trustManager)  //指定 SSLSocketFactory 和 trustManager,确保使用自定义证书校验。
                .build();
            Request request = new Request.Builder()
                .url("https://www.52pojie.cn/forum.php")
                .get()
                .build();
            Call call = client.newCall(request);
            try {
                Response response = call.execute();
                Log.e("请求发送成功", "状态码:" + response.code());
            } catch (IOException e) {
                Log.e("请求发送失败", "网络异常" + e);
            }
        }
    }).start();
}

4.3.2 anti脚本

function hookCA(){
    // 获取X509TrustManager接口,实现一个没有证书管理器的类
    let X509TrustManager = Java.use("javax.net.ssl.X509TrustManager")
    let voidTrustManager = Java.registerClass({
        name:'com.example.test.voidTrustManager',
        implements: [X509TrustManager],
        methods:{
            checkClientTrusted:function (chain,str){},
            checkServerTrusted:function (chain,str){},
            getAcceptedIssuers:function() {return []; }
        }
    })
    let voidTrustManagers = [voidTrustManager.$new()];
    //拿到SSLContext实例
    let SSLContext= Java.use("javax.net.ssl.SSLContext");
    //先获取方法,因为后续需要主动调用
    let SSLContext_init = SSLContext.init.overload('[Ljavax.net.ssl.KeyManager;', '[Ljavax.net.ssl.TrustManager;', 'java.security.SecureRandom'
    )
    //调用init方法
    try {
        // 覆盖init方法的实现,指定使用自定义的TrustManager
        SSLContext_init.implementation = function (keyManager, trustManagers, secureRandom) {
            SSLContext_init.call( this,keyManager, voidTrustManagers, secureRandom);
        };
    } catch (err) {
        // 如果覆盖init方法失败,打印错误信息
        console.log(err); 
    }

}

5.双向校验

5.1 原理

双向验证就是多了服务器校验客户端证书,请求的使用需要把 app 证书携带上。

  1. 客户端发起请求,服务端收到后把自己的一些列信息以及公钥发给客户端
  2. 客户端使用服务端返回的信息验证服务器的合法性
  3. 如果合法,客户端将自己证书以及公钥发送至服务端
  4. 服务端对客户端证书进行校验,校验结束后获得客户端公钥
  5. 客户端告诉服务端自己有哪些加密方式
  6. 服务端告诉客户端使用哪种加密方式(以客户端的公钥进行加密)
  7. 客户端使用服务端提供的公钥将该种加密方式的密钥加密,然后发给服务端
  8. 服务端用自己的私钥解密,获取加密方式的密钥,然后进行通信。

5.1.1 典型的客户端身份认证流程(双向 TLS 认证)

步骤 1:服务器和客户端获取证书

服务器和客户端都需要从 CA(证书颁发机构) 获取证书:

  • 服务器获取:server.crt(服务器证书),server.key(服务器私钥)。
  • 客户端获取:client.crt(客户端证书),client.key(客户端私钥)。
  • 服务器和客户端都信任 ca.crt(CA 根证书)。

步骤 2:TLS 连接建立

  1. 客户端请求连接服务器(Client Hello)
    • 客户端发送 Client Hello 消息,其中包括支持的加密算法、TLS 版本等信息。
  2. 服务器返回 server.crt(Server Hello)
    • 服务器发送 Server Hello,包含服务器证书 server.crt,并请求客户端提供 client.crt 进行身份验证。
  3. 客户端验证 server.crt
    • 客户端检查 server.crt 是否由受信任的 ca.crt 签发,并验证其有效性(如过期时间、CN 是否匹配域名/IP)。
    • 如果 server.crt 不可信,连接失败。
  4. 客户端发送 client.crt(Client Certificate)
    • 服务器要求客户端提供 client.crt,用于身份验证。
    • 客户端发送自己的证书 client.crt 以及签名的身份验证信息。
  5. 服务器验证 client.crt
    • 服务器检查 client.crt 是否由受信任的 ca.crt 签发,并验证其有效性。
    • 服务器使用 client.crt 中的公钥解密客户端的身份验证信息,确保客户端确实持有 client.key
  6. 密钥交换并建立安全连接
    • 服务器和客户端完成密钥交换,建立安全的 TLS 加密通道。
    • 之后的数据传输将受到加密保护,防止窃听和篡改。

5.2 实现

5.2.1 CA相关文件

ca.key(CA 私钥)

  • 证书颁发机构(CA)的私钥。
  • 用于签发证书(server.crt)并验证其合法性。
  • 需要妥善保管,不能泄露,否则任何人都可以伪造受信任的证书。

ca.crt(CA 证书)

  • ca.key 生成的 自签名 CA 证书
  • 服务器证书(server.crt)由此 CA 签发,因此客户端需要 信任该 CA 才能信任服务器证书。
  • 需要分发给客户端,让客户端信任此 CA。

ca.srl(CA 序列号文件)(自动生成)

  • 记录 CA 颁发的证书序列号,防止证书重复。
  • -CAcreateserial 选项会自动创建该文件,后续颁发证书时序列号会递增。

5.2.2 服务器证书相关文件

server.key(服务器私钥)

  • 服务器的私钥,必须保密。
  • 服务器用它来解密客户端的 SSL/TLS 握手数据,并用于签名操作。
  • server.crt 配合使用,实现 HTTPS 加密通信。

server.csr(证书签名请求)

  • 服务器私钥 server.key 生成的 CSR 文件,用于请求 CA 颁发证书。
  • 包含服务器的公钥和身份信息(如 CN=192.168.0.101)。
  • 需要提交给 CA(或自签名)才能获得正式的证书。

server_cert.conf(证书配置文件)

  • 定义证书的字段信息,如:
    • CN(Common Name):服务器的主机名或 IP 地址。
    • subjectAltName(SAN):扩展证书的适用域名/IP 地址,防止 “Common Name mismatch” 错误。

server.crt(服务器证书,CA 签发)

  • 服务器的 正式证书,由 CA 使用 ca.key 签名生成。
  • 服务器向客户端提供此证书,客户端使用 ca.crt 进行验证。
  • 含有服务器的公钥和身份信息。

server.cer(自签名证书)

  • server.crt 区别
    • server.crt 由 CA 签名,客户端信任 CA 后才会信任此证书。
    • server.cer 直接由 server.key 自签名,适用于测试,但不会被 CA 信任。
  • 适用于开发或本地测试,正式环境不推荐。

5.2.3 客户端证书相关文件

client.key(客户端私钥)

  • openssl genrsa -out client.key 2048 生成。
  • 必须保密,不能泄露,否则攻击者可以冒充客户端。
  • 用于签署客户端的请求,并在 SSL/TLS 双向认证中配合 client.crt 进行加密和解密。

client.csr(客户端证书签名请求)

  • openssl req -new -out client.csr -key client.key 生成。
  • 作用:向 CA 请求签发客户端证书。
  • 包含了客户端的 公钥身份信息(如 CN=Client Name)。
  • CA 需要验证 client.csr 的信息,签发 client.crt

client.crt(CA 签名的客户端证书)

  • 由 CA 使用 ca.keyca.crt 签名生成:
  • 作用:
    • 用于客户端身份认证:当服务器启用 双向 TLS 认证(Mutual TLS),服务器要求客户端提供 client.crt 进行身份验证。
    • 服务器会使用 ca.crt 来验证 client.crt 是否由受信任的 CA 签发。

client.p12(客户端 PKCS#12 证书文件,带密码)

  • PKCS#12 格式(.p12.pfx 是一种可以存储 私钥 + 证书 的安全容器。
  • 需要 密码保护,用于安全存储和分发客户端证书。
  • 常见用途
    • 浏览器导入:在客户端访问双向 TLS 认证的服务器时,浏览器需要 client.p12 进行身份验证。
    • Android 设备导入
      • 某些 Android 版本需要转换为 BKS(Java Keystore 格式) 才能用于 HTTPS 双向认证。

| 文件名 | 作用 | | —————— | ———————————————————— | | ca.key | 证书颁发机构(CA)的私钥 | | ca.crt | CA 颁发的根证书,客户端需要信任 | | ca.srl | 记录 CA 颁发证书的序列号 | | server.key | 服务器私钥,必须保密 | | server.csr | 服务器证书签名请求,提交 CA 签发 | | server_cert.conf | 证书字段信息配置文件 | | server.crt | CA 签名的服务器证书,客户端信任 CA 后可用 | | server.cer | 自签名的服务器证书,仅用于测试 | | client.key | 客户端私钥,必须保密,客户端用它来进行加密、解密和签名 | | client.csr | 客户端证书签名请求,包含公钥和身份信息,提交给 CA 以获取正式证书 | | client.crt | CA 签名的客户端证书,用于客户端身份验证,服务器用它来验证客户端 | | client.p12 | 包含客户端证书 + 私钥的 PKCS#12 格式文件,用于导入到浏览器或设备 | ——

服务端证书

1.生成CA私钥

openssl genrsa -out ca.key 2048

2.生成CA自签名证书

openssl req -x509 -new -nodes -key ca.key -sha256 -days 1024 -out ca.crt

3.生成一个2048位的RSA私钥

openssl genrsa -out server.key 2048

4.基于上一步生成的私钥创建一个新的证书签名请求(CSR)

​ CSR包含了公钥和一些身份信息,这些信息在证书颁发过程中用于识别证书持有者。

openssl req -new -key server.key -out server.csr -config server_cert.conf
#server_cert.conf

[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
CN = 192.168.0.101

[v3_req]
subjectAltName = @alt_names

[alt_names]
IP.1 = 192.168.0.101

使用CA证书签发服务器证书。

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365 -extfile server_cert.conf -extensions v3_req

生成cer证书供服务端验证。

openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.cer

客户端证书

openssl genrsa -out client.key 2048
openssl req -new -out client.csr -key client.key
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 500 -sha256

生成客户端带密码的p12证书(这步很重要,双向认证的话,浏览器访问时候要导入该证书才行;可能某些Android系统版本请求的时候需要把它转成bks来请求双向认证):

openssl pkcs12 -export -out client.p12 -inkey client.key -in client.crt -certfile ca.crt

5.2.4 服务端实现

from flask import Flask, request,jsonify

app = Flask(__name__)

@app.route('/ca')
def ssl_verify():
    return jsonify({"message":"HTTPS server with mutual SSL verification started."})

def get_ssl_context():
    #CA根证书路径
    ca_crt_path = 'ca.crt'
    # 服务端证书和密钥路径
    server_crt_path = 'server.crt'
    server_key_path = 'server.key'
    #创建SSL上下文,使用TLS服务器模式
    ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
    #设置验证模式为需要客户端证书
    ssl_context.verify_mode = ssl.CERT_REQUIRED
    # 启用主机名检查
    ssl_context.check_hostname=False
    # 设置加密套件
    ssl_context.set_ciphers("HIGH:!SSLv3:!TLSv1:!aNULL:@STRENGTH")
    #加载CA根证书,用于验证客户端证书
    ssl_context.load_verify_locations(cafile=ca_crt_path)
    # 加载服务端证书和私钥
    ssl_context.load_cert_chain(certfile=server_crt_path, keyfile=server_key_path)
    return ssl_context

if __name__ == '__main__':
    # 配置SSL/TLS
    app.run(ssl_context=('server.crt', 'server.key'), host='0.0.0.0', port=8088, debug=True)

5.2.5 客户端实现

public OkHttpClient createClient() throws Exception {
    InputStream clientCert = getResources().openRawResource(R.raw.client);  // 使用 PKCS#12 证书
    // 加载根证书(用于信任管理)
    InputStream trustStore = getResources().openRawResource(R.raw.ca);      // 使用根 CA 证书

    // 加载客户端证书到 KeyStore
    KeyStore keyStore = KeyStore.getInstance("PKCS12");
    keyStore.load(clientCert, "123456".toCharArray());

    // 初始化 KeyManagerFactory,提供客户端证书
    KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    keyManagerFactory.init(keyStore, "123456".toCharArray());

    // 加载信任证书到 KeyStore
    KeyStore trustKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
    trustKeyStore.load(null, null);  // 创建一个空的 KeyStore

    // 加载 CA 根证书
    CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
    Certificate caCert = certificateFactory.generateCertificate(trustStore);
    trustKeyStore.setCertificateEntry("ca", caCert);

    // 初始化 TrustManagerFactory,提供根证书
    TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
    trustManagerFactory.init(trustKeyStore);  // 传入包含 CA 根证书的 KeyStore

    // 获取 TrustManager
    TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();

    // 创建 SSLContext
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(keyManagerFactory.getKeyManagers(), trustManagers, null);

    // 创建 OkHttpClient 使用自定义的 SSLContext
    OkHttpClient client = new OkHttpClient.Builder()
        .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0])
        .build();

    return client;

}

注:此写法调用时需要开新线程

new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            makeRequest();  // 执行网络请求
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}).start();

5.2.6 anti脚本

function hook_KeyStore_load() {
    Java.perform(function () {
        var ByteString = Java.use("com.android.okhttp.okio.ByteString");
        var myArray=new Array(1024);
        var i = 0
        for (i = 0; i < myArray.length; i++) {
            myArray[i]= 0x0;
         }
        var buffer = Java.array('byte',myArray);
        var StringClass = Java.use("java.lang.String");
        var KeyStore = Java.use("java.security.KeyStore");
        KeyStore.load.overload('java.security.KeyStore$LoadStoreParameter').implementation = function (arg0) {
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            console.log("KeyStore.load1:", arg0);
            this.load(arg0);
        };
        KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (arg0, arg1) {
            console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
            console.log("KeyStore.load2:", arg0, arg1 ? StringClass.$new(arg1) : null);
            if (arg0){
                var file =  Java.use("java.io.File").$new("/data/user/0/com.zj.wuaipojie/files/client"+".p12");
                var out = Java.use("java.io.FileOutputStream").$new(file);
                var r;
                while( (r = arg0.read(buffer)) > 0){
                    out.write(buffer,0,r)
                }
                console.log("证书保存成功!")
                out.close()
            }
            this.load(arg0, arg1);
        };
    });
}

5.2.7 证书导入

要将dump的证书导入charles,以及charles的ca证书导入android

使用charles导入证书,进行抓包

android7以上导入系统证书有两种方案

1.在面具中刷入MoveCertificate模块

2.首先导出证书,然后使用openssl x509 -subject_hash_old -in <Certificate_File>查看哈希值

image-20250210104126651

然后将原证书文件改名为哈希值.0,先push到其他目录,再移动到/system/etc/security/cacerts/


5.2.8 绕过hostname校验

在我的教程中没有忽略hostname校验,所以需要进行绕过

首先看一下如果忽略,okttpclient要加入以下

OkHttpClient client = new OkHttpClient.Builder()
    .sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagers[0])
    .hostnameVerifier((hostname, session) -> true)  // 忽略主机名验证
    .build();

这里的hostnameVerifier是为Client设置hostnameVerifier属性,其本质是一个接口,实现了一个verify的方法

public interface HostnameVerifier {
    boolean verify(String var1, SSLSession var2);
}

这里给出一个特定的HostnameVerifier

public class CustomHostnameVerifier implements HostnameVerifier {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        // 允许的合法主机名列表
        String allowedHostname = "example.com";  // 这里改成你的目标域名

        if (hostname.equalsIgnoreCase(allowedHostname)) {
            System.out.println("[*] Hostname verified: " + hostname);
            return true;  // 只有匹配的 hostname 通过验证
        }

        System.out.println("[!] Hostname verification failed: " + hostname);
        return false;  // 其他域名全部拒绝
    }
}

在客户端发起请求时会处理这个拦截器

image-20250210112248739

调用OkhhtoClient本身的hostnameVerifier()方法获取传入的verify,赋值到Address

image-20250210112338840

在执行 TLS 握手时,会对其进行校验

image-20250210112610138

anti脚本

function hookHost() {
    let HostnameVerifier = Java.registerClass({
        name: "com.example.test.HostnameVerifier",
        implements: [Java.use("javax.net.ssl.HostnameVerifier")],
        methods: {
            verify: function (hostname, session) {
                console.log("[*] Bypassing hostname verification for: " + hostname);
                return true;
            }
        }
    });
    let OkHttpClient = Java.use('okhttp3.OkHttpClient');
    OkHttpClient.hostnameVerifier.implementation = function () {
        console.log(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()));
        return HostnameVerifier.$new();
    }
}

6.hook抓包与混淆对抗

在没有做混淆的情况下,可以直接使用objection -g com.example.test explore -s "android sslpinning disable"过掉SSL Pinning,这里还是以之前的案例做演示

package com.example.test;

import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import java.io.IOException;
import java.io.InputStream;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import okhttp3.Call;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                check_SSL_PINNING_CA();
                NetTest();
            }
        });
    }

    public void NetTest(){
        OkHttpClient httpClient = new OkHttpClient();
        //1.使用CertificatePinner.Builder构建一个证书固定器,添加服务器证书的指纹
        CertificatePinner certificatePinner = new CertificatePinner.Builder()
                .add("www.52pojie.cn","sha256/WnsD5UGdP5/a65xO1rpH8ru2EjyxkmPEaiNtKixhJLU=")
                .build();
        //2.构建新的客户端对象Client
        OkHttpClient client = httpClient.newBuilder()
                .certificatePinner(certificatePinner)
                .build();

        //3.构建请求Request
        String url = "https://www.52pojie.cn/?q=SSLPinningCode";
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();
        //4.生成call对象
        // 每一个Call只能执行一次。
        // 如果想要取消正在执行的请求,可以使用call.cancel(),通常在离开页面时都要取消执行的请求的。
        Call call = client.newCall(request);
        //4. Call发起请求
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Response response = call.execute();
                    Log.i("qfzwy", "指纹检测通过");
                } catch (IOException e) {
                    Log.d("qfzwy", "指纹检测不通过");
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }
    private void check_SSL_PINNING_CA(){
        X509TrustManager trustManager = new X509TrustManager() {

            // 检查客户端证书(在双向认证时使用)
            @Override
            public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException {

            }

            @Override
            public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException {
                //获取服务器返回的证书
                X509Certificate cf = chain[0];
                //提取服务器证书的公钥。将公钥转换为 Base64 编码,以便与本地证书进行比对。
                String serverPubkey = Base64.encodeToString(cf.getPublicKey().getEncoded(), Base64.DEFAULT);
                Log.e("qfzwy", "服务器返回的证书:" + serverPubkey);
                //获取本地证书
                InputStream clientInput = getResources().openRawResource(R.raw.wuai);
                //创建 X.509 证书工厂
                CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
                //将本地证书转换为 X509Certificate 对象
                X509Certificate clientCertificate = (X509Certificate) certificateFactory.generateCertificate(clientInput);
                //提取本地证书的公钥。将公钥转换为 Base64 编码,以便与服务器证书进行比对。
                String clientPubkey = Base64.encodeToString(clientCertificate.getPublicKey().getEncoded(), Base64.DEFAULT);
                Log.e("qfzwy", "客户端的证书:" + clientPubkey);
                //检查服务器证书的有效期,若证书过期则抛出 CertificateException。
                cf.checkValidity();

                boolean expected = clientPubkey.equals(serverPubkey);
                if (expected) {
                    Log.e("qfzwy", "证书校验通过");
                } else {
                    Log.e("qfzwy", "证书校验不通过");
                    throw new CertificateException("证书校验不通过");
                }

            }
            //返回信任的 CA 证书列表(这里返回空数组,表示不信任任何 CA,只信任指定的服务器证书)。
            @Override
            public X509Certificate[] getAcceptedIssuers() {
                return new X509Certificate[0];
            }
        };

        SSLSocketFactory factory = null;
        try{
            //获取 SSLContext 实例(用于管理 SSL 连接)。
            SSLContext sslContext = SSLContext.getInstance("SSL");
        /*
            初始化 SSLContext 实例。
            第一个参数:密钥管理器(null 表示默认)。
            第二个参数:信任管理器数组(此处传入 trustManager,即我们的自定义证书校验逻辑)。
            第三个参数:随机数生成器(SecureRandom())。
            */
            sslContext.init(null, new TrustManager[]{trustManager}, new SecureRandom());
            //获取 SSLSocketFactory,用于 HTTPS 请求。
            factory = sslContext.getSocketFactory();
        } catch (Exception e) {
            e.printStackTrace();
        }

        final SSLSocketFactory finalFactory = factory;
        new Thread(new Runnable() {
            @Override
            public void run() {
                OkHttpClient client = new OkHttpClient.Builder()
                        .sslSocketFactory(finalFactory, trustManager)  //指定 SSLSocketFactory 和 trustManager,确保使用自定义证书校验。
                        .build();
                Request request = new Request.Builder()
                        .url("https://www.52pojie.cn/forum.php")
                        .get()
                        .build();
                Call call = client.newCall(request);
                try {
                    Response response = call.execute();
                    Log.e("请求发送成功", "状态码:" + response.code());
                } catch (IOException e) {
                    Log.e("请求发送失败", "网络异常" + e);
                }
            }
        }).start();
    }
}

image-20250212112753781

相当于自动化帮我们实现了需要进行的hook,实现原理和上面的anti手段一样。

但是如果加入了混淆,无法hook到这些函数就无法过掉SSL Pinning,这时就需要反混淆。

6.1 案例-反混淆过证书绑定

先看一个正常未做混淆的指纹校验调用栈,这里hook了java.io.File.$init

image-20250212123852644

指纹校验的check()需要对服务器的证书做hash,与添加的证书固定器做比对,所以会触发文件操作。因此在反混淆时,对File进行hook是一种有效的手段。

以某app为例,它对check()函数做了混淆,但其上层函数没有完全混淆,可以选择hookjava.io.File.$init,也可以选择hookcom.android.org.conscrypt.TrustManagerImpl.checkTrusted

image-20250212113317620

我以hook后者为例,这里可以清晰的判断哪个是check函数

image-20250212124533408

使用wallbreaker查看实例

image-20250212125010521

由此可以判断z1.g.a就是check(),下面进行hook

function hook_check(){
    Java.use("z1.g").a.implementation = function (string, list) {
        return;
    };

}

function main(){
    Java.perform(function (){
        hook_check()
    })
}

setImmediate(main)

image-20250212125314479

6.2 OkHttpLogger-Frida

https://github.com/siyujie/OkHttpLogger-Frida

原理:根据特征匹配识别okhttp框架。hookRealCall拿到requestresponse, 也可以缓存下来每一个请求的call对象,进行再次请求,

frida -U -l okhttp_poker.js -f com.example.demo --no-pause

7.底层网络自吐

https://bbs.kanxue.com/thread-267940.htm

7.1 java层http抓包

首先http是处于应用层的一种封装。最终也是通过调用tcp连接来进行传输。

首先看一下android的三种发包方式。

7.1.1 三种发包方式

HttpURL

public void GetByHttpURL(final String url) {
    new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                StringBuilder resultData= new StringBuilder();
                URL connUrl = new URL(url);
                HttpURLConnection urlConn = (HttpURLConnection) connUrl.openConnection();
                InputStreamReader in = new InputStreamReader(urlConn.getInputStream());
                BufferedReader buffer = new BufferedReader(in);
                String inputLine = null;
                while((inputLine=buffer.readLine())!=null){
                    resultData.append(inputLine).append("\n");
                }
                in.close();
                urlConn.disconnect();
                Log.d("qfzwy", "GetByHttpUrl:" + resultData);
            } catch (Exception e) {
                Log.d("qfzwy","GetByHttpUrl error:"+e.getMessage());
                e.printStackTrace();
            }
        }
    }).start();
}

okHttp

public void GetByOkHttp(String url) {
    try {
        OkHttpClient okHttpClient = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .get()
                .build();
        Call call = okHttpClient.newCall(request);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Response response = call.execute();
                    Log.d("qfzwy", "GetByOkHttp:" + response.body().string());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

tcp

public void GetByTcp() throws IOException, InterruptedException {
    Socket socket = new Socket("192.168.0.100",5000);
    socket.setSoTimeout(10000);
    //发送数据给服务端
    OutputStream outputStream = socket.getOutputStream();
    outputStream.write("hello server".getBytes("UTF-8"));
    Thread.sleep(2000);
    socket.shutdownOutput();
    //读取数据
    BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
    String line = br.readLine();
    //打印读取到的数据
    Log.d("qfzwy", "GetByTcp:" + line);
    br.close();
    socket.close();
}

7.1.2 tcp分析

客户端完整测试代码

package com.example.test;

import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import androidx.appcompat.app.AppCompatActivity;


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.net.URL;


import okhttp3.Call;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            GetByTcp();
                            Thread.sleep(3000);
                        }catch (Exception e){
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        });
    }

    //HttpURL
    public void GetByHttpURL(final String url) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    StringBuilder resultData= new StringBuilder();
                    URL connUrl = new URL(url);
                    HttpURLConnection urlConn = (HttpURLConnection) connUrl.openConnection();
                    InputStreamReader in = new InputStreamReader(urlConn.getInputStream());
                    BufferedReader buffer = new BufferedReader(in);
                    String inputLine = null;
                    while((inputLine=buffer.readLine())!=null){
                        resultData.append(inputLine).append("\n");
                    }
                    in.close();
                    urlConn.disconnect();
                    Log.d("qfzwy", "GetByHttpUrl:" + resultData);
                } catch (Exception e) {
                    Log.d("qfzwy","GetByHttpUrl error:"+e.getMessage());
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //okhttp
    public void GetByOkHttp(String url) {
        try {
            OkHttpClient okHttpClient = new OkHttpClient();
            Request request = new Request.Builder()
                    .url(url)
                    .get()
                    .build();
            Call call = okHttpClient.newCall(request);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Response response = call.execute();
                        Log.d("qfzwy", "GetByOkHttp:" + response.body().string());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //tcp
    public void GetByTcp() throws IOException, InterruptedException {
        Socket socket = new Socket("192.168.0.110",5000);
        socket.setSoTimeout(10000);
        //发送数据给服务端
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write("hello server".getBytes("UTF-8"));
        Thread.sleep(2000);
        socket.shutdownOutput();
        //读取数据
        BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        String line = br.readLine();
        //打印读取到的数据
        Log.d("qfzwy", "GetByTcp:" + line);
        br.close();
        socket.close();
    }


}

配置(否则无法进行http请求)

1.在res文件夹下创建一个xml文件夹,然后创建一个network_security_config.xml文件,文件内容如下:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="true" />
</network-security-config>

2.接着,在AndroidManifest.xml文件下的application标签增加以下属性:

<application
...
 android:networkSecurityConfig="@xml/network_security_config"
...
    />

服务端完整测试代码

import socketserver

# 定义一个自定义的请求处理器类,继承自 socketserver.BaseRequestHandler
class MyTCPHandler(socketserver.BaseRequestHandler):
    def handle(self):
        # 接收数据
        data = self.request.recv(1024).strip()
        print(f"Received: {data.decode('utf-8')}")

        # 发送回客户端
        response = bytes("server recv ", "UTF-8") + data
        self.request.sendall(response)
        print(f"Echoed: {response.decode('utf-8')}")


# 启动 TCP 服务器
if __name__ == "__main__":
    # 创建服务器对象,绑定到指定的 IP 和端口
    server = socketserver.TCPServer(("192.168.0.110", 5000), MyTCPHandler)

    print("Server running on 192.168.0.110:5000")

    # 启动服务器
    server.serve_forever()

图解tcp

发送数据包

img

那么hooksocketWrite0可以获得发送的数据包

 * @param fd the FileDescriptor
 * @param b the data to be written
 * @param off the start offset in the data
 * @param len the number of bytes that are written
private native void socketWrite0(FileDescriptor fd, byte[] b, int off,
                                 int len) throws IOException;

接收数据包

img

hooksocketRead0获取接收的数据包

 * @param fd the FileDescriptor
 * @param b the buffer into which the data is read
 * @param off the start offset of the data
 * @param len the maximum number of bytes read
 * @param timeout the read timeout in ms
private native int socketRead0(FileDescriptor fd,
                               byte b[], int off, int len,
                               int timeout)

7.1.3 hook

// java.net.Socket$init(ip,port) 获取ip和端口
// socketWrite0(FileDescriptor fd, byte[] b, int off,int len) 获取发送的数据
// socketRead0(FileDescriptor fd,byte b[], int off, int len,int timeout) 获取接受的数据
function hook_http(){
    //获取ip和端口
    Java.use("java.net.Socket").$init.overload('java.net.InetAddress', 'int').implementation =
        function (ip,port) {
            console.log("socket$init:", "Addr" + ip + ":" + port);
        };

    //获取发送的数据
    Java.use("java.net.SocketOutputStream").socketWrite0.implementation =
        function (fd, buff, off, len) {
            console.log("tcp write fd:", fd);
            print_bytes(buff)
            return this.socketWrite0(fd,buff,off,len);
        };

    Java.use("java.net.SocketInputStream").socketRead0.implementation =
        function (fd, buff, off, len, timeout) {
            console.log("tcp read fd:",fd)
            print_bytes(buff)
            return this.socketRead0(fd,buff,off,len,timeout);
        };


}

//将数组转换成c++的byte[]。并且hexdump打印结果
function print_bytes(bytes) {
    var buf  = Memory.alloc(bytes.length);
    Memory.writeByteArray(buf, byte_to_ArrayBuffer(bytes));
    let result = hexdump(buf, {offset: 0, length: bytes.length, header: false, ansi: true});
    console.log(result)
}
//将java的数组转换成js的数组
function byte_to_ArrayBuffer(bytes) {
    var size=bytes.length;
    var tmparray = [];
    for (var i = 0; i < size; i++) {
        var val = bytes[i];
        if(val < 0){
            val += 256;
        }
        tmparray[i] = val
    }
    return tmparray;
}



function main(){
    Java.perform(function (){
         hook_http()
    })
}

setImmediate(main)

7.2 java层https发包

https实际上就是http+ssl。由于http发送的数据直接就是明文。安全性非常差。https会在数据发送前,先用ssl进行加密。前面基本概念讲过不再赘述。

将请求的链接改为https

GetByHttpURL("https://missking.cc/");
GetByOkHttp("https://10.ip138.com");

7.2.1 ssl分析

img

7.2.2 hook

拦截了 SSLOutputStreamSSLInputStream 类的 writeread 方法,在进行数据读写时获取当前的调用栈信息

// 拦截 SSLOutputStream 类的 write 方法
Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream").write.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) {
    // 调用原始的 write 方法
    var result = this.write(bytearry, int1, int2);
    // 获取当前调用栈的字符串形式,存储 SSL 数据写入时的调用栈
    SSLstackwrite = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString();
    // 返回原始方法的结果
    return result;
}

// 拦截 SSLInputStream 类的 read 方法
Java.use("com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream").read.overload('[B', 'int', 'int').implementation = function (bytearry, int1, int2) {
    // 调用原始的 read 方法
    var result = this.read(bytearry, int1, int2);
    // 获取当前调用栈的字符串形式,存储 SSL 数据读取时的调用栈
    SSLstackread = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Throwable").$new()).toString();
    // 返回原始方法的结果
    return result;
}

7.3 native层http发包

img

7.4 native层https发包

img

7.5 r0capture

https://github.com/r0ysue/r0capture

Spawn 模式:
$ python3 r0capture.py -U -f com.coolapk.market -v

Attach 模式,抓包内容保存成pcap文件供后续分析:
$ python3 r0capture.py -U 酷安 -v -p iqiyi.pcap

8.ebpf

8.1 ebpf原理

https://ebpf.io/what-is-ebpf/

eBPF是一个运行在 Linux 内核里面的虚拟机组件,它可以在无需改变内核代码或者加载内核模块的情况下,安全而又高效地拓展内核的功能

道阻且长

8.2 ecapture

https://github.com/gojue/ecapture/releases

adb push ecapture /data/local/tmp/ 
adb shell chmod 777 /data/local/tmp/ecapture

使用方法

NAME:
        eCapture - 通过eBPF捕获SSL/TLS明文数据,无需安装CA证书。支持Linux/Android内核,适用于amd64/arm64架构。

USAGE:
        eCapture [flags]

VERSION:
        androidgki_arm64:v0.8.9:6.5.0-1025-azure

COMMANDS:
        bash    捕获bash命令的执行信息
        gotls   捕获使用TLS/HTTPS加密的Golang程序的明文通信
        help    获取有关任何命令的帮助信息
        tls     用于捕获TLS/SSL明文内容,无需CA证书。支持OpenSSL 1.0.x/1.1.x/3.x或更新版本。

DESCRIPTION:
        eCapture(旁观者)是一个可以捕获如HTTPS和TLS等明文数据包的工具,且不需要安装CA证书。
        它还可以捕获bash命令,适用于安全审计场景,比如mysqld数据库审计等(在Android中禁用)。
        支持Linux(Android)系统,内核版本为X86_64 4.18或aarch64 5.5及更高版本。
        项目仓库:https://github.com/gojue/ecapture
        官方主页:https://ecapture.cc

        使用方法:
          ecapture tls -h
          ecapture bash -h

        Docker使用示例:
        docker pull gojue/ecapture:latest
        docker run --rm --privileged=true --net=host -v ${HOST_PATH}:${CONTAINER_PATH} gojue/ecapture -h

NAME:
        tls - 用于捕获TLS/SSL明文内容,无需CA证书。支持OpenSSL 1.0.x/1.1.x/3.x及更新版本。

USAGE:
        eCapture tls [flags]

DESCRIPTION:
        使用eBPF uprobe/TC捕获进程事件数据和网络数据。还支持pcap-NG格式。

        示例:
        ecapture tls -m [text|keylog|pcap] [flags] [pcap过滤表达式(用于pcap模式)]
        ecapture tls -m pcap -i wlan0 -w save.pcapng host 192.168.1.1 and tcp port 443
        ecapture tls -l save.log --pid=3423
        ecapture tls --libssl=/lib/x86_64-linux-gnu/libssl.so.1.1
        ecapture tls -m keylog --pcapfile save_3_0_5.pcapng --ssl_version="openssl 3.0.5" --libssl=/lib/x86_64-linux-gnu/libssl.so.3
        ecapture tls -m pcap --pcapfile save_android.pcapng -i wlan0 --libssl=/apex/com.android.conscrypt/lib64/libssl.so --ssl_version="boringssl 1.1.1" tcp port 443

        Docker使用示例:
        docker pull gojue/ecapture
        docker run --rm --privileged=true --net=host -v /etc:/etc -v /usr:/usr -v ${PWD}:/output gojue/ecapture tls -m pcap -i wlp3s0 --pcapfile=/output/ecapture.pcapng tcp port 443

OPTIONS:
      --cgroup_path="/sys/fs/cgroup"            设置cgroup路径,默认值:/sys/fs/cgroup。
  -h, --help[=false]                            获取tls命令的帮助信息
  -i, --ifname=""                               (TC Classifier) 要附加探针的网络接口名称
  -k, --keylogfile="ecapture_openssl_key.og"    存储SSL/TLS密钥的文件,eCapture捕获加密通信中的密钥并将其保存到该文件
      --libssl=""                               指定libssl.so文件路径,默认从curl中自动查找
  -m, --model="text"                            捕获模型,可以是:text(明文内容),pcap/pcapng(原始数据包格式),key/keylog(SSL/TLS密钥)
  -w, --pcapfile="save.pcapng"                  将原始数据包以pcapng格式写入文件
      --ssl_version=""                          指定OpenSSL/BoringSSL版本,例如:--ssl_version="openssl 1.1.1g"--ssl_version="boringssl 1.1.1"

GLOBAL OPTIONS:
  -b, --btf=0                           启用BTF模式(0:自动选择;1:核心模式;2:非核心模式)
  -d, --debug[=false]                   启用调试日志
      --eventaddr=""                    设置接收捕获事件的服务器地址。默认值与logaddr相同(例如:tcp://127.0.0.1:8090)
      --hex[=false]                     以十六进制字符串打印字节数据
      --listen="localhost:28256"        设置HTTP服务器的监听地址,默认值:127.0.0.1:28256
  -l, --logaddr=""                      设置日志服务器的地址。例如:-l /tmp/ecapture.log 或 -l tcp://127.0.0.1:8080
      --mapsize=1024                    设置每个CPU的eBPF映射大小(事件缓冲区)。默认值:1024 * PAGESIZE(单位:KB)
  -p, --pid=0                           设置目标进程ID。如果为0,则目标为所有进程
  -u, --uid=0                           设置目标用户ID。如果为0,则目标为所有用户

无真机使用方案

不要选择googleAPI的,没有root权限

image-20250211223153820

长期使用内存给大点

image-20250211223208866

9.okhttp源码分析

okhttp3.internal.connection.RealCall.execute() //触发 HTTP 请求,进入 OkHttp 的网络请求逻辑。
	okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain() //处理请求拦截器,包括连接管理器、重试、缓存等。
		okhttp3.internal.http.RealInterceptorChain.proceed()
			okhttp3.internal.connection.ConnectInterceptor.intercept() //负责建立网络连接,调用 RealConnection 来管理实际的 TCP 连接。
				okhttp3.internal.connection.RealCall.initExchange() 
					okhttp3.internal.connection.ExchangeFinder.find()
						okhttp3.internal.connection.ExchangeFinder.findHealthyConnection()
							okhttp3.internal.connection.ExchangeFinder.findConnection()
							okhttp3.internal.connection.RealConnection.connect()
								okhttp3.internal.connection.RealConnection.establishProtocol()
									okhttp3.internal.connection.RealConnection.connectTls()
										okhttp3.CertificatePinner.check()

RealCall.execute()

  • 作用:同步执行 HTTP 请求。

  • 细节:会调用 getResponseWithInterceptorChain(),进入 OkHttp 的拦截器处理流程。

    image-20250208120748478

RealCall.getResponseWithInterceptorChain()

作用:构建并执行拦截器链,包括:

  • 重试拦截器
  • 连接拦截器
  • 缓存拦截器
  • 业务逻辑拦截器

细节:这一步会调用 RealInterceptorChain.proceed() 继续执行下一个拦截器。

image-20250208121037246

RealInterceptorChain.proceed()

作用:执行拦截器链,找到下一个拦截器并执行。

细节

  • proceed() 方法遍历 OkHttp 的内置拦截器(如连接拦截器、缓存拦截器等)。
  • 其中 ConnectInterceptor 负责网络连接。

image-20250208114838977

ConnectInterceptor.intercept()

作用:拦截 HTTP 请求,负责建立网络连接。

细节

  • 调用 RealCall.initExchange() 创建 HTTP 交换对象 (Exchange)。
  • 通过 ExchangeFinder.find() 查找可用的 TCP 连接。

image-20250208114940965

RealCall.initExchange()

作用:初始化 Exchange,它是 OkHttp 负责 HTTP 事务 (HTTP request-response exchange) 的对象。

细节

  • Exchange 负责将请求数据编码并发送,同时解析服务器返回的响应数据

image-20250208115836912

ExchangeFinder.find()

作用:在连接池中查找可复用的连接,或者创建新连接。

细节

  • 首先尝试复用已有的连接。
  • 如果没有可复用的连接,则调用 findHealthyConnection() 创建新连接。

image-20250208120046090

ExchangeFinder.findHealthyConnection()

作用:查找健康的连接(未关闭、未超时、未发生 TLS 证书错误等)。

细节

  • 直接调用 findConnection() 查找或创建新连接。

image-20250208120104186

ExchangeFinder.findConnection()

作用:查找或新建 RealConnection(TCP 连接)。

细节

  • 可能复用已有连接(如果可用)。
  • 如果没有可用连接,则调用 RealConnection.connect() 建立新的 TCP 连接。

image-20250208120149771

RealConnection.connect()

作用:负责创建 新的 TCP 连接

细节

  • 解析 IP 地址。
  • 进行 TCP 三次握手。
  • 如果是 HTTPS,则调用 establishProtocol() 进行 TLS 握手。

image-20250208115429401

RealConnection.establishProtocol()

作用:确定并执行 HTTPS 或 HTTP/2 协议的握手过程。

细节

  • 如果是 HTTPS,则调用 connectTls() 进行 TLS 握手。

image-20250208115458188

RealConnection.connectTls()

作用:执行 TLS 握手,验证服务器证书。

细节

  • 通过 SSLSocket 进行 TLS 连接。
  • 获取服务器返回的证书链。
  • 触发 证书固定(Certificate Pinning),调用 CertificatePinner.check()

image-20250208115532167

CertificatePinner.check()

作用:执行 证书固定检查 (Certificate Pinning)

细节

  • 获取服务器的 证书公钥 (Public Key) 指纹
  • 计算其 SHA-256 哈希值
  • 比对应用代码中固定的指纹 (CertificatePinner.add() 设置的指纹)。
  • 匹配:继续 TLS 连接;不匹配:抛出 SSLPeerUnverifiedException,连接失败。
  internal fun check(hostname: String, cleanedPeerCertificatesFn: () -> List<X509Certificate>) {
    val pins = findMatchingPins(hostname)
    if (pins.isEmpty()) return

    val peerCertificates = cleanedPeerCertificatesFn()

    for (peerCertificate in peerCertificates) {
      // Lazily compute the hashes for each certificate.
      var sha1: ByteString? = null
      var sha256: ByteString? = null

      for (pin in pins) {
        when (pin.hashAlgorithm) {
          "sha256" -> {
            if (sha256 == null) sha256 = peerCertificate.sha256Hash()
            if (pin.hash == sha256) return // Success!
          }
          "sha1" -> {
            if (sha1 == null) sha1 = peerCertificate.sha1Hash()
            if (pin.hash == sha1) return // Success!
          }
          else -> throw AssertionError("unsupported hashAlgorithm: ${pin.hashAlgorithm}")
        }
      }
    }

    // If we couldn't find a matching pin, format a nice exception.
    val message = buildString {
      append("Certificate pinning failure!")
      append("\n  Peer certificate chain:")
      for (element in peerCertificates) {
        append("\n    ")
        append(pin(element))
        append(": ")
        append(element.subjectDN.name)
      }
      append("\n  Pinned certificates for ")
      append(hostname)
      append(":")
      for (pin in pins) {
        append("\n    ")
        append(pin)
      }
    }
    throw SSLPeerUnverifiedException(message)
  }

10.抓包工具

10.1 charles

image-20250211171743983

电脑安装证书

image-20250211171918481

手机安装证书

image-20250211172117945