傳統的密碼安全性分析環境被稱為黑盒攻擊環境,攻擊者只能存取密碼系統的輸入與輸出,但隨著密碼系統部署環境的多樣化,該分析模型已經不能夠反映實際應用中攻擊者的能力。2002年,Chow等人[1]提出了白盒攻擊環境的概念,該攻擊環境中的攻擊者對演演算法執行環境具備完全的控制權,並且完全掌握演演算法的設計細節。白盒攻擊環境中攻擊者的能力包括但不限於:動態觀測演演算法程式執行過程、修改演演算法程式執行過程中的中間值、對演演算法程式進行偵錯分析等,其中包括了差分計算分析(DCA)。
差分計算分析(DCA):DCA不需要攻擊者掌握演演算法的設計細節,只需要採集演演算法程式在執行過程的中間狀態,通過相應的統計分析方法提取金鑰。DCA分析只需要掌握白盒實現的底層演演算法,能夠監測白盒實現程式的執行過程即可進行分析,極大地降低了分析的部署難度。
與DCA相似的,Sanfelix,Mune和de Haas在BlackHat Europe 2015上成功提出了針對相同白盒挑戰的差分故障分析(DFA)攻擊,也就是我們今天要研究的差分故障分析攻擊。在大部分白盒攻擊模型中,故障非常容易執行且成本低廉,並且不會導致程式自我毀滅
AES是著名的非對稱演演算法,通過每輪變換的輪金鑰實現加密。它由 10、12 或 14 輪(分別用於 AES-128、AES-192 和 AES-256)組成)通過重複操作逐步轉換 16 位元組輸入。在AES-128裡,第一輪的金鑰就是AES的金鑰,而在AES-256裡,第一輪的金鑰被拆分為兩個輪金鑰。
而問題就出在這裡,在白盒裡面,金鑰不會從原地進行輪金鑰加,而是在生成白盒時預先計算的,而且輪金鑰加會混合在其他輪次中來進行隱藏。
AES的整個加密解密流程如下圖
AddRoundKey (輪金鑰加)— 矩陣中的每一個位元組都與該次輪金鑰(round key)做XOR運算;每個子金鑰由金鑰生成方案產生。
**SubBytes(位元組替代) **— 通過非線性的替換函數,用查詢表的方式把每個位元組替換成對應的位元組。
**ShiftRows(行移位) **— 將矩陣中的每個橫列進行迴圈式移位。
MixColumns (列混淆)— 為了充分混合矩陣中各個直行的操作。這個步驟使用線性轉換來混合每列的四個位元組。
DFA 攻擊的一般要求是:
由AES加密演演算法流程可以看出,第10次輪祕鑰加之前是沒有列混淆的。
所以我們可以知道第九次輪金鑰加之前是這樣的:
如果我們在第九輪列混淆之前構造如下兩組資料:
可以看出這兩組資料只有第一個資料不一樣,以當前狀態繼續進行,那麼作為狀態矩陣的輸入就會影響到其餘地方,由於
通過上述表示式推算,可以得到一組K10的(0,7,10,13)的位置值,同理,如果換成其他位置的值,可以得到K10的其他位置值,從而推算得到整個K10的值,再根據AES祕鑰拓展演演算法(),最終可以還原原始的加密祕鑰。
ida開啟附件,findcrypto一下發現很明顯的AES加密
但是找了很久都沒有找到金鑰,於是可以聯想到是白盒AES(別問怎麼聯想的),用大爹的模擬執行程式碼進行還原金鑰
我們借用qiling框架來模擬程式執行。
函數開頭傳⼊引數, rcx 儲存輸⼊的地址
def hook_args(ql: Qiling):
ql.mem.write(0x500000000, b"\x01" * 16)
ql.arch.regs.write("rcx", 0x500000000)
#print(ql.mem.read(0x500000000, 16))
ql.mem.write(0x500000000 + 0x10, b"\x00" * 16)
ql.arch.regs.write("rdx", 0x500000000 + 0x10)
ql.mem.write(0x500000000 + 0x20, b"\x00" * 16)
ql.arch.regs.write("rbx", 0x500000000 + 0x20)
#print("Hook Success")
return
start_addr = 0x140004BF0
ql.hook_address(hook_args, start_addr)
上文中,我們說過要將構造多組資料將AES的金鑰攻擊出來,而我們是在第九次進行輪金鑰加之前加入的這種資料,這種資料後文叫缺陷資料。
找到列混合的地方可以很快找到輪金鑰加,在本題中我們很容易找到一個對16位元字元進行操作的函數,可以肯定這就是列混合的地方。
往下翻,就能找到輪金鑰加的地方
每輪v6加了1024*4個位元組,也就是1000個位元組,在經過八次相加之後就是8000,在這之後加入缺陷程式碼
在組合層面上就是下面的程式碼
def hookcode(ql:Qiling):
if ql.arch.regs.read("r12") == 0x8000:
global index
#第一次需要獲取正確的密文,所以不需要插入\x00,也就是說下面的write第一次不用寫
ql.mem.write(0x500000000 + index,b'\x00')
index += 1
#此處是獲取正確的密文
#print(ql.mem.read(0x500000000,16).hex())
#print(ql.mem.read(0x500000000,16))
return
index_addr = 0x1400052c5#這個地址就是判斷是否到第酒次的地址
ql.hook_address(hookcode,index_addr)
找到定義密文的地方,如下圖所示
所以可以直接hook這個地址,拿到密文
def hook_enc(ql:Qiling):
print(ql.mem.read(0x500000000,16).hex)
return
enc_addr = 0x1400053ca
ql.hook_addr(hook_enc,enc_addr)
執行兩次(每次的執行程式碼不同)驗證正確密文和錯誤密文,看結果是否滿足白盒AES原理
可以看出,分別得到了正確密文為
e14d5d0ee27715df08b4152ba23da8e0
第九輪列混淆時,錯誤的密文為
d24d5d0ee27715ac08b4bf2ba272a8e0
在第0,7,10,13個位元組分別與正確密文有錯誤,和原理一致。
寫個for迴圈即可得到剩下的密文,整個程式碼如下
from qiling import *
from qiling.const import QL_VERBOSE
index = 0
ql = Qiling(
["/home/nian/桌面/examples/m1_read.exe"],
r"/home/nian/桌面/examples/rootfs/x86_windows",
verbose=QL_VERBOSE.OFF,
)
def hook_args(ql: Qiling):
ql.mem.write(0x500000000, b"\x01" * 16)
ql.arch.regs.write("rcx", 0x500000000)
#print(ql.mem.read(0x500000000, 16))
ql.mem.write(0x500000000 + 0x10, b"\x00" * 16)
ql.arch.regs.write("rdx", 0x500000000 + 0x10)
ql.mem.write(0x500000000 + 0x20, b"\x00" * 16)
ql.arch.regs.write("rbx", 0x500000000 + 0x20)
#print("Hook Success")
return
def hook_code(ql: Qiling):
if ql.arch.regs.read("r12") == 0x8000:
global index
ql.mem.write(0x500000000 + index, b"\x00")
index += 1
#print(ql.mem.read(0x500000000, 16).hex())
#print(ql.mem.read(0x500000000 + 0x10, 16))
return
def hook_enc(ql: Qiling):
print(ql.mem.read(0x500000000, 16).hex())
return
index_addr = 0x1400052C5
start_addr = 0x140004BF0
end_addr = 0x14000542D
enc_after = 0x1400053CA
ql.hook_address(hook_args, start_addr)
ql.hook_address(hook_code, index_addr)
ql.hook_address(hook_enc, enc_after)
# e14d5d0ee27715df08b4152ba23da8e0
# e14d5d73e27708df0878152b843da8e0
for i in range(16):
ql.run(begin=start_addr, end=end_addr)
得到如下錯誤密文
"""d24d5d0ee27715ac08b4bf2ba272a8e0
e14d5d73e27708df0878152b843da8e0
e14dd50ee23415df7fb4152ba23da890
e16f5d0e537715df08b415e7a23dc6e0
e11a5d0e057715df08b4151ba23d99e0
574d5d0ee277157508b4df2ba234a8e0
e14d5d49e27785df0840152bff3da8e0
e14db80ee2d215dfceb4152ba23da868
e14dc60ee2bf15dfc4b4152ba23da8bf
e1425d0e5e7715df08b415b6a23d4ce0
5d4d5d0ee277159608b42f2ba297a8e0
e14d5d6ce2773ddf089d152ba93da8e0
e14d5dcde2772adf084b152bba3da8e0
e14df40ee27115df96b4152ba23da881
e11b5d0e337715df08b41544a23df3e0
fa4d5d0ee27715af08b42e2ba2c2a8e0"""
現在已知了正確的密文和所有的錯誤密文,可以用phoenixAES工具來還原原理中提到的k10
https://github.com/SideChannelMarvels/JeanGrey/tree/master/phoenixAES
程式碼如下:
import phoenixAES
with open("tracefile","wb") as t:
t.write(
""" e14d5d0ee27715df08b4152ba23da8e0
d24d5d0ee27715ac08b4bf2ba272a8e0
e14d5d73e27708df0878152b843da8e0
e14dd50ee23415df7fb4152ba23da890
e16f5d0e537715df08b415e7a23dc6e0
e11a5d0e057715df08b4151ba23d99e0
574d5d0ee277157508b4df2ba234a8e0
e14d5d49e27785df0840152bff3da8e0
e14db80ee2d215dfceb4152ba23da868
e14dc60ee2bf15dfc4b4152ba23da8bf
e1425d0e5e7715df08b415b6a23d4ce0
5d4d5d0ee277159608b42f2ba297a8e0
e14d5d6ce2773ddf089d152ba93da8e0
e14d5dcde2772adf084b152bba3da8e0
e14df40ee27115df96b4152ba23da881
e11b5d0e337715df08b41544a23df3e0
fa4d5d0ee27715af08b42e2ba2c2a8e0
""".encode("utf-8")
)
phoenixAES.crack_file("tracefile",verbose=0)
執行後得到k10
"""B4EF5BCB3E92E21123E951CF6F8F188E"""
得到第十輪的金鑰後,就可以著手還原原始金鑰了,用到的工具是Stark
https://github.com/SideChannelMarvels/Stark
下載好stark之後要記得在檔案目錄下面make把三個c檔案編譯成可執行檔案,再用可執行檔案操作,如下圖
這樣一來,就得到原始金鑰key0了
"""00000000000000000000000000000000"""
再利用原始金鑰解密原始密文,即可得到明文
out.bin裡面的密文
"""0B987EF5D94DD679592C4D2FADD4EB89"""
解密即可得到明文
from Crypto.Cipher import AES
enc = bytearray(bytes.fromhex("0B 98 7E F5 D9 4D D6 79 59 2C 4D 2F AD D4 EB 89"))
enc = bytes([enc[i] ^ 0x66 for i in range(16)])
key = bytes.fromhex("00000000000000000000000000000000")
aes = AES.new(key=key, mode=AES.MODE_ECB)
print(aes.decrypt(enc))
執行,得到flag
上述就是基本的AES差分故障分析方法和例題,而除了AES之外,還有DES和SM4的白盒,它們和AES的故障攻擊方法基本大同小異
這裡給出研究SM4的部落格供大家(wo)學習
https://www.anquanke.com/post/id/231483