1. 环境

因为curl-impersonate库的指纹直接被检测了,不得已自己生成指纹,试了sslcontext只能生成套件,不能自定义很多东西,所以java+bc生成指纹请求

  • jdk1.8+
  • bouncycastle依赖
  • jetty依赖(解析h2)
<dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk18on</artifactId>
            <!-- 请查看最新版本 -->
            <version>1.83</version>
        </dependency>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bctls-jdk18on</artifactId>
            <version>1.83</version> <!-- 替换为最新版本 -->
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty.http2</groupId>
            <artifactId>http2-common</artifactId>
            <version>11.0.21</version>
        </dependency>

        <dependency>
            <groupId>org.eclipse.jetty.http2</groupId>
            <artifactId>http2-hpack</artifactId>
            <version>11.0.21</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-util</artifactId>
            <version>11.0.21</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>jetty-http</artifactId>
            <version>11.0.21</version>
        </dependency>

2. 代码(只贴出关键代码)

1. 注册bc,且创建自定义tls客户端(tls拓展和签名算法自己去找,我用的我chrome相同的的扩展和签名算法,示例代码的tls extension和签名算法不完整)


import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.tls.*;
import org.bouncycastle.tls.crypto.TlsCrypto;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.Security;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Vector;

public class BcTlsClient extends DefaultTlsClient {
    String host;


    static {
        // 检查是否已经注册,避免重复注册
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }
    public BcTlsClient(TlsCrypto crypto,String host) {
        super(crypto);
        this.host = host;
    }

    @Override
    /**
     * tls拓展
     * todo 顺序问题还未解决,linkedHashMap已经替换源码
     */
    public Hashtable<Integer, byte[]> getClientExtensions() throws IOException { 
        Hashtable ext = TlsExtensionsUtils.ensureExtensionsInitialised(super.getClientExtensions());
        ext.clear();
         // ---------- 15) ALPN (16) ----------
        // ALPN 要用 Vector<ProtocolName> 并用 asUtf8 创建
        Vector<ProtocolName> alpn = new Vector<>();
        alpn.add(ProtocolName.asUtf8Encoding("h2"));         // 推荐写法
        alpn.add(ProtocolName.asUtf8Encoding("http/1.1"));   // 推荐写法

        TlsExtensionsUtils.addALPNExtensionClient(ext, alpn);


        Vector<ServerName> serverNames = new Vector<>();
        serverNames.add(new ServerName(NameType.host_name, host.getBytes()));
        TlsExtensionsUtils.addServerNameExtensionClient(ext, serverNames);

}
    @Override
    public boolean isFallback() {
        return false;
    }


//    @Override
//    public void notifyAlertReceived(short alertLevel, short alertDescription) {
//        // 忽略非致命 close_notify;
//        System.out.println("alertLevel=" + alertLevel + ", alertDescription=" + alertDescription);
//    }

    @Override
    public void notifyAlertRaised(short level, short description, String message, Throwable cause) {
        System.out.println("Alert raised: " + description + " msg=" + message);
    }


    @Override
    public ProtocolVersion[] getSupportedVersions() {
        return new ProtocolVersion[] { ProtocolVersion.TLSv13, ProtocolVersion.TLSv12 };
    }
    @Override
    public boolean allowLegacyResumption() {
        return false;
    }

    @Override
    public TlsAuthentication getAuthentication() throws IOException {
        return new TlsAuthentication() {

            // 1. 处理服务端证书
            @Override
            public void notifyServerCertificate(TlsServerCertificate serverCertificate) throws IOException {
                // ❗❗这里你必须自己处理证书验证,否则就算成功也可能被拦
                // 简化:直接信任所有证书(仅用于测试)
//                System.out.println("收到服务端证书,共 " +
//                        serverCertificate.getCertificate().getLength() + " 张");
            }

            // 2. 客户端证书认证(一般不用,返回 null 即可)
            @Override
            public TlsCredentialedSigner getClientCredentials(CertificateRequest certificateRequest)
                    throws IOException {
                return null;
            }

        };

    }


    @Override
    public Vector<SignatureAndHashAlgorithm> getSupportedSignatureAlgorithms() {
        return buildMySigList();
    }

    @Override
    public Vector<SignatureAndHashAlgorithm> getSupportedSignatureAlgorithmsCert() {
        return buildMySigList();
    }

    private Vector<SignatureAndHashAlgorithm> buildMySigList() {
        Vector<SignatureAndHashAlgorithm> v = new Vector<>();
		//签名算法
        return v;
    }



