ksnctf 4 Villager A
ksnctfの4問目を解いていきます。ポイントはかなり高め。解くにはアセンブリの知識と、スタックのしくみについての理解が必要です。
SSHでサーバにアクセスする
TeraTermと問題文の情報を使ってサーバへリモートでログインします。問題の分類的にはpwn(サーバにアクセスして脆弱性を見つけたり、それを利用してフラグを読みだしたりすること)ですね。
サーバにはflag.txtがありますが権限がないため読み出しができません。
q4の解析
q4を起動すると名前を聞かれます。その後flagが欲しいかと聞かれるのですがyesと言っても教えてくれません。
gdbによるq4プログラムの解析を行っていきます。以下のコマンドを実行してください。
gdb -q q4<br>disas main
するとかなり長いアセンブリが出力されると思います。1行ずつ解析するのは大変かもしれませんが、call文だけ見れば何をしているのかだいたいわかったりします。以下区切り事にざっくりとした解説を載せていきます。
①
「What's your name?」という文字列をputchar関数で出力しています。
②
fgetsでユーザーからの入力を受付け、printfで「Hi, (ユーザーの入力した名前)」と出力する箇所。
③
putchar関数を呼び出して改行した後、一気にmain+205までジャンプしています。
④
詳しく解説はしませんが、普通に実行するとmain+102にジャンプします。
⑤
「Do you want the flag?」と永遠に聞いてくる部分です。strcmp関数で入力文字列と「no」を比較して同じならプログラムを終了します。
⑥
fopenでflag.txtファイルを開いて、その中身をprintf関数で出力させる処理です。しかし直前(0x0804868f <+219>)にジャンプ命令があり、普通に実行するだけではこの処理は行われません。
⑥の処理を実行させられるかどうかが問題を解く鍵になります。
printfの脆弱性を付く
この問題を解決するにはprintf関数の性質とフォーマット指定子%nを使った脆弱性を利用します。
printf関数には引数の数に決まりがありません。上の画像のように書式文字列をたくさん指定することでprintf関数は存在しない第2引数以降の値を参照しようとして、スタックの値を読み出してしまうのです。ここでは入力した値aaaaがスタックの上から6番目(0x61616161)に保存されていることが分かりました。
また%nは出力した文字列の総バイト数を第2引数以降で指定されたアドレスに格納します。 例として、
int main(){ int i; printf("Hello%n\n" , &i); }
上記のプログラムでは「Hello」と5バイト分出力しているのでiには5が入ります。もしここに第2引数を指定しなければ、代わりにスタックの値が書き換えられてしまうことになります。
これらを組み合わせて攻撃を行います。具体的にはputchar関数のアドレスを示す箇所を⑥のfopen関数が始まる部分(0x8048691)にします。
objdump -d q4
putchar関数のアドレスは0x80499e0に格納されているのでこのアドレスの中身を0x8048691にすることで攻撃は成功します。
\xe0\x99\x04\x08\xe2\x99\x04\x08%\$hn6
指定子%hnを使うことで下位2バイトに対しての書き込みを行うことができます。指定子を記述する前にすでに8バイト分出力しているので、上記の例ではアドレス0x80499e0の上位2バイトには8が入ります。ここには0x8691を入れたいのでたくさん文字を出力させる必要がありますが、これもフォーマット指定子を使って簡単に実現できます。
\xe0\x99\x04\x08\xe2\x99\x04\x08%34441x%\$hn6
「%34441x」で34441バイト分出力します。34441 + 8 =34449 = 0x8691なので下位ビットには8691が入ります。
\xe0\x99\x04\x08\xe2\x99\x04\x08%34441x%\$hn6%33139%\$hn7
同じようにして上位2ビットを0x804にしていきます。すでに0x8691バイト出力していますが、0x10804にすれば5桁以上が切り捨てられて0x804になります。0x10804-0x8691=0x8173 = 33139なので残り33139バイトを出力します。まとめると以下のようになります。
echo -e "\xe0\x99\x04\x08\xe2\x99\x04\x08%34441x%6\$hn%33139x%7\$hn" | ./q4
まとめ:アセンブリ読もう
CTFをやる人はアセンブリは必須知識なので学習しておくとよいと思います。
Linuxをはじめよう!:アセンブリをやってみよう! 0x100
初めての人はこの辺から。アセンブリのしくみや書き方が基礎から書かれています。
ksnctf 3 Crawling Chaos
ksnctf の3問目を解説していきます。
ksnctf.sweetduet.info
問題文はunya.htmlへのリンクで、アクセスすると送信フォームがあるだけ。
XSSかなと思ったけどNoと表示されるだけでした。
Chromeでページのソースを表示します(Ctrl + U)。
するとJavascriptタグには意味不明の顔文字たちが。送信ボタンを押すとこのコードが実行されているようです。
難読化されていて読めませんが、console.logによる出力でデコードできないか試してみます。やり方は簡単で、ソースコードを全コピーしてJavaScriptのコードの部分をconsole.log()で囲み、htmlファイルとして保存するだけです。
ブラウザで見るとそれらしきコードが復元されています。入力文字列1文字目のアスキーコードを×1、2文字目を×2...していき、それが配列pの各値と等しいかを比べているだけなので逆算のプログラムは極めて簡単なものになります。
var p=Array(70,152,195,284,475,612,791,896,810,850,737,1332,1469,1120,1470,832,1785,2196,1520,1480,1449); var t="" for(var i=0; i<p.length; i++) t += String.fromCharCode(p[i]/(i+1)) console.log(t);
なぜこんなことができるのか?
JavaScriptでは空の配列に演算子を付け加えると、それが数字や文字列として解釈されるようになります。
例:
+ → 0
~ → -1
ここで+演算子は足し算をしているのではなく、オペランド(ここでは空配列の)を数値に変換しています。 空配列を数値変換すると0になります。
(![]+"")→ true
({}+"") → [object Object]
同様に上記の記号を使用することでfalseやtrueなどの文字列を記号だけで表現できます。
これらの記号を組み合わせることで任意のjavascriptを実行できるようになります。詳しくは以下のリンクを参考にしてください。
記号だけのJavaScriptプログラミングの基本原理 - JPerl Advent Calendar 2010 Sym Track
(」・ω・)」うー!(/・ω・)/にゃー!encode - kusano_kの日記
ksnctf2 Easy Cipher
これからセキュリティの復習もかねてksnctfの問題を解き直していこうと思います。最初はeasycipherから。
問題文
EBG KVVV vf n fvzcyr yrggre fhofgvghgvba pvcure gung ercynprf n yrggre jvgu gur yrggre KVVV yrggref nsgre vg va gur nycunorg. EBG KVVV vf na rknzcyr bs gur Pnrfne pvcure, qrirybcrq va napvrag Ebzr. Synt vf SYNTFjmtkOWFNZdjkkNH. Vafreg na haqrefpber vzzrqvngryl nsgre SYNT.
謎のアルファベットが羅列されていて、このままでは読むことができません。
Writeup
これはシーザー暗号の一つであるROT13と呼ばれるものです。
例えば元の文が「A」なら「N」へ、「B」なら「O」という風に13文字分アルファベットをずらしていくという、最も古典的な暗号です。
変換できるサイトがあったのでそれを利用します。
ROT XIII is a simple letter substitution cipher that replaces a letter with the letter XIII letters after it in the alphabet. ROT XIII is an example of the Caesar cipher, developed in ancient Rome. Flag is FLAGSwzgxBJSAMqwxxAU. Insert an underscore immediately after FLAG.
フラグが出てきました。FLAGの後に「_」をつけるのを忘れずに。
Tips
古典暗号にはシーザー暗号のほかにVigenere暗号があり、過去のSECON CTF においてVigenere暗号に関する問題が出題されました。知っておいて損はないと思います。
参考:
古典暗号 - Vigenere暗号とカシスキー・テスト - ₍₍ (ง ˘ω˘ )ว ⁾⁾ < 暗号楽しいです
SECCON 2016 Online CTF: Vigenere (過去問題のWriteup)
crkme09
http://doomo.xyz/crack/crkme09.exe
前↓
前回と同じく名前とパスワードを入力する問題だったので同じ要領で解析しようと思ったけどなにかおかしい。
DB文がたくさん羅列していて、ラベル名にも関数が少ない。
前にやった問題で似たようなコードがあった。圧縮されているらしいのは確かだが、[右クリック] - [分析(Analysis)] - [コードを分析(Analyse code)]しても変わらない。
↑UPXでアンパックできるみたい。
アンパックした後もう一度Ollyで開くと今度はちゃんとしたアセンブリが表示されている。
00401255 > 83F8 16 CMP EAX,16
今回のパスは0x16(=22)文字。長い。
その後2つのサブルーチンに飛んで、
TEST EAX, EAXによる比較が行われる。この時点でEAXが0なら正解処理に飛ぶため2つのサブルーチンを実行し終わった後のEAXは1でなければならない。
先に下の関数(00401329)から解析していく。
パスワードの先頭4文字のアスキーコード - 0x283A4132 = 0x11111111でないと関数が終了してしまう。
0x283A4132 + 0x11111111 = 0x394B5243 = 9KRC
リトルエンディアンなので逆にしてCRK9。
その次の処理。ECXに5文字目のパスワードを入れて0x2D(-)だったらOK。
それ以降の文字列は「MI5GQ51I-7V9WW2NT」と後ろから比較しているのでこれの逆を当てはめる。
「MI5GQ51I-7V9WW2NT」の部分はNameによって変わると思う。
crkme08
http://doomo.xyz/crack/crkme08.exe
※前
実行
名前とパスワードを聞かれる。もちろんどちらもわからないので今回は2つの情報をリバースエンジニアリングで見つける必要がある。
解析
「GetDlgItemTextA」にBPを仕掛けて、適当に名前とパスワードを入力して実行する。
上図のアセンブリから分かることは
・正解パスワードは8文字であること
・入力したNameを引数として関数004012A8を呼び出していること
その返り値をEDXに格納していること
・パスワードを引数として関数004012D0を呼び出していること
・EAXとEDXが等しいなら正解処理(0040128F)へ進むこと
004012A8の解析
①EAXに入力した1文字のASCIIコードを足す
②EAXを2乗する
③①~②を入力文字分やる
だいたいこんな感じ。
EAXの値は入力するNameの値によってさまざま。
004012D0の解析
見た感じ、
・パスワードが16進数で使う文字(0~9とA~F)
その数値がそのままEBXに代入される
・それ以外の文字が入ってたらEBXを0xFFFFFFFFにする。
最終的にEBXはEAXに代入されて戻り値になる。
このEAXが関数004012A8で生成された値と同じになればよい。NameがAの時に生成される値は
0x183FB4ACなので、これが正解のパスワード。
crkme07
http://doomo.xyz/crack/crkme07.exe
実行
crkme07.exeを実行すると4つのテキストボックスが出てきて、シリアル番号的なものをを要求される。
解析
OllyDbgでcrkme07.exeを開く。アセンブリが表示されているウィンドウで右クリック->検索->ラベル名(現在のモジュール)をクリックする。
入力したシリアルが正しいかどうかを判定するには必ず入力テキストの情報を取得しなければならないので、そのあたりの処理に着目しよう。
GetWindowTextAが参照されているところでBPをセットして実行してみる。
1文字入力した瞬間にBPにぶつかる。多分半角数字以外のものが入力されていないかのチェックなのでいったんBPを外して適当なシリアルを全部入力する。
入力し終えたらもう一度GetWindowTextAのところにBPをセットして「登録」を押下。
.
.
.
ブレークポイントに引っかからない。
↑もう一度ラベル名を見てみる。テキストを取得する関数にはGetWindowTextのほかに、GetDlgItemIntが見つかった。
テキストを整数値に変換してそれを戻り値とする。今回の入力は数値だけなのでこれが怪しい。ここにBPを設置してもう一度実行してみる。
↑今度はちゃんと引っかかった。
↑まずこのへんの処理から。ループ構造になっている。
GetDlgItem関数で戻ってきた値(入力した整数値)をEBXが示すアドレス(0x40311C~)に格納する。そしてEBXを+2、ECXとEDXを+1してにECX > 4ならもう一度ループする。
ECXを+1するのはループカウンタ(Cのfor文の変数iみたいな)だから。
EDXはGetDlgItemInt関数を呼び出すときの引数として使われている。
004012BC . 52 PUSH EDX ; |ControlID
ControllIDは整数値に変換したいテキストの識別子なのでこれを+1することで次のテキストを取得できるという仕組み。
EBXを足すのはテキストを格納するアドレスをずらすため。
004012C5 . 66:8903 MOV WORD PTR DS:[EBX],AX
↑ここではワード長(32ビット)でデータを格納するので+2している。
無事ループを抜けるとEBXが指定したアドレスに整数1234(=0x04D2)が4つ分入ってる。
0x40311Dに格納されている数値をESIに代入。一つ目のテキストに入力した整数値だ。
その数値と0x66666667を乗算する。
IMUL関数はEAXの乗算の結果をEAXに格納するが、おさまりきらないときは上位ビットがEDXに格納される。
さらにEDXを2ビット分算術右ビットシフト(=÷4する)。
さらに0x1F(=31)ビットシフトしたものを加えた結果が
EDXが0x3E7ならOK。
0x66666667と掛け算して上位9~12ビットが0x03E7ならなんでもいいらしいので逆算してみる(計算過程は略)。1つめのテキストボックスには9990を指定すればいいことが分かった。
0x40311F(2番目のテキスト)に入力する値は0x270F(=9999)以下でなければならないことが分かる。(3番目のテキストも同様。)
このへんはよくわからない。読むのが嫌になるくらい長い。CMP命令も一番下に行くまでなくて、入力した数値から別の数値を作ってる感じ。
この問題を作った方も解説できないとのこと。
なので、こういうときはCMP命令周りを見てみるといいらしい。
004013B0 . 66:3BE8 CMP BP,AX
今、BPには4番目のテキストボックスの数値、AXには謎のメカニズムで生成された謎の数値が入っている(ここでは0x1732)。これが一致すればいいので4番目のテキストに0x1732(=5938)を入力する。
登録済と表示された。今回もいろいろ正解パターンがある。
crkme06
前→
前と比べて正解処理にたどり着くまでの道のりがけっこう長くなっている。
・正解文字列は10文字(004011FD . 83F8 0A CMP EAX,0A)
0040120C . 0FBF06 MOVSX EAX,WORD PTR DS:[ESI]
EAXにESIの先頭2文字(16バイト分)を格納する。ESIには入力データへのポインタが格納されているのでEAXには入力文字列の先頭2文字が格納される。
00401214 . 66:3D 454F CMP AX,4F45
このEAXの値は0x454F(= EO)でなければならない。
(EAXの値は0x4F45だけど、リトルエンディアンなので格納するときに順序が逆になる)
・3文字目以降が数字かを判定して、EDXに7文字目以降の数字の合計を格納。
その後EAXにEDXを転送してEDXを0にする。
0040124A . F7F1 DIV ECX
この命令、単にEAXをECXで除算するだけでなく、商をEAXに、余りをEDXに格納する。その後CMP EDX, 0 となってるのでEAXがちょうど10で割り切れるようにしなければならない。
EAXには8文字目~10文字目の数字の合計が格納されているのでこれらの和が10の倍数になればいいはず。
・最初がEO
・3~7文字目は数字
・8~10文字目は数字で合計が10の倍数
なら何でもいいみたい。