Quantcast
Channel: koki-satoの記事 - Qiita
Viewing all articles
Browse latest Browse all 12

TokyoWesterns CTF 4th 2018 Write-up

$
0
0

TokyoWesterns CTF 4th 2018 に チーム m1z0r3 として参加して、1015点で53位でした。

自分は以下の4問を解き、416点を入れました。

  • SimpleAuth (55pt) - Warmup / Web
  • scs7 (112pt) - Warmup / Crypto
  • mondai.zip (95pt) - Warmup / Misc
  • Revolutional Secure Angou (154pt) - Crypto

忘れないうちに Write-up を書きます。

SimpleAuth

問題文のURLにアクセスするとソースコードが見える

if (!empty($_SERVER['QUERY_STRING'])) {
    $query = $_SERVER['QUERY_STRING'];
    $res = parse_str($query); // ココ
    if (!empty($res['action'])){
        $action = $res['action'];
    }
}

ここの parse_str($query) の処理が間違っており、 PHP Manual を読むと、第二引数がない場合は 現在のスコープに勝手に変数をセットする らしい。

というわけで $hashed_password をセットすれば良い。

/?action=auth&hashed_password=c019f6e5cd8aa0bbbcc6e994a54c757e

にアクセスすれば FLAG が降ってくる。

scs7

nc crypto.chal.ctf.westerns.tokyo 14791 をすると、暗号化された FLAG が送られ、その後メッセージを送信すると暗号化して返してくれる。

色々試した結果、以下のことがわかった。

  • 暗号文は、メッセージをHexエンコードしたものを59進法に変換したものである。
  • ただし、59進法で使われる 0-58 をどの記号に割り当てるかは毎回バラバラである。

したがって、はじめに 0-58 がどの記号に対応するか対応表を作ってしまえば、暗号化された FLAG を復号することができる。具体的には chr(59) から chr(117) までを試して、暗号文の最後の桁の文字を見ると、それが 0-58 の数字に対応する。

#!/usr/bin/env python
import socket

HOST = 'crypto.chal.ctf.westerns.tokyo'
PORT = 14791

def sock(ip, port):
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((ip, port))
    return s, s.makefile('rw', bufsize=0)

def read_until(f, delim='\n'):
    data = ''
    while not data.endswith(delim):
        data += f.read(1)
    return data

def main():
    print 'nc %s %s\n' % (HOST, PORT)
    s, f = sock(HOST, PORT)

    result = read_until(f).strip()
    print result
    encrypted_flag = result.split(': ')[1]
    print read_until(f).strip()

    table = {}
    for i in range(59, 118):
        print read_until(f, ': ').strip() + chr(i)
        s.send(chr(i) + '\n')
        result = read_until(f).strip()
        print result
        table[result[-1]] = i - 59

    flag = 0
    for i, s in enumerate(encrypted_flag[::-1]):
        flag += table[s] * (59 ** i)

    print
    print hex(flag)[2:-1].decode('hex')

if __name__ == '__main__':
    main()

mondai.zip

mondai.zip が渡され、解凍すると中からまた Zip ファイル(パスワード付き)が出てきて、パスワードを解読して解凍するとまた Zip ファイルが出てきて… みたいな問題。

ちなみに、1番目のパスワードが一番難しかった… 他の人が2番目まで解いてくれたので残りをやった。

1番目のパスワード

ファイル名がそのままパスワード

2番目のパスワード

capture.pcapng を開くと、不自然なデータを持つ ICMP パケットが並んでおり、データサイズに着目するとパスワードがわかる

$ tshark -n -t e -r capture.pcapng -Y 'icmp and ip.dst == 192.168.11.5' -T fields -e data.len
print(''.join([chr(i) for i in [87, 101, 49, 99, 111, 109, 101]]))

3番目のパスワード

list.txt がパスワードリストで、どれかが正解

from zipfile import ZipFile

password_list = open('list.txt', 'r').read().split('\n')
with ZipFile('mondai.zip') as zf:
    for password in password_list:
        try:
            zf.extractall(pwd=password)
            print '+ Completed! Pass: ' + password
            break
        except:
            continue

4番目のパスワード

ファイル名が MD5 ハッシュ値で、元のメッセージがパスワード

5番目のパスワード

README.txt を見ると、 "password is too short" とのことなので総当たりを試す

解凍すると secret.txt が出てくるので、指示通り ( TWCTF{(2)_(5)_(1)_(4)_(3)} ) に FLAG を組み立てる

Revolutional Secure Angou

:warning: (私は数学弱者なので、以下は数学的に正しくない可能性が大いにあります。ご注意ください)

$ q \times e \equiv 1 \pmod{P} $ より、次のように考えた。

qe = px + 1
x = \frac{q}{p}e - \frac{1}{p} \approx \frac{q}{p}e \left(\because 0 \lt \frac{1}{p} \ll 1 \right)

ここで、 $p$ と $q$ は巨大な素数であり、 $\frac{q}{p}$ の値はそこまで大きくならないと仮定すると、 $x$ の値は $e$ と大幅には変わらない(ブルートフォース可能)と判断した。

また、

qe = px + 1
ne = p^2 x + p
p^2 = \frac{ne - p}{x} \approx \frac{ne}{x} \left(\because ne \gg p \right)

となるので、$x$ を徐々に増加さながら、 $\frac{ne}{x}$ を超えない最大の平方数を計算し、その平方根を元の式に代入して成立するか確かめた。

require 'openssl'

key = OpenSSL::PKey::RSA.new File.read('publickey.pem')
e, n = key.e.to_i, key.n.to_i

# num 以下の最大の平方数を求める
def search_square(num)
  ds = num.to_s.reverse.scan(/..|.$/).reverse.map(&:reverse)
  k, n = 0, 0
  ds.each do |d|
    k = (k.to_s + d.to_s).to_i
    j = 0
    j += 1 while ((n.to_s + j.to_s).to_i + 1) ** 2 <= k
    n = (n.to_s + j.to_s).to_i
  end
  n
end

2.upto(1000000) do |x|
  puts x if x % 1000 == 0
  p = search_square(n * e / x)
  q = n / p
  if p * (p * x + 1) == n * e
    puts "+ p: #{p}"
    puts "+ q: #{q}"
    break
  end
end

# rsatool.py を用いて、秘密鍵 (privatekey.pem) を生成する
#   $ python rsatool.py -f PEM -o privatekey.pem -p [p] -q [q]

key = OpenSSL::PKey::RSA.new File.read('privatekey.pem')
File.binwrite('flag', key.private_decrypt(File.binread('flag.encrypted')))

Viewing all articles
Browse latest Browse all 12