SECCON 13 Quals - Jump
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が流行ってきているので、そろそろ出しても怒られないかなと思って出しました。結局アーキテクチャ関係ない部分でやらかしをした訳ですが…
以降、この記事には解法のヒントを含むネタバレがあるので、もし自力で解きたいという方は一旦読み進めるのを辞め、以下のリポジトリからバグを修正したバージョンのバイナリをダウンロードして解析してください。
難読化
バイナリは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
しかし、0x400e20
と0x400e48
の間がうまくディスアセンブルされていないようです。
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バイト毎に行われ
- 定数との比較
- XOR + 定数との比較
- XOR + 定数との比較
- XOR + 定数との比較
- 前パートの4バイトを足した上で定数と比較
- 前パートの4バイトを足した上で定数と比較
- 前パートの4バイトを足した上で定数と比較
- 前パートの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");
}
}
state
の変更を忘れているcase 12
でreturn
を忘れている
非常に初歩的なミスを犯しました。というのも直前で実装を変えたのが悪いと思っています。
反省
作問、及びレビューは早めに済ませ、前日に作業をするのを辞めましょう。
これに尽きる。
バグらせてGuess要素を含むクソ問になったにも関わらず、解いてくださった方々には非常に感謝しています。また、解こうとしてくださった方々にも非常に申し訳ないと思っています。
そしてもう一つ重要な点ですが、今回作問ミスを犯した結果、非常に多くの問い合わせをいただきました。
こちらとしても申し訳無さと恥ずかしさで、修正したバイナリを配布したい気持ちは大きかったのですが、バグに気づいた時点で解いていたチームが存在していたため、公平性の観点からヒントになるような情報や、解くのが簡単になるような修正したバイナリの配布を行うわけにはいかず、FlagのHashのみを提供する。という判断をしました。
この判断自体は間違っていなかったと信じていますが、不便を強いたことを改めてお詫びします。
終わりに
またこの記事はCTF Advent Calendar 2024及びn01e0 Advent Calendar 2024の3日目の記事とします。
Comments