動的基本ブロック(Dynamic Basic Block)と静的基本ブロック(Static Basic Block)の違いを自分なりに考えた記事です。
DynamoRIO System Overviewによると、コードキャッシュへコピーされるコードは動的基本ブロック(Dynamic Basic Block)と記されています。
この動的基本ブロックについて、DynamoRIOのinstrlist_disassemble
を用いながら静的基本ブロックとの違いを確認していきます。
DynamoRIOについては、DynamoRIOでシンプルな基本ブロックトレーサーを実装してみるを参照してください。
call-pop trick
32bitのシェルコードは、文字列のアドレスを取得するためによくcallとpopが用いられます。
次のコードは、call命令の後に文字列を置くことで、リターンアドレスが文字列ポインタとなります。
したがって、文字列の取得にpop
を使っています。この場合、ret
によって呼び出し元に戻ることは考慮されません。
1SECTION .text
2global _start
3
4_print:
5 ; write(STDOUT, ...)
6 mov eax, 4
7 mov ebx, 1
8 pop ecx ; mov ecx, DWORD [esp]
9 mov edx, 13
10 int 0x80
11
12 ; exit(0)
13 mov eax, 1
14 mov ebx, 0
15 int 0x80
16
17_start:
18 call _print
19 db "Hello world!", 10
上のコードのファイル名をmain.asm
とする場合、次のようにビルドします。
以降、出力したa.out
を見ていきます。
nasm -f elf main.asm -o a.o
ld -m elf_i386 a.o -o a.out
静的基本ブロック
個人的には、静的基本ブロックとはIDAやRadare2などで見られる基本ブロックだと考えています。
生成された実行ファイルの_start
をr2
で逆アセンブルしてみましょう。
0x08049023
〜0x0804902b
より、Hello world!
が逆アセンブルされていることが分かります。
このような基本ブロックは、終点が分岐命令やジャンプ命令です。本来、call
命令は元の基本ブロックに戻るため、終点となりません。
しかし、今回のように、リターンを意図しないcall
命令もあります。そのような場合、静的基本ブロックでは誤った逆アセンブル結果を返します。
$ r2 ./a.out
[0x0804901e]> aa
[x] Analyze all flags starting with sym. and entry0 (aa)
[x] Analyze all functions arguments/locals
[0x0804901e]> pdb @ loc._start
;-- _start:
;-- eip:
┌ 18: entry0 ();
│ 0x0804901e e8ddffffff call loc._print
│ 0x08049023 48 dec eax
│ 0x08049024 656c insb byte es:[edi], dx
│ 0x08049026 6c insb byte es:[edi], dx
│ 0x08049027 6f outsd dx, dword [esi]
│ 0x08049028 20776f and byte [edi + 0x6f], dh
│ ┌─< 0x0804902b 726c jb 0x8049099
0x08049023
以降のコードが実行されるか否かは、実際に実行してみない限り確認の仕様がありません。
動的基本ブロック
DynamoRIO System Overviewによると、動的基本ブロックをコードキャッシュにコピーすると書いています。
そこで、コードキャッシュをinstrlist_disassemble
で確認してみます。
このコードの詳細は、DynamoRIOでシンプルな基本ブロックトレーサーを実装してみるを参照してください。
1static module_data_t *app;
2static dr_emit_flags_t disassemble_codecache(void *drcontext, void *tag, instrlist_t *bb, bool for_trace, bool translating)
3{
4 app_pc pc = dr_fragment_app_pc(tag);
5 if (dr_module_contains_addr(app, pc))
6 {
7 instrlist_disassemble(drcontext, pc, bb, STDERR);
8 }
9 return DR_EMIT_DEFAULT;
10}
./a.out
がelf_i386
なので32bitでビルドします。
CXXFLAGS=-m32 CFLAGS=-m32 cmake ..
cmake --build .
実行した結果が次のとおりです。静的基本ブロックと異なり、call
命令が基本ブロックの終端です。
これにより、コードとして実行可能なアセンブリのみを出力できました。
$ drrun -syntax_intel -c ./show_codecache.so -- a.out
TAG 0x0804901e
+0 L3 @0x3ff07d7c e8 dd ff ff ff call 0x08049000
END 0x0804901e
TAG 0x08049000
+0 L3 @0x3ff0755c b8 04 00 00 00 mov eax, 0x00000004
+5 L3 @0x3ff078d8 bb 01 00 00 00 mov ebx, 0x00000001
+10 L3 @0x3ff07d7c 59 pop ecx
+11 L3 @0x3ff07f38 ba 0d 00 00 00 mov edx, 0x0000000d
+16 L3 @0x3ff07760 cd 80 int 0x80
END 0x08049000
Hello world!
TAG 0x08049012
+0 L3 @0x3ff074c8 b8 01 00 00 00 mov eax, 0x00000001
+5 L3 @0x3ff07760 bb 00 00 00 00 mov ebx, 0x00000000
+10 L3 @0x3ff07a18 cd 80 int 0x80
END 0x08049012
これをCFGで示そうとすると冗長です。特に、間接ジャンプ(Indirect Jump)をCALL
する場合、そこでCFGが途切れてしまいます。
静的基本ブロックの場合は、同じ間接ジャンプでもJMP
でなければ(今回のように正しくない結果となる場合もありますが)CFGを生成できます。