CBC Bit Flipping Attack

XDU-MoeCTF-BigOreo

题目给了源码,flag是放在远程交互式进程(81.68.112.139:10005)上的。没办法人工读取加密过后的乱码且交互时间较短,我们需要写一个exp。

题目

from Crypto.Util.number import *
from Crypto.Cipher import AES
import os
from hashlib import sha256
import socketserver
import signal
import string
import random
from secret import flag

table = string.ascii_letters+string.digits
BANNER = br'''
oooooooooo.  ooooo   .oooooo.                  .oooooo.   ooooooooo.   oooooooooooo   .oooooo.
`888'   `Y8b `888'  d8P'  `Y8b                d8P'  `Y8b  `888   `Y88. `888'     `8  d8P'  `Y8b
 888     888  888  888                       888      888  888   .d88'  888         888      888
 888oooo888'  888  888                       888      888  888ooo88P'   888oooo8    888      888
 888    `88b  888  888     ooooo             888      888  888`88b.     888    "    888      888
 888    .88P  888  `88.    .88'              `88b    d88'  888  `88b.   888       o `88b    d88'
o888bood8P'  o888o  `Y8bood8P'   ooooooooooo  `Y8bood8P'  o888o  o888o o888ooooood8  `Y8bood8P'

'''

BASE_MENU = br'''
[+] 1.Register:
[+] 2.Exit:
'''

MENU = br'''[+] 1.Encrypt:
[+] 2.Decrypt:
[+] 3.GetFlag:
[+] 4.Exit:
'''

def Pad(msg):
    return msg + os.urandom((16-len(msg)%16)%16)

class Task(socketserver.BaseRequestHandler):
    def _recvall(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 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):
        proof = (''.join([random.choice(table)for _ in range(12)])).encode()
        sha = sha256( proof ).hexdigest().encode()
        self.send(b"[+] sha256(XXX+" + proof[3:] + b") == " + sha )
        XXX = self.recv(prompt = b'[+] Plz Tell Me XXX :')
        if len(XXX) != 3 or sha256(XXX + proof[3:]).hexdigest().encode() != sha:
            return False
        return True

    def Register(self):
        self.send(b'[+]USERNAME:')
        username = self.recv()
        return username

    def encrypt(self,username,iv,key,nonce):
        aes = AES.new(key,AES.MODE_CBC,iv)
        plain = Pad(nonce + b"||GiveyourFLAG||" + username)
        return aes.encrypt(plain)

    def decrypt(self,iv,key,cipher):
        aes = AES.new(key,AES.MODE_CBC,iv)
        return aes.decrypt(cipher)

    def handle(self):
        signal.alarm(50)
        if not self.proof_of_work():
            return
        self.send(BANNER,newline = False)
        self.key = os.urandom(16)
        self.iv = os.urandom(16)
        self.nonce = os.urandom(16)
        self.send(b'[+]iv:'+ self.iv)
        self.send(b'[+]nonce:'+ self.nonce)
        self.send(BASE_MENU,newline = False)

        option = self.recv()
        if option == b'1':
            try:
                self.username = self.Register()
                for i in range(3):
                    self.send(MENU,newline= False)
                    self.send(b"Hello " + self.username + b'! Let\'s eat this big OREO')
                    op = self.recv()
                    if op == b'1':
                        cipher = self.encrypt(self.username,self.iv,self.key,self.nonce)
                        self.send(b"[+]cipher:" + cipher)

                    elif op == b'2':
                        self.send(b"[+] Give Me Your Cipher:")
                        cipher = self.recv()
                        self.send(b"[+] Give Me IV:")
                        iv = self.recv()
                        plain = self.decrypt(iv,self.key,cipher)
                        self.send(b"[+] Plain is :" + plain)

                    elif op == b'3':
                        self.send(b"[+] Give Me Your Cipher:")
                        cipher = self.recv()
                        self.send(b"[+] Give Me IV:")
                        iv = self.recv()
                        plain = self.decrypt(iv,self.key,cipher)
                        non,msg,id = plain.split(b'||')
                        if non != self.nonce or msg != b'Give_me_FLAG' or id != self.username:
                            self.send(b'[!]FAILED')
                        else:
                            self.send(b'FLAG:' + flag)

                    else:
                        break
            except:
                self.request.close()
        self.request.close()

class ThreadedServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

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

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

分析

handle函数,一上来给了个字符串的sha256,先让我们搞proof_of_work,爆破3个字符还是很快:

