逆アセンブルで遊んでみる(7)スタック破壊検出コードの自動生成
今回は、関数内で配列を確保する。です。
1 void func() 2 { 3 char array[] = "12345678"; 4 }
00000000 <func>: 0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 18 sub $0x18,%esp 6: 65 a1 14 00 00 00 mov %gs:0x14,%eax c: 89 45 f4 mov %eax,-0xc(%ebp) f: 31 c0 xor %eax,%eax 11: c7 45 eb 31 32 33 34 movl $0x34333231,-0x15(%ebp) 18: c7 45 ef 35 36 37 38 movl $0x38373635,-0x11(%ebp) 1f: c6 45 f3 00 movb $0x0,-0xd(%ebp) 23: 8b 45 f4 mov -0xc(%ebp),%eax 26: 65 33 05 14 00 00 00 xor %gs:0x14,%eax 2d: 74 05 je 34 <func+0x34> 2f: e8 fc ff ff ff call 30 <func+0x30> 34: c9 leave 35: c3 ret
なんか、難しそうなのが出てきましたが…。
また、スタックの図を描きながら、アセンブリコードを執拗に追いかけてみます。
スタックフレームの設定
0: 55 push %ebp 1: 89 e5 mov %esp,%ebp 3: 83 ec 18 sub $0x18,%esp
上のコードを実行した時点での、スタックの状態です(1段階4バイト単位です)。
ebp | Stack | esp |
---|---|---|
★(空き) | ← | |
★(空き) | ||
★(空き) | ||
★(空き) | ||
★(空き) | ||
★(空き) | ||
→ | Saved EBP | |
Saved EIP | ||
… | ||
呼出元関数のスタックベース |
24バイト分の空間が確保されました。
カナリア値の埋め込み
6: 65 a1 14 00 00 00 mov %gs:0x14,%eax c: 89 45 f4 mov %eax,-0xc(%ebp) f: 31 c0 xor %eax,%eax
gccによって自動生成される、SSP(Stack Smashing Protector)のコードが埋め込まれています。空き領域に、カナリア値が埋め込まれました。%gsというのはセグメントレジスタですが、「%gs:014」というコードの意味は、要調査です。また、eaxレジスタが、0に初期化されています。
ebp | Stack | esp |
---|---|---|
(空き) | ← | |
(空き) | ||
(空き) | ||
★カナリア値 | ||
(空き) | ||
(空き) | ||
→ | Saved EBP | |
Saved EIP | ||
… | ||
呼出元関数のスタックベース |
配列の初期化
11: c7 45 eb 31 32 33 34 movl $0x34333231,-0x15(%ebp) 18: c7 45 ef 35 36 37 38 movl $0x38373635,-0x11(%ebp) 1f: c6 45 f3 00 movb $0x0,-0xd(%ebp)
ebp | Stack | esp |
---|---|---|
★__ __ __ 31 | ← | |
★32 33 34 35 | ||
★36 37 38 00 | ||
カナリア値 | ||
(空き) | ||
(空き) | ||
→ | Saved EBP | |
Saved EIP | ||
… | ||
呼出元関数のスタックベース |
「-0x15」「-0x11」というオフセット指定が4の倍数でないところが、ちょっと気色悪いですが、実際にスタックに格納してみると、確かにカナリア値に隣接して格納されますね。1バイトでもオーバーランしたら、Stack Smashingが検出されるです。うまく出来てるなー。
ところで、配列の初期化は、4バイト単位で格納されるのですね(ゆえにmovl命令)。末尾のnull terminatorは1バイトなので、movb命令。
23: 8b 45 f4 mov -0xc(%ebp),%eax 26: 65 33 05 14 00 00 00 xor %gs:0x14,%eax 2d: 74 05 je 34 <func+0x34> 2f: e8 fc ff ff ff call 30 <func+0x30> 34: c9 leave 35: c3 ret
スタック上のカナリア値をeaxレジスタに転送して、ebpから-12バイト先にあるオリジナルのカナリア値と比較しています。XORの結果はZFにストアされ、ZFの値をもとに、条件ジャンプ命令(JE)を実行します。これで、スタックに埋め込まれたカナリア値が改変されたかどうかを検出するのですね。
カナリア値が改変されていない場合、XORの結果は0となるので、ZFフラグは0となります。JE命令は、ZF==1の場合、ジャンプを実行するので、ジャンプは実行されず、後続のleave命令〜ret命令が実行されます。カナリア値が改変された場合、XORの結果は1となり、ZF==1となるので、オフセット0x30にジャンプします。
ebp | Stack | esp |
---|---|---|
__ __ __ 31 | ← | |
32 33 34 35 | ||
36 37 38 00 | ||
★カナリア値 | ||
(空き) | ||
(空き) | ||
→ | Saved EBP | |
Saved EIP | ||
… | ||
呼出元関数のスタックベース |
カナリア値が改変された場合のジャンプ先(オフセット0x30)は、「__stack_chk_fail()」の番地だと思ったんですけど、なんで
プロシージャコール元へのリターン処理は、省略。