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やリターンアドレスを書き換えられます。
たぶん想定解
しかしこれは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でした。なぜ違いが出るんでしょうか。よくわかりませんでした。)
もう一つの解法
自分的には結構難しいです。
//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配列にシェルコードをいれ、実行することが目的です。
- 最初のモンスターたちの名前入力で、バッファオバーフローを使い、runGame関数のスタックフレームの、(スタックピボットのために)old EBPと(最後の異常なreadInput呼び出しの準備のために)リターンアドレスを書き換えます。
- そしてドラゴンに負ける前の最後の一回の、コマンド読み込み時readWrapperで通常どおりinputStr(0x080ed044)に書き込みを受付ますので、パディング兼コマンドと1stペイロード(のちの異常なreadInput呼び出し時の引数の準備)を渡します。
- ドラゴンに負けた後、リターンアドレスが書き換えられているので通常とは異なり、上記のアセンブリのとこに飛びます。readInputが呼ばれるため、そこで2ndペイロード(シェルコードへのリターンアドレスとシェルコード本体)を積みます。0x80ed040(inputStar近辺)から積むみたい。中途半端に見えますが、ここから積むとぴったりになるように計算されています。
- readWrapperからリターンするときにスタックピボットが起こりESPがinputStr配列のあたりを指すようになり、そこに用意しておいたリターンアドレスの指す先にリターンするのでシェルコードが実行されます。
上記の説明でなにがなにやら…な場合は素直にCaesurusさんのエクスプロイトコードを読むとわかると思います。
1.リターンアドレスとold EBPの書き換え
2.最後の異常なreadInput関数呼び出し時に渡す引数の準備(1stペイロード)
3.inputStr周辺にリターンアドレスとシェルコードを入力(2ndペイロード)
4.スタックピボットして、ESPがinputStr周辺を指すようにしてシェルコード起動
- 呼び出し元で引数をスタックにプッシュしてcall命令。
- call命令で、スタックにリターンアドレスをプッシュ。
- 呼び出され側で、EBPレジスタをスタックにold EBPとしてプッシュ。
- EBPレジスタにESPの値をコピー。
- …処理…
- 呼び出され側でスタックをいじった場合、ESPを元に戻す。
- leave命令でESPをEBPレジスタから復元。次にEBPレジスタにold EBPをポップ。
- ret命令でリターンアドレスをポップし、呼び出し元に戻る。
そのため、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}
コメント