作者:天融信阿爾法實驗室
公眾號:https://mp.weixin.qq.com/s/OtGw-rALwpBkERfvqdZ4kQ
1、padding oracle 簡介
首先我們先看一下padding oracle漏洞簡單描述,以下是來自百度百科的解釋
Padding的含義是“填充”,在解密時,如果算法發現解密后得到的結果,它的填充方式不符合規則,那么表示輸入數據有問題,對于解密的類庫來說,往往便會拋出一個異常,提示Padding不正確。Oracle在這里便是“提示”的意思,一開始看到漏洞名稱中有oracle的時候我也被誤導了,單實際上和甲骨文公司沒有任何關系。
2、常見的加密模式
首先我們知道,加密的方法有很多種,分為兩大類 對稱加密和非對稱加密,
對稱加密又稱單密鑰加密,也就是字面意思,加密解密用的都是同一個密鑰,常見的對稱加密算法,例如DES,3DES和AES等
非對稱加密,就是說密鑰分兩個,一個公鑰,一個私鑰,加解密過程就是公鑰加密私鑰解密和私鑰加密公鑰匙解密,常見的非對稱加密算法有,RSA DSA等
初次接觸這個漏洞的人,多會認為此漏洞是一個加密算法的漏洞,然而實際卻并非如此隨著講解就會明白真正的漏洞點出在何處
我們常用的加密可以分為兩部分來理解,一部分是加密算法,這部分的話過于高深需要相當程度的密碼學知識做基礎。而另一部分,就是加密模式,這部分相較于加密算法來說就簡單太多了,而此次出現padding oracle漏洞的就是CBC加密模式
這里我從網上截幾個圖解釋下這個CBC模式是個什么東西,出了這個CBC模式外還有哪些個加密模式。
AES是對稱加密,對稱加密呢有兩大類加密模式,即分組加密和流加密,AES分組加密有五種加密模式:
-
電碼本模式(Electronic Codebook Book (ECB));
-
密碼分組鏈接模式(Cipher Block Chaining (CBC));
-
計算器模式(Counter (CTR));
-
密碼反饋模式(Cipher FeedBack (CFB));
-
輸出反饋模式(Output FeedBack (OFB))。
此次出現問題的是CBC加密模式,為了方便理解加密模式 我們就順便也講一下ECB加密模式。
3、ECB加密模式簡介
首先我們要明白加密算法和加密模式是兩個概念,AES是加密算法,加密算法是通過接收方和發送方協商產生密鑰,結合一系列的各種位運算之后得出的結果。而加密模式是在加密算法的基礎上,把加密的方式變得更加復雜一點,首先我們看下最簡單直接的ECB加密模式

可以看到ECB加密模式的思想非常直白,就是把明文分為等長的塊,然后一塊一塊的加密,最后再把每一塊加密后的密文拼接在一起。這就是ECB加密模式
4、CBC加密模式簡介
接下來就講一講這次的重點,也就是CBC加密模式

