Edge 浏览器都不陌生, 微软自家的浏览器, 今天我们来使用 SSL/TLS 中最纯粹的力量, 破解 Secure Network VPN

介绍

Secure Network VPN 是微软官方与 Cloudflare 合作推出的 VPN 服务, 出口IP 为 Cloudflare WARP, 其他内容请自己看官方介绍

Secure Network VPN 官方介绍 | Cloudflare Blog | Cloudflare 企业合作

Secure Network VPN 需要登陆微软账户, 流量限制 5 GB, 但是是由浏览器上报的, 并非后端统计, 拿到 Token以后可以随便跑, 乐

SSL/TLS 中最纯粹的力量

公式是数学中最纯粹的力量

数学中最纯粹的力量之一

那什么是 SSL/TLS 中最纯粹的力量?

答案是 SSLKEYLOGFILE

它记录了 TLS 流量的解密密钥

在 Edge 中设置 SSLKEYLOGFILE

找到 Edge 图标, 右键选择属性

在 目标后加上

--ssl-key-log-file="文件路径"

Edge 属性

然后打开 CMD / 终端

杀死正在运行的 Edge 进程

taskkill /f /im msedge.exe

打开 Wireshark

选择网卡, 填写过滤器 port 443

Wireshark 主界面

启用 Secure Network VPN

点击修改好属性的 Edge 图标启动 Edge

打开 PC Edge 浏览器右上角的 更多按钮 (三个点 官方名称 "设置与其他")

点击 浏览器概要, 找到 Secure Network VPN

新版本在 更多工具 -> 浏览器健康助手

Edge Secure Network

此服务无法在受到网络审查的国家/地区使用, 例如 中国大陆, 伊朗 (看不见这个的请挂 VPN, 然后杀死 Edge 重新启动 [概率出现, 可能是缓存])

打开 VPN 开关

随便打开一个网站, 在右边选择 "始终为此站点打开 VPN"

然后刷新页面

解密 TLS 流量

Token 捕获

在过滤器中输入 tls.handshake.extensions_server_name == "cp5.cloudflare.com"

Wireshark 捕获

右键链接, 选择 协议首选项 -> Transport Layer Security -> 打开 Transport Layer Security 首选项

设置 (Pre)-Master-Secret log filename 为刚才设置的 SSLKEYLOGFILE 文件路径

点击确定

右键链接, 选择 跟踪流 -> TLS Stream, 已经可用看见 HTTP2 的明文内容

Wireshare 解密

如图可见, 这是一个 HTTP2 PROXY, 请求方式为 CONNECT

找到 Header 中的 proxy-authorization, 将值复制出来, 就大功告成了

Token 申请

新用户首次激活VPN可确保100%抓到

  • 正式版请求路径前缀 https://edge.microsoft.com/vpntokenservice
  • 测试版请求路径前缀 https://edge-staging.microsoft.com/vpntokenservice

详细请求参考

逆向工程

没实力,做不到

捆绑在 Chromium 内核 DLL, 反编译几亿行代码那是够呛

导入至 Golang Proxy

推荐使用 HTTP/1.1

此服务无法在受到网络审查的国家/地区使用, 例如 中国大陆, 伊朗 (直接 403)

Go Proxy via HTTP/1.1

package http

import (
    "bufio"
    "context"
    "crypto/tls"
    "fmt"
    "net"
    "net/http"
    "net/url"

    "golang.org/x/net/proxy"
)

func init() {
    proxy.RegisterDialerType("http", New)
    proxy.RegisterDialerType("https", New)
}

type httpProxy struct {
    u *url.URL

    forward proxy.Dialer
}

func New(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
    if u == nil {
        return nil, fmt.Errorf("uri is empty")
    }

    if forward == nil {
        forward = proxy.Direct
    }

    switch u.Scheme {
    case "http", "https":
    default:
        return nil, fmt.Errorf("unsupported scheme")
    }

    s := &httpProxy{
        u:       u,
        forward: forward,
    }

    return s, nil
}

