Jump

SECCON 13 Qualsで作問をし、バグらせました。

TL;DR

作問、及びレビューは早めに済ませ、前日に作業をするのを辞めましょう。

Reversingの問題で、69チームが正答し、最終的な特典は118ptでした。

問題文

Who would have predicted that ARM would become so popular?

配布ファイル

jump: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, stripped

aarch64なバイナリです。

最近はApple Silicon以外にもARMが流行ってきているので、そろそろ出しても怒られないかなと思って出しました。結局アーキテクチャ関係ない部分でやらかしをした訳ですが…

以降、この記事には解法のヒントを含むネタバレがあるので、もし自力で解きたいという方は一旦読み進めるのを辞め、以下のリポジトリからバグを修正したバージョンのバイナリをダウンロードして解析してください。

jump fixed

難読化

バイナリはstripされている他、難読化処理が施されています。

例えば0x400ddcのあたりを見てみましょう。この関数ではIncorrectが参照されています。

[0x004004c0]> pdf @0x400ddc
            ; CODE XREF from entry0 @ +0x38(x)
┌ 116: sub.shstrtab_400ddc (int64_t arg1, int64_t arg2, int64_t arg_20h);
│           ; arg int64_t arg1 @ x0
│           ; arg int64_t arg2 @ x1
│           ; arg int64_t arg_20h @ sp+0x50
│           ; var int64_t var_0h_2 @ sp+0x0
│           ; var int64_t var_8h_2 @ sp+0x8
│           ; var int64_t var_0h @ sp+0x10
│           ; var int64_t var_8h @ sp+0x18
│           ; var int64_t var_0h_3 @ sp+0x1c
│           ; var int64_t var_10h @ sp+0x20
│           ; var int64_t var_10h_2 @ sp+0x28
│           0x00400ddc      ff8300d1       sub sp, sp, 0x20
│           0x00400de0      fd7b01a9       stp x29, x30, [arg_20h]
│           0x00400de4      fd430091       add x29, sp, 0x10
│           0x00400de8      bfc31fb8       stur wzr, [x29, -4]         ; arg1
│           0x00400dec      e00b00b9       str w0, [var_8h_2]          ; arg1
│           0x00400df0      e10300f9       str x1, [sp]                ; arg2
│           0x00400df4      e80b40b9       ldr w8, [var_8h_2]
│           0x00400df8      08090071       subs w8, w8, 2
│       ┌─< 0x00400dfc      61020054       b.ne 0x400e48
│       │   0x00400e00      e80340f9       ldr x8, [sp]
│       │   0x00400e04      000540f9       ldr x0, [x8, 8]
│       │   0x00400e08      ff4300d1       sub sp, sp, 0x10
│       │   0x00400e0c      e00300f9       str x0, [sp]
│       │   0x00400e10      a0000010       adr x0, 0x400e24
│       │   0x00400e14      e00700f9       str x0, [var_8h_2]
│       │   0x00400e18      9ef1ff10       adr x30, 0x400c48
│       │   0x00400e1c      e00340f9       ldr x0, [sp]
│       │   0x00400e20      c0035fd6       ret
        │   ; STRN XREF from sub.shstrtab_400ddc @ 0x400e10(r)
..
│      ││   ; CODE XREF from sub.shstrtab_400ddc @ 0x400dfc(x)
│      ││   ; CODE XREF from sub.shstrtab_400ddc @ +0x4c(x)
│      └└─> 0x00400e48      00000090       adrp x0, segment.LOAD0      ; segment.ehdr
│                                                                      ; 0x400000
│           0x00400e4c      00403d91       add x0, x0, 0xf50           ; 0x400f50 ; "Incorrect" ; const char *s
│           0x00400e50      94fdff97       bl sym.imp.puts             ; int puts(const char *s)
│           0x00400e54      ff4300d1       sub sp, sp, 0x10
│           0x00400e58      fe0700f9       str x30, [var_8h_2]
│           ; STRN XREF from fcn.00400e38 @ 0x400e40(r)
│           0x00400e5c      fe0740f9       ldr x30, [var_8h_2]
│           0x00400e60      ff430091       add sp, sp, 0x10
│           0x00400e64      a0c35fb8       ldur w0, [x29, -4]
│           0x00400e68      fd7b41a9       ldp x29, x30, [arg_20h]
│           0x00400e6c      ff830091       add sp, sp, 0x20
└           0x00400e70      c0035fd6       ret

