安洵杯-2021


出了一个题目

题目

#!/usr/bin/python
import socketserver
import random
import os
import string
import binascii
import hashlib
from Crypto.Cipher import AES
from Crypto.Util import Counter 
from Crypto.Util.number import getPrime
from hashlib import sha256
from sec import FLAG
import gmpy2


def init():
    q = getPrime(512)
    p = getPrime(512)
    e = getPrime(64)
    n = q*p
    phi = (q-1) * (p-1)
    d = gmpy2.invert(e, phi)
    hint = 2 * d + random.randint(0, 2**16) * e * phi
    mac = random.randint(0, 2**64)
    c = pow(mac, e, n)
    counter = random.randint(0, 2**128)
    key = os.urandom(16)
    score = 0
    return n, hint, c, counter, key, mac, score

class task(socketserver.BaseRequestHandler):

    def POW(self):
        random.seed(os.urandom(8))
        proof = ''.join([random.choice(string.ascii_letters+string.digits) for _ in range(20)])
        result = hashlib.sha256(proof.encode('utf-8')).hexdigest()
        self.request.sendall(("sha256(XXXX+%s) == %s\n" % (proof[4:],result)).encode())
        self.request.sendall(b'Give me XXXX:\n')
        x = self.recv()

        if len(x) != 4 or hashlib.sha256((x+proof[4:].encode())).hexdigest() != result: 
            return False
        return True

    def recv(self):
        BUFF_SIZE = 2048
        data = b''
        while True:
            part = self.request.recv(BUFF_SIZE)
            data += part
            if len(part) < BUFF_SIZE:
                break
        return data.strip()

    def padding(self, msg):
        return  msg + chr((16 - len(msg)%16)).encode() * (16 - len(msg)%16)

    def encrypt(self, msg):
        msg = self.padding(msg)
        if self.r != -1:
            self.r += 1
            aes = AES.new(self.key, AES.MODE_CTR, counter = Counter.new(128, initial_value=self.r))
            return aes.encrypt(msg)
        else:
            return msg

    def send(self, msg, enc=True):
        print(msg, end= '   ')
        if enc:
            msg = self.encrypt(msg)
        print(msg, self.r)
        self.request.sendall(binascii.hexlify(msg) + b'\n')

    def set_key(self, rec):
        if self.mac == int(rec[8:]):
            self.r = self.counter

    def guess_num(self, rec):
        num = random.randint(0, 2**128)
        if num == int(rec[10:]):
            self.send(b'right')
            self.score += 1
        else:
            self.send(b'wrong')

    def get_flag(self, rec):
        assert self.r != -1
        if self.score ==  5:
            self.send(flag, enc=False)
        else:
            self.send(os.urandom(32) +  flag)

    def handle(self):
        self.r = -1

        if not self.POW():
            self.send(b'Error Hash!', enc= False)
            return

        self.n, self.hint, self.c ,self.counter, self.key, self.mac, self.score = init()

        self.send(str(self.n).encode(), enc = False)
        self.send(str(self.hint).encode(), enc = False)
        self.send(str(self.c).encode(), enc = False)

        for _ in range(6):
            rec = self.recv()
            if rec[:8] == b'set key:':
                self.set_key(rec)
            elif rec[:10] == b'guess num:':
                self.guess_num(rec)
            elif rec[:8] == b'get flag':
                self.get_flag(rec)
            else:
                self.send(b'something wrong, check your input')

class ForkedServer(socketserver.ForkingMixIn, socketserver.TCPServer):
    pass

def main():
    HOST, PORT = '127.0.0.1', 10086
    server = ForkedServer((HOST, PORT), task)
    server.allow_reuse_address = True
    server.serve_forever()

if __name__ == '__main__':
    main()

连上之后给了6次交互机会, 但其中最少有一次需要用来set key

有三个选项:

  • set key : 初始化aes-ctr的counter
  • guess num: 每猜中一次随机数, 分数+1
  • get flag : 当分数不为4的时候, 发送的为有填充的加密的flag, 分数为4的时候发送明文flag

思路

由于题目set key没有校验次数, 可以多次重置密钥, 且密钥为每一次连接生成的随机值, 加上aes-ctr的特性, 只需要获取到足够长的明文即可

guess key中, 猜对随机数服务端会发送填充加密后的right, 猜错随机数会发送填充加密后wrong, 实际上, 这里的明文都不够长,

于是这样会出现只能获取到一半flag的情况。

正确是思路的是故意输入不符合要求的命令, 由于self.send(b'something wrong, check your input'), 填充和加密操作被内置到了send方法里面, 所以这里会发送很长的密文, 重复三次, 去除重合的部分即可得到足够长的密钥流

于是6次机会 = 1次set key初始化 + 3次报错guess num获取密钥流 + 1次set key重置密钥流 + 1次get flag获取加密后的flag

本地解密即可

exp

#!/usr/bin/python
from pwn import *
from pwnlib.util.iters import mbruteforce
from hashlib import sha256
import string
import time
import binascii
context.log_level = 'debug'
r = remote('happi0.club', 10086)

def padding( msg):
    return  msg + chr((16 - len(msg)%16)).encode() * (16 - len(msg)%16)

def xor_bytes(var, key):
    return bytes(a ^ b for a, b in zip(var, key))

def decrypt(ct):
    msg = padding(b'something wrong, check your input')
    pt = xor_bytes(msg, ct)
    return pt

# pow
data = r.recvline()
print(data[12:28], data[33:97])
found = mbruteforce(lambda x:sha256(x.encode() + data[12:28]).hexdigest().encode() == data[33:97], string.ascii_letters+string.digits, 4)
r.sendline(found)
r.recvline()

# set key
n = int(binascii.unhexlify(r.recvline()[:-1]))
d = int(binascii.unhexlify(r.recvline()[:-1])) // 2
c = int(binascii.unhexlify(r.recvline()[:-1]))
m = pow(c,d,n)
r.sendline(b'set key:' + str(m).encode())
time.sleep(0.5)

# guess num
key_stream = b''
for i in range(3):
    r.sendline(b'happi0')
    time.sleep(0.5)
    ct = binascii.unhexlify(r.recvline()[:-1])
    pt = decrypt(ct)
    if i != 2:
        key_stream += pt[:16]
    else:
        key_stream += pt
    print('pt:' + str(pt) + '\n' + 'length: ' + str(len(pt)))
    print('key_stream:' + str(key_stream) + '\n' + 'length: ' + str(len(key_stream)) + '\n')

# reset key
r.sendline(b'set key:' + str(m).encode())
time.sleep(0.5)
r.sendline(b'get flag')
time.sleep(0.5)

# decrypt flag
flag = binascii.unhexlify(r.recvline()[:-1])
print(flag, type(flag), key_stream)
flag = xor_bytes(flag, key_stream)
print(flag)

#b'\x8a\xa8\x83\xed\xe9\xe0\xe5\x11\xf4\x9c\xcc\xb6K\x91\xbb\xa9\xf0\xd4\t\x15\x19r\xf5Z\x9d.\x9368\x90\xe8\xd5flag{c836b2abae33d2e5b9a0e50b28ba5e95}\n\n\n\n\n\n\n\n\n\n'

  TOC