首先多了一個IV,這個IV我們一般稱作初始向量,首先明文還是那個明文,分塊還是要分塊,在通過AES加密之前我們需要先將明文塊0,也就是第一塊明文,和我們的初始向量IV做異或操作,這個初始向量IV是隨機的,而且長度是和我們的每一塊明文塊等長,因為要按位進行異或嘛。這樣無疑就在加密之前就已經先行打亂的我們的明文,與初始化向量異或后的明文,我們暫且稱它為中間值,我們此時再對這個中間值進行AES加密,這樣第一塊明文的加密就完成了。
從上面哪個截圖我們不難看的出,CBC模式是一個鏈式結構,這個鏈接的關鍵點就在于,我們加密第二塊明文的時候同樣也需要一個初始化向量來和我們第二塊明文也就是明文塊1來進行異或,那這個初始話向量哪來的呢,總不可能系統為每一塊明文都分配一個隨機的初始化向量吧,這樣成本過高。所以我們將第一塊明文加密后的密文,作為第二個明文加密時的初始化向量,而這個就是這個鏈式結構的連接點,后續的步驟就是不斷重復加密第一塊明文時所做的操作,直至最后一塊明文加密完成。
感覺上CBC模式比ECB模式流程上復雜這么多,應該比ECB模式更安全才對,理論上講確實,因為引入了初始化向量這個一個操作,所以CBC加密的結果隨機性更高,相同的明文ECB加密每一次的結果都是相同的,也就是明文和密文一對一。 而CBC由于多了一個隨機的初始化向量,所以同樣的明文CBC每一次加密出來的結果都是不一樣的。由此來看CBC明顯比ECB更安全,但是CBC這個模式在設計上存在缺陷 ,而這個缺陷就導致了著名的padding oracle攻擊
既然有加密,那肯定就會有解密,而且此次被攻擊的是服務端,那肯定就是我們客戶端發送加密數據,然后服務端解密我們的數據,然后給我們反饋,要么解密成功要么解密不成功,而攻擊就發生在服務端解密和反饋這個過程。
不知道講解到此處大家心里有沒有一個疑惑。
回過頭來看一下,前文說了分組加密的分組要怎么樣?等長對不對 而且 初始化向量和每一組分組都要等長對不對?
用AES和DES兩個加密算法來舉例子 AES的分組長度為每塊16字節,DES呢則是每塊8字節,那么怎么能保證我們的明文長度是16或者8的整數倍呢?
當然沒辦法保證,所以我們就要采取措施強制讓明文為16或者8的整數倍,最直接的方法自然就是直接填充,不夠就補到它夠為止。這也就是所謂的padding 填充。
5、padding oracle 原理
而CBC加密模式的設計者自然也考慮到了這問題,剩余的幾位當然不能隨便填充,而要填充一些有價值的數值。
假如說此時我們按8字節為一個明文分組,分到最后發現最后一組缺了一個字節,程序不會填一些隨機數,亦或者將不夠的位數全填零。CBC模式最后的填充方法,就是缺了一位就填一個0x01,缺了兩位就填兩個0x02,缺了三位就填三個0x03,以此往后類推缺n個就填n個0x0n。哪怕當明文正好時分組的整數倍時,也會填充8個0x08,即使是整數倍也要填充。這樣就導致了無論我們明文的長度是多少,我們CBC模式加密是都會在明文的最后進行填充,以確保分段的長度是8的整數倍。
不理解的可以看一下具體的填充算法
add = length - (count % length)
plaintext = plaintext + ('\0' * add) #填充
通過下圖可以更好的讓我們理解這種填充的思想

之所以選擇這么填充,就肯定是有它的道理的。
前文說了,有加密就有解密,那解密的時候這個填充位就會起到很大的作用,CBC模式解密的流程其實就是加密流程再反過來。
我們再看一下加密的流程

下面是解密的流程

此時我們先思考一個問題就是我們平常是通過什么來判斷一個業務邏輯或者是功能點是存在漏洞的?那就是通過服務端的回顯來判斷對不對?如果說服務端給我們的回復一直都是一樣的我們能判斷這個功能點就是存在漏洞么?舉個例子拿暴力破解這個漏洞來說,通常都是用在攻擊網站的登陸點上,通常存在暴力破解漏洞的登陸點都會返回這樣的信息“該用戶名不存在”,“密碼輸入錯誤”。通過這兩條返回信息我們可以判斷用戶名是否存在和密碼是否輸入正確。判斷的依據就是返回信息的不同,正是因為用戶名錯誤和密碼錯誤的返回消息的差異導致了我們可以去判斷我們輸入究竟是用戶名有錯還是密碼有錯。
而padding oracle攻擊,同樣是通過服務端返回的信息的差異而產生的,在這里我要先提一下解密時的一個步驟,同樣也是我們padding oracle的核心利用點。
之前加密的時候我們就知道了,為了保證分組加密時每一組都能保證等長,我們在加密時需要對最后一組不等長的情況進行填充,缺n位就填n個0x0n。此時解密的時候這些個填充位就派上用場了,我們在解密步驟時,按照順序,首先是密文第一組,會先被解密掉,揭秘出來的結果呢就是我們的初始向量IV和第一段明文異或的結果也就是我們之前說的中間值。此時我們將中間值和初始向量IV進行異或,得到的就是我們第一組的明文,然后以此類推知道解密完最后一組密文后。此時此刻,按理說程序會將解密好的數據交由業務代碼來進行后續的判斷,比如驗證揭秘后的用戶名密碼是否匹配。或者用于校驗用戶身份的Cookie值是否正確。 理論是如此但是實際上這中間還有一個步驟就是,程序要判斷明文最后的填充位是否正確。 這個判斷本身是沒問題的,可以直接排除掉一些錯誤的加密數據,和被人惡意篡改的數據。但是一旦判斷出明文最后的填充位是錯誤的,返回給客戶端的信息,給攻擊者提供攻擊思路。 首先如果密文解密成明文后,填充位判斷正確,而且經過業務邏輯代碼的校驗后,也是正確,那么服務端會返回200的狀態碼。 如果密文解密成明文后,填充位判斷正確,但是業務邏輯判斷不通過,也就是說這個明文有問題,納悶服務端會返回200或者300等狀態碼。 最后如果密文解密成明文后,填充位判斷不正確,就會返回500等狀態碼。
不知道大家有沒有發現一個問題就是,填充位的正確與否,服務端返回的狀態碼是不一樣的!!
Padding orlace正是通過這一點的不同來做文章的
那么如何進行padding 我們首先就從一個簡單的例子開始講起,也是很多大佬都用的例子。

