[amateurs CTF 2024] crypto/pilfer-techies

这题费了几天,昨天写到11点半才基本完成程序,需要交互2000多,远程太慢了交互两次就断掉了,反正本地能成,程序逻辑上正确了。小鸡块也写了WP了等写完马上去看小鸡块神的思路。也许有的问题很大呢?

先简单看下题:

#!/usr/local/bin/python3

import hmac
from os import urandom

def strxor(a: bytes, b: bytes):
    return bytes([x ^ y for x, y in zip(a, b)])

class Cipher:
    def __init__(self, key: bytes):
        self.key = key
        self.block_size = 16
        self.rounds = 256
        self.debug = False
    
    def F(self, x: bytes):
        return hmac.new(self.key, x, 'md5').digest()[:15]
    
    def encrypt(self, plaintext: bytes):
        plaintext = plaintext.ljust(((len(plaintext)-1)//self.block_size)*16+16, b'\x00')
        ciphertext = b''
        
        for i in range(0, len(plaintext), self.block_size):
            block = plaintext[i:i+self.block_size]
            idx = 0
            for _ in range(self.rounds):
                L, R = block[:idx]+block[idx+1:], block[idx:idx+1]
                L, R = strxor(L, self.F(R)), R
                block = L + R
                idx = R[0] % self.block_size
                if self.debug:
                    print(block.hex())
            ciphertext += block
        
        return ciphertext.hex()


key = urandom(16)
cipher = Cipher(key)
flag = open('flag.txt', 'rb').read().strip()

print("pilfer techies")
while True:
    choice = input("1. Encrypt a message\n2. Get encrypted flag\n3. Exit\n> ").strip()
    if choice == '1':
        pt = input("Enter your message in hex: ").strip()
        pt = bytes.fromhex(pt)
        print(cipher.encrypt(pt))
    elif choice == '2':
        print(cipher.encrypt(flag))
    else:
        break

print("Goodbye!")

先取idx为0,从明文取第idx位(这个字节%10作为下下轮的idx),用hmac加密生成一个15字节的密钥(F函数)与明文其它部分异或,后边加上idx位的明文。

前边有一题是只有1轮,那个直接可以恢复。这个有256轮比较麻烦。

获取密钥R:

题目是通过R来加密,只需要获取所有的F(R)就可以解密了(256组),第1步是恢复R[0]

先构造一个明文看它的加密过程:

01...EF
构造的时文0x...
第1轮idx=0x^F(0)[0]0
第2轮idx=0xf0^F(x^F(0)[0])[15]x^F(0)[0]

当某个idx位置的字符%10==0xf时,后边的idx变为0xf则一直用这个值对前边加密一直到256次结束,由于加密直接是异或,所以后边的每两个会相互抵消。

第1步 取得R[0]的第1位

当构造一个串,第1位为0,第2位为x,如果x^F(0)[0]%0x10=0xf时,只需要两轮便会结束(后边互相抵消)。这个可以通过爆破获取所有值,但需要判断哪一个是正确的,因为所有密文值的尾字节都是F结束。

这样我来构造两个明文:

pt1 = bytes([0,i]+[0]*14)
pt2 = bytes([0,i^0x70]+[0xff]*14)

如果1的i^F(0)[0]%0x10==0xf只有两轮加密,则2也成立。则1与2的尾(i^F(0))[-1]^(i^0x80^F[0])[-1]==0x70 ,反之如果1不成立虽然最后也会是0xf但1和2加密次数不一定相同则尾号极大概率不同。

这时候通过i和尾号可以得到F(0)的第1字节: i^F(0)[0]=c[-1] => F(0)[0] = c[-1]^i

第2步 取得全部的R[0]

这是个来构造第2个明文,让流程idx变成0->0->f->f,由于已知F(0)的第1字节,所以设定让第2字节加密后为0,然后爆破第3字节让它再次加密后为尾号f

构造明文0f00xy
第1轮f00^f00=0x^f(0)[1]y^f(0)[-1]0
第2轮x^f(0)[1]^f(0)[0]=xfy^f(0)[-1]^f(0)[-2]0^F(0)[-1]0
第3轮0^F(0)[-1]^F(xf)[-2]0^F(xf)[-1]xf
第4轮y^f(0)[-1]^f(0)[-2]0^F(0)[-1]0xf

当有两轮f的情况,最后两轮的f(xf)会互相抵消,第4轮与第2轮比只是向前错了一位。c[13]=F(0)[-1]而当设置x,...y都为0时c[12]=f(0)[-1]^f(0)[-2]这样一直向前可以得到R[0]剩余的1-14位。

第3步 取得R的第F列

回到第1步取得的数据,第1步当设置流程为0->F时只有两轮加密F0和Fxf

现在已经知道R[0]那么就可以推出R[xf],直接设置第1个为0第2个为R[0][0]^0xXF取得的密文与R[0]异或即可

第4步 取得全部的R

也是第1步的两轮流程,设置第1字节为s,第s%0x10+1字节为i爆破,由于只有两轮加密,而第2轮分部的尾号f的R已经在上一步全都得到,可以异或出其它值。但同第1步一样需要在尾字节符合的情况下作个反转其它字符来验证。

由于一共256-1-16组,每组爆破16种情况加几次验证直到通过,总次平均在8*239次以上。所以爆破量还是满大的。

这步其实并不需要解出全部的R,只需要在解密时需要哪一个再去爆破,每个平均9次,可以减少爆破量

解密:

获得全部的密钥后,解密就是从后向前查表。

第用最后一字节A对应的密钥对密文解密,通过解密后的尾字节B判断A插回到哪个位置。

同样有个问题,尾号为F时,由于加密是偶数次,可能出现尾号两轮都是F的情况,需要分别处理两次。

这个流程因为轮数未知无法判断结束,需要通过flag明文的特征(可见字符和pad \0)来判断。

解题代码:

包装的公共函数

from pwn import *

cnt = 0
def get_v(pt):
    global cnt
    cnt+=1
    p.sendlineafter(b'> ', b'1')
    p.sendlineafter(b"Enter your message in hex: ", pt.hex().encode())
    msg = p.recvline().strip().decode()
    return bytes.fromhex(msg)

def get_tv(r0,r1=0,r2=0):
    return get_v(bytes([r0,r1,r2]+[0]*13))

def get_flag():
    p.sendlineafter(b'> ', b'2')
    msg = p.recvline().strip().decode()
    return bytes.fromhex(msg)

总流程

def step1():
    global R
    
    for i in range(0x10):
        v1 = get_v(bytes([0,i]+[0]*14))
        v2 = get_v(bytes([0,i^0x70]+[0xff]*14))
        if v1[-1]^v2[-1]==0x70:
            print(i, v1.hex())
            R0 = get_r00(i^v1[-1])   #1,取得R0
            R[0] = R0[:-1]
            print('R[0]=', bytes(R[0]).hex())
            print(f"{cnt =}")
            get_rf()                 #2,取得第F列  16
            #get_rx()                 #3,取得第0列 +15
            #print(R)
            print(f"{cnt =}")
            #解密
            for i in range(0, len(enc_flag),16):
                decrypt(enc_flag[i:i+16])
            print(f"{cnt =}")
            break
 

取得R[0][0]


#取得F(b'\x00')
#L^F(0)^F(0)^F(f)
def get_r00(f_0_0):
    print(f_0_0)
    for i in range(0x10):
        v = get_tv(0,f_0_0,i)
        if v[-2] != 0 : continue
        v2 = get_tv(0,f_0_0,i^0x70)
        if v2[-2] != 0: continue
        print('Found:',i,v.hex())
        #f_0_1 = i^f_0_0
        R0 = [0]
        for j in range(13,-1,-1):
            R0.append(v[j]^R0[-1])
        R0.append(f_0_0)
        R0 = R0[::-1]
        print('R[0]',bytes(R0).hex())  #6bb5b072e39d1faf5dcad9f197837a
        break
    return R0

取得尾号f列的R

'''
>>> get_v(0,0x6b^0xf)
0fb5b072e39d1faf5dcad9f197837a00
688f9f6f38387a4f4c9dffe49977ad0f
>>> get_v(0,0x6b,0x6b^0xb5^0xf)
0064b072e39d1faf5dcad9f197837a00
0f05c2917e82b0f29713286614f97a00
d8fd7cf227972785956c6867e377ad0f
05c2917e82b0f29713286614f97a000f #偶数次

R(0)
688f9f6f38387a4f4c9dffe49977ad0f F0[100]
6bb5b072e39d1faf5dcad9f197837a   R0
dd3fed8ca527d51286440e731a0dad   Rf
'''
#0xf 0x1f 0x2f ... f列
#取得第F列

def get_rf():
    global R
    for i in range(0xf,0x100,0x10):
        v = get_tv(0, R[0][0]^i)
        R[i] = [i for i in xor(v[:-1], bytes(R[0]+[0])[1:])]
        print(f'R_{i:x}', bytes(R[i]).hex())

取得剩余的全部R

#补全其它
'''
01 A B C ...     01000800000000000000000000000000
0f A C ...  01     dd7775c35d921c46f404c7682c5d43
                 7fdd75c35d921c46f404c7682c5d4301
A  C ... 01 0f     8009b22d640ecb6f2e636ba734310e
                 5d7c7170f6128d9b2aa4038b69720f7f
'''
def get_rx(s):
    global R
     
    s1 = s%0x10
    for i in range(0x100):
        pt = [0]*16
        pt[0],pt[s1 + 1]=s,i 
        v = get_v(bytes(pt))
        if v[-2] != s^R[v[-1]][-1]: continue
        
        pt = [0xff]*16            #第2次取数,两次比较,减少碰撞取错
        pt[0],pt[s1 + 1]=s,i^0x80 
        v1 = get_v(bytes(pt))
        if v[-1]^v1[-1] != 0x80: continue
        
        t1 = [_ for _ in xor(v[:-2],bytes(R[v[-1]][:-1]))]
        R[s] = t1[:s1]+[i^v[-1]]+t1[s1:]
        #print(f">>> {s:x} {i:x} {bytes(R[s]).hex()}={cipher.F(bytes([s])).hex()} ")
        break

解密

'''
bf1a15ab9aead5616aa5190645377261
f254c239818ee6efbf22fd26a6b3711a
e972490815d417dbdace83652e4b4dfd
bdb42fad8f66745e0268bf39f2800e4b
590abff1ca6e45ca77266575e46ce839
3bf4dc2aedd787ea8bd281b165f4b026
d59eb0020556f207401d454d9863b187
f122b2591ace01be2ee552c890e3e607
dfed5b62b533dc7f25d06e48df632abe
e676085c75fd3053209d69c345a3342a
1800c0c8428c3e8e5bbc1bac23838b69
8d70d881e274ac1db5be359be0d40abc
cfc99964d2f8b21fa6a6a61a04868be0
f83546173cf934c079a84e273e3d3bcf
'''
def decrypt(enc):
    senc = enc
    for _ in range(2):
        enc = senc
        print('way:',_)
        if _ == 1:  #两个xf结尾
            if R[enc[-1]] == '':
                get_rx(enc[-1])
            enc = xor(enc[:-1],bytes(R[enc[-1]]))+enc[-1:]
            #print(enc.hex())
        m = 30
        while m:
            if R[enc[-1]] == '':
                get_rx(enc[-1])
            idx = enc[-1]
            enc1 = xor(enc[:-1],bytes(R[idx]))
            pos = enc1[-1]%0x10
            m -=1
            if all([1 if 0x20<=i<0x7f or i==0 else 0 for i in enc1.rstrip(b'\x00')]):
                enc = bytes([idx])+ enc1
                print(enc)
                break
            else:
                enc = enc1[:pos] + bytes([idx]) + enc1[pos:] 
            #print(enc.hex())
    return enc 

开始

#------ready-----------------------
R = ['']*0x100

#context.log_level = 'debug'
#p = remote('chal.amt.rs', 1415)
p = process(['py', './pilfer-techies.py'])

enc_flag = get_flag()
print(enc_flag)
step1()

其它

看了小鸡块的WP Crypto趣题-Oracle | 糖醋小鸡块的blog

大神的思路完全不一样,但是显然更简单一点。

由于给定的原因第1个字节v0已知(后边块虽然未知但也可以猜,猜不了的就爆破) 那么通过v0确定下一字符的位置,然后猜下一字符v1。然后反复连接远端,由于密钥是动态的有1/16的概率会使第2个字节v1^F(v0)后的值=xF ,当密文(每次连接重取)与爆破的密文后两字节相同时说明字符正确并且都是走过0->v0->v1->xf->xf 的加密过程,给出的flag是经过这个流程加密猜的也是相同,则两个异或就能得到明文的14字节,再加上猜的两个自己整理成一段。。。。

  • 30
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值