OS やベアメタル環境では OS が提供する機能(ファイル, スレッド, ヒープ)が存在しないため、標準ライブラリ std に依存しない フリースタンディング (freestanding) な Rust バイナリ を構築する必要がある。[[concepts/blog-os-rust-kernel]][[entities/redox-slab-allocator]] の前提となる土台。

no_std と最小ランタイム

  • #![no_std]: std を切り、core (+ 必要なら alloc) のみで書く。core は OS 非依存のプリミティブ群。
  • #[panic_handler]: パニック時に呼ばれる関数を自前実装する必要がある(std が提供していたものが消えるため)。
  • language item: コンパイラ内部が必要とする関数・型(eh_personality 等)。これらも自前で潰すか無効化する。
  • スタックアンワインド(全スタック変数のデストラクタ実行)は OS 機能を要するため無効化し、panic = "abort" に設定する。
  • エントリポイントは #[no_mangle] extern "C" fn _start() -> !no_mangle でシンボル名のマングリングを抑止し、extern "C" で C 呼び出し規約を使う。[[concepts/elf-format]] のリンカが期待する _start を提供する。

カスタムターゲット

  • target triple <arch>-<vendor>-<os>-<abi> で対象を指定。OS が無い環境用に独自ターゲットを JSON で定義する(--target custom.json)。
  • 重要フラグ:
    • disable-redzone: redzone(スタックポインタ最適化で確保される128バイト)を無効化し、割り込みでスタックが破損しないようにする。[[concepts/operating-system-kernel]] に必須。
    • features: +/- で命令拡張を制御。SIMD はレジスタ退避コストが高いので無効化、soft-float で浮動小数演算を整数演算でエミュレートする。
  • core をカスタムターゲット向けにビルドし直すため、[unstable] build-std = ["core", "compiler_builtins"] を指定(nightly)。

alloc のための extern crate

  • no_std でもヒープが欲しい場合は alloc crate を使う。edition 2018 以降でも alloc だけは extern crate alloc; の明示が必要。
  • core::alloc::GlobalAlloc trait を実装して #[global_allocator] に登録すると Box/Vec/String が使えるようになる。実装例は [[entities/redox-slab-allocator]](slab)や [[concepts/operating-system-kernel]]
  • core::alloc::Layout(size, power-of-two な align)が確保要求を表す。
  • std 環境では preludeString/Vec が自動 use されるが no_std + alloc では prelude が無いため、alloc::string::String 等を明示 use する。

関連

  • ブートまわりの実装ツールは [[entities/rust-osdev-tooling]]
  • 確保アルゴリズムは [[concepts/operating-system-kernel]] / [[concepts/malloc-allocator-internals]]
  • 所有権・unsafe は [[concepts/rust-ownership-model]]

Rust によるシェルコード開発 (no_std)

攻撃ペイロードとして実行する位置独立な機械語列(シェルコード)を Rust で書く手法。no_std で標準ライブラリを切り離し、極小バイナリを生成して実行する。Black Hat Rust Ch.12 の内容。_moc-systems

コンパイルパイプライン

Rust のコンパイルは AST → HIR (High-level IR) → MIR (Mid-level IR) → 型情報付き MIR →(最適化・codegen)→ オブジェクトファイル →(リンク)→ バイナリ。最終的に ELF 実行ファイルになる(.text=コード, .rodata=定数, .data=データ)。

極小バイナリ (#![no_std])

標準ライブラリ(OS 依存)を外し、core だけで書く。nightly 機能が要る。

#![no_std]
#![no_main]
#![feature(start)]
 
#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize { 0 }
 
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! { loop {} }
  • never!: 到達しない型(exit() の戻り値等)。panic_handler-> ! に使う。
  • サイズ最適化: [profile.release] opt-level = "z" + LTO(オブジェクトを跨いだリンク時最適化)。
  • インライン asm (#![feature(asm)]) で直接 syscall を書ける(write = syscall 1/4, connect, dup2 等)。

アセンブリ確認のツールチェーン

  • cargo +nightly rustc --release -- --emit asm で生成 asm を確認。
  • objdump -D -b binary -mi386 -Mx86-64 -Mintel hoge.bin で生バイナリを逆アセンブル。
  • objcopy -O binary でセクションを抜き出し shellcode.bin 化、strip でシンボル除去、nm でシンボル確認、hexyl でバイト列確認。
  • lea(実効アドレス計算、位置独立に必須)、push/syscall 等を組む。文字列(/bin/sh)は .rodata ではなくコード中に埋める。

シェルコードの実行方法

メモリ上の命令を実行する 3 つの方法:

  1. .text セクションに埋め込むinclude_bytes! で取り込むが普通だと .rodata に入るのでトリックが要る。#[link_section = ".text"] で静的配列を配置し、mem::transmute(C++ の reinterpret_cast 相当)で関数ポインタに再解釈して呼ぶ。配列長は const fn であるバイト列の .len() で取る。
  2. mmap で実行可能領域を確保して書き込む。
  3. mprotect で既存領域を実行可能に変える。

Reverse TCP シェルコード

被害側から攻撃側へシェルを差し出す Reverse Shell。攻撃側は nc -vnlp 1234 で listen、被害側は socketconnectdup2(fd を 0/1/2 に複製)→execve("/bin/sh") を syscall で組む。ファイアウォール越えに有利なため実戦で多用される。

関連