ROPOB
ROPOB
ROP based binary obfuscation
Agenda
- ROPOB
- ROP
- paper
- 俺のROPOB
ROP
Return Oriented Programming
命令+ret
の組み合わせを単位(gadget)とし,それらをスタック上で組み合わせてプログラミングする
PwnではBOFからROPを組む事があるが,最初からROPでコード書いてれば難読化になるのでは?
思いついた瞬間,マジで天才かもしれんと思った.
先行研究があった.泣いちゃった
paper
オーバーヘッドは10%くらいらしい
この論文の実装ではオリジナルのELFから難読化ELFへの変換を行っている
ELF中の機械語から,jmp
,jcc
,call
,ret
などのControl Flow Related Instruction(CFRI)をROPに置き換える事で難読化を行っている
この実装では,block中(終端)のCFRIを書き換えるだけで,mov
やlea
などのデータに対する操作は変更していない
これらもROPで実装する事は出来るが,オーバヘッドがデカそうなのでやってないっぽい.多分
バイナリにパッチ当てる方式,なんかスマートじゃなくない?って感想.
俺のROPOB
Timing
難読化のタイミングにはいくつか種類がある
- コンパイル(ソースコードから実行可能ファイルを生成する)前
- ソースコードから難読化したソースコードを生成
- コンパイル時
- ソースコードを難読化コンパイラでコンパイル
- 中間表現のレベルで難読化し,通常のコンパイラに渡す
- ソースコード中の一部の要素を難読化する加工をした上で通常のコンパイラに渡す
- コンパイル後
- バイナリ中の機械語を難読化
- 論文の実装はこれ
- バイナリ中の機械語を難読化
どのタイミングでROPを適用出来るか考える
ソースコードの難読化
→ ここでROPを適用するのは難しい
IRの変換
→ 色々な言語やアーキテクチャに対応できる為,一番有能だが,抽象度が高すぎてROPは難しい.
ソースコードを難読化コンパイラでコンパイル
→ これが一番簡単そう.
implementation
いま0からコンパイラを実装するのは時間がかかる
流石に1日でコンパイラ実装出来る気がしない
LLVM?
ちょっとC++は読みたくないですね.今は
r9cc
https://github.com/utam0k/r9cc
詳しくはこちら
これをベースにROPコンパイラを実装する
patch
何を変更すれば良いか
論文で実装していた通り,CFRIを置き換える
とりあえずcall, jmp, ret
から
r9ccではreturn
に対してret
命令を発行していないので,そこから書き換える.
patch
diff --git a/src/gen_x86.rs b/src/gen_x86.rs
index 771ca67..025925d 100644
--- a/src/gen_x86.rs
+++ b/src/gen_x86.rs
@@ -87,6 +87,7 @@ fn argreg(r: usize, size: u8) -> &'static str {
fn gen(f: Function) {
use self::IROp::*;
let ret = format!(".Lend{}", *LABEL.lock().unwrap());
+ let mut call_gadget_id: usize = 0;
*LABEL.lock().unwrap() += 1;
println!(".text");
@@ -108,7 +109,12 @@ fn gen(f: Function) {
Mov => emit!("mov {}, {}", REGS[lhs], REGS[rhs]),
Return => {
emit!("mov rax, {}", REGS[lhs]);
- emit!("jmp {}", ret);
+ emit!("lea rsp, [rsp-8]");
+ emit!("push rax");
+ emit!("lea rax, {}", ret);
+ emit!("mov [rsp+8], rax");
+ emit!("pop rax");
+ emit!("ret");
}
Call(name, nargs, args) => {
for i in 0..nargs {
@@ -117,11 +123,20 @@ fn gen(f: Function) {
emit!("push r10");
emit!("push r11");
emit!("mov rax, 0");
- emit!("call {}", name);
+ emit!("lea rsp, [rsp-16]");
+ emit!("push rax");
+ emit!("lea rax, {}", name);
+ emit!("mov [rsp+8], rax");
+ emit!("lea rax, .Lcall_gadget_{}_{}", f.name, call_gadget_id);
+ emit!("mov [rsp+16], rax");
+ emit!("pop rax");
+ emit!("ret");
+ println!(".Lcall_gadget_{}_{}:", f.name, call_gadget_id);
emit!("pop r11");
emit!("pop r10");
emit!("mov {}, rax", REGS[lhs]);
+ call_gadget_id += 1;
}
Label => println!(".L{}:", lhs),
LabelAddr(name) => emit!("lea {}, {}", REGS[lhs], name),
@@ -151,7 +166,14 @@ fn gen(f: Function) {
emit!("div {}", REGS[rhs]);
emit!("mov {}, rdx", REGS[lhs]);
}
- Jmp => emit!("jmp .L{}", lhs),
+ Jmp => {
+ emit!("lea rsp, [rsp-8]");
+ emit!("push rax");
+ emit!("lea rax, [.L{}]", lhs);
+ emit!("mov [rsp+8], rax");
+ emit!("pop rax");
+ emit!("ret");
+ },
If => {
emit!("cmp {}, 0", REGS[lhs]);
emit!("jne .L{}", rhs);
ret
mov rax,
- jmp
+ lea rsp, [rsp-8]
+ push rax
+ lea rax,
+ mov [rsp+8], rax
+ pop rax
+ ret
call
mov rax, 0
- call
+ lea rsp, [rsp-16]
+ push rax
+ lea rax,
+ mov [rsp+8], rax
+ lea rax, .Lcall_gadget__
+ mov [rsp+16], rax
+ pop rax
+ ret
+.Lcall_gadget__
jmp
- jmp .L
+ lea rsp, [rsp-8]
+ push rax
+ lea rax, .L
+ mov [rsp+8], rax
+ pop rax
+ ret
repo
評価
とりあえず適当にcatを実装して,
torvalds/linuxのファイル全部表示させてみた
上がデフォルト,下がROPに変更後
7%くらい遅くなった.
CFG
めちゃくちゃ読みづらい(個人の感想).
CFGが意味を成していない.
総評
いい感じっぽいけどIntel CETで死ぬよね.
Comments