好奇心の足跡

飽きっぽくすぐ他のことをしてしまうので、忘れないため・形にして頭に残すための備忘録。

picoCTF2019 Warmup Challenge on twitter 問題のwrite-up

中高生向けのCTF、picoCTF 2019 の warmup問題のwrite-up です。本番問題の write-up へのリンクはこちらを参照。

kusuwada.hatenablog.com

大会開催の一週間くらい前に、公式のtwitterアカウントから "Warmup Challenge" と題してtwitterで問題が出されていました。これのwriteupが見つからなかったので残しておきます。

解説を見ずに問題だけときたい場合はこちら。

  1. https://twitter.com/picoctf/status/1169647893088743425
  2. https://twitter.com/picoctf/status/1170014360510640128
  3. https://twitter.com/picoctf/status/1170460648746233856
  4. https://twitter.com/picoctf/status/1171113644475764736
  5. https://twitter.com/picoctf/status/1171428844739297282
  6. https://twitter.com/picoctf/status/1171796439846047744

Challenge 1

Message from A: 0x1afe93e83137e76d2226d97c40512040; d=31337; N=0x50618b968b8603e9e870e7d878e866e3

message, d, N ということなので、RSA暗号でしょう。

from Crypto.Util.number import long_to_bytes

c = 0x1afe93e83137e76d2226d97c40512040
d = 31337
n = 0x50618b968b8603e9e870e7d878e866e3 #106844723640410863741046875242417907427

plain = pow(c, d, n)
print(long_to_bytes(plain))

実行結果

$ python 1.py
b'Are you awake?'

Challenge 2

Challenge 2 in our warmup series! Incoming Transmission: aHR0cHM6Ly93d3cucGFzdGViaW4uY29tL3Jhdy80VE5mQXZNSg==

base64っぽいですね。base64 decodeしてみます。ちょいなのでオンラインでdecodeしてもらいました。

https://www.pastebin.com/raw/4TNfAvMJ

urlが出てきた。

f:id:kusuwada:20191018135948p:plain

読めない。これ横から(Display傾けて)読んだら読めるやつとか?うーん?読めないが?

と思ってまるごと等幅フォント設定のテキストエディタに突っ込んだら読めた。

f:id:kusuwada:20191018140039p:plain

THE NAMELESS MUST BE STOPPED

Challenge 3

A suspicious transmission...

f:id:kusuwada:20191018140105p:plain

この文字列をテキストに起こすと

                 13
GVZR VF EHAAVAT BHG
JR ARRQ LBHE URYC

定期的にスペースが入っていることから、換字暗号っぽいなーと予想。右上の13が ROT13 を連想させるので、ROT13にかけてみます。(アルファベットを13文字ずらして読む)

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

import string

cipher = """GVZR VF EHAAVAT BHG
JR ARRQ LBHE URYC"""
alphabet = list(string.ascii_uppercase)

def rot13(c):
    return alphabet[(alphabet.index(c) + 13) % 26]

plain = ''
for c in cipher:
    if c.isalpha():
        plain += rot13(c)
    else:
        plain += c
print(plain)
        

実行結果

$ python rot13.py 
TIME IS RUNNING OUT
WE NEED YOUR HELP

Challenge 4

A flier with some faded writing. Maybe there is more to it... https://t.co/R6YUHe9EXk

リンク先からはbmpが入手できます。

f:id:kusuwada:20191018140133j:plain

JOIN US だそうです。

のっぺりした小さめの画像は2値化するといいことが多いので、輝度255以外を全部黒塗りしてみると、こんな画像になりました。

f:id:kusuwada:20191018140153p:plain

赤線の部分が輝度254ピクセルを抜き出したところです。バイト列の先頭がアヤシイ。ということで、先頭行だけ 245=1, それ以外の輝度=0 として2値化してみました。

111110010010101101100100001010100110101001111001001100000011001101100100001010100111101001111

これをモールス信号に見立ててみたり、01に変換して8個ずつ切り出してstringに変換してみたり、もしかしてバーコードかもということで縦に引き伸ばしてバーコード状の画像にして読み込んでみたり。

f:id:kusuwada:20191018140219p:plain

うーーん、なかなかflagに繋がりません。これを考えていたらpicoCTF本番が始まってしまいました。

悔しいので、前から気になっていたForensicsツールが揃っているこちらの環境を用意しました。

github.com

まずはDockerをinstall

