Securinets CTF Quals 2021 Writeup (2021/03/23)

#1. はじめに
　2021/3/21 02:00 JST ～ 2021/3/22 02:00 JST に開催された「Securinets CTF Quals 2021」にチーム「N30Z30N」としてソロ参加しました。Welcome 以外に Crypto を 2 問解いたので、Writeup を残します。

※2021/07/22 はてなブログより移転。

#2. Writeup
##2-1. MiTM (Crypto, 559pt)
>You managed to get in the middle and control the entire discussion between Alice, Bob and Carol. What are they saying ?

ファイル：「app.py」

```python:app.py
from Crypto.Util.number import long_to_bytes
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from secret import flag
import hashlib, random, os
import signal

class DHx():
	def __init__(self):
		self.g = 2
		self.p = 0xf18d09115c60ea0e71137b1b35810d0c774f98faae5abcfa98d2e2924715278da4f2738fc5e3d077546373484585288f0637796f52b7584f9158e0f86557b320fe71558251c852e0992eb42028b9117adffa461d25c8ce5b949957abd2a217a011e2986f93e1aadb8c31e8fa787d2710683676f8be5eca76b1badba33f601f45
		self.private = random.randint(1, self.p-1)
		self.secret = None

	def getPublicKey(self):
		return pow(self.g, self.private, self.p)

	def share(self, x : int):
		assert x > 1 and x < self.p
		return pow(x, self.private, self.p)

	def getSharedSecret(self, x : int):
		assert x > 1 and x < self.p
		self.secret = pow(x, self.private, self.p)

	def getFingerprint(self):
		return hashlib.sha256(long_to_bytes(self.secret)).hexdigest()

	def checkFingerprint(self, h1 : str, h2 : str ):
		return h1 == h2 == self.getFingerprint()

	def encryptFlag(self):
		iv = os.urandom(16)
		key = hashlib.sha1(long_to_bytes(self.secret)).digest()[:16]
		return iv.hex() + AES.new(key, AES.MODE_CBC, iv).encrypt(pad(flag, 16)).hex()

signal.alarm(60)

Alice = DHx()
Bob = DHx()
Carol = DHx()

A = Alice.getPublicKey()
print("Alice sends to Bob: {}".format(A))
A = int(input("Forward to Bob: "))
B = Bob.share(A)
print("Bob sends to Carol: {}".format(B))
B = int(input("Forward to Carol: "))
Carol.getSharedSecret(B)

B = Bob.getPublicKey()
print("Bob sends to Carol: {}".format(B))
B = int(input("Forward to Carol: "))
C = Carol.share(B)
print("Carol sends to Alice: {}".format(C))
C = int(input("Forward to Alice: "))
Alice.getSharedSecret(C)

C = Carol.getPublicKey()
print("Carol sends to Alice: {}".format(C))
C = int(input("Forward to Alice: "))
A = Alice.share(C)
print("Alice sends to Bob: {}".format(A))
A = int(input("Forward to Bob: "))
Bob.getSharedSecret(A)

print ("Alice says: ")
if (Alice.checkFingerprint(Carol.getFingerprint(), Bob.getFingerprint())):
	print (Alice.encryptFlag())
else:
	print ("ABORT MISSION! Walls have ears; Be careful what you say as people may be eavesdropping.")
```

　Diffie-Hellman 的な鍵交換を Alice、Bob、Carol の 3 人で [Alice→Bob→Carol]、[Bob→Carol→Alice]、[Carol→Alice→Bob]の 3 フェイズで行いますが、どのフェイズでもプレーヤーは配送中のデータを盗聴及び改ざんできます。そして、結果的に 3人の Shared Secret の SHA256 ハッシュが一致すれば暗号化フラグがもらえます。復号キーは Shared Secret なので、ハッシュの一致のほかに Shared Secret を把握する必要があります。

　3 人の秘密鍵をそれぞれ a、b、cとすると、改ざんを何もしない場合 Shared Secret は 2^(abc) で一致するので、暗号化フラグはもらえますが Shared Secret そのものが分からないと復号できません。

　そこで、SharedSecret を把握もしくはコントロールできないか考えます。getSharedSecret の引数に入れられる整数値は 2 以上 p-1 以下ですが、 これを 「p-1」 に揃えれば良さげです。なぜなら、「3 人の秘密鍵が全部偶数」は 1/8 の確率で発生し、かつそのとき SharedSecret の値は全て「1」となります。

ソルバ「solve.py」

