ROPOB

ROP based binary obfuscation

Agenda

ROP

Return Oriented Programming

命令+retの組み合わせを単位(gadget)とし,それらをスタック上で組み合わせてプログラミングする

PwnではBOFからROPを組む事があるが,最初からROPでコード書いてれば難読化になるのでは?

思いついた瞬間,マジで天才かもしれんと思った.


先行研究があった.泣いちゃった


paper

ROPOB

オーバーヘッドは10%くらいらしい

この論文の実装ではオリジナルのELFから難読化ELFへの変換を行っている

ELF中の機械語から,jmp,jcc,call,retなどのControl Flow Related Instruction(CFRI)をROPに置き換える事で難読化を行っている

この実装では,block中(終端)のCFRIを書き換えるだけで,movleaなどのデータに対する操作は変更していない

これらもROPで実装する事は出来るが,オーバヘッドがデカそうなのでやってないっぽい.多分

バイナリにパッチ当てる方式,なんかスマートじゃなくない?って感想.

俺のROPOB

Timing

難読化のタイミングにはいくつか種類がある

どのタイミングでROPを適用出来るか考える

ソースコードの難読化

→ ここでROPを適用するのは難しい

IRの変換

→ 色々な言語やアーキテクチャに対応できる為,一番有能だが,抽象度が高すぎてROPは難しい.

ソースコードを難読化コンパイラでコンパイル

→ これが一番簡単そう.

implementation

いま0からコンパイラを実装するのは時間がかかる

流石に1日でコンパイラ実装出来る気がしない

LLVM?

ちょっとC++は読みたくないですね.今は

r9cc

https://github.com/utam0k/r9cc

utam0k氏による9ccのRust実装.

詳しくはこちら

これをベースに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

https://github.com/n01e0/r9cc

評価

とりあえず適当にcatを実装して,

torvalds/linuxのファイル全部表示させてみた

上がデフォルト,下がROPに変更後

benchmark

7%くらい遅くなった.

CFG

デカすぎて表示できなそう.png

めちゃくちゃ読みづらい(個人の感想).

CFGが意味を成していない.

総評

いい感じっぽいけどIntel CETで死ぬよね.