rev問のソルバを書くときとかに使えるかもしれない小テク
CTF Advent Calendar 2016 15日目です。
今回は、rev問のソルバを書くときとかに使えるかもしれない小テクを紹介します。
rev問を解くときの悩み
rev問を解いていて、「バイナリ中の特定の関数を自由に呼び出したい……」と思ったことはありませんか?
例えば、こんな感じのrev問があったとしましょう。
#include <stdio.h> #include <stdlib.h> #include <string.h> unsigned char extremely_mendokusai_function1(unsigned char input){ // 見るからに読むのがめんどくさそうな処理 } unsigned char extremely_mendokusai_function2(unsigned char input){ // まじめに読んでいたら日が暮れそうな処理 } unsigned char extremely_mendokusai_function3(unsigned char input){ // IDAおばさんですらさじを投げるレベルで読むのがつらそうな処理 } // : // (snip) // : unsigned char extremely_mendokusai_function100(unsigned char input){ // 簡単そうに見せかけて実は読むのがしんどい処理 } unsigned char scramble(unsigned char input){ unsigned char result; result = extremely_mendokusai_function1(input); result = extremely_mendokusai_function2(result); result = extremely_mendokusai_function3(result); // : // (snip) // : result = extremely_mendokusai_function100(result); return result; } int main(int argc, char **argv){ char *key; int i; char buf[512] = {}; if(argc != 2){ fprintf(stderr, "usage: %s key\n", argv[0]); exit(1); } key = argv[1]; if(strlen(key) != 500){ puts("Nope :(") exit(0); } for(i = 0; i < strlen(key); i++){ buf[i] = scramble(key[i]); } if(memcmp("you_don't_wanna_waste_your_time_for_reading_this_binary_do_you?...(snip)...lol", buf, 500) == 0){ puts("Good job :)"); }else{ puts("Nope :("); } return 0; }
入力の各文字に対してscramble
関数で変換をかけ、変換結果が特定の状態になればOKという、rev問でよくあるやつです。
このようなrev問を解くときは
- 変換処理を読む
- 変換処理の逆変換を考える or 入力と出力の対応表を作る
- チェック処理を通過するような入力を求める
というのが基本的な方針ですが、scramble
関数を読むのが現実的ではないのは明らかです。
バイナリ中の関数を流用して
unsigned int i; unsigned char table[256]; // 入力と出力の対応表を作成 for(i = 0; i < 256; i++){ table[scramble(i)] = i; }
みたいなコードが書けたら、scramble
関数を読まずに済んで楽なのになー(´・ω・`) となるわけです。
このようなとき、LD_PRELOAD
環境変数を使うという選択肢があります。
※ バイナリの内容次第ではangrやKLEEであっさり解けるため、この辺のツールにも慣れておきたいものです(自戒)
LD_PRELOAD
について
ここで詳しく説明することはしませんが、LD_PRELOAD
を使うと、ライブラリ関数をフックして動作を変えることができます。
例えば、こんなプログラムに対して、
// cc -o test test.c #include <stdio.h> #include <string.h> int main(int argc, char **argv){ char *str = "AAAA"; if(argc != 2){ return 1; } if(strcmp(argv[1], str) == 0){ printf("\"%s\" == \"%s\"\n", argv[1], str); }else{ printf("\"%s\" != \"%s\"\n", argv[1], str); } return 0; }
こんなライブラリを用意すると、
// cc -o inject.so inject.c -shared -fPIC #include <stdio.h> int strcmp(const char *s1, const char *s2){ puts("strcmp called, but it's not my business lol"); return 0; }
strcmp
が必ず0を返すようになります。
# LD_PRELOADを使用しない場合 $ ./test AAAA "AAAA" == "AAAA" $ ./test BBBB "BBBB" != "AAAA" # LD_PRELOADを使用した場合 $ LD_PRELOAD=./inject.so ./test AAAA strcmp called, but it's not my business lol "AAAA" == "AAAA" $ LD_PRELOAD=./inject.so ./test BBBB strcmp called, but it's not my business lol "BBBB" == "AAAA"
main
関数を呼び出すための関数である__libc_start_main
も例外ではありません。
// cc -o inject_main.so inject_main.c -shared -fPIC #include <stdio.h> #include <stdlib.h> int __libc_start_main (int (*main) (int, char **, char **), int argc, char **argv, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void *stack_end){ puts("I'm not going to work now, come back later"); exit(0); // __libc_start_mainをフックする場合、最後はreturnではなくexitすること }
$ LD_PRELOAD=./inject_main.so ./test AAAA I'm not going to work now, come back later
LD_PRELOAD
を使って、バイナリ中の関数を自由に呼び出す
LD_PRELOAD
でロードされたライブラリは、libc等と同様に、実行バイナリのプロセスと同じ仮想メモリ空間上にマッピングされます。
gdb-peda$ vmmap Start End Perm Name 0x00400000 0x00401000 r-xp /tmp/test/test 0x00600000 0x00601000 r--p /tmp/test/test 0x00601000 0x00602000 rw-p /tmp/test/test 0x00007ffff7813000 0x00007ffff79cd000 r-xp /lib/x86_64-linux-gnu/libc-2.19.so 0x00007ffff79cd000 0x00007ffff7bcd000 ---p /lib/x86_64-linux-gnu/libc-2.19.so 0x00007ffff7bcd000 0x00007ffff7bd1000 r--p /lib/x86_64-linux-gnu/libc-2.19.so 0x00007ffff7bd1000 0x00007ffff7bd3000 rw-p /lib/x86_64-linux-gnu/libc-2.19.so 0x00007ffff7bd3000 0x00007ffff7bd8000 rw-p mapped 0x00007ffff7bd8000 0x00007ffff7bd9000 r-xp /tmp/test/inject.so <-これ 0x00007ffff7bd9000 0x00007ffff7dd8000 ---p /tmp/test/inject.so 0x00007ffff7dd8000 0x00007ffff7dd9000 r--p /tmp/test/inject.so 0x00007ffff7dd9000 0x00007ffff7dda000 rw-p /tmp/test/inject.so 0x00007ffff7dda000 0x00007ffff7dfd000 r-xp /lib/x86_64-linux-gnu/ld-2.19.so 0x00007ffff7fe0000 0x00007ffff7fe3000 rw-p mapped 0x00007ffff7ff6000 0x00007ffff7ff8000 rw-p mapped 0x00007ffff7ff8000 0x00007ffff7ffa000 r-xp [vdso] 0x00007ffff7ffa000 0x00007ffff7ffc000 r--p [vvar] 0x00007ffff7ffc000 0x00007ffff7ffd000 r--p /lib/x86_64-linux-gnu/ld-2.19.so 0x00007ffff7ffd000 0x00007ffff7ffe000 rw-p /lib/x86_64-linux-gnu/ld-2.19.so 0x00007ffff7ffe000 0x00007ffff7fff000 rw-p mapped 0x00007ffffffde000 0x00007ffffffff000 rw-p [stack] 0xffffffffff600000 0xffffffffff601000 r-xp [vsyscall]
ということは、アドレスさえ事前に調べておけば、LD_PRELOAD
したライブラリからバイナリ中の関数を自由に呼んだり、bss領域の値を読み書きしたりできるということです。
実際に試してみましょう。
まず、グローバル変数や関数のアドレスを表示して終わるだけのプログラムを書きます。
// cc -o test2 test2.c #include <stdio.h> #include <ctype.h> char buffer[256]; void capitalize(char *str){ if(islower(*str)){ *str -= 0x20; } } void print_buffer(){ puts(buffer); } int main(){ printf("buffer = %p\n", buffer); printf("capitalize = %p\n", capitalize); printf("print_buffer = %p\n", print_buffer); }
実行してアドレスを確認します。
$ ./test2 buffer = 0x601080 capitalize = 0x4005dd print_buffer = 0x400625
調べたアドレスを使って、バイナリ中の関数を呼ぶライブラリを書きます。
// cc -o inject_test2.c inject_test2.so -shared -fPIC #include <string.h> #include <stdlib.h> int __libc_start_main (int (*main) (int, char **, char **), int argc, char **argv, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void *stack_end){ char *buffer = (void*)0x601080; void (*capitalize)(char*) = (void*)0x4005dd; void (*print_buffer)(void) = (void*)0x400625; strcpy(buffer, "hello world"); puts("[*] before capitalize"); print_buffer(); puts("[*] after capitalize"); capitalize(buffer); print_buffer(); exit(0); }
LD_PRELOAD
をつけて実行してみると、bss領域を書き換えたり、バイナリ中の関数を呼んだりできていることがわかります。
$ LD_PRELOAD=./inject_test2.so ./test2 [*] before capitalize hello world [*] after capitalize Hello world
ということで、冒頭で示したrev問に対してLD_PRELOAD
を使ったソルバを書くならば、
// cc -o solver.so solver.c -shared -fPIC #include <stdlib.h> #include <stdio.h> int __libc_start_main (int (*main) (int, char **, char **), int argc, char **argv, void (*init) (void), void (*fini) (void), void (*rtld_fini) (void), void *stack_end){ unsigned char (*scramble)(unsigned char) = (void*)0x401337; // 事前に調べたscrambleのアドレス unsigned int i; unsigned char table[256]; unsigned char *expected = "you_don't_wanna_waste_your_time_for_reading_this_binary_do_you?...(snip)...lol"; unsigned char answer[1024] = {}; // 対応表の作成 for(i = 0; i < 256; i++){ table[scramble(i)] = i; } for(i = 0; i < 500; i++){ answer[i] = table[expected[i]]; } puts(answer); exit(0); }
となります。
注意点とか応用とか
注意点
- 静的リンクされたバイナリだと
LD_PRELOAD
は意味がありません。 - 当たり前のことですが、x86_64なバイナリに対してはx86_64なライブラリを、x86なバイナリに対してはx86なライブラリを用意して
LD_PRELOAD
しましょう。
応用
- 関数のアドレスを調べたけどバイナリがPIEだった……という場合は、フックした関数内でスタック上のリターンアドレスを調べるという荒技で何とかすることができます。
- ソルバを全部Cで書くのはつらいンゴ……という場合は、標準入力からのデータをパースしてバイナリ中の関数に渡し、処理結果を標準出力に返すようなライブラリを作れば、他の言語でも外部プロセス実行用の関数(Rubyの
Open3
, Pythonのsubprocess
とか)経由でバイナリ中の関数を呼べるようになります。
練習問題
最後に、今回紹介したテクニックが役に立つ過去問を練習問題として置いておきます。
monkey (Defcamp CTF 2016 Finals)
("demo"が問題バイナリです)
printableな解を見つければクリアです。(解は一意には定まりません)
ぜひ解いてみてください(*´ω`*)
CTF Advent Calendar 2016 16日目は、818uuuさんの「recon easy list writeup」です!