$ git clone git@github.com:DominicBreuker/stego-toolkit.git
$ docker build -t <image_name> .
$ docker run -it --rm -v <$pwd>/stego-toolkit/data:/data <image_name> /bin/bash

これでimageが起動しました。早い!楽ちん!そしてすぐにツールが使えます。上記ではstego-toolkitフォルダに予め用意してあるdataディレクトリをマウントしました。
最初は/dataディレクトリにいます。<$pwd>/stego-toolkit/data ディレクトリに解析対象のdataファイルを突っ込みます。

/data# ls
README.md  suspicious_flier.bmp

README(dataディレクトリではなくroot直下)に書いてあるツールを上から順にbmpに対応しているものをかけていきます。試したコマンドを。大体ホストにも入れてあるツールでしたが念の為。

# exiftool suspicious_flier.bmp
# foremost suspicious_flier.bmp
# identify -verbose suspicious_flier.bmp
# stegoveritas suspicious_flier.bmp

この次に実行したzstegの時にurlが出てきました!
zsteg実行結果

# zsteg -a suspicious_flier.bmp 
b1,bgr,lsb,xy       .. text: "!https://pastebin.com/raw/45SxeQkE"
b2,rgb,lsb,xy       .. file: SoftQuad DESC or font file binary
b2,rgb,msb,xy       .. file: VISX image file
...(略)

ツールにまるっと頼ってしまいましたが、Forensics不勉強なもので致し方なし。どうやって隠したんだろう?

https://pastebin.com/raw/45SxeQkE に飛んでみると、flagっぽいテキストが現れました。(これも私のブラウザ環境では読めなかったので、等幅フォント設定のエディタに貼り付けてみました)

f:id:kusuwada:20191018140405p:plain

Are you up to the task? I must be sure that you are ready... Further instruction will follow. MOST Importantly: If you find yourself stuck, you must remember that prime numbers will help show you the way!

Challenge 5

P8ooboe.eu2rluM4Ortc8ef9ls4y3htrodeua3.tsoyy3UTgt4lt3iw4hoe!ok.M5y3rr8L3cL6oSN3ehehbiout3r5gsg5aeeaTtaaolwerl5oyaesf6rnseddtoiasy.ncs.p8gci

f:id:kusuwada:20191018140441j:plain

scramble()されたtextが問題文かな?ということで、このコードの逆をやってあげます。※元コードがpython2だったので、python3で scramble 関数も少し書き直しています。
warmupだから舐めてましたが、結構時間かかった…。プログラムも多分冗長になってしまった。もっとシュッと書けそうな気がする。。。

#!/usr/bin/env python3
# -*- coding:utf-8 -*-

from functools import reduce

ctext = "P8ooboe.eu2rluM4Ortc8ef9ls4y3htrodeua3.tsoyy3UTgt4lt3iw4hoe!ok.M5y3rr8L3cL6oSN3ehehbiout3r5gsg5aeeaTtaaolwerl5oyaesf6rnseddtoiasy.ncs.p8gci"

def scramble(msg):
    ctext = ""
    words = msg.split()
    wi = 0
    offs = list(range(len(words)))  # wordsのindex配列
    ctrs = [len(w) for w in words]  # wordsの長さ配列
    while reduce(lambda a,b: a+b, ctrs, 0): # ctrsの合計が0になるまで
        if ctrs[wi]:
            ctext += (words[wi][offs[wi] % len(words[wi])])
            if not(offs[wi] % len(words[wi])):
                ctext += str(len(words[wi]))
            offs[wi] += 1
            ctrs[wi] -= 1
        wi = (wi + 1) % len(words)
    return ctext

