逆アセンブルで遊んでみる(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()」の番地だと思ったんですけど、なんでなんだろ。これも調べてみます。

プロシージャコール元へのリターン処理は、省略。