# [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 %}