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でもヒープが欲しい場合はalloccrate を使う。edition 2018 以降でもallocだけはextern crate alloc;の明示が必要。core::alloc::GlobalAlloctrait を実装して#[global_allocator]に登録するとBox/Vec/Stringが使えるようになる。実装例は[[entities/redox-slab-allocator]](slab)や[[concepts/operating-system-kernel]]。core::alloc::Layout(size, power-of-two なalign)が確保要求を表す。std環境ではpreludeでString/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 つの方法:
.textセクションに埋め込む —include_bytes!で取り込むが普通だと.rodataに入るのでトリックが要る。#[link_section = ".text"]で静的配列を配置し、mem::transmute(C++ のreinterpret_cast相当)で関数ポインタに再解釈して呼ぶ。配列長はconst fnであるバイト列の.len()で取る。mmapで実行可能領域を確保して書き込む。mprotectで既存領域を実行可能に変える。
Reverse TCP シェルコード
被害側から攻撃側へシェルを差し出す Reverse Shell。攻撃側は nc -vnlp 1234 で listen、被害側は socket→connect→dup2(fd を 0/1/2 に複製)→execve("/bin/sh") を syscall で組む。ファイアウォール越えに有利なため実戦で多用される。
関連
- black-hat-rust — 出典
- elf-format / binary-exploitation / cpu-architecture-isa
- rat-c2-architecture — シェルコードを運ぶ上位の攻撃基盤