簡単なプログラムをgdb-pedaで解析する

CTFのBinary系問題が全然解けないので,基礎を見直すつもりで書きます(;^ω^)
(ので,おそらくどこかしら間違ってると思います.何かあれば教えてください<(_ _)>)

触れている内容は,基礎的なアセンブリ命令とスタックフレームについてです.

使用するプログラム

パスワード照合を行う単純なCプログラムです.
ユーザが入力した文字列がkeyならCorrect!と表示し,不一致ならInvalid!と表示します.

gdb-prac.c

#include <stdio.h>
#include <string.h>

int main(){
  char passwd[] = "key";
  char input[32];

  puts("Input the password.");
  fgets(input, sizeof(input), stdin);
  strtok(input, "\n");

  if(!strcmp(passwd, input)){
    puts("Correct!");
  }else{
    puts("Invalid!");
  }

  return 0;
}

実行結果

root@kali:# ./gdb-prac 
Input the password.
idontknow
Invalid!

root@kali:# ./gdb-prac 
Input the password.
key
Correct!

gdb-pedaで逆アセンブル

読み込んで,main関数を逆アセンブルします.

root@kali:# gdb -q gdb-prac
Reading symbols from gdb-prac...(no debugging symbols found)...done.
gdb-peda$ disas main
Dump of assembler code for function main:
   0x000000000000075a <+0>:	push   rbp
   0x000000000000075b <+1>:	mov    rbp,rsp
   0x000000000000075e <+4>:	sub    rsp,0x30
   0x0000000000000762 <+8>:	mov    DWORD PTR [rbp-0x4],0x79656b
   0x0000000000000769 <+15>:	lea    rdi,[rip+0xf4]        # 0x864
   0x0000000000000770 <+22>:	call   0x600 <puts@plt>
   0x0000000000000775 <+27>:	mov    rdx,QWORD PTR [rip+0x2008d4]        # 0x201050 <stdin@@GLIBC_2.2.5>
   0x000000000000077c <+34>:	lea    rax,[rbp-0x30]
   0x0000000000000780 <+38>:	mov    esi,0x20
   0x0000000000000785 <+43>:	mov    rdi,rax
   0x0000000000000788 <+46>:	call   0x610 <fgets@plt>
   0x000000000000078d <+51>:	lea    rax,[rbp-0x30]
   0x0000000000000791 <+55>:	lea    rsi,[rip+0xe0]        # 0x878
   0x0000000000000798 <+62>:	mov    rdi,rax
   0x000000000000079b <+65>:	call   0x630 <strtok@plt>
   0x00000000000007a0 <+70>:	lea    rdx,[rbp-0x30]
   0x00000000000007a4 <+74>:	lea    rax,[rbp-0x4]
   0x00000000000007a8 <+78>:	mov    rsi,rdx
   0x00000000000007ab <+81>:	mov    rdi,rax
   0x00000000000007ae <+84>:	call   0x620 <strcmp@plt>
   0x00000000000007b3 <+89>:	test   eax,eax
   0x00000000000007b5 <+91>:	jne    0x7c5 <main+107>
   0x00000000000007b7 <+93>:	lea    rdi,[rip+0xbc]        # 0x87a
   0x00000000000007be <+100>:	call   0x600 <puts@plt>
   0x00000000000007c3 <+105>:	jmp    0x7d1 <main+119>
   0x00000000000007c5 <+107>:	lea    rdi,[rip+0xb7]        # 0x883
   0x00000000000007cc <+114>:	call   0x600 <puts@plt>
   0x00000000000007d1 <+119>:	mov    eax,0x0
   0x00000000000007d6 <+124>:	leave  
   0x00000000000007d7 <+125>:	ret    
End of assembler dump.

下準備

   0x000000000000075a <+0>:	push   rbp
   0x000000000000075b <+1>:	mov    rbp,rsp
   0x000000000000075e <+4>:	sub    rsp,0x30

まず,プログラム実行にあたっての下準備が行われています.
最初に,ペースポインタ(rbp)の保存を行っています.
その後,スタックポインタ(rsp)をベースポインタに代入し,スタックポインタの値を減じることで,スタックフレームの位置を移動させています.

   0x0000000000000762 <+8>:	mov    DWORD PTR [rbp-0x4],0x79656b

その後,rbp-4のアドレスの位置に0x79656bという値が代入されています.
おそらく変数の初期化をしているのだと予測できますが,一応その位置までステップ実行して中身を見てみます.

