EXP5 Padding Oracle攻击
实验5若因网络原因无法访问,可以参考下面的链接自行搭建本地环境完成。
https://github.com/mithi/simple-cryptography/tree/master/04-padding-oracle
参考实验:
cryptography-stanford/padding_oracle.py at master · beiluoshimeng/cryptography-stanford
实验目的及要求
目的:理解 padding oracle 攻击的过程,验证攻击的可行性。
环境:网站 crypto-class.appspot.com 部署了一个填充预言机的模拟示例,请搭建可以访问上述地址的实验环境。
👆观察到每次返回结果不同
要求:假设攻击者想要利用上述网站的返回结果窃取信息。攻击者通过观察得知:网站将用户的数据加密后利用 URL 参数进行传输,例如:
<http://crypto-class.appspot.com/po?er=f20bdba6ff29eed7b046d1df9fb7000058b1ffb4210a580f748b4ac714c001bd4a61044426fb515dad3f21f18aa577c0bdf302936266926ff37dbf7035d5eeb4>
当用户 Alice 和网站进行交互时,网站将上述 URL 发送给 Alice。攻击者猜测在"po?er="这一 URL 变量的值是一个密文文本,它是用有随机IV的CBC模式下的AES加密的Alice会话中的一些秘密数据,密文为16进制编码。
攻击者发现上述网站存在 padding oracle 攻击:当解密的CBC 密文出现填充错误时,网站服务器返回 403 错误(forbidden request),当 CBC 填充合法,但是 MAC 验证失败时,网站返回 404 错误(URL not found)。
请利用以上信息,解密上述代码框中的密文。为了完成解密,你可以发送任意的如下格式的 HTTP 请求并获取其对应的错误代码。Padding oracle 可以进行逐字节的解密,你需要发送 256 个 HTTP 请求来完成一个字节的解密。需要注意的是密文的第一个分组是随机的初始向量,且解密得到的消息使用ASCII 编码。
<http://crypto-class.appspot.com/po?er=>"your ciphertext here"
Padding Oracle攻击原理(CBC模式加密)
假设攻击者获取密文
c
=
(
c
[
0
]
,
c
[
1
]
,
c
[
2
]
)
c=(c[0],c[1],c[2])
c=(c[0],c[1],c[2]),并且想要获取明文
m
[
2
]
m[2]
m[2]。
解密步骤如下:
- 猜测明文 m [ 2 ] m[2] m[2]的最后一个字节是 g g g(从0~255依次尝试),修改密文的最后一个字节为 c [ 1 ] l a s t ⊕ g ⊕ 0 x 01 c[1]{last} \oplus g \oplus 0x01 c[1]last⊕g⊕0x01。密文解密后最后一个字节与密文最后一个字节的异或结果为 m [ 2 ] l a s t = D [ 2 ] l a s t ⊕ c [ 1 ] l a s t ⊕ g ⊕ 0 x 01 = m [ 2 ] l a s t ⊕ g ⊕ 0 x 01 m[2]{last}=D[2]{last} \oplus c[1]{last} \oplus g \oplus 0x01=m[2]{last} \oplus g \oplus 0x01 m[2]last=D[2]last⊕c[1]last⊕g⊕0x01=m[2]last⊕g⊕0x01。若 g g g猜测正确,即原始,最终结果应为 m [ 2 ] l a s t = 0 x 01 m[2]_{last}=0x01 m[2]last=0x01。
- 同理猜测明文的倒数第二个字节是 g ′ g' g′,修改 c [ 1 ] c[1] c[1]后两个字节为 c [ 1 ] l a s t _ s e c o n d c [ 1 ] l a s t ⊕ g ′ g ⊕ 0 x 020 x 02 c[1]{last\_second}c[1]{last} \oplus g'g \oplus 0x020x02 c[1]last_secondc[1]last⊕g′g⊕0x020x02。由第一步可得 g g g,则不断修改 g ′ g' g′,使修改后的解密结果最后两个字节 m [ 2 ] l a s t _ s e c o n d m [ 2 ] l a s t = 0 x 020 x 02 m[2]{last\_second}m[2]{last}=0x020x02 m[2]last_secondm[2]last=0x020x02,即可获得倒数第二个字节 g ′ g' g′。
- 依次从明文的最后一个分组开始,修改前一组密文的最后一个字节,猜测最后一组明文的最后一个字节,向前推导即可获得所有明文。
- 正向猜测原理相同,同样从IV的最后一个字节修改,使最终解密结果。
发送HTTP请求
URL介绍:
什么是 URL? - 学习 Web 开发 | MDN
使用request.get(url)
方法,该函数的返回值是一个Response
对象,该对象包含HTTP请求的响应信息,包括状态码、响应头、响应体等信息。可以通过该对象的属性和方法来获取和处理响应信息。
常用的Response
对象属性和方法如下:
-
status_code
:HTTP响应状态码。HTTP状态码用于表示HTTP请求的处理结果,通常由三个数字组成,第一个数字表示响应的类型,后两个数字没有分类的作用。常见的HTTP状态码有以下几种:
- 1xx(信息响应类):表示接收到请求并且继续处理中。
- 2xx(成功响应类):表示操作成功完成并且返回相应内容。
- 200 OK:请求成功。
- 201 Created:请求已经被实现,且创建了新的资源。
- 204 No Content:请求成功,但是没有响应内容。
- 3xx(重定向响应类):表示需要进一步操作以完成请求。
- 301 Moved Permanently:请求的资源已经永久转移。
- 302 Found:请求的资源临时从不同的URI响应请求。
- 304 Not Modified:请求的资源未被修改,可以使用缓存的版本。
- 4xx(客户端错误响应类):表示客户端发生了错误,如请求不存在的资源等。
- 400 Bad Request:请求无效,参数有误等。
- 401 Unauthorized:请求需要用户验证。
- 403 Forbidden:请求被拒绝。
- 404 Not Found:请求的资源不存在。
- 408 Request Timeout:请求超时。
- 5xx(服务器错误响应类):表示服务器端发生错误,如服务器繁忙等。
- 500 Internal Server Error:服务器内部错误。
- 503 Service Unavailable:请求的服务器暂时不可用。
-
headers
:HTTP响应头。 -
text
:HTTP响应体的文本内容。 -
content
:HTTP响应体的二进制内容。 -
json()
:将HTTP响应体的JSON数据转换为Python对象。 -
raise_for_status()
:如果HTTP请求返回错误码,则抛出异常。
代码及运行结果
代码:
import requests
def xor(bytes1, bytes2): # 逐字节异或
return bytes([a ^ b for a, b in zip(bytes1, bytes2)])
def query(mod_url): # 发送HTTP请求
# mod_url为十六进制形式
r = requests.get(mod_url)
if r.status_code == 403: # 填充错误
return False
elif r.status_code == 404: # 填充正确
return True
else:
raise Exception(f"得到意外的状态码: {r.status_code}")
class PaddingOracle:
def __init__(self, url, ct, block_size):
self.url = url
self.ct = ct
self.block_size = block_size
self.ct_bytes = bytearray(bytes.fromhex(self.ct)) # 转换为字节组
self.ct_blocks = [self.ct_bytes[i: i + self.block_size] for i in range(0, len(self.ct_bytes), self.block_size)] # 分组
self.block_decryption = bytearray(self.block_size) # 分组解密的结果
def decrypt_ct(self): # 整个密文
# 转换为字节串
# 分组解密
pt = ""
for i in range(1, len(self.ct_blocks)): # 正向解密
pt = pt + self.decrypt_block(i).hex()
# for i in range(len(self.ct_blocks)-1, 0, -1): # 反向解密
# pt = pt + self.decrypt_block(i).hex()
return pt
def decrypt_block(self, block_num): # 分组解密
print(f"正在解密分组#{block_num}...")
# 从后往前解密,猜测每个字节
block_before = self.ct_blocks[block_num-1]
block = self.ct_blocks[block_num]
bb_backup = bytearray(block_before) # 备份
start = 0
i = self.block_size-1
while i > -1:
if block_num == len(self.ct_blocks) - 1: # 最后一个分组
try:
self.guess(block, block_before, block_num, i, start) # 解密结果,正在解密的分组,正在解密的分组序号,正在修改的字节序号,最后一个分组开始尝试的字节
except Exception as e:
start = self.block_decryption[self.block_size - 1] + 1
i = self.block_size-1 # 重新猜测最后一个字节
# print("字节", i)
# print("开始", start)
self.block_decryption = bytearray(self.block_size) # 重置分组解密的结果
block_before[:] = bb_backup # 恢复前一组密文
self.guess(block, block_before, block_num, i, start)
else:
start = 0
self.guess(block, block_before, block_num, i, start)
i = i - 1
print(f"分组{block_num}的解密结果为: {self.block_decryption.hex()}")
print(f"分组{block_num}的ascii码为: \\"{self.block_decryption.decode('ascii')}\\"")
return self.block_decryption
def guess(self, block, block_before, block_num, byte_num, start): # 倒着尝试修改->密文分组,前一组密文,正在解密的分组序号,正在修改的字节序号,最后一个分组开始尝试的字节
pad_byte = self.block_size - byte_num # 填充内容
pad_content = bytearray([pad_byte] * pad_byte)
bb_backup = bytearray(block_before) # 备份
if block_num == len(self.ct_blocks)-1: # 最后一个分组
# 最后一个字节
if byte_num == self.block_size - 1:
for g in range(start, 256):
# print(g)
self.block_decryption[byte_num] = g # 猜测正在解密的字节明文为g
block_before[byte_num:] = xor(bb_backup[byte_num:], xor(self.block_decryption[byte_num:], pad_content))
new_url = (block_before + block).hex()
if query(self.url + new_url) is True:
print(f"byte #{byte_num + 1}: {hex(g)}") # .hex()接收整数参数,hex()接收字节或字节数组
block_before[:] = bb_backup
return
raise Exception("Unable to guess byte")
# 非最后一个字节
else:
for g in range(256):
self.block_decryption[byte_num] = g # 猜测正在解密的字节明文为g
block_before[byte_num:] = xor(bb_backup[byte_num:], xor(self.block_decryption[byte_num:], pad_content))
new_url = (block_before + block).hex()
if query(self.url + new_url) is True:
print(f"byte #{byte_num + 1}: {hex(g)}") # .hex()接收整数参数,hex()接收字节或字节数组
block_before[:] = bb_backup
return
raise Exception("Unable to guess byte")
# 不是最后一个分组
else:
for g in range(256): # 猜测字节内容
self.block_decryption[byte_num] = g # 猜测正在解密的字节明文为g
# print(g)
# 修改后的密文前一个字节的内容
# c[1]_{last_second}c[1]_{last} xor g'g xor 0x020x02
block_before[byte_num:] = xor(bb_backup[byte_num:], xor(self.block_decryption[byte_num:], pad_content))
# 修改后的url:前一个密文分组和该密文分组
new_url = (block_before + block).hex()
if query(self.url + new_url) is True:
print(f"byte #{byte_num + 1}: {hex(g)}") # .hex()接收整数参数,hex()接收字节或字节数组
block_before[:] = bb_backup
return
raise Exception("Unable to guess byte")
if __name__ == "__main__":
URL = "<http://crypto-class.appspot.com/po?er=>"
CipherText = "f20bdba6ff29eed7b046d1df9fb7000058b1ffb4210a580f748b4ac714c001bd4a61044426fb515dad3f21f18aa577c0bdf302936266926ff37dbf7035d5eeb4"
BlockSize = 16 # 16B/128b
# query(URL+CipherText) # 200
padding_oracle = PaddingOracle(URL, CipherText, BlockSize)
result = bytes.fromhex(padding_oracle.decrypt_ct()).decode("ascii")
print(f"Decryption Result: f'{result}'")
代码部分可能有点繁琐了,可以取消判断是否为最后一个密文分组的情况,即如果解密到分组前面发现没有符合要求的返回,则从最后一个字节开始重新猜测。
运行结果:
C:\\Users\\86186\\AppData\\Local\\Microsoft\\WindowsApps\\python3.9.exe "C:\\Users\\86186\\Desktop\\学习资料\\大三\\大三春\\信息安全技术\\实验\\实验五\\Padding Oracle Attack.py"
正在解密分组#1...
byte #16: 0x20
byte #15: 0x73
byte #14: 0x64
byte #13: 0x72
byte #12: 0x6f
byte #11: 0x57
byte #10: 0x20
byte #9: 0x63
byte #8: 0x69
byte #7: 0x67
byte #6: 0x61
byte #5: 0x4d
byte #4: 0x20
byte #3: 0x65
byte #2: 0x68
byte #1: 0x54
分组1的解密结果为: 546865204d6167696320576f72647320
分组1的ascii码为: "The Magic Words "
正在解密分组#2...
byte #16: 0x73
byte #15: 0x4f
byte #14: 0x20
byte #13: 0x68
byte #12: 0x73
byte #11: 0x69
byte #10: 0x6d
byte #9: 0x61
byte #8: 0x65
byte #7: 0x75
byte #6: 0x71
byte #5: 0x53
byte #4: 0x20
byte #3: 0x65
byte #2: 0x72
byte #1: 0x61
分组2的解密结果为: 6172652053717565616d697368204f73
分组2的ascii码为: "are Squeamish Os"
正在解密分组#3...
byte #16: 0x1
byte #16: 0x9
byte #15: 0x9
byte #14: 0x9
byte #13: 0x9
byte #12: 0x9
byte #11: 0x9
byte #10: 0x9
byte #9: 0x9
byte #8: 0x9
byte #7: 0x65
byte #6: 0x67
byte #5: 0x61
byte #4: 0x72
byte #3: 0x66
byte #2: 0x69
byte #1: 0x73
分组3的解密结果为: 73696672616765090909090909090909
分组3的ascii码为: "sifrage "
Decryption Result: f'The Magic Words are Squeamish Ossifrage '
进程已结束,退出代码0
问题记录
如果为403填充错误,网页显示:
如果为404MAC错误,网页显示:
最后一个分组明文猜测从0开始时,最后一个字节猜测为1时也会报404错误,且如果继续向前猜测时没有满足需求的g值,抛出异常"Unable to guess byte”。原因是明文的最后一个分组本身就会有填充,则最后一个分组最后几个字节应当满足填充格式,即N字节个N。若猜测到某个字节不满足填充格式或者往前猜测时找不到正确的填充,应当从最后一个字节开始,从上一次猜测值之后重新找到可以返回404错误的填充内容。