チートツール(メモリエディタ)でメモリ内を覗いたり書き換えたりして遊んでみた
プログラミングをしていても、イマイチポインタやメモリ構造、オーバーフローが理解できないし、イメージしづらいてってのがありました。んで、じゃあメモリをのぞいてみようということで。それが手軽にできるツールがメモリエディタです。普段はゲームのチートとかによく使われますね。これから書くようなことを理解すればチートコードの意味も見えてくるかも。それでチートコードが自分で見つけられるようになるかは保証できませんがw あ、エディタとしては「うさみみハリケーン」を利用させていただきました。こちらからダウンロードできます。→http://www.vector.co.jp/soft/win95/prog/se375830.html
なんかいろいろと使い方があるみたいですが、ここではそんなに複雑なことをしません。指定アドレスを探して書き換えるだけです。。自分で作ったプログラムをいじるだけなんで変数のアドレスは自分で表示させることができますしね。
ってことで今回利用するプログラムのほうは以下のようになります。あ、ぼくの環境はBCCでコンパイルして書き換えたりしました。もしかしたら他の環境では違う結果になるかもしれません。そこらへんはご了承ください。
#include<stdio.h> void foo(void){ printf("正常動作\n"); } void bar(void){ printf("this program was cracked!!\n"); } int main(){ //変数の書き換え実験用 int a=3; //オーバーフローの実験用変数 int b=6; char danger[2]; char danger2[2]; //関数ポインタ書き換え実験用 void (*func) (void); void (*func2) (void); func=foo; func2=bar; printf("*関数のアドレス\nfoo:%p\n",foo); printf("bar:%p\n",bar); printf("*関数ポインタのアドレス\nfunc:%p\n",&func); printf("func2:%p\n\n",&func2); printf("*変数のアドレス\na:%p\n",&a); printf("b:%p\n",&b); printf("danger:%p\n\n",&danger); //書き換えの実験 printf("〜〜変数の書き換え実験〜〜\n"); printf("aの最初の値%d\n書き換えてみてください(何か入力で進める)",a); scanf("%s", &danger2); printf("aの書き換え後の値%d\n\n",a); //オーバーフローの実験 printf("〜〜オーバーフローの実験〜〜\n"); printf("文字列の入力(2文字以上)"); scanf("%s", &danger); printf("結果\ndanger:%s\nb:%d\n",danger,b); printf("〜〜関数ポインタ書き換えの実験〜〜\n"); //関数ポインタ書き換え実験 printf("そのまま実行\n"); (*func)(); printf("書き換えてみてください(何か入力で進める)"); scanf("%s", &danger2); (*func)(); return 0; }
とりあえず、このプログラムを実行してみてください。次にうさみみハリケーンを開いて対象のプロセス(実行したプログラム)を選択します。ぼくはanalysis.exeという実行ファイルを作ったのでそれを選択しました。これで好きなようにいじり放題ですね。まずはどういう風に並んでいるか確認してみます。
変数aのアドレスは0x0012FF50なのでそこから見てみましょうか。「メニュー」→「移動」→「表示アドレスを指定」をクリック、新しいウィンドウが開くのでアドレス(僕の環境では0012FF50)を入れてアドレス変更で移動。そうすると確かにaの変数のところに初期値の3が格納されていることがわかります。(下図)
頭では分かっていたんですが、やっぱり目に見えると感動です。
ついでに別の視点からも見てみます。プログラム上では初期化した変数はa→bの順でした。しかしメモリを見るとbのほうが前、aのほうが後に配置されています。不思議です。アドレスだけを見てもそれはわかりますが、メモリエディタ上でみるとbのほうに格納されている6が見えてよりわかりやすいと思います。そして、よく本に書かれているようにやっぱりintは4byteってのも確認できます。また、関数ポインタfuncの格納している値(つまり0x0012FF44の位置に格納されている値)を確認してみると4byteで区切ってみると「50 11 40 00」ってのが見えます。最初これはなんでかな?と思いました。格納しているfoo関数のアドレスは「00 40 11 50」のはずなのに...ここにCPUの罠が。僕の使っているCPUはintel製なのでリトルエンディアン。つまり逆順に並んでいる。だから格納されている値を逆に読んでみると見事に一致しました。そういうトリックだったんですね。気づいてしまえば簡単ですけど気づかないとはまります。ちなみにこれに気づいた理由は「関数ポインタを続けて宣言しているのでメモリ上に連続して並んでいるはず」って過程で見てそのアドレス差をみると4だけずれている。てことは大きさは4byte...とかやっているうちに昔本の片隅で読んだことを思い出しましたw そういえば、さっきのaの06 00 00 00ってのもリトルエンディアンで配置されてましたね。いや、勉強になりました。
さて、見ているだけじゃ面白くないです。書き換えてみましょう。aのアドレス(0x0012FF40)のところを03から08ににしてみます。それで、プロンプト(実行しているソフト)のほうに戻り適当にaとでもいれてEnterですすめると...見事に表示されるのが8に変わっています。プログラムは変数の値をやはりメモリから呼び出しているってのが確認できます。↓
これがチートの正体だったり。チートコードってのは特殊なコードでもなんでもなくて単なる変数のアドレスだったわけですね。それで、そこの値を書き換えているだけなのです。
さて、次はオーバーフローの実験。よくscanfは危険だから使うなと言われます。このプログラムは意図して危険さが目に見えるように作っているわけですが。まず、dangerっていういかにも脆弱性を含んでいそうな変数のアドレスはプロンプトに表示されているように0012FF4A。その2byte先のアドレスを計算してみると...bの変数のアドレスですね。メモリエディタを見ると恐ろしいことにそこにはしっかりとbの値「6」が格納されています。ここでdangerに2byte以上の値を書き込むとどうなるのでしょうか?試してみましょう。僕はabcdefgと入れてみました。結果は以下の通り。
bの値がおかしなことになっています。いったい何が?ってことでメモリエディタをのぞいてみましょう。まずは入力する前の画像。
このがぞうをみてわかるように連続した61〜67までの値がdangerの範囲を突き抜けてbの範囲まで入ってしまっていますね。これがbがおかしくなった原因だと思います。今回は意図的に組み込んだためわかりやすく出ますが、ふつうはこんなにわかりやすくは出ないはずなので...考えるだけで恐ろしいバグが組み込まれそうです。ところで、ちょっと本題とはそれるのですがdangerの内容は入力したとおりに正確に表示されています。これはなんででしょうか?おそらくprintfの仕様で%sはポインタを進めていってNULLまで読みだすという仕様になっているからだと思います。
さて、最後が結構面白いもの。関数ポインタに格納されている関数を書き換えてしまいます。funcのアドレスは0x0012FF44なのでそこを見てみましょう。ここには先ほども見たようにfoo関数のポインタが入っています。これをbar関数のアドレスに書き換えてみましょう。最初の50を60に変えるだけです。それでプロンプトに戻り進めてみると...
明らかに危なげな文字列が表示されています。そうbar関数が呼び出されてしまったわけですね。
これとさっきのオーバーフローを利用すると好きな関数が実行できたりするかも...とかほんとはやりたかったんですが、勉強不足でできませんでした。理論上はできる気がします。
と今回の記事はここで終了なんですが、関数の中身を書き換えたりなんてこともできそうです。そこらへんは機械語の知識が必要なのでちょっと今の僕には手におえないのですが...もっと勉強してメモリ上でプログラミングなんてしたいですねw 最後に全然関係ないですが、画像よく見たら実行ファイル名誤字ってましたw 今気づきました。