しゃろの日記

CTFのwriteup置き場になる予定(`・ω・´)

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問を解くときは

  1. 変換処理を読む
  2. 変換処理の逆変換を考える or 入力と出力の対応表を作る
  3. チェック処理を通過するような入力を求める

というのが基本的な方針ですが、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で書くのはつらいンゴ……という場合は、標準入力からのデータをパースしてバイナリ中の関数に渡し、処理結果を標準出力に返すようなライブラリを作れば、他の言語でも外部プロセス実行用の関数(RubyOpen3, Pythonsubprocessとか)経由でバイナリ中の関数を呼べるようになります。

練習問題

最後に、今回紹介したテクニックが役に立つ過去問を練習問題として置いておきます。

monkey (Defcamp CTF 2016 Finals)
("demo"が問題バイナリです)

printableな解を見つければクリアです。(解は一意には定まりません)
ぜひ解いてみてください(*´ω`*)


CTF Advent Calendar 2016 16日目は、818uuuさんの「recon easy list writeup」です!