7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoでTCPプロキシを作るシリーズ

Part1 net.Listener Part2 透過プロキシ Part3 HTTPS MITM Part4 ロードバランサー
✅ Done ✅ Done 👈 Now -

はじめに

今回はHTTPS通信の中身を覗くプロキシを作る。

「え、HTTPSって暗号化されてるから見れないんじゃ...」

そう、普通は見れない。でもMITM(Man-in-the-Middle)攻撃の手法を使えば可能なんだよね。セキュリティの学習にもなるから、やってみよう。

⚠️ 注意
このテクニックは、自分が管理するネットワーク・端末でのみ使用してください。他人の通信を傍受することは犯罪です。

MITMプロキシの仕組み

通常のHTTPS通信:
┌──────────┐                           ┌──────────┐
│  Client  │ ══════ TLS Tunnel ══════► │  Server  │
└──────────┘                           └──────────┘
              暗号化されてて中身見えない

MITMプロキシ:
┌──────────┐    TLS①    ┌──────────┐    TLS②    ┌──────────┐
│  Client  │ ═════════► │  Proxy   │ ═════════► │  Server  │
└──────────┘            └──────────┘            └──────────┘
  偽の証明書を信頼      2つのTLS接続を中継      本物のサーバー

ポイントは「偽の証明書をクライアントに信頼させる」こと。

CA証明書の作成

まず、自己署名のCA(認証局)証明書を作成する。

package main

import (
    "crypto/ecdsa"
    "crypto/elliptic"
    "crypto/rand"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/pem"
    "math/big"
    "os"
    "time"
)

func generateCA() (*x509.Certificate, *ecdsa.PrivateKey, error) {
    // 秘密鍵生成(ECDSA P-256)
    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        return nil, nil, err
    }

    // シリアル番号(ランダム)
    serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))

    // CA証明書テンプレート
    template := &x509.Certificate{
        SerialNumber: serialNumber,
        Subject: pkix.Name{
            Organization: []string{"My Proxy CA"},
            CommonName:   "My Proxy Root CA",
        },
        NotBefore:             time.Now(),
        NotAfter:              time.Now().AddDate(10, 0, 0), // 10年有効
        KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        BasicConstraintsValid: true,
        IsCA:                  true,
        MaxPathLen:            1,
    }

    // 自己署名証明書を作成
    certDER, err := x509.CreateCertificate(
        rand.Reader, template, template, &privateKey.PublicKey, privateKey)
    if err != nil {
        return nil, nil, err
    }

    cert, err := x509.ParseCertificate(certDER)
    if err != nil {
        return nil, nil, err
    }

    return cert, privateKey, nil
}

func saveCA(cert *x509.Certificate, key *ecdsa.PrivateKey) error {
    // 証明書をPEM形式で保存
    certFile, err := os.Create("ca.crt")
    if err != nil {
        return err
    }
    defer certFile.Close()

    pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})

    // 秘密鍵をPEM形式で保存
    keyFile, err := os.Create("ca.key")
    if err != nil {
        return err
    }
    defer keyFile.Close()

    keyBytes, _ := x509.MarshalECPrivateKey(key)
    pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})

    return nil
}

サーバー証明書の動的生成

各ドメインごとに、CA証明書で署名したサーバー証明書を動的に生成する。

type CertificateCache struct {
    mu     sync.RWMutex
    certs  map[string]*tls.Certificate
    caCert *x509.Certificate
    caKey  *ecdsa.PrivateKey
}

func NewCertificateCache(caCert *x509.Certificate, caKey *ecdsa.PrivateKey) *CertificateCache {
    return &CertificateCache{
        certs:  make(map[string]*tls.Certificate),
        caCert: caCert,
        caKey:  caKey,
    }
}

func (c *CertificateCache) GetCertificate(host string) (*tls.Certificate, error) {
    // ポート番号を除去
    hostname := strings.Split(host, ":")[0]

    // キャッシュを確認
    c.mu.RLock()
    if cert, ok := c.certs[hostname]; ok {
        c.mu.RUnlock()
        return cert, nil
    }
    c.mu.RUnlock()

    // なければ生成
    cert, err := c.generateCert(hostname)
    if err != nil {
        return nil, err
    }

    // キャッシュに保存
    c.mu.Lock()
    c.certs[hostname] = cert
    c.mu.Unlock()

    return cert, nil
}

