linux x64での関数呼び出し
今回、久しぶりにCTFに参加しました。1問も解けず(泣
ただ、単なる勘違いで逃したようなものなので、その勘違いを供養するためにメモを書きます。
x64の関数呼び出しをx86と同じものだと勘違いしていて、いきなりrdiとかでてきて「ナンジャコリャ」となりました。
なので、そこらへんのちょっとしたメモをば。
呼び出し規約(Call Convention)
サブルーチンが呼び出される際に従わなければいけないルールのこと。ABI(Application Binary Interface)*1の一部。
今回は、サブルーチン(関数)が呼ばれる前にセットされる引数の扱いについてまとめます。
今回の環境は以下の通り
$ uname -a Linux forensic-virtual-machine 4.13.0-36-generic #40~16.04.1-Ubuntu SMP Fri Feb 16 23:25:58 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux
$ cat /etc/os-release NAME="Ubuntu" VERSION="16.04.4 LTS (Xenial Xerus)" ID=ubuntu ID_LIKE=debian PRETTY_NAME="Ubuntu 16.04.4 LTS" VERSION_ID="16.04" HOME_URL="http://www.ubuntu.com/" SUPPORT_URL="http://help.ubuntu.com/" BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" VERSION_CODENAME=xenial UBUNTU_CODENAME=xenial
この環境において、例として以下のようなプログラムを考えます。
// sample.c #include<stdio.h> int add(int a, int b, int c); int main(){ int x,y,z; int sum; x = 2; y = 3; z = 5; printf("x = %d, y = %d, z = %d\n", x, y, z); sum = add(x,y,z); printf("x + y + z = %d\n",sum); return 0; } int add(int a, int b, int c){ return a + b + c; }
着目するのは、定義したadd関数です。
add関数は、3つの引数を足した値を戻り値として返すものになります。
そして、このソースコードを以下のようにコンパイルし、x86バイナリとx64バイナリを生成します
# x86 binary gcc -m32 sample.c -o sample_x86
# x64 binary gcc sample.c -o sample_x64
では、ここからx86とx64でadd関数の引数がどのように扱われるのか見ていきます。
x86における引数の扱い
まずは、sample_x86のバイナリをデバッガで見た場合。
add関数の引数は以下のように設定されます。
0x0804844a <+63>: push DWORD PTR [ebp-0x10] ; 5 0x0804844d <+66>: push DWORD PTR [ebp-0x14] ; 3 0x08048450 <+69>: push DWORD PTR [ebp-0x18] ; 2 0x08048453 <+72>: call 0x804847e <add>
add関数の引数は3つともスタックにpushされ、その後にadd関数がコールされている形になります。
つまり、x86の場合、関数の引数はスタックに積まれることになります。
(実際、サブルーチンに入った後はebpのオフセット指定で引数の値を取り出して演算を行います)
x64における引数の扱い
では、ここからが本題。x64においてはどうなるのか。
まずは、sample_x64バイナリをデバッガで見た場合を確認。
add関数の引数が以下のように設定されます
0x000000000040055d <+55>: mov edx,DWORD PTR [rbp-0x8] ; 5 0x0000000000400560 <+58>: mov ecx,DWORD PTR [rbp-0xc] ; 3 0x0000000000400563 <+61>: mov eax,DWORD PTR [rbp-0x10] ; 2 0x0000000000400566 <+64>: mov esi,ecx 0x0000000000400568 <+66>: mov edi,eax 0x000000000040056a <+68>: call 0x40058d <add>
add関数の引数はレジスタのedi, esi, edxに格納された後、add関数のコールがかかっている状態です。
つまり、x64の場合、関数の引数がレジスタに格納されるという形になります。
また、どの引数がどのレジスタに格納されるかについては、linux64-abiの資料*2を確認してみました。
引数 | 第1引数 | 第2引数 | 第3引数 | 第4引数 | 第5引数 | 第6引数 |
---|---|---|---|---|---|---|
レジスタ | rdi | rsi | rdx | rcx | r8 | r9 |
第6引数まではレジスタを使用し、第7引数以上はスタックが使用されるということらしい。
なので、試しにadd関数の引数を8個に引き伸ばした関数を作り、どうなるか試して見ます。
ソースはこちら。
// ext_sample.c #include<stdio.h> int add(int a, int b, int c, int d, int e, int f, int g, int h); int main(){ int s,t,u,v,w,x,y,z; int sum; s = 1; t = 2; u = 3; v = 4; w = 5; x = 6; y = 7; z = 8; sum = add(s,t,u,v,w,x,y,z); printf("%d\n",sum); return 0; } int add(int a, int b, int c, int d, int e, int f, int g, int h){ return a + b + c + d + e + f + g + h; }
このソースでは、add関数で8つの引数を指定します。なので、後ろの2つの引数についてはスタックを使っているはずですので、その部分にも注目していきたいと思います。
では、コンパイルした後のx64バイナリをデバッガにかけて、add関数の引数設定を見ていきます。
0x000000000040052e <+8>: mov DWORD PTR [rbp-0x24],0x1 0x0000000000400535 <+15>: mov DWORD PTR [rbp-0x20],0x2 0x000000000040053c <+22>: mov DWORD PTR [rbp-0x1c],0x3 0x0000000000400543 <+29>: mov DWORD PTR [rbp-0x18],0x4 0x000000000040054a <+36>: mov DWORD PTR [rbp-0x14],0x5 0x0000000000400551 <+43>: mov DWORD PTR [rbp-0x10],0x6 0x0000000000400558 <+50>: mov DWORD PTR [rbp-0xc],0x7 0x000000000040055f <+57>: mov DWORD PTR [rbp-0x8],0x8 0x0000000000400566 <+64>: mov r9d,DWORD PTR [rbp-0x10] 0x000000000040056a <+68>: mov r8d,DWORD PTR [rbp-0x14] 0x000000000040056e <+72>: mov ecx,DWORD PTR [rbp-0x18] 0x0000000000400571 <+75>: mov edx,DWORD PTR [rbp-0x1c] 0x0000000000400574 <+78>: mov esi,DWORD PTR [rbp-0x20] 0x0000000000400577 <+81>: mov eax,DWORD PTR [rbp-0x24] 0x000000000040057a <+84>: mov edi,DWORD PTR [rbp-0x8] ; 8th argument 0x000000000040057d <+87>: push rdi 0x000000000040057e <+88>: mov edi,DWORD PTR [rbp-0xc] ; 7th argument 0x0000000000400581 <+91>: push rdi 0x0000000000400582 <+92>: mov edi,eax ; 1st argument 0x0000000000400584 <+94>: call 0x4005ab <add>
意図的に改行を入れて少し見やすくしています。
上の段では、1から8までの値をメモリに格納している部分になります。
次の段では、引数として1から8の値を設定するため、レジスタへそれぞれ値を格納しています。
ただし、<+81>の部分では、第1引数の値をeaxに入れています。通常であれば第1引数の値はrdiに入れるはずです。
なぜそんなことをするかというと、その下でrdi(edi)を使っているからです。
その下の段にいくと、値をediにいれてスタックにpushしています。この部分が第7引数と第8引数の設定部分になります。
引数をスタックにpushする際は、rdiレジスタを使っています。スタックに値を入れる順番は後ろに引数からです(ここはx86と変わらない)。
ここでrdiを使ってしまっているので、その上の段で第1引数をrdiに入れるとまずいですね。
(だけど、なんでわざわざrdiレジスタを使うんだろう?他のレジスタじゃだめなのだろうか....)
で、第7引数と第8引数を入れ終わった後はrdiが使えるので、eaxに入っている第1引数の値をrdiに入れ、add関数をコールするという流れになります。
今後の課題
今まで32bitのプログラムの解析ばっかりしていたので、x64の知識が全くついていない。
今回みたいな勘違いが起きないように、x64についてもちゃんと勉強しておこうと思う。
というわけで、以下の本を買ったので読み進めていこう(時間ばあればまとめていきたい)。
www.shoeisha.co.jp
*1:CPUの命令セットや呼出規約といった、ユーザーのプログラムとOS・ライブラリ間のバイナリレベルのインターフェース
*2:https://software.intel.com/sites/default/files/article/402129/mpx-linux64-abi.pdf