picoCTF2017 choose解説

pwnable.twに比べればpicoCTFは易しめらしいですがそれでさえ私は解答できません

hgarrereynのwriteup
Caesurusのwriteup

ソースコードの中には、
「//TODO: These are all the same size, so remove in future version for brevity
read」
とか

//Can use an orc ptr as all the structs are the same size.
        orc * tempEnemy = (orc *)(enemies + enemOffset);

とか、ドラゴンを除く全てのモンスターの構造体のサイズが同じとして考えているようなコメントがあります。
p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Menlo; color: #008400; background-color: #ffffff}

公式ヒントの指摘している、「間違った前提」とはまさにこのことで、構造体の中に変数を記述する順番が違うだけで、アラインメントのため、構造体のサイズはしばしば異なるものになります。
冒頭のhgarrereynのwriteupが詳しく説明しています。
アラインメントについてはこちら。ハードウェアを意識したプログラミングの基礎 データ型のアラインメントとは何か,なぜ必要なのか?

ユニコーンとケンタウロスのみが、他のモンスターの構造体と変数の宣言の順が違うため、サイズが違います。24バイトです。
しかし、11匹のモンスターたちを格納する配列enemiesは次のように宣言されており、20バイトのオークを基準としてサイズを計算し、確保しています。

#define NUMMONSTERS 11
#define ENEMSIZE sizeof(orc)

char enemies[ENEMSIZE * NUMMONSTERS];

そして、enemiesに実際にデータを格納してゆくcollectEnemData関数は次のように実装されているため、ユニコーンかケンタウロスを指定した場合、バッファオーバーフローします。

void collectEnemData(char * enemies, char * enemyChain){
    unsigned int offset = 0;
    for(int i = 0; i < NUMMONSTERS; i++){
        char index = enemyChain[i];
        switch(index){
            case 'O':
            case 'o':
                buildOrc((orc *)(enemies + offset));
                offset += sizeof(orc);
                break;
            case 'G':
            case 'g':
                buildGoblin((goblin *)(enemies + offset));
                offset += sizeof(goblin);
                break;
            //省略
        }

    }
}

各モンスターには12バイト、name配列が用意されており好きな名前をつけられます。これらを利用し、バッファオーバーフローを利用して、enemies配列が確保されているrunGame関数のスタックフレームでのold EBPやリターンアドレスを書き換えられます。

たぶん想定解

hgarrereynのwriteupで説明されている、想定解と思われる解法です。
シェルコードとして思いつくのはexecveに”/bin/sh”とかを渡してシェルの起動をするものです。
しかしこれはname配列の12バイト(末尾ヌルバイトを除き11バイト)に収まりません。
なので、複数のモンスターたちのname配列に、シェルコードを細切れにして格納し、ジャンプ命令で飛び飛びに実行して行きます。

ASLRがオフなのでスタックアドレスのリークは一度すればよく、これはprintEnemy関数の最後で次のように”魔法使いの視力”を持ってればアドレスを表示することができることを利用します。

void printEnemy(char * enemy){
    //...省略...
    if(ctfer.wizardSight){
        printf("Your sight shows the enemy at %pn", enemy);
    }
}

そして、勝利したときの処理が書いてあるprocessWinnings関数をみると、トロールに勝利したときにこれがもらえることがわかります。
よって、モンスター指定時の1匹めにトロールを指定、残りは適当に選び、一匹めのトロールに勝利することで、アドレスが表示されます。

(アドレスはローカルで試して調べても同じなのでは…?と思って試しましたが、ASLRをオフにした手持ちのubuntu x64環境では0xffffce94、picoCTFのサーバでは0xffffdba4でした。なぜ違いが出るんでしょうか。よくわかりませんでした。)

もう一つの解法

Caesurusのwriteupで説明されています。 結構難しいので、こっちじゃない解法を思いつく人の方が多いはず
自分的には結構難しいです。

readInput関数は、
//Reads input from user, and properly terminates the string
unsigned int readInput(char * buff, unsigned int len){
   //...省略...
}

のように、バッファのアドレスと長さを受け取り、入力を受け付ける関数です。

   0x08048eb7 <+28>:    pushl  0xc(%ebp)    ;readInputの第二引数、len
   0x08048eba <+31>:    pushl  0x8(%ebp)    ;readInputの第一引数、buff
   0x08048ebd <+34>:    call   0x8048e3c <readInput>

readWrapper関数内部で上記のアセンブラのようにreadInput関数を呼んでおり、ここに飛んでくれば、readInput関数が利用できます。そのとき入力先として指定するバッファはスタック上にあるため、渡すアドレスは、EBPレジスタをベースアドレスとしてアドレスが計算されるようです。

バッファオーバーフローで書き換えられるのはEBPレジスタでなくスタック上のold EBPということを覚えておいてください。