func (s *httpProxy) Dial(network, address string) (net.Conn, error) {
    switch network {
    case "tcp", "tcp4", "tcp6":
    default:
        return nil, fmt.Errorf("unsupported network: %v", network)
    }

    c, err := s.forward.Dial("tcp", s.u.Host)
    if err != nil {
        return nil, err
    }

    if s.u.Scheme == "https" {
        c = tls.Client(c, &tls.Config{
            ServerName:         s.u.Hostname(),
            InsecureSkipVerify: true,
        })
    }

    req := &http.Request{
        Method:     "CONNECT",
        Host:       address,
        URL:        &url.URL{Host: address},
        Proto:      "HTTP/1.1",
        ProtoMajor: 1,
        ProtoMinor: 1,
        Header:     make(http.Header),
        Body:       http.NoBody,
        Close:      true,
    }

    req.Header.Set("Proxy-Authorization", "PrivacyToken token="+s.u.RawQuery)

    err = req.Write(c)
    if err != nil {
        c.Close()
        return nil, err
    }

    response, err := http.ReadResponse(bufio.NewReader(c), req)
    if err != nil {
        c.Close()
        return nil, err
    }

    if response.Body != nil {
        response.Body.Close()
    }

    if response.StatusCode != 200 {
        c.Close()
        return nil, fmt.Errorf("proxy response code %d", response.StatusCode)
    }

    return c, nil
}

func (s *httpProxy) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
    dialer, ok := s.forward.(proxy.ContextDialer)
    if !ok {
        return s.Dial(network, address)
    }

    c, err := dialer.DialContext(ctx, "tcp", s.u.Host)
    if err != nil {
        return nil, err
    }

    if s.u.Scheme == "https" {
        c = tls.Client(c, &tls.Config{
            ServerName:         s.u.Hostname(),
            InsecureSkipVerify: true,
        })
    }

    req := (&http.Request{
        Method:     "CONNECT",
        Host:       address,
        URL:        &url.URL{Host: address},
        Proto:      "HTTP/1.1",
        ProtoMajor: 1,
        ProtoMinor: 1,
        Header:     make(http.Header),
        Body:       http.NoBody,
        Close:      true,
    }).WithContext(ctx)

    req.Header.Set("Proxy-Authorization", "PrivacyToken token="+s.u.RawQuery)

    err = req.Write(c)
    if err != nil {
        c.Close()
        return nil, err
    }

    response, err := http.ReadResponse(bufio.NewReader(c), req)
    if err != nil {
        c.Close()
        return nil, err
    }

    if response.Body != nil {
        response.Body.Close()
    }

    if response.StatusCode != 200 {
        c.Close()
        return nil, fmt.Errorf("proxy response code %d", response.StatusCode)
    }

    return c, nil
}

使用方法

token := "提取的 private token 等于号后面的内容"

u := &url.URL{
    Scheme:   "https",
    Host:     "cp5.cloudflare.com:443",
    RawQuery: token,
}

dialer, err := proxy.FromURL(u, proxy.Direct)
if err != nil {
    clog.Fatalf("[Proxy] Failed to initial proxy, error: %s", err)
    return
}

Go Proxy via HTTP/2

package h2

import (
    "context"
    "crypto/tls"
    "fmt"
    "io"
    "net"
    "net/http"
    "net/url"
    "time"

    "golang.org/x/net/http2"
    "golang.org/x/net/proxy"
)

var (
    transport = &http2.Transport{}
)

func init() {
    proxy.RegisterDialerType("h2", New)
}

type h2Proxy struct {
    u       *url.URL
    forward proxy.Dialer

    laddr, raddr net.Addr
    conn         *http2.ClientConn
}

func New(u *url.URL, forward proxy.Dialer) (proxy.Dialer, error) {
    if u == nil {
        return nil, fmt.Errorf("uri is empty")
    }

    if forward == nil {
        forward = proxy.Direct
    }

    switch u.Scheme {
    case "h2":
    default:
        return nil, fmt.Errorf("unsupported scheme")
    }

    s := &h2Proxy{
        u:       u,
        forward: forward,
    }

    err := s.reconnect()
    if err != nil {
        return nil, err
    }

    go s.keepAlives()

    return s, nil
}