しかし、0x400e200x400e48の間がうまくディスアセンブルされていないようです。

ghidraで確認してもディスアセンブルに失敗していました。

そこで、radare2でpdfではなく、pdを使ってみると

[0x004004c0]> pd @0x400ddc
            ; CODE XREF from entry0 @ +0x38(x)
┌ 116: sub.shstrtab_400ddc (int64_t arg1, int64_t arg2, int64_t arg_20h);
│           ; arg int64_t arg1 @ x0
│           ; arg int64_t arg2 @ x1
│           ; arg int64_t arg_20h @ sp+0x50
│           ; var int64_t var_0h_2 @ sp+0x0
│           ; var int64_t var_8h_2 @ sp+0x8
│           ; var int64_t var_0h @ sp+0x10
│           ; var int64_t var_8h @ sp+0x18
│           ; var int64_t var_0h_3 @ sp+0x1c
│           ; var int64_t var_10h @ sp+0x20
│           ; var int64_t var_10h_2 @ sp+0x28
│           0x00400ddc      ff8300d1       sub sp, sp, 0x20
│           0x00400de0      fd7b01a9       stp x29, x30, [arg_20h]
│           0x00400de4      fd430091       add x29, sp, 0x10
│           0x00400de8      bfc31fb8       stur wzr, [x29, -4]         ; arg1
│           0x00400dec      e00b00b9       str w0, [var_8h_2]          ; arg1
│           0x00400df0      e10300f9       str x1, [sp]                ; arg2
│           0x00400df4      e80b40b9       ldr w8, [var_8h_2]
│           0x00400df8      08090071       subs w8, w8, 2
│       ┌─< 0x00400dfc      61020054       b.ne 0x400e48
│       │   0x00400e00      e80340f9       ldr x8, [sp]
│       │   0x00400e04      000540f9       ldr x0, [x8, 8]
│       │   0x00400e08      ff4300d1       sub sp, sp, 0x10
│       │   0x00400e0c      e00300f9       str x0, [sp]
│       │   0x00400e10      a0000010       adr x0, 0x400e24
│       │   0x00400e14      e00700f9       str x0, [var_8h_2]
│       │   0x00400e18      9ef1ff10       adr x30, 0x400c48
│       │   0x00400e1c      e00340f9       ldr x0, [sp]
│       │   0x00400e20      c0035fd6       ret
        │   ; STRN XREF from sub.shstrtab_400ddc @ 0x400e10(r)
        │   0x00400e24      08040071       subs w8, w0, 1
       ┌──< 0x00400e28      01010054       b.ne 0x400e48               ; sub.shstrtab_400ddc+0x6c
       ││   0x00400e2c      00000090       adrp x0, segment.LOAD0      ; segment.ehdr
       ││                                                              ; 0x400000
       ││   0x00400e30      00203d91       add x0, x0, 0xf48
       ││   0x00400e34      9bfdff97       bl sym.imp.puts             ; int puts(const char *s)
