ごちゃごちゃしたIT勉強記録

自分用メモ。セキュリティ関連の内容を書いてます。

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