func (s *h2Proxy) reconnect() error {
    conn, err := s.forward.Dial("tcp", s.u.Host)
    if err != nil {
        return err
    }

    tlsConn := tls.Client(conn, &tls.Config{
        ServerName: s.u.Hostname(),
        NextProtos: []string{"h2"},
    })

    err = tlsConn.Handshake()
    if err != nil {
        return err
    }

    h2, err := transport.NewClientConn(tlsConn)
    if err != nil {
        return err
    }

    if s.conn != nil {
        s.conn.Close()
    }

    s.conn = h2
    s.laddr = conn.LocalAddr()
    s.raddr = conn.RemoteAddr()
    return nil
}

func (s *h2Proxy) keepAlives() {
    for {
        if err := s.conn.Ping(context.Background()); err != nil {
            s.reconnect()
        }

        time.Sleep(time.Second)
    }
}

func (s *h2Proxy) Dial(network, address string) (net.Conn, error) {
    return s.DialContext(context.Background(), network, address)
}

func (s *h2Proxy) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
    switch network {
    case "tcp", "tcp4", "tcp6":
    default:
        return nil, fmt.Errorf("unsupport network: %v", network)
    }

    r, w := io.Pipe()

    req := (&http.Request{
        Method:        "CONNECT",
        Host:          address,
        URL:           &url.URL{Scheme: "https", Host: address},
        Proto:         "HTTP/2",
        ProtoMajor:    2,
        ProtoMinor:    0,
        Header:        make(http.Header),
        Body:          r,
        ContentLength: -1,
    }).WithContext(ctx)

    req.Header.Set("Proxy-Authorization", "PrivacyToken token="+s.u.RawQuery)

    response, err := s.conn.RoundTrip(req)
    if err != nil {
        return nil, err
    }

    if response.StatusCode != 200 {
        if response.Body != nil {
            response.Body.Close()
        }

        return nil, fmt.Errorf("proxy response code %d", response.StatusCode)
    }

    return &wrappedConn{
        rc:    response.Body,
        w:     w,
        laddr: s.laddr,
        raddr: s.raddr,
    }, nil
}

type wrappedConn struct {
    rc io.ReadCloser
    w  io.WriteCloser

    laddr, raddr net.Addr
}

func (c *wrappedConn) Read(b []byte) (int, error) {
    return c.rc.Read(b)
}

func (c *wrappedConn) Write(b []byte) (int, error) {
    return c.w.Write(b)
}

func (c *wrappedConn) Close() error {
    c.rc.Close()
    return c.w.Close()
}

func (c *wrappedConn) LocalAddr() net.Addr {
    return c.laddr
}

func (c *wrappedConn) RemoteAddr() net.Addr {
    return c.raddr
}

func (c *wrappedConn) SetDeadline(t time.Time) error {
    return nil
}

func (c *wrappedConn) SetReadDeadline(t time.Time) error {
    return nil
}

func (c *wrappedConn) SetWriteDeadline(t time.Time) error {
    return nil
}

使用方法

token := "提取的 private token 等于号后面的内容"

u := &url.URL{
    Scheme:   "h2",
    Host:     "cp5.cloudflare.com:443",
    RawQuery: token,
}

dialer, err := proxy.FromURL(u, proxy.Direct)
if err != nil {
    clog.Fatalf("[Proxy] Failed to initial proxy, error: %s", err)
    return
}

后记

Edge Security Network VPN 更新日志

  • v1

    • Token 过期时间 30天
    • IP属地 拜访地 (客户端IP来自哪个地区 IP就是那个地区)
    • IP池 2个C段 (每个TCP链接IP都不同 使用 HTTP/1.1 可秒换IP 使用 HTTP/2 可保持同一IP)
  • v2

    • Token 过期时间 4-7天
    • IP属地 归属地/国际漫游 (Token注册与哪个地区 IP就是那个地区)
    • IP池 1-2个
最后修改:2025 年 04 月 30 日
如果觉得我的文章对你有用,请随意赞赏