┌ 16: fcn.00400e38 ();
│      ││   ; var int64_t var_0h @ sp+0x8
│      ││   0x00400e38      ff4300d1       sub sp, sp, 0x10
│      ││   0x00400e3c      fe0700f9       str x30, [var_0h]
│      ││   0x00400e40      fe000010       adr x30, 0x400e5c
└      ││   0x00400e44      c0035fd6       ret
│      ││   ; CODE XREF from sub.shstrtab_400ddc @ 0x400dfc(x)
│      ││   ; CODE XREF from sub.shstrtab_400ddc @ +0x4c(x)
│      └└─> 0x00400e48      00000090       adrp x0, segment.LOAD0      ; segment.ehdr
│                                                                      ; 0x400000
│           0x00400e4c      00403d91       add x0, x0, 0xf50           ; 0x400f50 ; "Incorrect" ; const char *s
│           0x00400e50      94fdff97       bl sym.imp.puts             ; int puts(const char *s)
│           0x00400e54      ff4300d1       sub sp, sp, 0x10
│           0x00400e58      fe0700f9       str x30, [var_8h_2]
│           ; STRN XREF from fcn.00400e38 @ 0x400e40(r)
│           0x00400e5c      fe0740f9       ldr x30, [var_8h_2]
│           0x00400e60      ff430091       add sp, sp, 0x10
│           0x00400e64      a0c35fb8       ldur w0, [x29, -4]
│           0x00400e68      fd7b41a9       ldp x29, x30, [arg_20h]
│           0x00400e6c      ff830091       add sp, sp, 0x20
└           0x00400e70      c0035fd6       ret
            ;-- section..fini:
            0x00400e74      1f2003d5       nop                         ; [14] -r-x section size 20 named .fini
┌ 16: sub.shstrtab_400e78 ();
│           ; var int64_t var_10h @ sp+0x0
│           ; var int64_t var_10h_2 @ sp+0x8
│           0x00400e78      fd7bbfa9       stp x29, x30, [sp, -0x10]!
│           0x00400e7c      fd030091       mov x29, sp
│           0x00400e80      fd7bc1a8       ldp x29, x30, [sp], 0x10
└           0x00400e84      c0035fd6       ret

このように、途中の命令もディスアセンブルすることができました。

注目すべきは以下の部分です

││   0x00400e2c      00000090       adrp x0, segment.LOAD0      ; segment.ehdr
││                                                              ; 0x400000
││   0x00400e30      00203d91       add x0, x0, 0xf48
││   0x00400e34      9bfdff97       bl sym.imp.puts             ; int puts(const char *s)

0x400f48にはCorrectという文字列があります。成功時はここに遷移するという訳です。

[0x004004c0]> izq
0x400f48 8 7 Correct
0x400f50 10 9 Incorrect

このようにディスアセンブルが失敗するのは難読化の効果です。

今回はソースコードをコンパイルする際、遷移に用いられる一部の命令をROPに置き換えています。

│      ││   ; var int64_t var_0h @ sp+0x8
│      ││   0x00400e38      ff4300d1       sub sp, sp, 0x10
│      ││   0x00400e3c      fe0700f9       str x30, [var_0h]
│      ││   0x00400e40      fe000010       adr x30, 0x400e5c
└      ││   0x00400e44      c0035fd6       ret

この部分を例に説明すると、スタックを確保した上でリンクレジスタと呼ばれる、リターンアドレスが格納されたレジスタであるx30の内容をsaveし、0x400e5cで上書きしてからretしています。

遷移先の0x400e5cでは

│           ; var int64_t var_8h_2 @ sp+0x8
│           0x00400e5c      fe0740f9       ldr x30, [var_8h_2]
│           0x00400e60      ff430091       add sp, sp, 0x10

のように、レジスタとスタックの復元を行っています。

今回のバイナリにおける難読化処理は主にこれだけなので、一部のチームではバイナリにパッチを当ててディスアセンブル・デコンパイルを行っていたようです。想定解法の一つで嬉しいです。

また、難読化処理はこれだけなので、全体を読んでそれっぽい比較部分を探すのもアリです。

しかし、問題は比較処理にあります。

本来の比較処理は4バイト毎に行われ

  1. 定数との比較
  2. XOR + 定数との比較
  3. XOR + 定数との比較
  4. XOR + 定数との比較
  5. 前パートの4バイトを足した上で定数と比較
  6. 前パートの4バイトを足した上で定数と比較
  7. 前パートの4バイトを足した上で定数と比較
  8. 前パートの4バイトを引いた上で定数と比較

というように、適用される順序が重要になります。

「本来の」と言ったのは、バグのせいでこれらの比較が正常に適用されていなかったためです。これにより、Guess要素が追加されカスの問題になりました。本当に申し訳ありません。

