SECCON 2017 Online CTFのWriteupを書いてみる
他の方のWriteup読んでたら自分も書きたくなった. Writeupを書くのは初めてだし,そもそもこういったOnlineのCTFに参加すること自体,今まで1,2回くらいしかなかったので,ミスは大目に見てほしい.
7問+1を解いて1100ptで190位だった.チーム名は"ttt". もうちょっとちゃんとした名前にすればよかった.
Vigenere3d 100
i文字目の暗号文c[i]
を,変換テーブルtを用いてc[i]=t[k][m][n]
と表すとき,c[i]
は
c[i] = s[(k+m+n)%65] s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}"
のように表すことができる.p
を平文,k1
,k2
を鍵とすると,k=s.find(p[i]), m=s.find(k1[i]), n=s.find(k2[i])
なのでc[i]
は
c[i] = s[(s.find(p[i])+s.find(k1[i])+s.find(k2[i]))%65]
と書くことができる.
平文の最初の7文字は既知であり,k1
の鍵長は14文字,さらにk2=k1[::-1]
であることから以下の対応が得られる.
P = s[15] = s[18+s.find(k1[0])+s.find(k1[13]))%65] O = s[14] = s[4+s.find(k1[1])+s.find(k1[12]))%65] R = s[17] = s[2+s.find(k1[2])+s.find(k1[11]))%65] 4 = s[30] = s[2+s.find(k1[3])+s.find(k1[10]))%65] d = s[39] = s[14+s.find(k1[4])+s.find(k1[9]))%65] n = s[49] = s[13+s.find(k1[5])+s.find(k1[8]))%65] y = s[60] = s[63+s.find(k1[6])+s.find(k1[7]))%65]
さらに0<=i<=64
より
s.find(k1[0])+s.find(k1[13]) = 62 s.find(k1[1])+s.find(k1[12]) = 10 s.find(k1[2])+s.find(k1[11]) = 15 s.find(k1[3])+s.find(k1[10]) = 28 s.find(k1[4])+s.find(k1[9]) = 25 s.find(k1[5])+s.find(k1[8]) = 36 s.find(k1[6])+s.find(k1[7]) = 62
となる.
8文字目は
T = s[19] = s[(s.find(p[7])+s.find(k1[7])+s.find(k1[6]))%65]
ここにs.find(k1[7])+s.find(k1[6]) = 62
を代入して
s[19] = s[(s.find(p[7])+62)%65] 19 = (s.find(p[7]) + 62) % 65 s.find(p[7]) = 22 p[7] = "W"
同様にして9文字目以降も求めることができる. コード
s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz_{}" cipher = "POR4dnyTLHBfwbxAAZhe}}ocZR3Cxcftw9" table = [62, 10, 15, 28, 25, 36, 62] table = table +table[::-1] p = "" for i, c in enumerate(cipher): p += s[(s.find(c) - table[i % len(table)]) % len(s)] print p
SECCON{Welc0me_to_SECCON_CTF_2017}
Run me! 100
フィボナッチ数列.コード書くのが面倒だったので計算サイトに投げた.
SECCON{65076140832331717667772761541872}
putchar music 100
映画のタイトルと聞いてスターウォーズかなぁと思い文字列を見てみると
###<f_YM\204g_YM\204g_Y_H #<f_YM\204g_YM\204g_Y_H #+-?[WKAMYJ/7 #+-?[WKgH #+-?[WKAMYJ/7hk\206\203tk\\YJAfkkk";for(i=0;t=1;i=(i+1)%
はじめの###
とかそのあとの繰り返し部分でああこれスターウォーズだと確信した.てか映画に疎いので他に世界的に知られている映画の曲はわからんです.
SECCON{STAR_WARS}
他の方のWriteupを見て実際に動かしてみた.soxというツールを使えばいいらしい.
gcc -Wall putchar_music.c -o putchar_music -lm ./putchar_music | sox -r 8000 -b 8 -c 1 -t raw -s - -d
聴けた.すごい.
SHA-1 is dead 100
適当にググるといい感じのツールがあった. 2つのpdfファイルのSHA-1を衝突させてくれるらしい.
これを用いて,一文字違いのtxtファイルを生成し,pdfに変換,上記のツールを使用してSHA-1が衝突するpdfを生成,ファイルサイズが2017KiBと2018KiBの間なら終了する雑なプログラムを書いた.range(3200,3300)
は大体の当たりをつけて書いた.
import subprocess import string import random import os.path def stringSplit(string, num): s = [] for i in range(len(string) // num): s.append(string[num*i:num*(i+1)]) r = len(string) % num if r: s.append(string[-r:]) return s def writeFile(filename, str): f = open(filename, 'w') for r in str: f.writelines(r+"\n") f.close() def makePdf(filename): cmd = "a2ps "+filename+".txt -o "+filename+".ps" subprocess.call( cmd.strip().split(" ") ) cmd = "ps2pdf "+filename+".ps" subprocess.call( cmd.strip().split(" ") ) for n in range(3200,3300): random_str = ''.join([random.choice(string.ascii_letters) for i in range(n)]) i = random.randint(1, n) random_str2 = random_str[:i]+"_"+random_str[i+1:] random_str = stringSplit(random_str, 50) writeFile("text1.txt", random_str) random_str2 = stringSplit(random_str2, 50) writeFile("text2.txt", random_str2) makePdf("text1") makePdf("text2") cmd = "python3 sha1collider/collide.py text1.pdf text2.pdf" subprocess.call( cmd.strip().split(" ") ) n = os.path.getsize("out-text1.pdf") print n if n > 1024*2017 and n < 1024*2018: break
SECCON{SHA-1_1995-2017?}
この資料によるとSHA-1はメッセージをブロックに分割して,ブロックを一つずつ読み込みながら内部状態を更新し,最終的なハッシュを出力しているようである.よってある時点で内部状態が一致し,かつその後のデータが同じであれば,後に何を追加してもハッシュは同じである.これとpdfの仕様をうまく利用してあらゆる(?)pdfにおいてハッシュの衝突が可能になる.
このことから出題者の意図としては上記の解法よりも,衝突しているpdfにデータを追加して目的のサイズに合わせる,という解法のほうが趣旨にあっていると思う.如何に自分がsha-1を分かっていなかったか.
Powerful_Shell 300
難読化されたPowerShellスクリプト.このままでは分からないのでコードを出力してみる.コードの出力は$ECCON+=
の処理の末尾に,以下を追加すれば良い.
"$ECCON"
$ErrorActionPreference = "ContinueSilently" [console]::BackgroundColor = "black";...-n;x;x; <# Host Check #> Write-Host -b 00 -f 15 Checking Host... Please wait... -n Try{ If ((Get-EventLog -LogName Security | Where EventID -Eq 4624).Length -Lt 1000) { Write-Host "This host is too fresh!" Exit } }Catch{ Write-Host "Failed: No admin rights!" Exit } Write-Host "Check passed" $keytone=@{'a'=261.63} $pk='a' ForEach($k in ('w','s','e','d','f','t','g','y','h','u','j','k')){ $keytone+=@{$k=$keytone[$pk]*[math]::pow(2,1/12)};$pk=$k } Write-Host -b 00 -f 15 "Play the secret melody." Write-Host -b 15 -f 00 -n ' ' ... Write-Host -b 15 -f 00 ' ' Write-Host $stage1=@();$f=""; While($stage1.length -lt 14){ $key=(Get-Host).ui.RawUI.ReadKey("NoEcho,IncludeKeyDown") $k=[String]$key.Character $f+=$k; If($keytone.Contains($k)){ $stage1+=[math]::floor($keytone[$k]) [console]::beep($keytone[$k],500) } } $secret=@(440,440,493,440,440,493,440,493,523,493,440,493,440,349) If($secret.length -eq $stage1.length){ For ($i=1; $i -le $secret.length; $i++) { If($secret[$i] -ne $stage1[$i]){ Exit } } x "Correct. Move to the next stage." } $text=@" YkwRUxVXQ05DQ1N...BE2FxROE10VShZOTBFTF2E= "@ $plain=@() $byteString = [System.Convert]::FromBase64String($text) $xordData = $(for ($i = 0; $i -lt $byteString.length; ) { for ($j = 0; $j -lt $f.length; $j++) { $plain+=$byteString[$i] -bxor $f[$j] $i++ if ($i -ge $byteString.Length) { $j = $f.length } } }) iex([System.Text.Encoding]::ASCII.GetString($plain))
出力したコードを実行してみるが動かない.
Write-Host -b 00 -f 15 Checking Host... Please wait... -n Try{ If ((Get-EventLog -LogName Security | Where EventID -Eq 4624).Length -Lt 1000) { Write-Host "This host is too fresh!" Exit } }Catch{ Write-Host "Failed: No admin rights!" Exit } Write-Host "Check passed"
この箇所を見る限り,Admin権限でないまたはEventID4624に関するログが1000件未満だと動作を停止するそう. しかしここの処理は後の処理に関係ないので,この箇所を削除したコードを直接実行する.
すると鍵盤のようなものが出てきた.対応するキーを叩くとちゃんと音が出る.
押したキーが以下の場合は次に行けるようである.
$secret=@(440,440,493,440,440,493,440,493,523,493,440,493,440,349)
440って「ラ」だよなぁと思ったのでググりながら音階の周波数を調べ,押すキーを求めた.
hhjhhjhjkjhjhf
ここを突破するとパスワードが求められた.該当のコードはbase64になっていたので,上と同様にしてコードを出力する.
${}=+$();${=}=${;};${+}=++${;};${@}=++${;};${.}=++${;};${[}=++${;};${]}=++${;};${(}=++${;};${)}=++${;};${&}=++${;};${|}=++${;};${"}="["+"$(@{})"[${)}]+"$(@{})"["${+}${|}"]+"$(@{})"["${@}${=}"]+"$?"[${+}]+"]";${;}="".("$(@{})"["${+}${[}"]+"$(@{})"["${+}${(}"]+"$(@{})"[${=}]+"$(@{})"[${[}]+"$?"[${+}]+"$(@{})"[${.}]);${;}="$(@{})"["${+}${[}"]+"$(@{})"[${[}]+"${;}"["${@}${)}"];"${"}${.}${(}+${"}${(}${|}+${"}${(}${)}+${"}${(}${)}+${"}${)}${|}+${"}${)}${&}+${"}${(}${+}+${"}${&}${@}+${"}${+}${=}${+}+${"}${|}${)}+${"}${+}${=}${=}+${"}${[}${]}+${"}${)}${@}+${"}${+}${+}${+}+${"}${+}${+}${]}+${"}${+}${+}${(}+${"}${.}${@}+${"}${[}${]}+${"}${&}${=}+${"}${+}${+}${[}+${"}${+}${+}${+}+${"}${+}${=}${|}+${"}${+}${+}${@}+${"}${+}${+}${(}+${"}${.}${@}+${"}${.}${|}+${"}${(}${|}+${"}${+}${+}${=}+${"}${+}${+}${(}+${"}${+}${=}${+}+${"}${+}${+}${[}+${"}${.}${@}+${"}${+}${+}${(}+${"}${+}${=}${[}+${"}${+}${=}${+}+${"}${.}${@}+${"}${+}${+}${@}+${"}${|}${)}+${"}${+}${+}${]}+${"}${+}${+}${]}+${"}${+}${+}${|}+${"}${+}${+}${+}+${"}${+}${+}${[}+${"}${+}${=}${=}+${"}${.}${|}+${"}${+}${.}+${"}${+}${=}+${"}${)}${.}+${"}${+}${=}${@}+${"}${[}${=}+${"}${.}${(}+${"}${(}${|}+${"}${(}${)}+${"}${(}${)}+${"}${)}${|}+${"}${)}${&}+${"}${.}${@}+${"}${[}${]}+${"}${+}${=}${+}+${"}${+}${+}${.}+${"}${.}${@}+${"}${.}${|}+${"}${&}${=}+${"}${[}${&}+${"}${+}${+}${|}+${"}${(}${|}+${"}${+}${+}${[}+${"}${.}${(}+${"}${)}${@}+${"}${]}${+}+${"}${[}${|}+${"}${[}${|}+${"}${.}${|}+${"}${[}${+}+${"}${+}${@}${.}+${"}${+}${.}+${"}${+}${=}+${"}${|}+${"}${&}${)}+${"}${+}${+}${[}+${"}${+}${=}${]}+${"}${+}${+}${(}+${"}${+}${=}${+}+${"}${[}${]}+${"}${)}${@}+${"}${+}${+}${+}+${"}${+}${+}${]}+${"}${+}${+}${(}+${"}${.}${@}+${"}${.}${|}+${"}${)}${+}+${"}${+}${+}${+}+${"}${+}${+}${+}+${"}${+}${=}${=}+${"}${.}${@}+${"}${)}${[}+${"}${+}${+}${+}+${"}${|}${&}+${"}${.}${.}+${"}${.}${|}+${"}${]}${|}+${"}${+}${.}+${"}${+}${=}+${"}${|}+${"}${&}${)}+${"}${+}${+}${[}+${"}${+}${=}${]}+${"}${+}${+}${(}+${"}${+}${=}${+}+${"}${[}${]}+${"}${)}${@}+${"}${+}${+}${+}+${"}${+}${+}${]}+${"}${+}${+}${(}+${"}${.}${@}+${"}${.}${[}+${"}${&}${.}+${"}${(}${|}+${"}${(}${)}+${"}${(}${)}+${"}${)}${|}+${"}${)}${&}+${"}${+}${@}${.}+${"}${.}${(}+${"}${(}${|}+${"}${(}${)}+${"}${(}${)}+${"}${)}${|}+${"}${)}${&}+${"}${+}${@}${]}+${"}${.}${[}+${"}${+}${.}+${"}${+}${=}+${"}${+}${@}${]}|${;}"|&${;}
すごい.
こっから全然わからんなーと思いつつ色々やったところ,Get-Variable
で変数一覧を表示した時に以下が出てきた.
Name Value ---- ----- " [CHar] $ .\powerfullshell.ps1 & 8 ( 6 ) 7 . 3 ; iex ? True @ 2 [ 4 ] 5 ^ .\powerfullshell.ps1 | 9 + 1 = 0
ここから${}
の中は上記の変換をすれば良いと考え,変換した.
解読してみたところ,If($ECCON -eq 'P0wEr$H311')
みたいな文字列と,Write-Host "SECCON{$ECCON}"
みたいな文字列を見つけたので,これをフラグとして出したら通った.
SECCON{P0wEr$H311}
Log search 100
よくわからず.とりあえず"flag"という文字列とマイナス検索を駆使して/flag-xxx
となっているものでResponseが200のものを見つけたのでアクセスしたらフラグを見つけた.
SECCON{N0SQL_1njection_for_Elasticsearch!}
他の方のWriteupを見る限り,どうやらElastic SearchのURIサーチが使えるそう.
+request:"/flag" +response:200
Ps and Qs 200
某Crypto Challenges List(https://pastebin.com/cSfZW2yX)にのってたRSALOTと同じ解法で解けた.素数使いまわしているとgcdを求めることで素数の推定ができるよって問題.
公開鍵のコンポーネントを抽出し
openssl rsa -pubin -in pub1.pub -text -noout
公約数を求めるコードを書き
import math n1 = int(open('pub1.pub.mod.txt', 'r').read().strip(), 16) n2 = int(open('pub2.pub.mod.txt', 'r').read().strip(), 16) cp = math.gcd(n1, n2) if cp != 1: q1 = n1 // cp q2 = n2 // cp print ('q1: ', q1) print ('q2: ', q2)
python rsatool/rsatool.py -f PEM -o key1 -p 28491351268021265...2684652311433029 -q 29756285957217824...9605071066540057
復号する.
openssl rsautl -decrypt -inkey key1 -in cipher
SECCON{1234567890ABCDEF}
RSAは鍵生成時の素数の選び方などをミスると簡単に秘密鍵が推測できてしまうことがある.(参考資料) しかし実際に運用でそういったことって起こるのだろうか.
Thank you for playing! Thank you! 100
初めてのSECCON CTFにしては頑張ったほうだと思う.ただ実際にWriteupを書いてみるとほとんど真面目に解いてないことがわかったのでちゃんと復習したい.来年は高い得点の問題を解くこととpwn問を最低一問解くことを目標にしたい.あともっといろんなCTFに参加したいと思う.
SECCON{We have done all the challenges. Enjoy last 12 hours. Thank you!}