防衛省サイバーコンテスト2024
防衛省サイバーコンテストについて書いておこうと思います。12時間で8ジャンル32問を解きます。難易度は優しめのものが多いですが、普段やっているCTFと異なりあまり意図が分からない設定のものも多くあり、これらはひらめきが必要なことが多いです。
そんな中でも上位の人たちは安定しているので、来年むけに少し対策を考えておこうと思いました。鍵になるのは12時間の使い方で、時間配分というよりは「他人が簡単にやっていることに時間をかけない」ところだと思っています。使うべきツールがあれば使うし、多くの人が通れるguessを素直なアプローチで通ることが必要です。その上で以下のように対策をまとめてみました。
- 5位以内を目標とする場合はヒントは必要なだけ開いてよさそう。ただし取っ掛かりが分からない場合のみ。それ以外は自明なものが出てきがちなのでアイデアが尽きたら、方針を絞り込むために開くことを考える。
- 中盤以降残った問題の優先度は基本solve数、次いで
実装系のもの>取っ掛かりが見つかっているもの>guess系のもの
以下は解けなかったもの、解くのに時間がかかったものを挙げて、それぞれ対策を考えました。またアプローチがたまたまよかったり、推測が上手くいって解けたケースも挙げて、自分の思考を見比べられるようにしました。
short rsa public key
小さなnのrsaで秘密鍵を作る問題です。地道な実装をします。ツールに任せても動いたそうですが、RsaCtfToolを試していたときはうまく動かなかったのであきらめた経緯があります。もう少し粘ると良かったかと思います。
pub = bytes.fromhex(''.join("ad:81:c9:26:41:c0:b1:8c:4e:da:55:1c:1d:78:28:04:4e:3e:4a:75:19:aa:c9:0e:e4:69:1c:4a:86:dc:e2:e1".split(':')))
intpub = int.from_bytes(pub, 'big')
p = 1011146650909449935800449563521726151
q = 77614294907759846691928156982114516291863
def egcd(a, b):
if a == 0:
return (b, 0, 1)
g, y, x = egcd(b%a,a)
return (g, x - (b//a) * y, y)
def modinv(a, m):
g, x, y = egcd(a, m)
if g != 1:
raise Exception('No modular inverse')
return x%m
phi = (p-1)*(q-1)
e = 0x10001
d = modinv(e, phi)
with open('./RSA-cipher.dat', 'rb') as f:
inp = int.from_bytes(f.read(), 'big')
pt = pow(inp, d, p*q)
from Crypto.Util.number import long_to_bytes
print(long_to_bytes(pt))
cryptographically insecure prng
これは平文がasciiになっていることを期待して先頭2ブロックを試しに復号して、うまくいったら続きを表示するというスクリプトを書いて探しました。
がんばって出てきた文字列が英単語かどうかを直接目で見て判定していましたが、もっと早い言語で全部いちいち復号すれば良かったという学びを得ました。
時間はかかりましたがこれはコストはそこまでかかっていないはず。
ntfs data hide
パワーポイントにスクリプトが隠されている問題です。入っているオブジェクトやマクロを確認しますが、何もありません。FTKImagerで隅々まで覗いてみても、特に情報量がなさそうです。ヒントを開けたところ、「NTFSのAlternative Data Stream」という情報をもらい、ググってマウントして、dir /r
で見つけられました。
仕組みがよくわからないがforensはautopsyが良いことを理解しました。2byte文字でサーチするのも有効そうです。
何を見ればいいのかわからなかったケースで、この場合ヒントは有効でした。
hidden value
voltility。手元のkaliに入っていなくて慌てて入れました。envarsは明らかにFLAG
という値が大量に入っていますが、base64で解いてもフラグになりません。別の何かが必要なのかとしばらく探し続けましたがbase58だったそうです。どうして。
これはかなり時間をかけて、バイナリの出力は何のデータなのか、ほかに何の情報を組み合わせるべきか、など考えていましたが、発想が切り替えられなければ解けないので優先度としては下の方かなと思いました。(それはそれとして試すべきではある。) またこの状況ではヒントは開けるべきではありませんでした。
une maison
大きめの画像が与えられます。いろいろ重なったような見た目で、低ビットを少し見たり、適当なステガノサイトに突っ込んだりしてみますがはずれ。かなり時間を溶かしてからヒント。ヒントはバイナリ解析不要、違和感に注目とのことだったのですが、全部違和感なのでわからないねとなり、真ん中のバーコードっぽいところをトリミングしてバーコードサイトに突っ込んだところ正解。どうして。
これはバーコードに気づいたら終わりなので、solve数を信じて解くし、取っ掛かりがなければヒントを得るのは正解だったと思います。バイナリ解析をしないで良いというヒントは掛ける時間を減らせてよかったです。
utter darkness
「バイナリ解析」が必要な唯一のmiscです。1ビット=1ピクセルだったので、普通にプロットすればよいのではと思ってスクリプトを書きましたがバグってたようでかなり時間が解けました。これもヒントに助けられ、パレットを見て、2つ全部が同じ色になっているので片方を白にして表示できました。
これは時間短縮のため、単に某青空解析ツールをそろそろ入れた方がいいかもしれないなと思いました。
serial port signal
おもしろかったです。UARTのビットと時間のcsvが与えられる問題です。これもヒントに助けられました。UARTのbaudrateやストップビット、パリティなどを実装して、最後は微妙にレートを調整しながらパリティチェックが通る出力を探しました。
これもヒントでUARTパースの作業ということが確定したので良かったと思います。
with open('./Tx.csv', 'r') as f:
inp = f.read().split('\n')
baudrate = 9600+4
inp =inp[:-1]
out = ''
sample = 0
ntime = (sample/baudrate) * 1000000
for line in inp:
t = int(line.split(',')[0])
v = line.split(',')[1]
if t > ntime: # sample
out += v
sample += 1
ntime = (sample/baudrate * 1000000)
#print(out)
out2 = ''
start = False
for bit in out:
if start == False and bit == '1':
continue
elif start == False and bit == '0':
#print('start')
start = True
data = ''
isparity = False
isstop = False
continue
# start == True
if isparity == True:
isparity = False
check = data + bit
#print('checking', data, bit)
if check.count('1') % 2 != 0:
print('parity fail')
else:
pass
isstop = True
elif isstop == True:
#print(data)
assert bit == '1', f'{len(out2)}'
isstop = False
start = False
#print('stop done')
out2 += chr(int(data[::-1], 2))
print(out2)
else:
data += bit
if len(data) == 7 and isstop == False:
isparity = True
discovery
HTBのような問題です。dirsearchがかなり頑張ってくれて、早い段階で必要な情報をそろえてログインできました。バージョンのところは一度目にアクセスしたときは権限がありませんエラーが出て、もう一度試したら表示されたので、運がよかったです。pentest系の問題は混みあうと地獄なので早めに抜けたいなと思っていましたが、ここは思った通り抜けられて時間が節約できたと思います。
exploit
discoveryの続き。いい感じのrceが見つかったのでスムーズでした。同時期に攻略していた人には解法を開示する形になるので、わからなくても張り付いておくのは重要だと思いました。
do_the_best
これはwriteupをみても無理だったと思います。使われているリポジトリまでは上手く行けて、バージョンも確認して、最新版との差分をチェックし、goのコマンドインジェクションを確認しましたがすべてはずれ。正解は自アドレスのクエリだったとのこと。
これは今回時間をかけても解けない枠だったと思っています。solve数による優先度で最後に取り掛かれると良いなと思いました。
pivot
discoveryの続きでこれはsshから入る問題です。base64が光っている時点で終わりかと思いましたが出てくるのはデータベースのクレデンシャルで、データベースはローカルでは動作していません。インタフェースを確認してからpingを通すと3,4台ほど見つかり、3306が開いていると期待してmysqlを祈りながらたたいて回ったら通りました。これは運がよかったです。
twisted text
これ解けなかったですが、長々とバグらせていました。numpyなど使うのをためらって、回転行列で係数をかけ忘れていたのを見落としていました。intで丸めると外側の回転は再現できないのかな、などと考えて諦めていましたが、これは本当にやるだけでした。悔しい。
実装系は優先度高めでやっていこうと思いました。
are you introspective?
これは敗因が2つあって、ヒントでバージョンの話を受け取ったあと、seclistのgraphqlのエンドポイントリストにバージョンを100くらいまで追加するように改造しましたが、これを左に加えるのはやりましたが、右に加えるのを忘れていたこと。また仮に加えていたとしても、ステータスコード400ではffufのデフォルトでは引っかからないことを確認しました。列挙が成功したときにどうなるかについて想像しておくことが大事と思いました。
アプローチは悪いのでそれは修正すべき、あとはsolve数に応じてがんばって、という対応で行きたいと思います。
insecure
ログインして他人のIDをのぞく問題です。まあブラインドSQLIと思って、クエリやユーザ名などにペイロードを仕込みますがはずれ。これはヒントを開いて、タイミングの問題だと書かれていたので遷移の間違い探しに移行し、リファラの有無が条件になっていたので追加して成功。どうして。
これは結果的にidという取っ掛かりが見つかってからヒントを開いても役に立ったという例です。が、そもそもそういう実装が存在すると思っていなかったので、アプローチ改善で解決できるような気がしています。
variation
これは%が吸われた時点でエンコーディングの問題だろうなと思って、nameが複数回変な感じで入るので、この辺とカンマでがんばるのかなと思っていたができず。ヒントはやっぱりエンコーディングの話をしてきますが、タグが入らず終わり。文字コードは気軽に全部突っ込んで様子を見ることもできるんですねというのが学びでした。
多くの文字を試すというアイデアがなかったので、自分には解けない枠だったと思っています。これもsolve数で後回しにできそうと思っています。
bruteforce
おもしろかったです。これはAPIの実装がもらえて、jwtが手に入るところから始まる問題です。鍵が弱くて任意のローカルファイルを読める状態になります。最後は別のエンドポイントのbasic認証を抜けばOKですが、元ファイルをリークしてadminのパスワードを入れても上手くいきません。
jwt側がflaskで、basic認証側がpythonのhttp.serverだったので、これでbasic認証の設定をする方法を調べてみると、引数から与えてやる実装が見つかります。https://gist.github.com/mauler/593caee043f5fe4623732b4db5145a82
seclistでLFIのファイルを眺めている中で/proc
が覗けることは確認していたので、プロセスのcmdlineを順にみていくことにして、うまくbasic認証の設定をしているpidを見つけることができました。あまり意識せず、同じインスタンスで動いていると決めてかかっていましたが、運がよかったです。
これはやってたときはguessだと思っていませんでしたが、うまく推測が通ったケースと見ることができます。