def descramble(txt):
    # wordsの数を数える
    words_len = 0
    for c in txt:
        if c.isnumeric():
            words_len += 1
    print('words length: ' + str(words_len))
    
    ctrs = [100] * words_len
    
    # ctrsを埋める
    wi = 0
    for c in txt:
        if c.isnumeric():
            ctrs[(wi - 1) % words_len] = int(c)
        else:
            # 全部出てきたwordは飛ばす。今何周目かと、ctrsリストの数値を比較
            wi += 1
            while ((wi - 1) // words_len) >= ctrs[(wi - 1) % words_len]:
                wi += 1
    
    offs = list(range(words_len))
    words = []
    
    for i in range(words_len):
        words.append([''] * ctrs[i])

    wi = 0
    for c in txt:
        if not c.isnumeric():
            while (wi // words_len) >= ctrs[wi % words_len]:
                wi += 1
            idx = wi % words_len
            words[idx][(offs[idx]) % ctrs[idx]] = c
            offs[idx] = (offs[idx] + 1) % ctrs[idx]
            wi += 1
    msg = ''
    for word in words:
        msg += ''.join(word) + ' '
    return msg

msg = descramble(ctext)
print(msg)

# confirm
print('[*] confirm by reversing.')
assert scramble(msg) == ctext

実行結果

$ python q5.py
words length: 24
Progress looks good. Maybe you are ready... Let us proceed. Lastly you MUST NOT forget that clearing the fibonacci will show you the rest! 
[*] confirm by reversing.

逆変換のチェックも通ったので、答えは

Progress looks good. Maybe you are ready... Let us proceed. Lastly you MUST NOT forget that clearing the fibonacci will show you the rest!

FINAL WARM-UP CHALLENGE!

(look at solutions to challenges 4&5 for hints) | Could this be a secret message?

f:id:kusuwada:20191018140538p:plain

Challenge 4, 5をヒントにしてね!だそうです。
Challenge4の答え

prime numbers will help

と、Challenge5の答え

Lastly you MUST NOT forget that clearing the fibonacci will show you the rest!

がヒントになりそう。

まずは、画像の不自然な16進を10進に直します。

s: 0x22(34)  $75     a: 0x43(67)  $10
i:  0xb(11) $39
e: 0x15(21) $5      i: 0x3d(61)  $65
m: 0x2b(43) $145    P:  0x5(5)   $55
k: 0x37(55) $15     R: 0x61(97)  $12
w:  0xf(15) $25     L: 0x17(23)  $99
g: 0x13(19) $120    j: 0x71(113) $5
e: 0x18(24) $35     F: 0x2f(47)  $1
u: 0x3b(59) $189    H:  0xd(13)  $20
h:  0x8(8)  $65     F:  0x7(7)   $13
r: 0x1f(31) $199    2: 0x29(41)  $10
w:  0x3(14) $11

この中で素数は、左の列の11,43,19,59,31と右の列の全部です。
左の列の対応する行の赤でハイライトされた文字をつなげると、imgur

                 a: 0x43(67)  $10
i:  0xb(11) $39
                    i: 0x3d(61)  $65
m: 0x2b(43) $145    P:  0x5(5)   $55
                    R: 0x61(97)  $12
                    L: 0x17(23)  $99
g: 0x13(19) $120    j: 0x71(113) $5
                    F: 0x2f(47)  $1
u: 0x3b(59) $189    H:  0xd(13)  $20
                    F:  0x7(7)   $13
r: 0x1f(31) $199    2: 0x29(41)  $10

次に、画像の中にある数くらいまでのフィボナッチ数列は下記です。

1,1,2,3,5,8,13,21,34,55,89,144,233

clearing the fibonacci will show you the rest

ということなので、フィボナッチ数列に含まれる数があるものを除外します。

                 a: 0x43(67)  $10
i:  0xb(11) $39
                    i: 0x3d(61)  $65
m: 0x2b(43) $145    
                    R: 0x61(97)  $12
                    L: 0x17(23)  $99
g: 0x13(19) $120    j: 0x71(113) $5
                    
u: 0x3b(59) $189    
                    F:  0x7(7)   $13
r: 0x1f(31) $199    2: 0x29(41)  $10

残った文字列+数が与えられていなかった/を組み合わせるとこうなります。

imgur/a/iRLjFF2

なんとなくwarmupのflagは文章みたいなので、これはflagではなさそう。/が入っていることから、urlっぽいな?
picoCTFのドメインにくっつけてhttps://2019game.picoctf.com/imgur/a/iRLjFF2としてアクセスしてみても、404が返ってきます。
imgurで検索してみると、こんなサイトが。

https://imgur.com/

このテイスト、memeでよく画像・GIF置き場に使われてるサイトな気がします。ドメインimgur.comらしいので、試しに先程の文字列に.comを足してアクセスしてみます。

imgur.com/a/iRLjFF2

f:id:kusuwada:20191018140605p:plain

おおお!ありました。

If you made it here, you got real potential. It's up to you now. Find a way in. And SHUT IT DOWN.

これにてWarmupチャレンジは完了のようです。お疲れさまでした。ちなみにこの画像はpicoCTF2019のGamesの画像っぽい。