类型 Crypto
题目连接:https://github.com/google/google-ctf/tree/master/2018/quals/crypto-mitm
参考:https://github.com/p4-team/ctf/tree/master/2018-06-23-google-ctf/crypto_mitm
https://mhackeroni.it/assets/docs/mitm_mhackeroni.pdf
考察知识点:ECDH,ECC,Curve25519
这道题目只给了一个服务器端的脚本,脚本中有一些有趣的函数:
1.Challenge
2.Client
3.Server
4.Handshake
此外还用了一个第三方模块curve25519
下面简单分析一下这些函数
Challenge
def Challenge(password, flag, reader, writer):
try:
server_or_client = ReadLine(reader)
is_server = server_or_client[0] in b'sS'
is_client = server_or_client[0] in b'cC'
if is_server:
return Server(password, flag, reader, writer)
elif is_client:
return Client(password, reader, writer)
else:
WriteLine(writer, b'Error: Select if you want to speak to the (s)erver or (c)lient.')
return 1
except Exception as e:
WriteLine(writer, b'Error')
return 1
可以看到,每次连接,我们可以选择和一个服务端通信,也可以选择和一个客户端通信。服务端和客户端共享一个password,flag在服务端。当然,我们可以把服务端的数据转发给客户端,把客户端数据转发给服务端,而且能任意修改这些数据再转发,相当于一个中间人。
Client
def Client(password, reader, writer):
sharedKey = Handshake(password, reader, writer)
if sharedKey is None:
WriteLine(writer, b'Error: nope.')
return 1
mySecretBox = nacl.secret.SecretBox(sharedKey)
line = mySecretBox.decrypt(ReadBin(reader))
if line != b"AUTHENTICATED":
WriteLine(writer, b'Error: nope.')
return 1
WriteBin(writer, mySecretBox.encrypt(b"whoami"))
line = mySecretBox.decrypt(ReadBin(reader))
if line != b'root':
return 1
WriteBin(writer, mySecretBox.encrypt(b"exit"))
return 0
可以看到如果我们和客户端通信,必须先正确完成握手部分,并生成一个共享密钥,后续通信都需使用该密钥加密。
Server
def Server(password, flag, reader, writer):
sharedKey = Handshake(password, reader, writer)
if sharedKey is None:
WriteLine(writer, b'Error: nope.')
return 1
mySecretBox = nacl.secret.SecretBox(sharedKey)
WriteBin(writer, mySecretBox.encrypt(b"AUTHENTICATED"))
while 1:
cmd = mySecretBox.decrypt(ReadBin(reader))
if cmd == b'help':
rsp = b'help|exit|whoami|getflag'
elif cmd == b'exit':
return 0
elif cmd == b'whoami':
rsp = b'root'
elif cmd == b'getflag':
rsp = flag
else:
return 1
WriteBin(writer, mySecretBox.encrypt(rsp))
服务端和客户端的逻辑很像,也必须先完成握手部分,生成共享密钥,后续通信都需用该密钥 加密。我们需要向其发送加密的‘getflag’才能获得flag。
Handshake
def Handshake(password, reader, writer):
myPrivateKey = Private()
myNonce = os.urandom(32)
WriteBin(writer, myPrivateKey.get_public().serialize())
WriteBin(writer, myNonce)
theirPublicKey = ReadBin(reader)
theirNonce = ReadBin(reader)
if myNonce == theirNonce:
return None
if theirPublicKey in (b'\x00'*32, b'\x01' + (b'\x00' * 31)):
return None
theirPublicKey = Public(theirPublicKey)
sharedKey = myPrivateKey.get_shared_key(theirPublicKey)
myProof = ComputeProof(sharedKey, theirNonce + password)
WriteBin(writer, myProof)
theirProof = ReadBin(reader)
if not VerifyProof(sharedKey, myNonce + password, theirProof):
return None
return sharedKey
握手部分是该密钥协商过程的关键,包含以下部分:
- 随机生成公私钥对
- 随机生成的nonce(Number once)
- 发送自己的公钥
- 发送自己的nonce
- 接受对方的 公钥 和nonce
- 如果对方的nonce和自己一样,或者对方的公钥是一些特殊值就 握手失败
- 根据自己的私钥和对方的公钥生成共享密钥 。
- 根据 password和对方的nonce,用共享密钥 通过哈希算法计算proof
- 发送proof
- 读取对方的proof
- 验证两个proof是否相同
- 如果相同则握手成功,返回共享密钥
显然,由于哈希算法的不可逆性,在不知道password的情况下,我们不可能伪造出一个通过检查的proof。如果我们想要通过握手,只能转发我们从服务端和客户端得到的proof,而且必须保证他们使用的是相同的sharedkey。值得注意的是因为我们是分别同服务端和客户端建立通信的,他们的privatekey是不同的,即使我们作为中间人发送相同的Publickey,生成的sharedkey也是不同的 。那是否存在这样的Publickey,能让不同的Privatekey生成出相同的sharedkey呢?我们注意到,在第六步,会判断公钥是否是两个特殊的值 b'\x00'*32, b'\x01' + (b'\x00' * 31),这其实就是一个提示,说明当key是某些特殊值时,会危害该密钥协商算法的安全性。
通过搜索Curve25519 的信息,可以找到一些资料:
https://cr.yp.to/ecdh.html
在这里有提到
果然0,1 以及 一些其他值会破坏密钥协商过程的安全性。所以攻击过程就是在握手过程中,使用一个特殊的公钥,使客户端和服务端生成了同样的sharedkey,然后我们只需要转发proof,就能通过验证,然后我们用sharedkey加密getflag发给服务端,即可。
这里的另一个问题是我们最好使用和题目一样的第三方模块,来确保能生成同样的sharedkey。可能题目用的是这个库?https://github.com/Muterra/donna25519
最终的代码:
from curve25519 import Private, Public
from crypto_commons.generic import long_to_bytes
def riggedHandshake(server, client):
zeroPublicKey = long_to_bytes(39382357235489614581723060781553021112529911719440698176882885853963445705823)[::-1]
print("Rigging handshake")
clientPublicKey = Public(readBin(client))
print("Client key = "+str(clientPublicKey))
clientNonce = readBin(client)
print("Client nonce = " + clientNonce.encode("hex"))
serverPublicKey = Public(readBin(server))
print("Server key = " + str(serverPublicKey))
serverNonce = readBin(server)
print("Server nonce = " + serverNonce.encode("hex"))
print("Sending to server")
writeBin(server, zeroPublicKey)
writeBin(server, clientNonce)
print("Sending to client")
writeBin(client, zeroPublicKey)
writeBin(client, serverNonce)
serverProof = readBin(server)
clientProof = readBin(client)
print("Server proof = "+serverProof.encode("hex"))
print("Client proof = "+clientProof.encode("hex"))
print("Forwarding proofs")
writeBin(server, clientProof)
writeBin(client, serverProof)
return Private().get_shared_key(Public(zeroPublicKey))
from binascii import hexlify
from binascii import unhexlify
import nacl.secret
from crypto_commons.netcat.netcat_commons import nc, receive_until, send
def readBin(socket):
try:
data = receive_until(socket, "\n")[:-1]
return unhexlify(data)
except:
print('error', data)
def writeBin(socket, data):
send(socket, hexlify(data))
def main():
url = "mitm.ctfcompetition.com"
port = 1337
server = nc(url, port)
client = nc(url, port)
send(server, "sS")
send(client, "cC")
sharedKey = riggedHandshake(server, client)
mySecretBox = nacl.secret.SecretBox(sharedKey)
print(mySecretBox.decrypt(readBin(server)))
writeBin(server, mySecretBox.encrypt("getflag"))
print(mySecretBox.decrypt(readBin(server)))
main()
Output:
Rigging handshake
Client key =
Client nonce = bfaf0c97e8030e0bc59f6371438ea348bd51ad60127302d5ccd9a08add8190d3
Server key =
Server nonce = 1b50d096795799effc8e1dbdf5a45e71a612aa0f3ccb60d5d09e94639c128f73
Sending to server
Sending to client
Server proof = 30e2f5990849450ca851bd99cc7b15569167e04c7068fa4383d25469e32916ef
Client proof = 3ae7e589e04fec309f5c6a235c35d70ff2cd031d4bc9e1104a3fdbc13e0bd567
Forwarding proofs
AUTHENTICATED
CTF{********}
那么如果没有这个第三方模块,我们能不能不通过调用get_shared_key(),直接获得sharedkey呢?
如果进一步了解Curve25519,可以知道:
- Curve25519(any scalar,0x0000...0000)=0x0000...0000
- Curve25519(any scalar,0x0000...0001)=0x0000...0000
- 椭圆曲线是在有限域上的,Curve25519 是定义在模(2^255-19)上的。
由此可知,如果我们输入的公钥是(2^255-19),会被规约为0,最终生成的公钥也为0。这样就可以在不知道get_shared_key()具体实现的情况下,得到sharedkey
具体代码如下:
#!/usr/bin/env python3
from binascii import hexlify
from binascii import unhexlify
import logging,sys,os
import nacl.secret
import hmac,hashlib,pwn
pwn.context.log_level=logging.DEBUG
from hashlib import sha256,sha512
def ReadLine(reader):
data=b''
while not data.endswith(b'\n'):
cur=reader.recv(1)
data+=cur
if cur ==b'':
return data
return data[:-1]
def WriteLine(writer,msg):
writer.send(msg+b'\n')
def ReadBin(reader):
return unhexlify(ReadLine(reader))
def WriteBin(writer,data):
WriteLine(writer,hexlify(data))
def main():
pk=long_to_bytes(57896044618658097711785492504343953926634992332820282019728792003956564819949).rjust(32,'\0')[::-1]
pk1=long_to_bytes(57896044618658097711785492504343953926634992332820282019728792003956564819950).rjust(32,'\0')[::-1]
with pwn.remote('mitm.ctfcompetition.com'.1337) as c:
c.sendline('c')
ckey=ReadBin(c)
cnonce=ReadBin(c)
WriteBin(c,pk1)
with pwn.remote('mitm.ctfcompetition.com'.1337) as s:
c.sendline('s')
skey=ReadBin(s)
snonce=ReadBin(s)
WriteBin(s,pk)
WriteBin(s,cnonce)
WriteBin(c,snonce)
cauth=ReadBin(c)
WriteBin(s,cauth)
sauth=ReadBin(s)
WriteBin(c,sauth)
authed=ReadBin(s)
WriteBin(c,authed)
WriteBin(s,ReadBin(c))
WriteBin(c,ReadBin(s))
k=ReadBin(c)
mySecretBox=nacl.secret.SecretBox(sha256(b"curve25519-shared:"+'\0'*32).digest())
WriteBin(s,mySecretBox.encrypt(b'getflag'))
flag=ReadBin(s)
print(mySecretBox.decrypt(flag))
if __name__ =='__main__':
sys.exit(main())