knqyf263's blog

自分のためのメモとして残しておくためのブログ。

cargo-auditableで依存するcrateをバイナリに埋め込む

概要

cargo-auditable というものがあるというのをGitHubで教えてもらいました。 github.com

これを使ってRustプロジェクトをビルドすると依存するcrateの情報をバイナリに埋め込むことが出来ます。ちなみにGoでは既に以前からやっており、詳細はこちらのブログに書いています。

knqyf263.hatenablog.com

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-auditablerust-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,
    );

https://github.com/rust-secure-code/cargo-auditable/blob/b1764c9f038cdd3d4a74837f39ab3ebb0e77ca5e/cargo-auditable/src/object_file.rs#L22-L26

ということで 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が作れず困っていると伝えたら足してくれることになりました。

github.com

余談

この情報があると何が嬉しいか?というとバイナリだけあれば脆弱性スキャンが出来ることです。あとはバグが見つかった場合に影響するかなどもバイナリだけあれば簡単に調査出来ます。 現在自分の開発しているTrivyというOSSでは既にGoのバイナリスキャンには対応しており、バイナリだけ渡されれば依存を勝手に取得してスキャンできます。特にコンテナイメージ内に go.modgo.sum を置くのは一般的ではないため、バイナリだけでスキャンできるというのは有用でした。

knqyf263.hatenablog.com

同様にRustも Cargo.tomlCargo.lock をイメージ内に置くのはレアケースであるためバイナリに依存ライブラリの情報を埋め込んでくれると助かります。Goはデフォルトで埋め込んでくれるのでRustも早くそうなってほしいです。

ちなみに cargo-auditableMicrosoftの人から教えてもらいました。教えてもらうどころか対応するPRまで貰いました。

github.com

Azure Defender for Cloudの中で使っているため、Microsoftの方々は割と頻繁にPRをくれます。

docs.microsoft.com

以前書いたようにGoバイナリに関する情報もGitHub上で教えてもらいましたし、何も知らないのにコミュニティの有志に支えてもらって何とかやれているなと感じる日々です。このブログでも何度も言っていることではありますが、こういう助け合いがOSSの楽しいところだなと思います。

最近話題のSBOMについてもCycloneDXの対応してよ、と3年前には来てました(当然自分は知らなかったのでIssueきっかけで調べた)。

github.com

あと全然関係ないですが、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."

stackoverflow.com

--dump-section なら動きましたがこの辺詳しい解説など見つけられなかったので詳細が気になっています。以下を読んでいましたが、本当にここが該当箇所なのか...?となった上にさっさとRustバイナリスキャンを実装しよう、となって放置しました。

sourceware.org Git - binutils-gdb.git/blob - bfd/binary.c

一応修正のPRを出したらマージされました。

github.com

まとめ

全Rustユーザに cargo auditable を使って欲しい。