首先,以加密解密“TEST”這個字符串為例,“TEST”字符串總共占四個字符串,如果按8字節進行分組,那么很明顯是不夠的,所以我們需要補充4個0x04
然后由程序進行加密,得出加密的結果是”F851D6CC68FC9537”每兩位16進制是一個字節,此時我們審視一下我們當前的已知條件
-
我們此時不是知道明文是什么,但是我們知道密文是“F851D6CC68FC9537”
-
同時我們還知道一個條件那就是該密文的初始化向量,沒錯,如果要進行padding oracle這個攻擊的話,已知初始化向量是一個必須的條件。
-
同時我們可以和服務端進行交互,這個交互是指我們發送加密數據到服務端,服務端回判斷我們發送的密文解密后填充位是否正確,并返回給我們填充位正確的狀態碼,或者填充位不正確的狀態碼。
剛才的已知條件中,我提到了初始化向量必須已知才能進行攻擊,那么這個初始化向量在哪呢?
一般是在密文的頭部
我們可以看到圖中初始化向量的值是“6D367076036E2239”
舉個例子,比如我們使用加密傳輸了一個值,值的名字叫padding
Padding= 6D367076036E2239F851D6CC68FC9537
我們可以看到“6D367076036E2239”放在密文的前面,又已知初始化向量和密文的分組等長所以,分組長度為8字節,那么初始化向量的長度自然也是8字節,由此我們就可以明確前八個字節是初始化向量,
理論上講,“F851D6CC68FC9537”這段密文我們如果知道密鑰的話,就可以直接解開這段密文得出他的明文,但是很明顯我們不知道,如果知道密鑰了那我們還折騰個啥
那么接下來的操作就是利用我們手上已有的條件,在不知道密鑰的情況下得到這段密文的明文
首先根據解密步驟

服務端收到密文的時候回先對密文進行解密,也就是對“F851D6CC68FC9537”
這段密文進行解密,的到我們的中間值,注意此時中間值是多少我們并不知道,因為服務端不可能把中間值返回給我們。
然后通過中間值和初始化向量異或我們就可以得到“F851D6CC68FC9537”的明文
初始化向量我們是已知的也就是“6D367076036E2239”也就是說我們離得到明文就差一個中間值,如果我們知道中間值是多少,那我們直接異或運算一下就可以得出明文了,關鍵就在于怎么得到這個中間值?
還記得之前說的程序回判斷填充位并返回不同的狀態么?
此時我們將初始化向量全部制為零,此時發送給服務端的數據就變成了
padding=0000000000000000F851D6CC68FC9537然后看圖

還是一樣的步驟,程序解密密文得到中間值,然后和初始化向量異或得到明文,然后程序再判斷填充位是否正確。
我們知道0和數異或的結果都是該數本身
所以中間值和0異或的結果還是中間值本身,我們可以從圖中看到異或的結果是3D,此時我們是不可能知道這個異或出來的結果是3D的,但是此時服務端會報一個錯,那就是填充位錯誤,為什么會報這個錯,因為之前說了,填充文在8個字節為分組的情況下,最多只可能填到8個0x08,所以怎么可能會有0x3D呢?
那怎么樣才能不報這個錯呢?以最后一位為例
如果此時異或出來的結果為 “39732322076a2601”也就是異或出來的結果最后一位為0x01時就不會報填充位錯誤了。但是后續還會在爆一個錯誤,那就是業務判斷你這個解密出來的明文數據也就是“39732322076a2601”不正確,因為我們初始化向量都制為零的,所以這個明文當然是錯誤的,不過這都不重要。
此時我們知道了,當最終解除出來的明文的最后一位位0x01時,我們的程序就不會報填充位錯誤,那一次類推如果解出來為“39732322076a0202”“ 3973232207030303”…… “ 0808080808080808”時也不會報填充位錯誤。
所以我們從假如最后一位為0x01開始,由于中間是是固定不變的,我們就需要變化初始化向量的最后一個字節讓其和中間值的最后一個字節異或的結果為0x01, 所以此時我們需要用到窮舉的方法,一個字節的范圍為,0x00-0xFF,最多也就是需要嘗試256次,