func (c *CertificateCache) generateCert(hostname string) (*tls.Certificate, error) {
    // 秘密鍵生成
    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
    if err != nil {
        return nil, err
    }

    serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))

    template := &x509.Certificate{
        SerialNumber: serialNumber,
        Subject: pkix.Name{
            Organization: []string{"My Proxy"},
            CommonName:   hostname,
        },
        NotBefore:   time.Now(),
        NotAfter:    time.Now().AddDate(1, 0, 0), // 1年有効
        KeyUsage:    x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
        ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        DNSNames:    []string{hostname},
    }

    // CA証明書で署名
    certDER, err := x509.CreateCertificate(
        rand.Reader, template, c.caCert, &privateKey.PublicKey, c.caKey)
    if err != nil {
        return nil, err
    }

    cert := &tls.Certificate{
        Certificate: [][]byte{certDER},
        PrivateKey:  privateKey,
    }

    return cert, nil
}

MITMプロキシの実装

package main

import (
    "bufio"
    "crypto/tls"
    "fmt"
    "io"
    "net"
    "strings"
)

type MITMProxy struct {
    listener  net.Listener
    certCache *CertificateCache
}

func NewMITMProxy(addr string, caCert *x509.Certificate, caKey *ecdsa.PrivateKey) (*MITMProxy, error) {
    listener, err := net.Listen("tcp", addr)
    if err != nil {
        return nil, err
    }

    return &MITMProxy{
        listener:  listener,
        certCache: NewCertificateCache(caCert, caKey),
    }, nil
}

func (p *MITMProxy) Start() {
    fmt.Println("MITM Proxy started")
    for {
        conn, err := p.listener.Accept()
        if err != nil {
            continue
        }
        go p.handleConnection(conn)
    }
}

func (p *MITMProxy) handleConnection(clientConn net.Conn) {
    defer clientConn.Close()

    reader := bufio.NewReader(clientConn)
    req, err := parseHTTPRequest(reader)
    if err != nil {
        return
    }

    if req.Method == "CONNECT" {
        // HTTPS: MITMモード
        p.handleMITM(clientConn, req)
    } else {
        // HTTP: 通常転送
        p.handleHTTP(clientConn, reader, req)
    }
}

func (p *MITMProxy) handleMITM(clientConn net.Conn, req *HTTPRequest) {
    hostname := strings.Split(req.Host, ":")[0]
    fmt.Printf("[MITM] Starting for %s\n", hostname)

    // 証明書を取得(キャッシュから or 新規生成)
    cert, err := p.certCache.GetCertificate(hostname)
    if err != nil {
        fmt.Println("Failed to get certificate:", err)
        return
    }

    // クライアントに接続成功を通知
    clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))

    // クライアントとTLSハンドシェイク(偽の証明書で)
    tlsConfig := &tls.Config{
        Certificates: []tls.Certificate{*cert},
    }
    clientTLS := tls.Server(clientConn, tlsConfig)
    if err := clientTLS.Handshake(); err != nil {
        fmt.Println("Client TLS handshake failed:", err)
        return
    }
    defer clientTLS.Close()

    // リクエストを読み込む
    reader := bufio.NewReader(clientTLS)
    innerReq, err := parseHTTPRequest(reader)
    if err != nil {
        fmt.Println("Failed to parse inner request:", err)
        return
    }

    fmt.Printf("[MITM] %s %s%s\n", innerReq.Method, hostname, innerReq.Path)

    // ターゲットサーバーに接続
    serverConn, err := tls.Dial("tcp", req.Host, &tls.Config{
        InsecureSkipVerify: true, // 開発用。本番では適切に検証
    })
    if err != nil {
        fmt.Println("Failed to connect to server:", err)
        return
    }
    defer serverConn.Close()

    // リクエストを転送
    serverConn.Write(innerReq.RawData)

    // レスポンスを読み込んで転送
    io.Copy(clientTLS, serverConn)
}

リクエスト・レスポンスの検査

MITMできたら、通信内容を検査できる。

