# RustのSerdeクレートを使ったシリアライズ・デシリアライズ Rustの構造化されたデータをシリアライズしたり, デシリアライズする際に使用される代表的なクレートとして`Serde`がある. `pwn`でよくあるのはファイルを読み書きしたり, ネットワーク経由でデータをやり取りしようとして, デシリアライズする際に本来意図しない型として値を読み込ませることで, + 範囲外書き込みや, 任意のメモリの読み書き(`unsafe`ブロック内で発火) + 任意のファイルへの読み書き. + `File::Seek`と合わせて`/proc/self/mem`経由で, 任意アドレスへの読み書き + パストラバーサルから, `flag`を不正に抽出. につなげるパターンが存在する. またSerdeのデシリアライズがRoot Causeなバグではないにしても, 問題の中でこれらのクレートを使ってくる可能性はあるため, デバッグ効率化のためにも基本的なデータ型のシリアライズについては知っておいたほうがよい. :::info ちなみに, `SERDE`とは, `SERialize` + `DEserialize`の意味らしい. ::: `Serde`では, Rustが扱う(プログラマが独自に定義した構造体も含めて)幅広い型を(デ)シリアライズできるのだが, 実態はプリミティブな型のメンバをそれぞれシリアライズして`cat`しているだけである. そのためRustが標準で提供しているプリミティブな型については, 予めメモしておく. ###### tags: `pwn`, `Rust`, `misc` ## 実験環境 検証に使用した環境は以下の通り. ```shell= root@Ubu2204x64:now# uname -a Linux Ubu2204x64 5.19.0-32-generic #33~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 30 17:03:34 UTC 2 x86_64 x86_64 x86_64 GNU/Linux root@Ubu2204x64:now# cargo version -v cargo 1.69.0-nightly (39c13e67a 2023-02-12) release: 1.69.0-nightly commit-hash: 39c13e67a5962466cc7253d41bc1099bbcb224c3 commit-date: 2023-02-12 host: x86_64-unknown-linux-gnu libgit2: 1.5.0 (sys:0.16.0 vendored) libcurl: 7.86.0-DEV (sys:0.4.59+curl-7.86.0 vendored ssl:OpenSSL/1.1.1q) os: Ubuntu 22.04 (jammy) [64-bit] root@Ubu2204x64:now# ``` ### 実験用コードを作成する `cargo new <プロジェクト>`から`bin`クレートを作成する. > `<プロジェクト>`は適宜変更すること. 作成したら`Cargo.toml`ファイルの, `[dependencies]`以下を修正しておく. また, 以下のようなテンプレートから`main.rs`を作成すると良い. ```rust= /* serdeでシリアライズ/デシリアライズするためのtemplateコード. Cargo.tomlの[dependencies]配下に以下のコードを記述. ~ snipped ~ [dependencies] bincode = "1.3.3" # バージョンはだいたい1.3xを使ってる場合が多い気がする. serde = { version = "^1.0.63", features = ["derive"] } */ use std::fs::File; //use serde::{Deserialize, Serialize}; const DUMP_TO : &'static str = "./serialized"; fn main() { // シリアライズするオブジェクトを用意. // ファイルに書き出すだけならimmutableでよい let obj : i8 = 0x12; // Cの`FILE *fp = fopen(filename "rw");`に相当する. (writeする内容によって, "b"が付与される) // ただしRustの場合は, 明確にcreateオプションを付けないとファイルを作成できない. // また`create`オプションを付けておくと, // ファイルが存在しない場合は作成する. // すでにファイルが存在する場合, 作成し直さず上書きする. let f_res = File::options() .read(true) .write(true) .create(true) .open(DUMP_TO); match f_res { Ok(f) => { // オブジェクトを書き出す処理.(serialize_intoはResultを返す.) match bincode::serialize_into(&f, &obj) { Ok(_) => { println!("Success serialized {:?}", obj); }, Err(_) => { panic!("Failed to dump object"); } } }, Err(e) => { // そもそもファイルに書き出せなかった場合. eprintln!("Failed to serialize_into {} {}", DUMP_TO, e) } } // hexdump -vC ./<DUMP_TO>でダンプ } ``` ## 整数(`[iu]8`,`[iu]16`,`[iu]32`,`[iu]64`,`[iu]128`/`[iu]size`) マルチバイトの場合, すべてCPUのエンディアンでバイト変換される.(x86/x64の場合はリトルエンディアン) + `i8`/`u8` ```rust // i8, u8はそのままバイトダンプする. // i8: (Pythonで)`struct.pack('b', x)` // u8: (Pythonで)`struct.pack('B', x)` let obj: i8 = 0x00; // => \x00 let obj: i8 = 0x12; // => \x12 let obj: i8 = -1; // => \xff let obj: i8 = 0x80; // => コンパイルエラー(i8: -0x7f ~ 0x7f) let obj: u8 = 0x00; // => \x00 let obj: u8 = 0xff; // => \xff let obj: u8 = 0xde; // => \xde ``` + `i16`/`u16` ```rust! // i16 : (Pythonで)`struct.pack('<h', x)` let obj: i16 = -0x7fff; // => \x01\x80 let obj: i16 = 0x7fff; // => \xff\x7f let obj: i16 = -1; // => \xff\xff let obj: i16 = 0x8000; // => コンパイルエラー(i16: -0x7fff ~ 0x7fff) // u16 : (Pythonで)`struct.pack('<H', x)` let obj: u16 = 0x0000; // => \x00\x00 let obj: u16 = 0x1234; // => \x34\x12 (リトルエンディアン) let obj: u16 = 0xdead; // => \xad\xde (リトルエンディアン) let obj: u16 = -1; // => コンパイルエラー(u16: 0 ~ 0xffff) ``` + `i32`/`u32` ```rust! // i32: (Pythonで)`struct.pack('<i', x)` let obj: i32 = -1; // \xff\xff\xff\xff let obj: i32 = 0x7fff_ffff; // \xff\xff\xff\x7f let obj: i32 = -0x7fff_ffff; // \x01\x00\x00\x80 let obj: i32 = 0x8000_0000; // => コンパイルエラー // u32: (Pythonで)`struct.pack('<I', x)` let ojb: u32 = 0; // \x00\x00\x00\x00 let obj: u32 = 0xdeadbeef; // \xef\xbe\xad\xde let obj: u32 = 0x80000000; // \x00\x00\x00\x80 ``` + `i64`/`u64` ```rust! // i64: (Pythonで)`struct.pack('<q', x)` let obj: i64 = 0x0deadbeefcafebab; // \xab\xeb\xaf\xfc\xee\xdb\xea\x0d // u64: (Pythonで)`struct.pack('<Q',x)` let obj: u64 = 0xdeadbeefcafebabe; // \xbe\xba\xfe\xca\xef\xbe\xad\xde ``` + `i128`/`i128` ```rust! // i128: (Pythonで) // `pack('<q', x&0xffffffffffffffff) + pack('<q', (x>>64)&0xffff_ffff_ffff_ffff) let x: i128 = 0x011223344556677_8899aabbccddeef; //→ ef de cd bc ab 9a 89 78 67 56 45 34 23 12 01 00 (16バイトをまるっとエンディアン変換する) // u128: (Pythonで) // `pack('<Q', x&0xffffffffffffffff) + pack('<Q', (x>>64)&0xffff_ffff_ffff_ffff) let x: u128 = 0x11223344556677_8899aabbccddeeff00; //→ 00 ff ee dd cc bb aa 99 88 77 66 55 44 33 22 11 (16バイトをまるっと変換する) ``` ## `bool`(`true`/`false`) Boolean型の値は1バイト表現でシリアライズされる. ```rust! let obj: boolean = true; // \1 : 1バイト let obj: boolean = false; // \0 : 1バイト ``` ## 浮動小数(`f32`,`f64`,`f128`) ```rust! // f32: (Pythonで)`struct.pack('<f', x)` let obj: f32 = 1.00; // \x00\x00\x80\x3f let obj: f32 = 1.10; // \xcd\xcc\x8c\x3f // f64: (Pythonで)`struct.pack('<d', x)` let obj: f64 = 1.00; // \x00\x00\x00\x00\x00\x00\xf0\x3f let obj: f64 = 1.10; // \x9a\x99\x99\x99\x99\x99\xf1\x3f let obj: f64 = 1.20; // \x33\x33\x33\x33\x33\x33\xf3\x3f ``` 標準では, `f128`はデータ型に存在しないのだが, サードパーティクレートを使うと行けるらしいので, 一緒に記載しておく. ## `char` ASCIIの場合1文字,マルチバイトの場合はUnicodeでシリアライズ. ```rust! let obj: char = -1; // (i8ではないので)コンパイルエラー let obj: char = 'A'; // \x41 let obj: char = '\x41'; // \x41 let obj: char = '\0'; // \x00 (NULL文字もOK) let obj: char = '\x80'; // コンパイルエラー. let obj: char = '日'; // \xe6\x97\xa5(マルチバイト文字はUnicode表現) ``` ## `&str`/`String` 文字列の場合は, 文字列のバイトサイズが`usize`で先頭に付与される. :::warning 【文字数】ではないことに注意. ::: コード中では明白に区別される`&str`/`String`だが, serializeされたら区別されない. ```rust! let obj : &str = "THIS IS TEST MESSAGE"; // 20文字. let obj : String = String::from("THIS IS TEST MESSAGE"); // &strと同じ様にシリアライズされる. let obj : &str = "THIS\0IS\0TEST\0MESSAGE"// NULL文字も入れられる. let obj : &str = "THIS\nIS\nTEST\nMESSAGE"// 改行もOK ``` また, serialize時にはアライメントされないため注意. > gccの`__attribute__((packed))`してるイメージ. ``` | バイトサイズ(QWORD) | NULL文字や改行もサイズに含まれる. | str[0x00:0x08](QWORD) | | str[0x08:0x10](QWORD) | : : | str[n:-1] | <- アライメント用のパディングや終端を示すNULLは付与されないことに注意. ``` ```shell root@Ubu2204x64:serde_poc# hexdump -vC ./serialized 00000000 14 00 00 00 00 00 00 00 54 48 49 53 20 49 53 20 |........THIS IS | 最初の8バイトはバイト数を`pack('<Q', x)`したもの. 00000010 54 45 53 54 20 4d 45 53 53 41 47 45 |TEST MESSAGE| 0000001c root@Ubu2204x64:serde_poc# ``` マルチバイトも同じ様にシリアライズされる. ```rust! let obj : &str = "日本語の文字列"; // 7文字だがUTF-8で21バイト. ``` ```shell= root@Ubu2204x64:serde_poc# hexdump -vC ./serialized 00000000 15 00 00 00 00 00 00 00 e6 97 a5 e6 9c ac e8 aa |................| 00000010 9e e3 81 ae e6 96 87 e5 ad 97 e5 88 97 |.............| ★アライメントされてない. 0000001d root@Ubu2204x64:serde_poc# ``` ## 配列 配列の場合, 各要素をserializeしたバイト列をcatするだけ. ```rust! // u32の配列:(Pythonで) b''.join(map(lambda x: struct.pack('<I', x), arr)) 相当. let arr: [u32, 2] = [0xdeadbeef, 0xcafebabe]; ``` ### 多次元配列 Matrixのような, 多次元の配列は, 一次元としてSerializeされる. ```rust! let mut obj : [[u8; 4]; 4] = [ [ 0x00, 0x11, 0x22, 0x33 ], [ 0x44, 0x55, 0x66, 0x77 ], [ 0x88, 0x99, 0xaa, 0xbb ], [ 0xcc, 0xdd, 0xee, 0xff ] ]; // → \x00\x11\x22\x33\x44\x55\x66\x77\x88\x99\xaa\xbb\xcc\xdd\xee\xff ``` ## タプル 配列と同じ. Serialize後は区別されない. ## `Vec` `Vec`をシリアライズする際は, 先頭`QWORD`がサイズとして使用され, 以降にその要素をシリアライズしたバイトを連結していく. ``` Vec<T>をシリアライズした時のバイトパターン |<--- QWORD ---> | | usize | ← 要素の数(全体のバイト数ではない) | elems... | <-- 各要素はLittle Endian ``` ```rust! let obj: Vec<u32> = vec![0xdeadbeef, 0xcafebabe]; //→ 02 00 00 00 00 00 00 00 ef be ad de be ba fe ca // ↑リトルエンディアン8バイトで 2(= obj.len()) // (Pythonで書くと) // pack('<Q', len(obj)) + b''.join(map(lambda x: pack('<I', x), obj)) ``` また, デシリアライズするバイトが正確に`Vec`の型にunpackできない場合の挙動についても書いておく. :::spoiler 検証コード ```rust! use std::fs::File; fn main() { // echo -ne "\x..." > ./serialized let file_name : &str = "./serialized"; if let Ok(mut f) = File::options().read(true).write(true).open(file_name) { println!("[+] Success : File opened"); let res = bincode::deserialize_from(&mut f); match res { Ok(x) => { // 一旦, u32としてunpackする. // シグネチャとして記述されているsize < 9バイト目以降バイト数/sizeof(u32=4). になってないとだめ. let length = obj.len(); for (i, e) in obj.iter().enumerate() { if i == length - 1 { print!("0x{:x} ", e); } else { print!("0x{:x}, ",e); } } }, Err(_) => { eprintln!("[+] Failed to deserialize file.."); } } } else { eprintln!("Failed to open file {}", file_name); } } ``` ::: 一旦,`Vec<u32>`でDeserializeする. シリアライズしてファイルにダンプする処理は書いていないため, 予め, デシリアライズ対象のバイナリファイルを作成しておく. ```shell= ## Vec<u32> でunpackできるバイナリファイル. echo -ne '\x0f\x00\x00\x00' > ./serialized # 要素数15(Low) echo -ne '\x00\x00\x00\x00' >> ./serialized # 要素数15(High) echo -ne '\x11\x22\x33\x44' >> ./serialized # Vec[0](u32) echo -ne '\x55\x66\x77\x88' >> ./serialized # Vec[1](u32) echo -ne '\x99\xaa\xbb\xcc' >> ./serialized # Vec[2](u32) echo -ne '\xdd\xee\xff\x00' >> ./serialized # Vec[3](u32) ``` これは, シグネチャの要素数(`0x0f`)に対して, 以降のバイトが`480バイト= sizeof(u32) * 0xf`もないため, デシリアライズできない. ```shell= [root@kali2104x64]vec_ex1# cargo run -r Finished release [optimized] target(s) in 0.01s Running `target/release/vec_ex1` [+] Success : File opened [+] Failed to deserialize file.. [root@kali2104x64]vec_ex1# ``` だが, このシグネチャの要素数を`0x03`に編集すればデシリアライズできる. ```shell= [root@kali2104x64]vec_ex1# cargo run -r Finished release [optimized] target(s) in 0.01s Running `target/release/vec_ex1` [+] Success : File opened [+] unpacked : [ 0x44332211, 0x88776655, 0xccbbaa99 ] [root@kali2104x64]vec_ex1# ``` このファイルに対して, 上記の検証コードを動かすとデシリアライズに成功する. ```shell= [+] Success : File opened [+] unpacked : [ 0xd0c0b0a, 0x11100f0e ] ``` ### 空配列 シリアライズされたファイルの先頭のバイトは要素の型に関わらず`usize`(バイト数ではなく, 要素数)であるため, 64bit CPUであれば常に, 8バイトの`\0`が書き込まれる. 空の`Vec`をシリアライズすると, 要素の型に関わらず, サイズ0を示すためのシグネチャが生成される. ```rust! // `Vec::new()`/`vec![]`どっちで作っても一緒 let v_u8: Vec<u8> = Vec::new(); => \0 * 8 let v_u16: Vec<u16> = Vec::new(); => \0 * 8 let v_u32: Vec<u32> = Vec::new(); => \0 * 8 let v_u64: Vec<u32> = Vec::new(); => \0 * 8 ``` ## `VecDeque` `Vec`と同じ. Serialize後は区別されない. ## `HashMap` `HashMap<K, V>`では, キー`K`として使用できるデータは`Eq`/`Hash`トレイトを実装している必要がある. Rust標準のデータ型でこれらのトレイトを実装しているのは, + `bool` + 整数(`[iu]8`,`[iu]16`,`[iu]32`,`[iu]64`,`[iu]128`/`[iu]size`) + `String`と`&str` :::warning `f32`と`f64`は`Hash`トレイトを実装して **いない** ことに注意. https://doc.rust-jp.rs/rust-by-example-ja/std/hash/alt_key_types.html ::: HashMapのシリアライズは, ``` | K,Vの組み合わせの数(QWORD) | +-------------------------+ | K_0のシリアライズ表現 | <-+ --K_i, V_iで一つの組み合わせ | V_0のシリアライズ表現 | <-+ +-------------------------+ | K_1のシリアライズ表現 | <-+ | V_1のシリアライズ表現 | <-+ --K_i, V_iで一つの組み合わせ +-------------------------+ : : ``` 例えば、以下のようなHashMapを考える. ```rust! let mut obj: HashMap<u32, u32> = HashMap::new(); obj.insert(0x00, 0xcafe_babe); obj.insert(0xff, 0xdead_beef); ``` シリアライズされた表現は以下の通り. ``` | 0x0000_0000_0000_0002 | ← 要素数. (usize: Little Endian) | 0xcabe_babe | 0x0000_0000 | ← 右からK_0, V_0 | 0xdead_beef | 0x0000_00ff | ← 右からK_0, V_0 ``` ## `HashSet` `Vec`と同じ. ただし順番が保証されない. ## `Option` `Option<T>`では先頭に1バイトのシグネチャが付与されて, その後シリアライズされた中身が連結される. + `Option<T>` ```rust! let obj : Option<u32> = None; //→\x00 (1バイトの\0) let obj : Option<u32> = Some(0xdeadbeef); //→ \x01\xef\xbe\xad\xde // ↑ 先頭の\x01がSomeを示すシグネチャ. 後の4バイトは中身(ここではu32の0xdeadbeef) ``` ## Unit シリアライズ処理が無視されている気がする. ```rust! let obj = (); // →シリアライズされてもバイト列が出力されない. ``` ## `Result`/関数ポインタ Resultや関数ポインタをシリアライズすることはできない模様. 試しに, 以下のようなコードを書いてみたがコンパイルエラーだった. ```rust= use std::fs::File; const DUMP_TO : &'static str = "./serialized"; fn add(a: u32, b: u32) -> u32 { // 適当な関数を用意する. a + b } fn main() { let obj: fn(u32, u32) -> u32 = add; // Cの, FILE *f = fopen(filename "rw")に相当する. // ただし, Rustの場合は, 明確にcreateオプションを付けないとファイルを作成できない. let f_res = File::options() .read(true) .write(true) .create(true) .open(DUMP_TO); match f_res { Ok(f) => { // オブジェクトを書き出す処理. match bincode::serialize_into(&f, &obj) { // serialize_intoはResultを返す. Ok(_) => { println!("Success serialized {:?}", obj); }, Err(_) => { panic!("Failed to dump object"); } } }, Err(e) => { eprintln!("Failed to serialize_into {} {}", DUMP_TO, e) } } } ``` :::spoiler エラー内容 ``` root@Ubu2204x64:serde_poc# cargo run Compiling serde_poc v0.1.0 (/tmp/now/serde_poc) error[E0277]: the trait bound `fn(u32, u32) -> u32: serde::ser::Serialize` is not satisfied --> src/main.rs:32:47 | 32 | match bincode::serialize_into(&f, &obj) { // serialize_intoはResultを返す. | ----------------------- ^^^^ the trait `serde::ser::Serialize` is not implemented for `fn(u32, u32) -> u32` | | | required by a bound introduced by this call | note: required by a bound in `bincode::serialize_into` --> /root/.cargo/registry/src/github.com-1ecc6299db9ec823/bincode-1.3.3/src/lib.rs:93:8 | 93 | T: serde::Serialize, | ^^^^^^^^^^^^^^^^ required by this bound in `serialize_into` help: use parentheses to call this function pointer | 32 | match bincode::serialize_into(&f, &obj(/* u32 */, /* u32 */)) { // serialize_intoはResultを返す. | ++++++++++++++++++++++ For more information about this error, try `rustc --explain E0277`. error: could not compile `serde_poc` due to previous error root@Ubu2204x64:serde_poc# ``` ::: `serialize_into`関数が引数に取るオブジェクトは, `serde::Serialize`トレイトを実装している必要があるらしい. 更にSerdeのページを見るに, `serde::Serializer`トレイトが必要とある. つまりResult・関数ポインタは, `serde::{Serialize, Serializer}`トレイトを自前実装する必要がある. カスタムなシリアライズを実装する場合の, 簡単なチュートリアルは[Serdeのページ](https://serde.rs/custom-serialization.html)に書いてあるので, 関数ポインタのシリアライザを記述できると思われるが, やり方がよくわかってない. ## 構造体 例えば, 以下の構造体をシリアライズしたい場合を考える. ```rust! // 構造体の場合は, Serialize/Deserializeトレイトをderiveしておく必要がある. #[derive(Debug, Serialize, Deserialize)] struct SomeStruct { id : usize, name : String } ~ snipped ~ let obj = SomeStruct { id : 0xdeadbeef, name : String::from("THIS IS TEST") }; ``` 構造体の場合は, 上のメンバから順番にシリアライズされるため, ダンプすると以下のようなバイト列になる. ```shell= root@Ubu2204x64:serde_poc# hexdump -vC ./serialized 00000000 ef be ad de 00 00 00 00 0c 00 00 00 00 00 00 00 |................| 00000010 54 48 49 53 20 49 53 20 54 45 53 54 |THIS IS TEST| 最初の8バイトがusizeな`id`, その後ろ8バイトがnameのバイト数. 0000001c root@Ubu2204x64:serde_poc# ``` メンバにコンテナを持っている場合も同じ ```rust! struct SomeStruct { id : usize, names : Vec<String> } let mut obj = SomeStruct { id : 0xdeadbeef, name : vec![ String::from("A_SOME_MESSAGE_1"), // 16bytes String::from("A_SOME_MESSAGE_2"), // 16bytes ] }; ``` シリアライズ結果は以下の通り. ```shell= root@Ubu2204x64:serde_poc# hexdump -vC ./serialized 00000000 ef be ad de 00 00 00 00 02 00 00 00 00 00 00 00 |................| 00000010 10 00 00 00 00 00 00 00 41 5f 53 4f 4d 45 5f 4d |........A_SOME_M| 00000020 45 53 53 41 47 45 5f 31 10 00 00 00 00 00 00 00 |ESSAGE_1........| 00000030 41 5f 53 4f 4d 45 5f 4d 45 53 53 41 47 45 5f 32 |A_SOME_MESSAGE_2| 00000040 root@Ubu2204x64:serde_poc# ``` # 参考文献 https://tech-blog.optim.co.jp/entry/2020/12/23/200000 https://serde.rs/ https://docs.rs/serde/1.0.160/serde/ser/trait.Serializer.html https://www.youtube.com/watch?v=BI_bHCGRgMY