    @Override
    public int[] getSupportedCipherSuites() {
        //chrome 证书套件
        return new int[]{
                47802, //GREASE
                CipherSuite.TLS_AES_128_GCM_SHA256,
                CipherSuite.TLS_AES_256_GCM_SHA384,
                CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
                CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
                CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
                CipherSuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
                CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
                CipherSuite.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
                CipherSuite.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
                CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
                CipherSuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
                CipherSuite.TLS_RSA_WITH_AES_128_GCM_SHA256,
                CipherSuite.TLS_RSA_WITH_AES_256_GCM_SHA384,
                CipherSuite.TLS_RSA_WITH_AES_128_CBC_SHA,
                CipherSuite.TLS_RSA_WITH_AES_256_CBC_SHA,
        };
    }
}

2. h2请求解析

http2请求和http1的解析不一样,这里直接用jetty的代码解析了

public void parseHTTP2() throws Exception {
        ByteBufferPool pool = new ArrayByteBufferPool();

        CountDownLatch streamDone = new CountDownLatch(1);
        Parser parser = new Parser(pool, 16 * 1024);
        parser.init(new Parser.Listener.Adapter() {
            @Override
            public void onSettings(SettingsFrame frame) {
                settingsReceived.set(true);
                // send SETTINGS ACK once
                if (settingsAckSent.compareAndSet(false, true)) {
                    try {
                        byte[] ack = new byte[]{
                                0x00, 0x00, 0x00,
                                0x04,
                                0x01,
                                0x00, 0x00, 0x00, 0x00
                        };
                        tlsOut.write(ack);
                        tlsOut.flush();
                        sendWindowUpdate(tlsOut, 0, 1024 * 1024); // connection window +1MB
                        sendWindowUpdate(tlsOut, 1, 1024 * 1024); // stream window +1MB


                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                Integer value = frame.getSettings().get(SettingsFrame.MAX_FRAME_SIZE);
                if (value != null) {
                    maxFrameSize = value;
                    log.debug(">>> Server SETTINGS_MAX_FRAME_SIZE = " + value);
                }
            }

            @Override
            public void onHeaders(HeadersFrame frame) {
                try {
                    log.debug(">>> HEADERS: endStream=" + frame.isEndStream());

                    MetaData meta = frame.getMetaData();
                    if (meta instanceof MetaData.Response res) {
                        statusCode = res.getStatus();

                        HttpFields fields = res.getFields();
                        for (HttpField field : fields) {
                            headers.put(field.getName(), field.getValue());
                        }
                        log.debug(">>> Server headers = " + headers);
                    }

                    if (meta instanceof MetaData.Request req) {
                        log.debug("Method: " + req.getMethod());
                        log.debug("URI: " + req.getURI());

                        HttpFields fields = req.getFields();
                        for (HttpField field : fields) {
                            log.debug(field.getName() + ": " + field.getValue());
                        }
                    }

                    if (frame.isEndStream()) {
                        log.debug(">>> HEADERS frame has END_STREAM");
                        streamDone.countDown();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onData(DataFrame frame) {
                ByteBuffer data = frame.getData();
                // 每收到 N 字节,就把窗口加回去(flow control)
                try {
                    sendWindowUpdate(tlsOut, frame.getStreamId(), data.remaining());
                    sendWindowUpdate(tlsOut, 0, data.remaining());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
                String s = BufferUtil.toString(data);
                response.append(s);
                if (frame.isEndStream()) {
                    log.debug(">>> data frame has END_STREAM");
                    streamDone.countDown();
                }
            }

        });


        byte[] buf = new byte[8192];
        ByteBuffer buffer = ByteBuffer.allocate(8192);

        while (streamDone.getCount() > 0) {
            int n = tlsIn.read(buf);
            if (n == -1) break;

            buffer.clear();
            buffer.put(buf, 0, n);
            buffer.flip();

            parser.parse(buffer);
        }
    }

notice

1.note1 附上结果

在这里插入图片描述
在这里插入图片描述
代码非常粗糙,但是基本是这样的,因为tls extension的顺序问题还没搞定,所以拿到的指纹其他值还是有一定区别,要完全模拟得解决这个问题,后面再看吧,搞了三天,眼睛快炸了。

2.鸣谢

感谢 chatgpt,确实好用,特别是国外开源的结果搜索和分析。bc的大部分分析都是靠和chatgpt对,还有很多代码都是chatgpt直接生成的。

太辛苦了,本来想着要不要收费的,想想算了。路过的哥姐拿的时候点个赞吧

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