流れとしては、
inpuStr配列にシェルコードをいれ、実行することが目的です。

  1. 最初のモンスターたちの名前入力で、バッファオバーフローを使い、runGame関数のスタックフレームの、(スタックピボットのために)old EBPと(最後の異常なreadInput呼び出しの準備のために)リターンアドレスを書き換えます。
  2. そしてドラゴンに負ける前の最後の一回の、コマンド読み込み時readWrapperで通常どおりinputStr(0x080ed044)に書き込みを受付ますので、パディング兼コマンドと1stペイロード(のちの異常なreadInput呼び出し時の引数の準備)を渡します。
  3. ドラゴンに負けた後、リターンアドレスが書き換えられているので通常とは異なり、上記のアセンブリのとこに飛びます。readInputが呼ばれるため、そこで2ndペイロード(シェルコードへのリターンアドレスとシェルコード本体)を積みます。0x80ed040(inputStar近辺)から積むみたい。中途半端に見えますが、ここから積むとぴったりになるように計算されています。
  4. readWrapperからリターンするときにスタックピボットが起こりESPがinputStr配列のあたりを指すようになり、そこに用意しておいたリターンアドレスの指す先にリターンするのでシェルコードが実行されます。

上記の説明でなにがなにやら…な場合は素直にCaesurusさんのエクスプロイトコードを読むとわかると思います。

1.リターンアドレスとold EBPの書き換え

前述したように、一部のモンスターが、アラインメントのせいで想定より大きいサイズであることを利用し、runGame関数のスタックフレームにあるリターンアドレスとold EBPを書き換えます。
全ての戦いが終わりrunGame関数からリターンするときに、readInput関数を呼び出すために、リターンアドレスには上記のアセンブラの場所(0x08048eb7)、スタックピボットをしてESPがinputStr周辺を指すようにするために、old EBPには(0x80ed044)から-6した値を格納します。

2.最後の異常なreadInput関数呼び出し時に渡す引数の準備(1stペイロード)

runGame関数からのリターン時に呼び出されるreadInput関数に渡す引数の準備です。
1stペイロードをつむ段階では、ctfer構造体をダンプすればわかりますが体力はまだ15 (0xf)残っています。
1stペイロードの先頭でパディングついでにコマンドHを入力、次にreadInputの第一引数*bufとしてinputstr周辺(0x80ed040)を、そのあとにreadInputの第二引数lenとしてAAAを入力します。
コマンドはAでも良いと思います。また、lenはAAAでなく十分大きいものならなんでも良いです。
先頭のコマンドを読むので、ドラゴンに負け、runGame関数からリターンします。

3.inputStr周辺にリターンアドレスとシェルコードを入力(2ndペイロード)

runGame関数のスタックフレームのリターンアドレスが書き換えられていたので、リターン時にreadInput呼び出しの手前に飛びます。また、old EBPが書き換えられていたのでEBPレジスタが(0x80ed044)から-6した値になります。
readInput関数への引数は、EBPレジスタをベースアドレスとして指定されるので、1stペイロードで準備したものが引数として渡されることになります。
そこで、readInput関数を用いて、inputStr周辺(0x80ed040)に、パディング2バイト、シェルコードへのリターンアドレス、シェルコード本体、の順に入力します。

4.スタックピボットして、ESPがinputStr周辺を指すようにしてシェルコード起動

readInput関数からリターンする先は、readWrapper関数の、call readInputの次の命令です。
ここで、通常の関数呼び出しの手順を書きますと、ABIによると思いますが今回は:
  1. 呼び出し元で引数をスタックにプッシュしてcall命令。
  2. call命令で、スタックにリターンアドレスをプッシュ。
  3. 呼び出され側で、EBPレジスタをスタックにold EBPとしてプッシュ。
  4. EBPレジスタにESPの値をコピー。
  5. …処理…
  6. 呼び出され側でスタックをいじった場合、ESPを元に戻す。
  7. leave命令でESPをEBPレジスタから復元。次にEBPレジスタにold EBPをポップ。
  8. ret命令でリターンアドレスをポップし、呼び出し元に戻る。
という風になってます。
readWrapper関数からのリターン時に、EBPレジスタの値がESPレジスタにコピーされるので、ESPはinputStr周辺を指すようになります。

つまりleave命令でEBPレジスタの値(0x80ed044 – 0x6 = 0x80ed03e)がESPに復元され、次にold EBPがポップされるため、ESP は+4され、0x80ed042になります。
ret命令でリターンアドレスがポップされるためESPに+4して、ESPは0x80ed046になります。
0x80ed042 ~ 0x80ed045にシェルコードへのリターンアドレス(0x80ed04a)が、そして0x80ed04aからシェルコードが積まれてます。
そのため、readWrapper関数の最後のret命令でシェルコードが実行されます。





ちなみにシェルコードのダンプは以下のようになってます…

(gdb) x/10 0x80ed04a
   0x80ed04a <inputStr+6>:    (bad)  
   0x80ed04b <inputStr+7>:    xor    %ecx,%ecx
   0x80ed04d <ctfer+1>:    xor    %edx,%edx
   0x80ed04f <ctfer+3>:    mov    $0xb,%al
   0x80ed051 <ctfer+5>:    int    $0x80
   0x80ed053 <ctfer+7>:    call   0x80ed048 <inputStr+4>
   0x80ed058:    das    
   0x80ed059:    bound  %ebp,0x6e(%ecx)
   0x80ed05c:    das    
   0x80ed05d:    jae    0x80ed0c7 <_dl_static_dtv+71>

0x80ed04aの中身は(bad)となっているんだけど、中断せずに実行されるのはなぜなんでしょうね。(無知)

p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Menlo; color: #000000; background-color: #ffffff}

コメント