# [CH] Secure Store GPT ###### tags: `Writeup` `Crypto` `Chinese` > [name=Curious] ## 思路 ### Padding Oracle 關於 padding oracle 詳細的方法和實作這邊就先不討論,詳細的可以參考 [Github](https://github.com/Curious-Lucifer/CTFLib/blob/master/Crypto/Block_Cipher.py#L6) ``` ################################################################## # AES CBC Mode (Decrypt) # ################################################################## # # # ----------- ----------- ----------- # # | cipher1 |---* | cipher2 |---* | cipher3 | # # ----------- | ----------- | ----------- # # | | | | | # # ----------- | ----------- | ----------- # # | | | | | | | | # # | Decrypt | | | Decrypt | | | Decrypt | # # | | | | | | | | # # ----------- | ----------- | ----------- # # ------ | | | | | # # | IV | --> ⊕ *-------> ⊕ *-------> ⊕ # # ------ | | | # # ----------- ----------- ----------- # # | plain1 | | plain2 | | plain3 | # # ----------- ----------- ----------- # # # ################################################################## ``` 思考一下 padding oracle 的運作方式,如上圖所示,如果我們想要知道 `plain3` 的話,那就需要知道 `cipher2` 和 `cipher3` 的值,然後可以把 `cipher` 縮減成 `cipher2 + cipher3`,透過 `cipher2` 各個 byte 的暴力嘗試可以得到 `plain3` 的值。相同的,如果我們想要知道 `plain1` 的值,就需要知道 `IV` 和 `cipher1` 的值,同樣的把 `cipher` 縮減成 `IV + cipher1`,就可以透過 padding oracle 得到 `plain1`。 所以說我們可以把 padding oracle 簡化成這個形式,其中 `cipher[0]` 就代表 `IV` ```python # i >= 1 plain[i] = padding_oracle(cipher[i - 1], cipher[i]) ``` --- ## 解法 先分析一下 `server.py`,可以發現題目不給我們 `IV` 的值,所以沒有辦法透過一般的 padding oracle 得到 `plain1`。但是題目給了一個 `token` 跟這段 `token` 的 CBC MAC。如果把 CBC MAC 的算法畫出來的話 ``` ################################################################## # AES CBC MAC # ################################################################## # # # ----------- ----------- # # | plain1 | | plain2 | # # ----------- ----------- # # ------ | | # # | IV | --> ⊕ *-------> ⊕ *---------* # # ------ | | | | | # # ----------- | ----------- | ----------- # # | | | | | | | | # # | Encrypt | | | Encrypt | | | Encrypt | # # | | | | | | | | # # ----------- | ----------- | ----------- # # | | | | | # # ----------- | ----------- | ----------- # # | cipher1 |---* | cipher2 |---* | CBC_MAC | # # ----------- ----------- ----------- # # # ################################################################## ``` 圖中的 `plain1 + plain2` 就是我們拿到的 `token`。已知任何東西跟同樣長度全部都是 `b'\x00'` 的 bytes 做 xor 後值不會改變,所以我們可以把 `cipher2` 和 `CBC_MAC` 的關係想像成 ``` ####################################### # # # ----------- ----------- # # | NULL |---* | CBC_MAC | # # ----------- | ----------- # # | | # # | ----------- # # | | | # # | | Decrypt | # # | | | # # | ----------- # # | | # # *-------> ⊕ # # | # # ----------- # # | cipher2 | # # ----------- # # # ####################################### ``` 這樣就可以透過 padding oracle 知道 `cipher2` 的值,然後同樣的方法用 `cipher2` 的值可以知道 `xor(cipher1, plain2)`,又因為我們知道 `token`,所以 `plain1` 和 `plain2` 都知道,所以可以推得 `cipher1` 的值。同樣得可以推得 `xor(IV, plain1)` 的值,最後推得 `IV` 的值。 知道 `IV` 的值後就是普通的 padding oracle 了,在這邊就不詳細討論。 Solve Script : ```python= from pwn import * from base64 import b64encode, b64decode def padding_oracle_attack(pre_cipher_block: bytes, cipher_block: bytes, oracle, r): ''' - input : `pre_cipher_block (bytes)`, `cipher_block (bytes)`, `oracle (func)`, `r (remote object)` - output : `plain_block (bytes)` , cipher_block's plaintext - oracle func : - input : `cipher (bytes)`, `r (remote object)` - output : `padding_right (bool)` , represent if the padding of cipher's plaintext is right ''' assert len(pre_cipher_block) == len(cipher_block) == 16 last_bytes = [] for i in range(256): cipher_test = pre_cipher_block[:15] + bytes([i]) + cipher_block if oracle(cipher_test, r): last_bytes.append(bytes([i])) if len(last_bytes) == 1: plain_block = xor(last_bytes[0], b'\x01', pre_cipher_block[-1:]) else: plain_block = xor(last_bytes[last_bytes.index(pre_cipher_block[-1:]) ^ 1], b'\x01', pre_cipher_block[-1:]) print(str(plain_block), end='\r') for j in range(1,16): for i in range(256): cipher_test = pre_cipher_block[:15 - j] + bytes([i]) + xor(pre_cipher_block[-j:], plain_block, bytes([j + 1]) * j) + cipher_block if oracle(cipher_test, r): plain_block = xor(bytes([i]), bytes([j + 1]), pre_cipher_block[-j - 1:-j]) + plain_block break print(str(plain_block), end='\r') print(f'result : {str(plain_block)}') return plain_block r = remote('lotuxctf.com', 30001) r.recvlines(2) token_sig = b64decode(r.recvline().strip().split(b': ')[1].decode()) token, sig = token_sig[:-16], token_sig[-16:] flag_enc = b64decode(r.recvline().strip().split(b': ')[1].decode()) r.recvlines(1) REQTIME = 0 def oracle(cipher, r): global REQTIME REQTIME += 1 print(REQTIME) r.sendlineafter(b': ', b64encode(cipher)) return b'Message Stored' in r.recvline() pre_token = padding_oracle_attack(b'\x00' * 16, sig, oracle, r) pre_token = xor(padding_oracle_attack(b'\x00' * 16, pre_token, oracle, r), token[-16:]) iv = xor(padding_oracle_attack(b'\x00' * 16, pre_token, oracle, r), token[:16]) flag_enc = iv + flag_enc flag = b'' for i in range(0, len(flag_enc) - 16, 16): flag += padding_oracle_attack(flag_enc[i:i + 16], flag_enc[i + 16:i + 32], oracle, r) print(flag) r.interactive() ``` {%hackmd M1bgOPoiQbmM0JRHWaYA1g %}