```python:solve.py
from pwn import *
from Crypto.Util.number import *
from Crypto.Cipher import AES
import hashlib
import sys

def decryptFlag(enc):
  iv =  enc[:16]
  key = hashlib.sha1(long_to_bytes(1)).digest()[:16]
  return AES.new(key, AES.MODE_CBC, iv).decrypt(enc[16:]) 

p = 0xf18d09115c60ea0e71137b1b35810d0c774f98faae5abcfa98d2e2924715278da4f2738fc5e3d077546373484585288f0637796f52b7584f9158e0f86557b320fe71558251c852e0992eb42028b9117adffa461d25c8ce5b949957abd2a217a011e2986f93e1aadb8c31e8fa787d2710683676f8be5eca76b1badba33f601f45

while(True):
  r = remote("crypto1.q21.ctfsecurinets.com", 1337)
  r.recv(1000)
  for i in range(6):
    r.sendline(str(p-1).encode())
    print(r.recv(1000))
  s = r.recv(1000)
  print(s)
  if not b"ABORT" in s:
    print(decryptFlag(bytes.fromhex(s.split(b"\n")[1].decode())))
    exit()
  else:
    r.close()
```

　面倒なので、input する値は必要な場所以外でもすべて「p-1」に揃えて実装しました。

**Securinets{monkey-in-the-middle_efa8cf7dad56f238cc1ff49473da3ae3}**


##2-2. MiTM Revenge (Crypto, 757pt)
>Oh Crap! You came late, again.

ファイル：「app.py」

```python:app.py
from Crypto.Util.number import long_to_bytes
from Crypto.Util.Padding import pad
from Crypto.Cipher import AES
from secret import flag
import hashlib, random, os
import signal

class DHx():
	def __init__(self):
		self.g = 2
		self.p = 0xf18d09115c60ea0e71137b1b35810d0c774f98faae5abcfa98d2e2924715278da4f2738fc5e3d077546373484585288f0637796f52b7584f9158e0f86557b320fe71558251c852e0992eb42028b9117adffa461d25c8ce5b949957abd2a217a011e2986f93e1aadb8c31e8fa787d2710683676f8be5eca76b1badba33f601f45
		self.private = random.randint(1, self.p-1)
		self.nonce = random.randint(1, self.p-1)
		self.secret = None

	def getPublicKey(self):
		return pow(self.g, self.private, self.p)

	def share(self, x : int):
		assert x > 1 and x < self.p
		return pow(x, self.private, self.p)

	def getSharedSecret(self, x : int, nonce : int):
		assert x > 1 and x < self.p
		self.secret = pow(x, self.private, self.p) ^ nonce

	def getFingerprint(self):
		return hashlib.sha256(long_to_bytes(self.secret)).hexdigest()

	def checkFingerprint(self, h1 : str, h2 : str ):
		return h1 == h2 == self.getFingerprint()

	def encryptFlag(self):
		iv = os.urandom(16)
		key = hashlib.sha1(long_to_bytes(self.secret)).digest()[:16]
		return iv.hex() + AES.new(key, AES.MODE_CBC, iv).encrypt(pad(flag, 16)).hex()

signal.alarm(120)

Alice = DHx()
Bob = DHx()
Carol = DHx()

A = Alice.getPublicKey()
print("Alice sends to Bob: {}".format(A))
B = Bob.share(A)
print("Bob sends to Carol: {}".format((B, Bob.nonce)))
Carol.getSharedSecret(B, Bob.nonce)

B = Bob.getPublicKey()
print("Bob sends to Carol: {}".format(B))
B = int(input("Forward to Carol: "))
C = Carol.share(B)
print("Carol sends to Alice: {}".format((C, Carol.nonce)))
data = input("Forward to Alice: ").strip().split()
C , Carol.nonce = int(data[0]), int(data[1])
Alice.getSharedSecret(C, Carol.nonce)

C = Carol.getPublicKey()
print("Carol sends to Alice: {}".format(C))
C = int(input("Forward to Alice: "))
A = Alice.share(C)
print("Alice sends to Bob: {}".format(A))
data = input("Forward to Bob: ").strip().split()
A , Alice.nonce = int(data[0]), int(data[1])
Bob.getSharedSecret(A, Alice.nonce)

print ("Alice says: ")
if (Alice.checkFingerprint(Carol.getFingerprint(), Bob.getFingerprint())):
	print (Alice.encryptFlag())
else:
	print ("ABORT MISSION! Walls have ears; Be careful what you say as people may be eavesdropping.")
```

　先問の応用編（？）です。盗聴の機会は同じですが改ざん機会が減っています。「Alice の公開鍵を Bob に送って、さらにCarol に送る」フェーズでの改ざんができません。さらに、getSharedSecret の処理で nonce をXOR するよう改められていますが、Alice と Carol の nonceは改ざん可能です。