一方、怪我の功名というか、バグってるせいで動的解析がまともに出来ず、armの環境を持っている人とそうでない人、それぞれに大きな差が生まれず、公平に両方が苦しむ事になった。という見方もあります。

バグ

さて、問題のソースコードはこんな感じです。

#include <stdio.h>
#include <stdbool.h>
#define FLAG_LEN 32

typedef enum { INIT, LOOP, CHECK, DONE, SUCCESS, FAILURE } State;
State state;
bool correct = true;
int idx;

void init() {
    idx = 0;
    state = LOOP;
}

void loop() {
    if (idx < FLAG_LEN) {
        state = CHECK;
    } else {
        state = DONE;
    }
}

void two(int input) {
    correct &= ((input ^ 0xcafebabe) == 0xf9958ed6);
}

void three(int input) {
    correct &= ((input ^ 0xc0ffee) == 0x5fb4ceb1);
}

void one(int input) {
    correct &= ((input ^ 0xdeadbeef) == 0xebd6f0a0);
}

void six(char *input) {
    int current = *(int*)(input + idx);
    int before = *(int*)(input + idx - 4);
    correct &= ((current + before) == 0x9d9d6295);
}

void four(char *input) {
    int current = *(int*)(input + idx);
    int before = *(int*)(input + idx - 4);
    correct &= ((current + before) == 0x94d3a1d4);
}

void seven(char *input) {
    int current = *(int*)(input + idx);
    int before = *(int*)(input + idx - 4);
    correct &= ((current - before) == 0x47cb363b);
}

void zero(int input) {
    correct &= (input == 0x43434553);
}

void five(char *input) {
    int current = *(int*)(input + idx);
    int before = *(int*)(input + idx - 4);
    correct &= ((current + before) == 0x9d949ddd);
}

void check(char *input) {
    state = LOOP;
    switch (idx) {
        case 24:
            six(input);
            return;
        case 4:
            one(*(int*)(input + idx));
            return;
        case 8:
            two(*(int*)(input + idx));
            return;
        case 20:
            five(input);
            return;
        case 0:
            zero(*(int*)(input));
            return;
        case 12:
            three(*(int*)(input + idx));
        case 28:
            seven(input);
            return;
        case 16:
            four(input);
            return;
    }
}

int checker(char *input) {
    state = INIT;

    while (1) {
        switch (state) {
            case INIT:
            case LOOP:
            case CHECK:
            {
                void (*vv[3])() = {[INIT]=init, [LOOP]=loop, [CHECK]=check};
                vv[state](input);
                idx += 4;
                break;
            }
            case DONE:
                if (correct) {
                    state = SUCCESS;
                } else {
                    state = FAILURE;
                }
                break;
            case SUCCESS:
                return 1;

            case FAILURE:
                return 0;
        }
    }
}

int main(int argc, char **argv) {
    if (argc == 2 && checker(argv[1]) == 1) {
        puts("Correct");
    } else {
        puts("Incorrect");
    }
}
  1. stateの変更を忘れている
  2. case 12returnを忘れている

非常に初歩的なミスを犯しました。というのも直前で実装を変えたのが悪いと思っています。

反省

作問、及びレビューは早めに済ませ、前日に作業をするのを辞めましょう。

これに尽きる。

バグらせてGuess要素を含むクソ問になったにも関わらず、解いてくださった方々には非常に感謝しています。また、解こうとしてくださった方々にも非常に申し訳ないと思っています。

そしてもう一つ重要な点ですが、今回作問ミスを犯した結果、非常に多くの問い合わせをいただきました。

こちらとしても申し訳無さと恥ずかしさで、修正したバイナリを配布したい気持ちは大きかったのですが、バグに気づいた時点で解いていたチームが存在していたため、公平性の観点からヒントになるような情報や、解くのが簡単になるような修正したバイナリの配布を行うわけにはいかず、FlagのHashのみを提供する。という判断をしました。

この判断自体は間違っていなかったと信じていますが、不便を強いたことを改めてお詫びします。

終わりに

またこの記事はCTF Advent Calendar 2024及びn01e0 Advent Calendar 2024の3日目の記事とします。