table = string.ascii_letters+string.digits
for i in table:
        for j in table:
                for k in table:
                        proof=i+j+k+s
                        if(sha256(proof.encode()).hexdigest()==tsha):
                                oreo.sendline((i+j+k).encode())
                                break

然后开始正式内容:AES-CBC加密

首先给了16字节的IV和nonce

IV是初始化向量,用于AES-CBC加密算法中加密第一个块

NonceNumber once的缩写,在密码学中Nonce是一个只被使用一次的任意或非重复的随机数值。在加密技术中的初始向量和加密散列函数都发挥着重要作用,在各类验证协议的通信应用中确保验证信息不被重复使用以对抗重放攻击(百度百科)

接下来Register()让我们输入用户名

然后给我们四个选项:

[+] 1.Encrypt:
[+] 2.Decrypt:
[+] 3.GetFlag:
[+] 4.Exit:

(当然是选4,Exit)

看第一个Encrypt选项相关源码:

   def Pad(msg):
        return msg + os.urandom((16-len(msg)%16)%16)        #补齐48字节

    ......

   def encrypt(self,username,iv,key,nonce):
        aes = AES.new(key,AES.MODE_CBC,iv)                  #创建一个aes对象
        plain = Pad(nonce + b"||GiveyourFLAG||" + username) #组装48字节的plain字符串
        return aes.encrypt(plain)

    ......

    self.encrypt(self.username,self.iv,self.key,self.nonce)
    self.send(b"[+]cipher:" + cipher)

我们看到,48字节的plaintext由16字节nonce、16字节||GiveyourFLAG||和补齐16字节的username组成:

plaintext = nonce||GiveyourFLAG||username

然后对其进行AES-CBC加密

AES-CBC加密

Plaintext被分成多个block,对于第i​个blockCiphertext$C_i$​,我们有
C_i=E_k(P_i\oplus C_{i-1})\\C_0=IV在这道题里,P_1就是nonceP_2就是||GiveyourFLAG||P_3就是username,而我们从输出获取到的C也可以分为3个16字节长的C_1,C_2,C_3​与之对应

我们继续看第二部分Decrypt的源码:

    def decrypt(self,iv,key,cipher):
        aes = AES.new(key,AES.MODE_CBC,iv)
        return aes.decrypt(cipher)

    ......

    cipher = self.recv()
    iv = self.recv()
    plain = self.decrypt(iv,self.key,cipher)
    self.send(b"[+] Plain is :" + plain)

我们看AES-CBC解密的过程

AES-CBC解密

因为C_i=E_k(P_i\oplus C_{i-1}),我们计算D_k(E_k(P_i\oplus C_{i-1}))\oplus C_{i-1}=P_i,所以对于第iblockPlaintext$P_i$,有
P_i=D_k(C_i)\oplus C_{i-1}\\C_0=IV好,我们看看第三个选项GetFlag:

    cipher = self.recv()
    iv = self.recv()
    plain = self.decrypt(iv,self.key,cipher)
    non,msg,id = plain.split(b'||')
    if non != self.nonce or msg != b'Give_me_FLAG' or id != self.username:
        self.send(b'[!]FAILED')
    else:
        self.send(b'FLAG:' + flag)

它将我们输入的cipheriv解密出plain,判断plain满足以下格式

plaintext = nonce||Give_me_FLAG||username

问题就在我们能拿到的cipher对应的plain中间部分是GiveyourFLAG,如果能把这里的your改成_me_就好了

但是我们不知道key,能调用的只有Decrypt

CBC Bit Flipping Attack

your_me_都是4个字节,此题中Decrypt的IV是由我们提供的,这为实施攻击成为可能。

观察P_2=D_k(C_2)\oplus C_1​​,(此时我们已经有原来的C​​),我们期望P_2​​被解密为P_2’​​​。而C_2​​被塞入D_k​​​中返回值无法被控制,于是我们希望改变C_1​​的字节来达到目的。

我们尝试计算新的C_1’​,列方程解之:
C_1’\oplus D_k(C_2)=P_2’\\C_1\oplus D_k(C_2)=P_2C_1’=P_2’ \oplus D_k(C_2) =P_2’ \oplus P_2 \oplus C_1​,这些都是已知的项,于是我们可以愉快的让Decrypt解密出想要的P_2’​啦