gdb-peda$ x/s $rbp-4
0x7fffffffe19c:	"key"

予想通り,指定した位置にkeyが代入されていました.

ここまでの流れを図に直すと,以下のような感じでしょうか.
f:id:Szarny:20170827164542p:plain

※追記 〇〇のプログラム はスタックフレーム(スタックセグメント)の意です.

fgetsのcall

   0x0000000000000788 <+46>:	call   0x610 <fgets@plt>
   0x000000000000078d <+51>:	lea    rax,[rbp-0x30]

この部分では,fgetscallされた後,rbp-0x30のアドレスが,raxレジスタに代入されています.
つまり,ユーザによって入力された値が,rbp-0x30の位置に代入された後,更にそれをraxにも代入していると考えられます.

f:id:Szarny:20170827165447p:plain:h350

strcmpのcallとtest命令

   0x00000000000007a0 <+70>:	lea    rdx,[rbp-0x30]
   0x00000000000007a4 <+74>:	lea    rax,[rbp-0x4]
   0x00000000000007a8 <+78>:	mov    rsi,rdx
   0x00000000000007ab <+81>:	mov    rdi,rax
   0x00000000000007ae <+84>:	call   0x620 <strcmp@plt>
   0x00000000000007b3 <+89>:	test   eax,eax
   0x00000000000007b5 <+91>:	jne    0x7c5 <main+107>

rbp-30のアドレスがrdxに代入された後,rsiに代入されています.
また,rbp-4のアドレスがraxに代入された後,rdiに代入されています.
さっき見た通り,rbp-30にはユーザの入力が,rbp-4にはkeyが存在しているため,
rsiにはユーザの入力が,rdiにはkeyがそれぞれ代入されていると考えられます.

入力箇所でuserinputと入力し,strcmpの直前までステップ実行を行ったあと,以下のように確認できました.

gdb-peda$ x/s $rsi
0x7fffffffe170:	"userinput"
gdb-peda$ x/s $rdi
0x7fffffffe19c:	"key"

そのあと,strcmpcallされた後に,test命令によってeax同士の論理積がとられます.
また,直後にjne命令 (=jump if not equal)があることから,このtest命令によってゼロフラグが成立しなかった(strcmpの結果が0ではなかった)場合,main+107にジャンプさせられていると考えられます.

ジャンプの後

   0x00000000000007b5 <+91>:	jne    0x7c5 <main+107>
   0x00000000000007b7 <+93>:	lea    rdi,[rip+0xbc]        # 0x87a
   0x00000000000007be <+100>:	call   0x600 <puts@plt>
   0x00000000000007c3 <+105>:	jmp    0x7d1 <main+119>
   0x00000000000007c5 <+107>:	lea    rdi,[rip+0xb7]        # 0x883
   0x00000000000007cc <+114>:	call   0x600 <puts@plt>

もし,jne命令によってジャンプしなかった(strcmpの返り値が一致していた)場合,rip+0xbcのアドレスをrdiに代入し,putscallしたのち,main+119まで無条件ジャンプしています.
一方で,jne命令によってジャンプした場合,rip+0xb7のアドレスをrdiに代入し,putscallしています.
一致していた場合としていなかった場合それぞれにおいて,rdiの中身を出力してみると,jne命令の有り無しによって異なる文字列が出力されることが分かります.

#一致時
gdb-peda$ x/s $rdi
0x55555555487a:	"Correct!"

#不一致時
gdb-peda$ x/s $rdi
0x555555554883:	"Invalid!"

leaveとretで終了

   0x00000000000007d6 <+124>:	leave  
   0x00000000000007d7 <+125>:	ret    

いよいよ最後です.プログラムでの処理をすべて終えた後は,それまで実行していたプログラムに処理を戻す必要があります.
そこで,leave命令ret命令が用いられます.これらの命令は,以下の命令と等価です.

leave mov rsp,rbp
pop rbp
ret pop rip

つまり,leave命令によってスタックフレームを管理するポインタを元に戻した後,ret命令によってプログラムカウンタが復元され,本プログラムでの処理はすべて終了します.

f:id:Szarny:20170827171920p:plain

※追記 スタックセグメントとコードセグメントを混同していました.インストラクションポインタ(rip)の矢印は無視してください.

おわりに

これだけ単純なプログラムでも,アセンブリ言語に直すと,途端に難しく感じてしまいます.
はやくバイナリがサクサク読めるマンになりたい(;´Д`A ```