GoogleCTF18-MITM

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

握手部分是该密钥协商过程的关键,包含以下部分:

  1. 随机生成公私钥对
  2. 随机生成的nonce(Number once)
  3. 发送自己的公钥
  4. 发送自己的nonce
  5. 接受对方的 公钥 和nonce
  6. 如果对方的nonce和自己一样,或者对方的公钥是一些特殊值就 握手失败
  7. 根据自己的私钥和对方的公钥生成共享密钥 。
  8. 根据 password和对方的nonce,用共享密钥 通过哈希算法计算proof
  9. 发送proof
  10. 读取对方的proof
  11. 验证两个proof是否相同
  12. 如果相同则握手成功,返回共享密钥

显然,由于哈希算法的不可逆性,在不知道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
在这里有提到

Unusual keys

果然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())

你可能感兴趣的:(GoogleCTF18-MITM)