　本質的には前問と同じく、「Shared Secret の値を揃えること」と「Shared Secret の値を把握する」ことを両立させる問題です。すなわち、「Carol の Shared Secret （改ざんの余地なし）に合うよう、他の Shared Secret をコントロールする」ことと、「Shard Secretの値をリークする」ことを、盗聴・改ざんの機会を駆使して実現させます。

　以下、3 人の秘密鍵をそれぞれ a、b、cとし、具体的な解法を示します。Shared Secret の値は「 2^(abc)  XOR [Bob の 
nonce]」であることに注意します。

・最初のフェイズで、「Bob の nonce」及び 2^(ab) の値を入手できます。

・2 番目のフェイズの最初の input では、改ざんを行いません（Bob の公開鍵をそのまま流します）。その結果、2^(bc)の値を入手できます。

・2 番目のフェイズの 2 番目の input では、 Shared Secret を最初のフェイズのものと一致させるため、 第二引数（nonce）を Bob の nonce に差し替えて送ります。

・3 番目のフェイズの最初の input では、入手済みの値である 2^(bc) を入れます。その結果、2^(abc) の値を入手できます。

・3 番目のフェイズの 2 番目の input では、Shared Secret を以前の 2 つと一致させるため、 第一引数 には「2」を、第二引数（nonce）には「2^(abc) XOR  [Bob の nonce] XOR 2^b」を入れます。この結果、 Shared Secret は「2^b XOR 2^(abc) XOR  [Bob の nonce] XOR 2^b」=「2^(abc) XOR  [Bob の nonce] 」で前の 2 つと一致します。さらに、2^(abc) の値と 「Bob の nonce」は入手済ですから、Shared Secret の値も判明し、復号にも成功します。


ソルバ「solve.py」

```python:solve.py
from pwn import *
from Crypto.Util.number import *
from Crypto.Cipher import AES
import hashlib
import sys

p = 169622824183424820825728324890204115101468714952998142585574034795946851153950475569207215681807529286667189170420372861538287664283023804761495759297626394111153684529019990561684722443184304549649494421130078368098045597169822975289983997491594344239614944483399038130689027660812095676588300142576532463429

def decryptFlag(enc, k):
  iv =  enc[:16]
  key = hashlib.sha1(long_to_bytes(k)).digest()[:16]
  return AES.new(key, AES.MODE_CBC, iv).decrypt(enc[16:]) 

r = remote("crypto1.q21.ctfsecurinets.com", 13337)

#Phase 1
_res1 = r.recvline()
pub_a = int(_res1.decode().replace("Alice sends to Bob: ", ""))
_res2 = r.recvline()
res2 = _res2.decode().replace("Bob sends to Carol: ", "").replace("(","").replace(")\n","").split(", ")
ab = int(res2[0])
Nonce_b = int(res2[1])

#Phase 2
_res3 = r.recvline()
pub_b = int(_res3.decode().replace("Bob sends to Carol: ", ""))
to_send = str(pub_b).encode()
_res4 = r.recv(1000)
r.sendline(to_send)
_res5 = r.recvline()
res5 = _res5.decode().replace("Carol sends to Alice: ", "").replace("(","").replace(")\n","").split(", ")
bc = int(res5[0])
to_send = str(bc).encode()
_res6 = r.recv(1000)
r.sendline(to_send + b" " + str(Nonce_b).encode())

#Phase 3
_res7 = r.recvline()
pub_c = int(_res7.decode().replace("Carol sends to Alice: ", ""))
to_send = str(bc).encode()
_res8 = r.recv(1000)
r.sendline(to_send)
_res9 = r.recvline()
res9 = _res9.decode().replace("Alice sends to Bob: ", "").replace("(","").replace(")\n","").split(", ")
abc = int(res9[0])
to_send = "2".encode()
ss = abc ^ Nonce_b #Shared Secret
Nonce2 = pub_b ^ ss
_resa = r.recv(1000)
r.sendline(to_send + b" " + str(Nonce2).encode())
s0 = r.recvline()
s = r.recv(1000)

ct = s.replace(b"\n",b"").decode()
print(decryptFlag(bytes.fromhex(ct), ss))
r.close()
```

　汚いプログラムですが、競技中使ったものを（確認用print文以外）ほぼそのまま上げました。

**Securinets{master-in-the-middle_bb8f4b012d02284aea258723179dff83}**


#3. おわりに　
　Crypto は この他に RSA 問を含め 3 問がありましたが、上記 2 問で力尽きました（泣）。