func (p *MITMProxy) handleMITMWithInspection(clientConn net.Conn, req *HTTPRequest) {
    hostname := strings.Split(req.Host, ":")[0]

    cert, _ := p.certCache.GetCertificate(hostname)
    clientConn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))

    tlsConfig := &tls.Config{Certificates: []tls.Certificate{*cert}}
    clientTLS := tls.Server(clientConn, tlsConfig)
    clientTLS.Handshake()
    defer clientTLS.Close()

    reader := bufio.NewReader(clientTLS)
    innerReq, _ := parseHTTPRequest(reader)

    // 🔍 リクエストの内容を検査
    fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
    fmt.Printf("📤 Request to %s\n", hostname)
    fmt.Printf("   Method: %s\n", innerReq.Method)
    fmt.Printf("   Path: %s\n", innerReq.Path)
    for k, v := range innerReq.Headers {
        fmt.Printf("   %s: %s\n", k, v)
    }

    // サーバーに接続・転送
    serverConn, _ := tls.Dial("tcp", req.Host, &tls.Config{InsecureSkipVerify: true})
    defer serverConn.Close()

    serverConn.Write(innerReq.RawData)

    // レスポンスを読み込み
    serverReader := bufio.NewReader(serverConn)
    resp, respBody := readFullResponse(serverReader)

    // 🔍 レスポンスの内容を検査
    fmt.Printf("📥 Response from %s\n", hostname)
    fmt.Printf("   Status: %d %s\n", resp.StatusCode, resp.StatusText)
    for k, v := range resp.Headers {
        fmt.Printf("   %s: %s\n", k, v)
    }
    if len(respBody) < 500 {
        fmt.Printf("   Body: %s\n", string(respBody))
    }
    fmt.Println("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")

    // クライアントに転送
    clientTLS.Write(resp.RawHeader)
    clientTLS.Write(respBody)
}

リクエストの改変

せっかくMITMできてるので、リクエストを改変することもできる。

func modifyRequest(req *HTTPRequest) *HTTPRequest {
    // User-Agentを変更
    req.Headers["User-Agent"] = "Modified-Agent/1.0"

    // 特定のCookieを削除
    delete(req.Headers, "Cookie")

    // ヘッダを追加
    req.Headers["X-Proxy-By"] = "My-MITM-Proxy"

    // RawDataを再構築
    var buf bytes.Buffer
    fmt.Fprintf(&buf, "%s %s %s\r\n", req.Method, req.Path, req.Version)
    for k, v := range req.Headers {
        fmt.Fprintf(&buf, "%s: %s\r\n", k, v)
    }
    buf.WriteString("\r\n")

    req.RawData = buf.Bytes()
    return req
}

レスポンスの改変

レスポンスも改変できる。広告ブロッカーとかに応用可能。

func modifyResponse(resp *HTTPResponse, body []byte) []byte {
    // HTMLの場合、スクリプトを注入
    contentType := resp.Headers["Content-Type"]
    if strings.Contains(contentType, "text/html") {
        injection := `<script>console.log("Injected by proxy!");</script>`
        bodyStr := string(body)
        bodyStr = strings.Replace(bodyStr, "</body>", injection+"</body>", 1)
        return []byte(bodyStr)
    }
    return body
}

証明書をブラウザに登録

生成したCA証明書をブラウザに登録すると、警告なしでMITMできる。

Windows

# 管理者権限で実行
certutil -addstore "Root" ca.crt

macOS

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca.crt

Linux (Firefox)

# certutil(NSS Tools)が必要
certutil -A -n "My Proxy CA" -t "C,," -i ca.crt -d ~/.mozilla/firefox/*.default/

セキュリティ上の考慮事項

MITMプロキシを使う際の注意点:

項目 説明
法的リスク 他人の通信を傍受するのは違法
HSTS 一度HTTPSで接続したサイトはHTTPにダウングレードできない
証明書ピンニング 一部のアプリは特定の証明書のみを信頼
CT(Certificate Transparency) 証明書の発行がログに記録される仕組み

HSTSとの戦い

HSTSが設定されているサイトは、ブラウザが強制的にHTTPSを使う。

Strict-Transport-Security: max-age=31536000; includeSubDomains

この場合、ユーザーが http:// でアクセスしても、ブラウザが自動で https:// にリダイレクトする。

まとめ

今回学んだこと:

技術 説明
CA証明書 自己署名の認証局を作成
動的証明書生成 ドメインごとに証明書を生成
TLSハンドシェイク クライアント・サーバー両方と
通信の検査 リクエスト・レスポンスの内容確認
通信の改変 ヘッダやボディの書き換え

次回はロードバランサーを実装するよ。複数のバックエンドサーバーに負荷分散させる。

改めて注意
この技術は、自分が管理するネットワーク・端末でのデバッグ目的でのみ使用してください。

この記事が役に立ったら、いいね・ストックしてもらえると嬉しいです!

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?