読者です 読者をやめる 読者になる 読者になる

Pwn De Ring

初心者がPwnを勉強していくために使っている標準出力先です。

配列の初期化について観察してみた

1.記事の目的

C言語による配列の初期化が逆アセンブルすると,どのように行われているかを確認する.

2.プログラム概要

// array.c
#include <stdio.h>
int main(){
    char buf[10] = {}; // <- こ↑こ↓
    fgets(buf, 10, stdin);
    return 0;
}

10byteのchar型配列を用意し,そこのfgetsを用いて標準入力をさせる.今回の記事では,コメント文のある箇所を変化させていき検証していく.まずは,プログラムを以下の環境でコンパイルした.

$ uname -a
Linux vagrant-ubuntu-trusty-64 3.13.0-68-generic #111-Ubuntu SMP Fri Nov 6 18:17:06 UTC 2015 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a                                   
Distributor ID: Ubuntu
Description:    Ubuntu 14.04.3 LTS
Release:        14.04
Codename:       trusty

$ gcc --version                               
gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4

$ gcc -m32 array.c

3.プログラム解析

作成した実行ファイルをgdb-pedaで読み込みdisas mainをした結果が以下である.短いので全て乗せた.

gdb-peda$ disas main 
Dump of assembler code for function main:
   0x0804848d <+0>:     push   ebp
   0x0804848e <+1>:     mov    ebp,esp
   0x08048490 <+3>:     and    esp,0xfffffff0
   0x08048493 <+6>:     sub    esp,0x20
   0x08048496 <+9>:     mov    eax,gs:0x14
   0x0804849c <+15>:    mov    DWORD PTR [esp+0x1c],eax
   0x080484a0 <+19>:    xor    eax,eax    
   0x080484a2 <+21>:    mov    DWORD PTR [esp+0x12],0x0 #①
   0x080484aa <+29>:    mov    DWORD PTR [esp+0x16],0x0 #②
   0x080484b2 <+37>:    mov    WORD PTR [esp+0x1a],0x0  #③
   0x080484b9 <+44>:    mov    eax,ds:0x804a024
   0x080484be <+49>:    mov    DWORD PTR [esp+0x8],eax
   0x080484c2 <+53>:    mov    DWORD PTR [esp+0x4],0xa
   0x080484ca <+61>:    lea    eax,[esp+0x12]
   0x080484ce <+65>:    mov    DWORD PTR [esp],eax
   0x080484d1 <+68>:    call   0x8048350 <fgets@plt>
   0x080484d6 <+73>:    mov    eax,0x0
   0x080484db <+78>:    mov    edx,DWORD PTR [esp+0x1c]
   0x080484df <+82>:    xor    edx,DWORD PTR gs:0x14
   0x080484e6 <+89>:    je     0x80484ed <main+96>
   0x080484e8 <+91>:    call   0x8048360 <__stack_chk_fail@plt>
   0x080484ed <+96>:    leave  
   0x080484ee <+97>:    ret    
End of assembler dump.

今回のコードでは,char型配列の宣言のみしかしていない.しかし①②③の部分を見てみるとDWORD(4byte),DWORD(4byte),WORD(2byte)となっており,それぞれに0を格納している.つまり合計すると10byteの領域に0を格納していることになるので,わざわざ10個0を書かなくとも{}と書くだけで配列の要素が全て0で初期化されることがわかった.

次にchar buf[10] = {};のある部分を変えて同様の処理を行った時のdisas main結果が以下である.前回と同じ処理の部分は削除し重要な部分のみを乗せた.

Dump of assembler code for function main:
   0x080484ab <+30>:    lea    ebx,[esp+0x14]
   0x080484af <+34>:    mov    eax,0x0
   0x080484b4 <+39>:    mov    edx,0xfa
   0x080484b9 <+44>:    mov    edi,ebx
   0x080484bb <+46>:    mov    ecx,edx
   0x080484bd <+48>:    rep stos DWORD PTR es:[edi],eax

今回は,前回のように何行も0をmovするという処理行われていない.どの部分をどう変化させたのかについて考えながら読んでほしい.

まずlea命令(アドレスを格納する命令)でebxに[esp+0x14]のアドレスつまりesp+0x14という値が格納される. 次にmov命令によりeaxが0になる,同様にmov命令でedx=0xfa(=250)が格納される. さらにmov命令でediにebx(=esp+0x14)が格納され,ecxにはedx(=0xfa)が格納される. そして次のrep stos DWORD PTR es:[edi],eaxという命令がとても重要である. この命令は,1つの命令というよりも以下の二つの命令に分かれているとイメージしたほうが理解しやすい.

  1. rep
  2. stos DWORD PTR es:[edi], eax

1のrepはrepetitionの略で反復を意味する.つまりループだ.このrep命令は,ecxに格納された回数繰り返すという命令で,今回の場合だと命令2を250回繰り返すことになる.競技プログラミング特有のマクロ定義でもよくrepという文字列は見かけるだろう.

次に2のstos命令は,レジスタの値をメモリに格納する.したがって[esp+0x14]←0の処理を行う.さらにeflagsレジスタの10bit目であるDF(direction flag)によってediに4byte加算あるいは減算する.ちなみに今回の場合だと4byte加算される.

上記の2点を踏まえるとこの命令は,ループにより配列の0初期化を行っていることになる.今回のプログラムでは,ある部分を変化させたと行ったが予測がつくだろう.変更箇所は配列の要素数である. その要素数はecxに入った回数分繰り返し,さらに配列のインデックスであるediが4byteずつ移動していることから,250 * 4 = 1000.したがってchar buf[1000] = {}である. 我々が配列に1,2,3,45を格納する際には,手打ちで行っているかもしれないが,1~1000の際にはいちいち手なんかでは打っていられないだろう.それと同様にコンピュータもループという手段を用いるのだと思うと面白かった.

実は本記事で言いたかったことは, mainの先頭部分にrep stos (ryという命令が見えたらそれは配列の初期化をしているものだと考えられるということだ. これにより配列のサイズがわかるとfgetsをつかっているのに配列のサイズより大きく入力を受け付けるようになっているなど脆弱性を探す時のヒントとなるはずだ.