Let's analyze system calls
x86-64版Linuxのシステムコールを解析してみましょう!
初心者向けヒント1: システムコールの呼び出し規約とgccの拡張インラインアセンブラ
x86-64版Linuxにおけるシステムコールの呼び出し規約は、 https://gitlab.com/x86-psABIs/x86-64-ABI からダウンロードできる System V Application Binary Interface AMD64 Architecture Processor Supplement 資料中の A.2.1 Calling Conventions や、 https://man7.org/linux/man-pages/man2/syscall.2.html に記述があります。
システムコールを実行するには、システムコール番号を rax レジスタに、最大6個までの引数を順に rdi, rsi, rdx, r10, r8, r9 レジスタに格納してから syscall 命令を実行します。
システムコール実行結果は rax レジスタに格納されます。
gccの拡張インラインアセンブラ構文は、 https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html や https://gcc.gnu.org/onlinedocs/gcc/Machine-Constraints.html に記述があります。
本問題で使用している1つ目の syscall 命令用の拡張インラインアセンブラでは、次のことを指定しています。
OutputOperands パラメーターは未指定です。
InputOperands パラメーターで、拡張インラインアセンブラを実行する前に、rax レジスタに 318 を、rdi レジスタに &result_1 を、rsi レジスタに sizeof(result_1) を、rdx レジスタに 0 を設定しています。
Clobbers パラメーターで、拡張インラインアセンブラの実行中に rcx レジスタと r11 レジスタ、メモリが変更されることを指定しています。
なお、syscall 命令そのものが rcx レジスタと r11 レジスタを変更します。
本問題で使用している2つ目の syscall 命令用の拡張インラインアセンブラでは、次のことを指定しています。
OutputOperands パラメーターで、拡張インラインアセンブラの実行後に、rax レジスタの内容をresult_2 へ設定しています。
InputOperands パラメーターで、拡張インラインアセンブラを実行する前に、rax レジスタに 102 を設定しています。
Clobbers パラメーターで、拡張インラインアセンブラの実行中に rcx レジスタと r11 レジスタが変更されることを指定しています。
初心者向けヒント2: 問題へのアプローチ
本問題を解くアプローチはいくつかあります。
1つの方法は、x86-64版Linuxのシステムコール番号を調査して、対応するシステムコールの引数や戻り値を確認する方法です。
別の方法は、逆コンパイラを使用する方法です。
逆コンパイラによって、システムコール呼び出しを最初から完璧に逆コンパイルできるものもあれば、初期状態では逆コンパイル結果が貧弱なものの追加操作で完璧に逆コンパイルできるものもあります。
やったーsyscall!私syscall大好き!ということでwriteupを書きます。
writeupを書かなくてもいいくらいヒントが充実していますが…
配布されるのはchal.c、chal、Dockerfile、compose.yamlの4つです。
てっきりELFだけかと思っていたので意外ですね。chal.cを読んでみます。
#include <stdio.h>
#include <stdlib.h>
int main() {
unsigned long result_1, result_2, sum, input;
asm("syscall"
: /* No outputs. */
: "a"(318), "D"(&result_1), "S"(sizeof(result_1)), "d"(0)
: "rcx", "r11", "memory");
asm("syscall"
: "=a"(result_2)
: "a"(102)
: "rcx", "r11");
sum = result_1 + result_2;
printf("result_1: %lu\n", result_1);
printf("What will be the sum? ");
fflush(stdout);
scanf("%lu", &input);
if (input == sum) {
const char *env_flag = getenv("FLAG");
printf("Correct! FLAG: %s\n", env_flag ? env_flag : "Alpaca{DUMMY}");
}
else {
puts("Incorrect...");
}
}
直接アセンブラ書いてる…
何かと何かの値を足したものを答えるとflagがもらえるみたいですね。
見た感じ、result_1は教えてくれそうなので、result_2が分かれば正解できそう。
syscallというのは、writeとかopenとかを書くとlinuxカーネルがそれを実行してくれる機能です。(素人の理解です)
ディスアセンブラすると関数名ではなくsyscallとかswiなどで出てきて一見中身を追うことはできなさそうですが、呼び出しに規則があって、「どのレジスタに何の値が入っているとこのsystem call」というのが決まっています。
man syscallするとどのアーキテクチャがどんな方法でsyscallを実行するかが分かります。
Arch/ABI Instruction System Ret Ret Error Notes
call # val val2
───────────────────────────────────────────────────────────────────
alpha callsys v0 v0 a4 a3 1, 6
arc trap0 r8 r0 - -
arm/OABI swi NR - r0 - - 2
arm/EABI swi 0x0 r7 r0 r1 -
arm64 svc #0 w8 x0 x1 -
blackfin excpt 0x0 P0 R0 - -
i386 int $0x80 eax eax edx -
ia64 break 0x100000 r15 r8 r9 r10 1, 6
loongarch syscall 0 a7 a0 - -
m68k trap #0 d0 d0 - -
microblaze brki r14,8 r12 r3 - -
mips syscall v0 v0 v1 a3 1, 6
nios2 trap r2 r2 - r7
parisc ble 0x100(%sr2, %r0) r20 r28 - -
powerpc sc r0 r3 - r0 1
powerpc64 sc r0 r3 - cr0.SO 1
riscv ecall a7 a0 a1 -
s390 svc 0 r1 r2 r3 - 3
s390x svc 0 r1 r2 r3 - 3
superh trapa #31 r3 r0 r1 - 4, 6
sparc/32 t 0x10 g1 o0 o1 psr/csr 1, 6
sparc/64 t 0x6d g1 o0 o1 psr/csr 1, 6
tile swint1 R10 R00 - R01 1
x86-64 syscall rax rax rdx - 5
x32 syscall rax rax rdx - 5
xtensa syscall a2 a2 - -
今回はx86-64なので、オペコードはsyscall、何を呼び出すかはraxの値によることが分かりました。
chalのディスアセンブル結果を見てみます。chal.cを見てももちろん大丈夫ですが、私はsyscallについてはアセンブリを読む方が分かりやすいと思っているのでそうします。
00000000000011e9 <main>:
11e9: f3 0f 1e fa endbr64
11ed: 55 push %rbp
11ee: 48 89 e5 mov %rsp,%rbp
11f1: 48 83 ec 30 sub $0x30,%rsp
11f5: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
11fc: 00 00
11fe: 48 89 45 f8 mov %rax,-0x8(%rbp)
1202: 31 c0 xor %eax,%eax
1204: b8 3e 01 00 00 mov $0x13e,%eax
1209: 48 8d 7d d0 lea -0x30(%rbp),%rdi
120d: be 08 00 00 00 mov $0x8,%esi
1212: ba 00 00 00 00 mov $0x0,%edx
1217: 0f 05 syscall
1219: b8 66 00 00 00 mov $0x66,%eax
121e: 0f 05 syscall
1220: 48 89 45 e0 mov %rax,-0x20(%rbp)
1224: 48 8b 55 d0 mov -0x30(%rbp),%rdx
syscallが二回登場します。上がresult_1、下がresult_2を出すためのsyscallです。
ここでraxの値に何が入っているか確認します。objdumpではeaxとなっていますが、eaxはraxの下位バイトなのでeaxを見れば大丈夫です。
1204: b8 3e 01 00 00 mov $0x13e,%eax
最初のsyscallのraxはこれです。次は
1219: b8 66 00 00 00 mov $0x66,%eax
これですね。
というわけで、次はこれが何を呼ぶかについて確認します。
代表的なものはChromiumのドキュメントに一覧が載っているので、私はよくここを見てます。
x86-64のtableを見てみると、%raxの値に何が入るとどのsyscallを呼び出すかが分かります。今回はresult_1が0x13e、result_2が0x66なので、result_1はgetrandom、result_2はgetuidであることが分かりました。
getuidは実行しているユーザのIDを教えてくれるsyscallです。このchalだけだと分からないので、配布ファイルを探したところ、Dockerfileの中に
# Sets UID and GID
USER 404:404
という親切な記述がありました。
というわけで、ncでつないでresult_1の値と404を足した数字を入力してFlagをもらえました。
syscall、CTFではそんなに出てこないんですが、私はこれをGhidraでちまちま探して関数名を変えてくところがGhidraでいちばん楽しいと思っています。面白かったです。ありがとうございました。