此時我們根據上圖可以看出,當最后一位異或結果為0x01時,我們此時的初始化向量為
‘’0000000000000066‘’ 又已知0x01是中間值和初始化向量異或得出的結果,所以我們將此時我們用來爆破的初始化向量的最后一位,也就是“0x66”與0x01向異或,就可以得出真正的中間值的最后一個字節,也就是0x67。
以此類推,直到異或結果為“0808080808080808”,我們多需要嘗試的次數,最多也不過256*8次也就是2048次,這樣我們就可以繞過加密,從而直接獲得密文的明文。
6、CBC字節翻轉攻擊
以上的手段可以讓我們繞過加解密從而直接獲得明文,不知道大家有沒有發現一個問題,來我們再次觀察一下解密過程

解密的第一步,首先用后臺用密鑰,將密文解密然后將解密得出的中間值與初始化向量IV做異或操作,得到第一段密文的明文。
解密的第二步,首先用后臺用密鑰,將密文解密然后將解密得出的中間值與上一段密文做異或操作,得到第二段密文的明文。
不難看出,下一段明文的內容是受到上一段密文的影響的,那么是否存在我們通過修改前一段密文或者初始化向量來達到修改下一段密文的明文的效果
打個比方說我們明文是“admin”然后加密傳輸到后端,后端解密出來的結果是“bdmin”
可不可以實現呢?當然是可以的
首先我們再理一下這個過程,“admin”首先和初始化向量異或得到一個8字節的密文,然后由于按照8字節來劃分,所以初始化向量自然也是8字節,為了方便傳遞給后臺識別,初始化向量轉化成8字節大小的十六進制數放在加密好的密文開頭,然后發送給后臺。
后臺受到密文后,將前八字節的十六進制出提取出來作為初始化向量,然后將剩下的密文,使用密鑰解密后然后和初始化向量做異或操作,得出最終的明文。
問題還是出在了解密過程中的異或操作,這個初始化向量是我們可控的
我們想要的結果就是密文和我們修改過后的初始化向量
此時我們要清楚一個基本的異或運算
我們使用“qwerasdf”來作為我們的初始化向量 “admin”作為要加密傳輸的明文,也就是說我們首先進行異或操作時是有“q”和“a”來進行異或的
所以有“q” xor “a” 來作為第一步,這個異或的結果會在后臺用密鑰解密出來后再與初始化向量“q”來異或得出明文“a”
所以此時有“q” xor “a” xor “q” == “a”這么一個式子
我們將“q” xor “a”的結果設為X
既X = “q” xor “a”
,X就是作為中間值被加密然后傳到后臺的
此時我們將X 作為參數再與我們的目標值“b”進行一次異或
也就是說 X xor “b” 這個結果我們設為Y
此時得到Y == X xor “b”
再根據上一個式子可以得到,Y = “q” xor "a" xor " b"
已知X是未經修改的IV與明文異或的結果也就是所謂的中間值,也就說解密時X時作為解密時的中間值同樣要參與到解密時的異或步驟,但是如果我們在傳遞數據時將“q”更改為我們的Y。讓Y去和X進行異或操作,最終得到的結果就變成了”b“ ,這樣我們就實現了更改明文的第一個字節,接下來的同樣是進行重復操作。
這就是所謂的CBC字節翻轉攻擊的原理,下面貼出實現代碼。
from pyDes import des, CBC, PAD_PKCS5
import binascii
# 秘鑰
KEY = 'mHAxsLYz'
#初始化向量
KEY2 = "qwerasdf"
def des_encrypt(s):
"""
DES 加密
:param s: 原始字符串
:return: 加密后字符串,16進制
"""
secret_key = KEY
iv = KEY2
k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
en = k.encrypt(s, padmode=PAD_PKCS5)
return binascii.b2a_hex(en)
def des_descrypt(s,iv):
"""
DES 解密
:param s: 加密后的字符串,16進制
:return: 解密后的字符串
"""
secret_key = KEY
iv = iv
k = des(secret_key, CBC, iv, pad=None, padmode=PAD_PKCS5)
de = k.decrypt(binascii.a2b_hex(s), padmode=PAD_PKCS5)
return de
str = des_encrypt("admin")
cipher = bytes(KEY2,encoding='utf-8')
print(cipher)
x = bytes([ord(chr(cipher[0]))^ord('a')^ord('b')])+cipher[1:]
x2 = cipher[0]
print(x)
str3 = des_descrypt(str,x)
print(str3)
下面是執行結果

本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1123/