Skip to main content

writeUP for ByteCTF 2021

·1559 words·8 mins Draft

笑死,xyb信号屏蔽拉满,蓝牙都不让用,只能来苟唯一一个不出网的题

总得来说水平还是比较绝绝子,延续了字节去年炼丹题的的优良传统,

EASYXOR #

主要难度在逆convert和猜ByteCTF{前缀大小写上🤭

convert

def shift(m, k, c):
    if k < 0:
        return m ^ (m >> (-k)) & c
    return m ^ ((m << k) & c)

大概是因为有个位移导致部分数据泄露,分清左右顺序和前后一点点慢慢把m恢复出来

大致流程如下:

inv convert:


def invshift_opt(c,k,mask):
    c = bin(c)[2:].rjust(64,'0')
    cip=[int(i) for i in c]
    
    mask = bin(mask)[2:].rjust(64,'0')
    mask=[int(i) for i in mask]
    ans={}
    idx = 63
    for i in range(k):
        ans[idx]=cip.pop()
        idx-=1

    for i in range(63-k,-1,-1):
    
        tmp = cip[i]^(ans[i+k]&mask[i])
        ans[i]=tmp
    
    flag =''
    for i in range(64):
        flag += str(ans[i]) 
    ans = int(flag,2)
    return ans
        

def invshift_ngt(c,k,mask):
    k=-k
    c = bin(c)[2:].rjust(64,'0')
    cip=[int(i) for i in c]
    
    mask = bin(mask)[2:].rjust(64,'0')
    mask=[int(i) for i in mask]
    
    ans={}
    for i in range(k):
        ans[i]=cip[i]
    
    
    for i in range(k,64):
        tmp = cip[i]^(ans[i-k]&mask[i])
        ans[i]=tmp
    flag =''
    for i in range(64):
        flag += str(ans[i]) 
    # ans=[str(ans[i]) for i in range(64)]
    # ans = "".join(ans)
    ans = int(flag,2)
    return ans
    
def invconvert(m, key):
    c_list = [0x37386180af9ae39e, 0xaf754e29895ee11a, 0x85e1a429a2b7030c, 0x964c5a89f6d3ae8c]
    for t in range(3,-1,-1):
        if(key[t]>0):
            m = invshift_opt(m, key[t], c_list[t])
        else:
            m = invshift_ngt(m, key[t], c_list[t])
    return m

然后打表爆破keys,把api全部拖出来用pypy几分钟就跑完了

❯❯ easyxor  10:22 pypy3 .\solve.py
5228891it [02:22, 34737.70it/s]b'15654747'
b'5u2t}$$$'
[-12, 26, -3, -31]

然后本题最难的地方来了,👴思索了2个小时愣是没有考虑前缀的情况

拿着flag{xxx爆了半天

然后才想起来学弟们打的是字节CTF

然后祥云被场地没网,工作人员不知道把👴的手机放哪里了,热点都找不到,只能猜前缀了

好在仗着👴不太优秀的变量命名功底给前缀ByteCTF{猜出来了

solve #


from tqdm import tqdm
# -----------------------------------
from itertools import *

from expApi import *



# pypy3爆破秘钥

ofb = "89b8aca257ee2748f030e7f6599cbe0cbb5db25db6d3990d"
cbc = "3b752eda9689e30fa2b03ee748e0da3c989da2bba657b912"
tab = [-32,-31, -30, -29, -28, -27, -26, -25, -24, -23, -22, -21, -20, -19, -18, -17, -16, -15, -14, -13, -12, -11, -10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31,32]
c = [int(cbc[i:i+16],16) for i in range(0,48,16)]
c2=c[1]
c3=c[2]
c1=c[0]
for k in tqdm(product(tab,repeat=4)):
    keys = list(k)
    # keys = [16 for _ in range(4)]    
    # IV = 10708643912928985573
    tmp = invconvert(c3,keys)
    g3 = long_to_bytes(tmp^c2)
    if(check(g3)==True):
        
        tmp = invconvert(c2,keys)
        g2 = long_to_bytes(tmp^c1)
        # print(g2)
        # print(g3)
        # print(keys)
        if(check(g2)==True):
            print(g2)
            print(g3)
            print(keys)
# keys = [-12, 26, -3, -31]

def getiv(keys,ofb):
    c = [int(ofb[i:i+16],16) for i in range(0,48,16)]
    m21 =bytes_to_long( b'ByteCTF{')
    IV = invconvert(m21^c[0],keys)
    return IV

keys = [-12, 26, -3, -31]
ofb = "89b8aca257ee2748f030e7f6599cbe0cbb5db25db6d3990d"
cbc = "3b752eda9689e30fa2b03ee748e0da3c989da2bba657b912"
IV = getiv(keys,ofb)
    
# CBC
iv = IV
groups = []
c = [int(cbc[i:i+16],16) for i in range(0,48,16)]

c3 = c[2]
c2 = c[1]
for i in range(3):
    tmp = invconvert(c[i],keys)
    groups.append(tmp^iv)
    iv = c[i]
flag2 = b''.join( [long_to_bytes(i) for i in groups])

# OFB
iv = IV
groups = []
c = [int(ofb[i:i+16],16) for i in range(0,48,16)]
for i in range(3):
    tmp = convert(iv,keys)
    g = c[i]^tmp
    groups.append(g)
    iv = tmp

flag1 = b''.join( [long_to_bytes(i) for i in groups])
print(flag1+flag2)

# ByteCTF{5831a241s-f30980q535af-2156547475u2t}$$$

expApi #


import struct
import sys

def bytes_to_long(s):
    """Convert a byte string to a long integer (big endian).

    In Python 3.2+, use the native method instead::

        >>> int.from_bytes(s, 'big')

    For instance::

        >>> int.from_bytes(b'\x00P', 'big')
        80

    This is (essentially) the inverse of :func:`long_to_bytes`.
    """
    acc = 0

    unpack = struct.unpack

    # Up to Python 2.7.4, struct.unpack can't work with bytearrays nor
    # memoryviews
    if sys.version_info[0:3] < (2, 7, 4):
        if isinstance(s, bytearray):
            s = bytes(s)
        elif isinstance(s, memoryview):
            s = s.tobytes()

    length = len(s)
    if length % 4:
        extra = (4 - length % 4)
        s = b'\x00' * extra + s
        length = length + extra
    for i in range(0, length, 4):
        acc = (acc << 32) + unpack('>I', s[i:i+4])[0]
    return acc

def long_to_bytes(n, blocksize=0):
    """Convert an integer to a byte string.

    In Python 3.2+, use the native method instead::

        >>> n.to_bytes(blocksize, 'big')

    For instance::

        >>> n = 80
        >>> n.to_bytes(2, 'big')
        b'\x00P'

    If the optional :data:`blocksize` is provided and greater than zero,
    the byte string is padded with binary zeros (on the front) so that
    the total length of the output is a multiple of blocksize.

    If :data:`blocksize` is zero or not provided, the byte string will
    be of minimal length.
    """
    # after much testing, this algorithm was deemed to be the fastest
    s = b''
    n = int(n)
    pack = struct.pack
    while n > 0:
        s = pack('>I', n & 0xffffffff) + s
        n = n >> 32
    # strip off leading zeros
    for i in range(len(s)):
        if s[i] != b'\x00'[0]:
            break
    else:
        # only happens when n == 0
        s = b'\x00'
        i = 0
    s = s[i:]
    # add back some pad bytes.  this could be done more efficiently w.r.t. the
    # de-padding being done above, but sigh...
    if blocksize > 0 and len(s) % blocksize:
        s = (blocksize - len(s) % blocksize) * b'\x00' + s
    return s

def check(s):
    for i in s:
        if(i>32 and i<127):
            continue
        else:
            return False
    return True

def shift(m, k, c):
    if k < 0:
        return m ^ (m >> (-k)) & c
    return m ^ ((m << k) & c)

def convert(m, key):
    c_list = [0x37386180af9ae39e, 0xaf754e29895ee11a, 0x85e1a429a2b7030c, 0x964c5a89f6d3ae8c]
    for t in range(4):
        m = shift(m, key[t], c_list[t])
    return m

def invshift_opt(c,k,mask):
    c = bin(c)[2:].rjust(64,'0')
    cip=[int(i) for i in c]
    
    mask = bin(mask)[2:].rjust(64,'0')
    mask=[int(i) for i in mask]
    
    ans={}
    idx = 63
    for i in range(k):
        ans[idx]=cip.pop()
        idx-=1

    for i in range(63-k,-1,-1):
    
        tmp = cip[i]^(ans[i+k]&mask[i])
        ans[i]=tmp
    
    flag =''
    for i in range(64):
        flag += str(ans[i]) 
    ans = int(flag,2)
    return ans
        

def invshift_ngt(c,k,mask):
    k=-k
    c = bin(c)[2:].rjust(64,'0')
    cip=[int(i) for i in c]
    
    mask = bin(mask)[2:].rjust(64,'0')
    mask=[int(i) for i in mask]
    
    ans={}
    for i in range(k):
        ans[i]=cip[i]
    
    
    for i in range(k,64):
        tmp = cip[i]^(ans[i-k]&mask[i])
        ans[i]=tmp
    flag =''
    for i in range(64):
        flag += str(ans[i]) 
    ans = int(flag,2)
    return ans
    
def invconvert(m, key):
    c_list = [0x37386180af9ae39e, 0xaf754e29895ee11a, 0x85e1a429a2b7030c, 0x964c5a89f6d3ae8c]
    for t in range(3,-1,-1):
        if(key[t]>0):
            m = invshift_opt(m, key[t], c_list[t])
        else:
            m = invshift_ngt(m, key[t], c_list[t])
    return m

document for JustDecrypt #

还是比较有意思的炼丹题,有点CBC打oracle的感觉,总得来说还是比较有意思

task

#!/usr/bin/env python3.9
# -*- coding: utf-8 -*-
import string
import random
import socketserver
import signal
import codecs
from os import urandom
from hashlib import sha256
from Crypto.Cipher import AES
from flag import FLAG

BANNER = rb"""

   ___           _    ______                           _   
  |_  |         | |   |  _  \                         | |  
    | |_   _ ___| |_  | | | |___  ___ _ __ _   _ _ __ | |_ 
    | | | | / __| __| | | | / _ \/ __| '__| | | | '_ \| __|
/\__/ / |_| \__ \ |_  | |/ /  __/ (__| |  | |_| | |_) | |_ 
\____/ \__,_|___/\__| |___/ \___|\___|_|   \__, | .__/ \__|
                                            __/ | |        
                                           |___/|_|        
"""

BLOCK_SIZE = 16


class AES_CFB(object):
    def __init__(self):
        self.key = urandom(BLOCK_SIZE)
        self.iv = urandom(16)
        self.aes_encrypt = AES.new(self.key, AES.MODE_CFB, self.iv)
        self.aes_decrypt = AES.new(self.key, AES.MODE_CFB, self.iv)

    def encrypt(self, plain):
        return self.aes_encrypt.encrypt(self.pad(plain))

    def decrypt(self, cipher):
        return self.unpad(self.aes_decrypt.decrypt(cipher))

    @staticmethod
    def pad(s):
        num = BLOCK_SIZE - (len(s) % BLOCK_SIZE)
        return s + bytes([num] * num)

    @staticmethod
    def unpad(s):
        return s[:-s[-1]]


class Task(socketserver.BaseRequestHandler):
    def _recvall(self):
        BUFF_SIZE = 1024
        data = b''
        while True:
            part = self.request.recv(BUFF_SIZE)
            data += part
            if len(part) < BUFF_SIZE:
                break
        return data.strip()

    def send(self, msg, newline=True):
        try:
            if newline:
                msg += b'\n'
            self.request.sendall(msg)
        except:
            pass

    def recv(self, prompt=b'> '):
        self.send(prompt, newline=False)
        return self._recvall()

    def proof_of_work(self):
        random.seed(urandom(32))
        alphabet = string.ascii_letters + string.digits
        proof = ''.join(random.choices(alphabet, k=32))
        hash_value = sha256(proof.encode()).hexdigest()
        self.send(f'sha256(XXXX+{proof[4:]}) == {hash_value}'.encode())
        nonce = self.recv(prompt=b'Give me XXXX > ')
        if len(nonce) != 4 or sha256(nonce + proof[4:].encode()).hexdigest() != hash_value:
            return False
        return True

    def timeout_handler(self, signum, frame):
        raise TimeoutError

    def handle(self):
        try:
            signal.signal(signal.SIGALRM, self.timeout_handler)
            signal.alarm(60)

            self.send(BANNER)

            # if not self.proof_of_work():
            #     self.send(b'\nWrong!')
            #     self.request.close()
            #     return

            self.send(b"It's just a decryption system. And I heard that only the Bytedancer can get secret.")

            aes = AES_CFB()

            # signal.alarm(300)

            for i in range(52):
                cipher_hex = self.recv(prompt=b'Please enter your cipher in hex > ')
                if len(cipher_hex) > 2048:
                    self.send(b"It's too long!")
                    continue
                try:
                    cipher = codecs.decode(cipher_hex, 'hex')
                except:
                    self.send(b'Not hex data!')
                    continue

                if len(cipher) == 0 or len(cipher) % BLOCK_SIZE != 0:
                    self.send(f'Cipher length must be a multiple of {BLOCK_SIZE}!'.encode())
                    continue

                plaintext = aes.decrypt(cipher)
                plaintext_hex = codecs.encode(plaintext, 'hex')
                self.send(b'Your plaintext in hex: \n%s\n' % plaintext_hex)

                if plaintext == b"Hello, I'm a Bytedancer. Please give me the flag!":
                    self.send(b'OK! Here is your flag: ')
                    self.send(FLAG.encode())
                    break

            self.send(b'Bye!\n')

        except TimeoutError:
            self.send(b'\nTimeout!')
        except Exception as err:
            self.send(b'Something Wrong!')
        finally:
            self.request.close()


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


if __name__ == "__main__":
    HOST, PORT = '0.0.0.0', 30002
    print(HOST, PORT)
    server = ForkedServer((HOST, PORT), Task)
    server.allow_reuse_address = True
    server.serve_forever()

大抵是一下几个知识点:

  • CFB模式机制
  • 如何padding oracle

输入一个消息块来设置定长iv

输入\x00参数来获取流秘钥跑64次可以完全oracle

跑不满64次也可以对低位嗯猜

from Crypto.Util.number import *
from pwn import *
from tqdm import tqdm
def main():
    r = remote('0.0.0.0', '30002')
    plaintext = b"Hello, I'm a Bytedancer. Please give me the flag!"+b"\x0f"*15

    def my_XOR(a, b):
        assert len(a) == len(b)
        return b''.join([long_to_bytes(a[i]^b[i]) for i in range(len(a))])

    def proof_of_work():
        rev = r.recvuntil(b"sha256(XXXX+")
        suffix = r.recv(28).decode()
        rev = r.recvuntil(b" == ")
        tar = r.recv(64).decode()

        def f(x):
            hashresult = hashlib.sha256(x.encode()+suffix.encode()).hexdigest()
            return hashresult == tar

        prefix = util.iters.mbruteforce(f, string.digits + string.ascii_letters, 4, 'upto')
        r.recvuntil(b'Give me XXXX > ')
        r.sendline(prefix.encode())

    def decrypt(msg):
        newmsg = msg + b'\x00'*(256+64-len(msg))
        r.recvuntil(b'Please enter your cipher in hex > ')
        r.sendline(newmsg.hex().encode())
        r.recvline()
        result = r.recvline().decode().strip()
        return bytes.fromhex(result)

    def decrypt_(msg):
        newmsg = msg + b'\x00'*(256-len(msg))
        r.recvuntil(b'Please enter your cipher in hex > ')
        r.sendline(newmsg.hex().encode())
        r.recvline()
        result = r.recvline().decode().strip()
        return bytes.fromhex(result)

    # proof_of_work()
    msg = b'\x00'*16
    decrypt(msg)
    c = b""
    for i in range(50):
        t = decrypt(c)[i]
        c += long_to_bytes(t^plaintext[i])

    decc = decrypt_(c)
    print(decc)
    res = r.recvline()+r.recvline()
    if b"Here is your flag" in res:
        print(r.recvline())
        print(r.recvline())
        r.close()
        return (True, len(decc))
    r.close()
    return (False, len(decc))

ll = []
while True:
    ss = main()
    ll.append(ss[1])
    if ss[0]: break
    print(len(ll), ll)