cargo-auditableで依存するcrateをバイナリに埋め込む
概要
cargo-auditable
というものがあるというのをGitHubで教えてもらいました。
github.com
これを使ってRustプロジェクトをビルドすると依存するcrateの情報をバイナリに埋め込むことが出来ます。ちなみにGoでは既に以前からやっており、詳細はこちらのブログに書いています。
cargo-auditable
の使い方はREADMEをそのままパクると以下のようになっています。
# Install the tooling cargo install cargo-auditable rust-audit-info # Open your Rust project cd your_project # Build your project with dependency lists embedded in the binaries cargo auditable build --release # Recover the dependency info from the compiled binary rust-audit-info target/release/your-project
cargo auditable
で埋め込み、 rust-audit-info
で情報を表示できます。まだCargo本体には入っていないようですが、既にGitHub上で提案はされています。ただ議論は長引いているのでどうなるかは分かりません。脆弱性スキャナの開発者としてはぜひ取り込まれて欲しいと思っています。
github.com
また、フォーマットもまだ定まっていないようなので今後変更される可能性があります。
詳細
では内部がどうなっているかを見ていきます。自分の携わるプロジェクトはGoで開発しているため上の rust-audit-info
をimportして使うことは出来ず、内部挙動を理解してGoで再実装する必要があります(実際にはMicrosoftがライブラリを提供してくれていたのでそれを使った)。
事前準備
Linuxでテストしたいので適当にコンテナを立ち上げます。
$ docker run --rm --name rust rust:1.62-buster bash
上の手順で cargo-auditable
と rust-audit-info
をインストールします。
root@09c9e76c6781:/# cargo install cargo-auditable rust-audit-info Updating crates.io index Downloaded cargo-auditable v0.4.0 ... Finished release [optimized] target(s) in 1m 06s Installing /usr/local/cargo/bin/rust-audit-info Installed package `rust-audit-info v0.4.0` (executable `rust-audit-info`) Summary Successfully installed cargo-auditable, rust-audit-info!
プロジェクトのビルド
次に適当なプロジェクトを cargo-auditable
でビルドします。今回はRustで書かれたlsであるexaをビルドしました。
root@09c9e76c6781:/# git clone https://github.com/ogham/exa root@09c9e76c6781:/# cd exa root@09c9e76c6781:/app# cargo auditable build --release info: syncing channel updates for '1.56.1-x86_64-unknown-linux-gnu' info: latest update on 2021-11-01, rust version 1.56.1 (59eed8a2a 2021-11-01) info: downloading component 'cargo' info: downloading component 'rust-std' info: downloading component 'rustc' info: installing component 'cargo' info: installing component 'rust-std' info: installing component 'rustc' Downloaded datetime v0.5.2 ... Compiling git2 v0.13.20 Finished release [optimized] target(s) in 3m 08s
.dep-v0 section
このバイナリのどこに保存されているのかという話ですが、 rust-auditable
のドキュメントには以下のように書かれています。
It is not yet stabilized, so we do not have extensive docs or a JSON schema. However, these Rust data structures map to JSON one-to-one and are extensively commented. The JSON is Zlib-compressed and placed in a linker section named .dep-v0.
.dep-v0
というセクションにZlibで圧縮されて保存されているようです。
コードを見ると確かにそんな雰囲気です。
let section = file.add_section( file.segment_name(StandardSegment::Data).to_vec(), b".dep-v0".to_vec(), SectionKind::ReadOnlyData, );
ということで readelf
でセクションヘッダを見てみます。
root@09c9e76c6781:/app# readelf -S target/release/exa There are 41 section headers, starting at offset 0x306aa8: Section Headers: [Nr] Name Type Address Offset Size EntSize Flags Link Info Align [ 0] NULL 0000000000000000 00000000 0000000000000000 0000000000000000 0 0 0 [ 1] .interp PROGBITS 00000000000002e0 000002e0 000000000000001c 0000000000000000 A 0 0 1 [ 2] .note.ABI-tag NOTE 00000000000002fc 000002fc 0000000000000020 0000000000000000 A 0 0 4 [ 3] .note.gnu.build-i NOTE 000000000000031c 0000031c 0000000000000024 0000000000000000 A 0 0 4 ... [26] .data PROGBITS 000000000015f000 0015e000 00000000000038b8 0000000000000000 WA 0 0 32 [27] .bss NOBITS 00000000001628c0 001618b8 00000000000007a8 0000000000000000 WA 0 0 32 [28] .comment PROGBITS 0000000000000000 001618b8 000000000000001c 0000000000000001 MS 0 0 1 [29] .dep-v0 PROGBITS 0000000000000000 001618d4 0000000000000360 0000000000000000 0 0 1 ...
長いので省略しましたが、 [29] に dep-v0
があることが確認できます。ということでdumpしてみます。
root@09c9e76c6781:/app# objcopy --dump-section .dep-v0=dep target/release/exa
解凍
dep
というファイルにdumsしましたが、zlibで圧縮されているため解凍するために適当なコマンド(今回は zlib-flate
)を入れます。
root@513914fa49f6:/app# apt update -y && apt -y install qpdf
そして解凍します。
root@513914fa49f6:/app# zlib-flate -uncompress < dep {"packages":[{"name":"ansi_term","version":"0.12.1","source":"crates.io","dependencies":[38]},{"name":"bitflags","version":"1.2.1","source":"crates.io","features":["default"]},{"name":"byteorder","version":"1.4.3","source":"crates.io","features":["default","std"]},{"name":"cc","version":"1.0.67","source":"crates.io","kind":"build","dependencies":[12],"features":["jobserver","parallel"]},{"name":"cfg-if","version":"1.0.0","source":"crates.io"},{"name":"datetime","version":"0.5.2","source":"crates.io","dependencies":[14,17,23,26,38],"features":["format","locale","pad"]},{"name":"exa","version":"0.10.1","source":"local","dependencies":[0,5,8,9,13,14,17,18,20,21,22,27,28,29,34,36,41],"features":["default","git","git2"]},{"name":"form_urlencoded","version":"1.0.1","source":"crates.io","dependencies":[19,24]},{"name":"git2","version":"0.13.20","source":"crates.io","dependencies":[1,14,15,18,35]},{"name":"glob","version":"0.3.0","source":"crates.io"},{"name":"hermit-abi","version":"0.1.18","source":"crates.io","dependencies":[14],"features":["default"]},{"name":"idna","version":"0.2.3","source":"crates.io","dependencies":[19,32,33]},{"name":"jobserver","version":"0.1.22","source":"crates.io","kind":"build","dependencies":[14]},{"name":"lazy_static","version":"1.4.0","source":"crates.io"},{"name":"libc","version":"0.2.93","source":"crates.io","features":["default","std"]},{"name":"libgit2-sys","version":"0.12.21+1.1.0","source":"crates.io","dependencies":[3,14,16,25]},{"name":"libz-sys","version":"1.1.2","source":"crates.io","dependencies":[3,14,25,37],"features":["libc"]},{"name":"locale","version":"0.2.2","source":"crates.io","dependencies":[14]},{"name":"log","version":"0.4.14","source":"crates.io","dependencies":[4]},{"name":"matches","version":"0.1.8","source":"crates.io"},{"name":"natord","version":"1.0.9","source":"crates.io"},{"name":"num_cpus","version":"1.13.0","source":"crates.io","dependencies":[10,14]},{"name":"number_prefix","version":"0.4.0","source":"crates.io","features":["default","std"]},{"name":"pad","version":"0.1.6","source":"crates.io","dependencies":[34]},{"name":"percent-encoding","version":"2.1.0","source":"crates.io"},{"name":"pkg-config","version":"0.3.19","source":"crates.io","kind":"build"},{"name":"redox_syscall","version":"0.1.57","source":"crates.io"},{"name":"scoped_threadpool","version":"0.1.9","source":"crates.io"},{"name":"term_grid","version":"0.2.0","source":"crates.io","dependencies":[34]},{"name":"terminal_size","version":"0.1.16","source":"crates.io","dependencies":[14,38]},{"name":"tinyvec","version":"1.2.0","source":"crates.io","dependencies":[31],"features":["alloc","default","tinyvec_macros"]},{"name":"tinyvec_macros","version":"0.1.0","source":"crates.io"},{"name":"unicode-bidi","version":"0.3.5","source":"crates.io","dependencies":[19],"features":["default"]},{"name":"unicode-normalization","version":"0.1.17","source":"crates.io","dependencies":[30],"features":["default","std"]},{"name":"unicode-width","version":"0.1.8","source":"crates.io","features":["default"]},{"name":"url","version":"2.2.1","source":"crates.io","dependencies":[7,11,19,24]},{"name":"users","version":"0.11.0","source":"crates.io","dependencies":[14,18],"features":["cache","default","log","logging","mock"]},{"name":"vcpkg","version":"0.2.12","source":"crates.io","kind":"build"},{"name":"winapi","version":"0.3.9","source":"crates.io","dependencies":[39,40],"features":["consoleapi","errhandlingapi","fileapi","handleapi","minwindef","processenv","sysinfoapi","winbase","wincon","winnt"]},{"name":"winapi-i686-pc-windows-gnu","version":"0.4.0","source":"crates.io"},{"name":"winapi-x86_64-pc-windows-gnu","version":"0.4.0","source":"crates.io"},{"name":"zoneinfo_compiled","version":"0.5.1","source":"crates.io","dependencies":[2,5]}]}
ということで取り出せました。GoはビルドしたGoのバージョンも入れるためにポインタを保持していたのでパースが若干面倒でしたが、Rustの方は依存crateだけなのでかなりシンプルですね。Goはgo.sumのような形式で入っていましたがRustはJSONなのでパースも簡単です。
そして特に嬉しいのが "source": "local"
が含まれており、そのバイナリのバージョンも分かる点です。 exa
の例だとバージョンが0.10.1であることが分かります。
{ "name": "exa", "version": "0.10.1", "source": "local", "dependencies": [ 0, 5, 8, 9, 13, 14, 17, 18, 20, 21, 22, 27, 28, 29, 34, 36, 41 ], "features": [ "default", "git", "git2" ] },
Goでは現在これが出来ません。例えばGrafanaのバイナリを見つけてもGrafanaのバージョンは分からず、Grafanaの依存しているライブラリのバージョンだけ取れるという状態です。
あと個人的には dependencies
が入っているのが好きです。ちゃんと仕様を見ていないですが見た感じではこのリストにおけるindexが保存されており、dependency graphが作れそうです。ただ現在はrootがどれか分からないのでうまくgraphが作れず困っていると伝えたら足してくれることになりました。
余談
この情報があると何が嬉しいか?というとバイナリだけあれば脆弱性スキャンが出来ることです。あとはバグが見つかった場合に影響するかなどもバイナリだけあれば簡単に調査出来ます。
現在自分の開発しているTrivyというOSSでは既にGoのバイナリスキャンには対応しており、バイナリだけ渡されれば依存を勝手に取得してスキャンできます。特にコンテナイメージ内に go.mod
や go.sum
を置くのは一般的ではないため、バイナリだけでスキャンできるというのは有用でした。
同様にRustも Cargo.toml
や Cargo.lock
をイメージ内に置くのはレアケースであるためバイナリに依存ライブラリの情報を埋め込んでくれると助かります。Goはデフォルトで埋め込んでくれるのでRustも早くそうなってほしいです。
ちなみに cargo-auditable
はMicrosoftの人から教えてもらいました。教えてもらうどころか対応するPRまで貰いました。
Azure Defender for Cloudの中で使っているため、Microsoftの方々は割と頻繁にPRをくれます。
以前書いたようにGoバイナリに関する情報もGitHub上で教えてもらいましたし、何も知らないのにコミュニティの有志に支えてもらって何とかやれているなと感じる日々です。このブログでも何度も言っていることではありますが、こういう助け合いがOSSの楽しいところだなと思います。
最近話題のSBOMについてもCycloneDXの対応してよ、と3年前には来てました(当然自分は知らなかったのでIssueきっかけで調べた)。
あと全然関係ないですが、READMEに書いてある objcopy -O binary --only-section=.dep-v0 target/release/hello-auditable /dev/stdout
だとdump出来なかったです。
困ったときのStack Overflow先生によると以下のようにセクションに特定のフラグが立ってない場合は無視されるようです。
objcopy will not copy out sections that are flagged neither loaded ("load") nor allocated ("alloc"). A comment in the source claims that "The contents of such a section are not meaningful in the binary format."
--dump-section
なら動きましたがこの辺詳しい解説など見つけられなかったので詳細が気になっています。以下を読んでいましたが、本当にここが該当箇所なのか...?となった上にさっさとRustバイナリスキャンを実装しよう、となって放置しました。
sourceware.org Git - binutils-gdb.git/blob - bfd/binary.c
一応修正のPRを出したらマージされました。
まとめ
全Rustユーザに cargo auditable
を使って欲しい。