orgbyte=b'your'
targetbyte=b'_me_'
#byte类型不能更改里面的值,我们只能通过拼装的方式构造新的Cipher
#your在P_1的第7~10个byte,对应的,我们改变C_1的第7~10个byte
#我们只用改变your所在的那4个byte,其他照抄原来的C_1(代码中叫e)
#byte类型不能直接异或,先得转化为long int类型
new_fixed_e=long_to_bytes(bytes_to_long(orgbyte)^bytes_to_long(e[7-1:10])^bytes_to_long(targetbyte))
newe= e[:7-1] + new_fixed_e + e[10:]

C’_1就构造完成啦

这样有个问题,C’​​是一个整体被送进Decrypt的,如果我们改变了C_1​​,那么解出来的P_1=D_k(C_1)\oplus VI​​就不是原来的P_1​​​了(在本题中就是nonceGetFlag()中有验证nonce有没被改)

但是我们可以改VI,让Decrypt解出原来的P_1

尝试计算新的VI’​​​​,列方程:
D_k(C_1)\oplus VI=P_1\\D_k(C_1’)\oplus VI’=P_1 D_k(C_1’)消不掉,这没法解,然而我们可以利用一次Decrypt,通过Decrypt(C’,VI)得到一个P’’,列方程:
D_k(C_1’)\oplus VI=P_1”联立上两个式子,我们可以解得VI’=P_1 \oplus D_k(C_1’)=P_1 \oplus P_1’’\oplus VI

oreo.sendline(b'2')
oreo.sendlineafter(b'[+] Give Me Your Cipher:',newe)#发送C'
oreo.sendlineafter(b'[+] Give Me IV:',iv)#发送原IV
d=oreo.recvuntil(b'\n',drop=True)#接收P''

newiv=long_to_bytes(bytes_to_long(iv)^bytes_to_long(nonce)^bytes_to_long(d[:16]))

至此CBC Bit Flipping Attack就完成了,Decrypt(C’,VI’)得到我们想要让它解密的结果P’​,得到flag。

回放一下结论,其中P_i’是目标明文的第i个block,P’’=Decrypt(C’,VI)
C_{i-1}’=P_i’ \oplus P_i \oplus C_{i-1}\\VI’=P_1’’ \oplus P_1\oplus VI
完整exploit

from pwn import *
import string
from hashlib import sha256
from Crypto.Util.number import*

'''
Section1 : FBI Open The Door!!!
'''

oreo=remote("81.68.112.139",10005)

pres=oreo.recvuntil(b'[+] sha256(XXX+',drop=True)
s=oreo.recvuntil(b') == ',drop=True).decode()
tsha=oreo.recvuntil(b'\n',drop=True).decode()

table = string.ascii_letters+string.digits

for i in table:
        for j in table:
                for k in table:
                        proof=i+j+k+s
                        if(sha256(proof.encode()).hexdigest()==tsha):
                                oreo.sendline((i+j+k).encode())
                                break

'''
Section2 : Get Ciphers!
'''

pres=oreo.recvuntil(b'[+]iv:',drop=True)
iv=oreo.recvuntil(b'\n',drop=True)

pres=oreo.recvuntil(b'[+]nonce:',drop=True)
nonce=oreo.recvuntil(b'\n',drop=True)


oreo.sendline(b'1')
oreo.sendlineafter(b'[+]USERNAME:\n',b'De3b4to_tql!!!!!')
oreo.sendlineafter(b'! Let\'s eat this big OREO\n',b'1')

pres=oreo.recvuntil(b'[+]cipher:',drop=True)
e=oreo.recvuntil(b'\n',drop=True)

'''
Section3 : CBC Bit Flipping Attack!
'''

orgbyte=b'your'
targetbyte=b'_me_'

new_fixed_e=long_to_bytes(bytes_to_long(orgbyte)^bytes_to_long(e[7-1:10])^bytes_to_long(targetbyte))
newe= e[:7-1] + new_fixed_e + e[10:]

oreo.sendline(b'2')
oreo.sendlineafter(b'[+] Give Me Your Cipher:',newe)
oreo.sendlineafter(b'[+] Give Me IV:',iv)

pres=oreo.recvuntil(b'[+] Plain is :',drop=True)
d=oreo.recvuntil(b'\n',drop=True)

newiv=long_to_bytes(bytes_to_long(iv)^bytes_to_long(nonce)^bytes_to_long(d[:16]))

'''
Section4 : Eat this bigggg Oreo!
'''

oreo.sendline(b'3')
oreo.sendlineafter(b'[+] Give Me Your Cipher:',newe)
oreo.sendlineafter(b'[+] Give Me IV:',newiv)

pres=oreo.recvuntil(b'FLAG:',drop=True)
flag=oreo.recvuntil(b'\n',drop=True)

print(flag)
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
下一篇