CRC原理及其逆向破解方法
介紹: 這篇短文包含CRC原理介紹和其逆向分析方法,很多程序員和破解者不是很清楚了解CRC的工作原理,而且幾乎沒人知道如何逆向分析它的方法,事實上它是非常有用的.首先,這篇教程教你一般如何計算CRC,你可以將它用在數據代碼保護中.第二,主要是介紹如何逆向分析CRC-32,你可以以此來分析程序中的CRC保護(象反病毒編碼).當然有很多有效的工具用來對付CRC,但我懷疑它是否會說明原理. 我要告訴你,這篇短文里中應用了很多數學知識,這不會影響一些人,而且會被一般的 程序員與逆向分析者很好理解.為什么?那么如果你不知道數學是如何被應用在CRC中, 我建議你可以停止繼續學習了.所以我假定你們(讀者)都是具備二進制算術知識的. 第一部分:CRC 介紹,CRC是什么和計算CRC的方法. 循環冗余碼 CRC 我們都知道CRC.甚至你沒有印象,但當你想到那些來自諸如RAR,ZIP等壓縮軟件發給你 由於錯誤連接和其他一些意外原因導致的文件錯誤的惱人的消息時,你就會知道.CRC是塊 數據的計算值,比如對每一個文件進行壓縮.在一個解壓縮過程中,程序會從新計算解壓文件 的CRC值,並且將之與從文件中讀取的CRC值進行比對,如果值相同,那么正確.在CRC-32中, 會有1/2^32的可能性發生對確認數據更改的校驗錯誤. 很多人認為CRC就是循環冗余校驗,假如CRC真的就是循環冗余校驗,那么很多人都錯用了 這個術語.你不能說 "這個程序的CRC是12345678 ".人們也常說某一個程序有CRC校驗,而不 說是 "循環冗余校驗 " 校驗.結論:CRC 代表循環冗余碼,而不是循環冗余校驗. 計算是如何完成的呢?好,主要的想法就是將一個文件看成一個被一些數字分割的很長的 位字串,這里會有一個余數---CRC!你總會有一個余數(可以是0),它至多比除數小一. (9/3=3 余數=0 ; (9+2)/3=3 余數=2) (或者它本身就包含一個除數在其中). 在這里CRC計算方法與除法有一點點區別,除法就是將被減數重復的減去除數X次,然后留下 余數.如果你希望得到原值,那么你就要把除數乘上X次,然后加上余數. CRC計算使用特殊的減法與加法完成的.也就是一種新的 "算法 ".計算中每一位計算的進位值 被 "遺忘 "了. 看如下兩個例子,1是普通減法,2和3是特殊的. -+ (1) 1101 (2) 1010 1010 (3) 0+0=0 0-0=0 1010- 1111+ 1111- 0+1=1 *0-1=1 ---- ---- ---- 1+0=1 1-0=1 0011 0101 0101 *1+1=0 1-1=0 在(1)中,右數第二列可以看成是0-1=-1,因此要從高位借1,就變成(10+0)-1=1.(這就象普通 的 'by-paper '十進制減法).特例(2,3)中,1+1會有正常的結果10, '1 '是計算后的進位.這個值 被忽略了.特殊情況0-1應該有正常結果 '-1 '就要退到下一位.這個值也被忽略了.假如你對編程 有一定了解,這就象,XOR 操作或者更好. 現在來看一個除法的例子: 在普通算法中: 1001/1111000\1101 13 9/120\13 1001 - 09 -| ---- -- | 1100 30 | 1001 - 27 - ---- -- 0110 3 -> 余數 0000 - ---- 1100 1001 - ---- 011 -> 3, 余數 在CRC算法中: 1001/1111000\1110 9/120\14 余數為 6 1001 - ---- 1100 1001 - ---- 1010 1001 - ---- 0110 0000 - ---- 110 -> 余數 (例 3) 這個除法的商並不重要,也沒必要去記住,因為他們僅僅是一組無關緊要的位串.真正 重要的是余數!它就是這個值,可以說比原文件還重要的值,他就是基本的CRC. |
過度到真正的CRC碼計算.
進行一個CRC計算我們需要選則一個除數,從現在起我們稱之為 "poly ".寬度W就是最高位
的位置,所以這個poly 1001的W 是3,而不是4.注意最高位總是1,當你選定一個寬度,那么你只
需要選擇低W各位的值.
假如我們想計算一個位串的CRC碼,我們想確定每一個位都被處理過,因此,我們要在目標
位串后面加上W個0位.在此例中,我們假設位串為1111.請仔細分析下面一個例子:
Poly = 10011, 寬度 W=4
位串 Bitstring
Bitstring + W zeros = 110101101 + 0000
10011/1101011010000\110000101 (我們不關心此運算的商)
10011|||||||| -
-----||||||||
10011|||||||
10011||||||| -
-----|||||||
00001||||||
00000|||||| -
-----||||||
00010|||||
00000||||| -
-----|||||
00101||||
00000|||| -
-----||||
01010|||
00000||| -
-----|||
10100||
10011|| -
-----||
01110|
00000| -
-----|
11100
10011 -
-----
1111 -> 余數 -> the CRC!
(例 4)
重要兩點聲明如下:
1.只有當Bitstring的最高位為1,我們才將它與poly做XOR運算,否則我們只是將
Bitstring左移一位.
2.XOR運算的結果就是被操作位串bitstring與低W位進行XOR運算,因為最高位總為0.
算法設計:
你們都應知道基於位運算的算法是非常慢的而且效率低下.但如果將計算放在每一字節上
進行,那么效率將大大提高.不過我們只能接受poly的寬度是8的倍數(一個字節;).可以形
象的看成這樣一個寬度為32的poly(W=32):
3 2 1 0 byte
+---+---+---+---+
Pop! <--| | | | | <-- bitstring with W zero bits added, in this case 32
+---+---+---+---+
1 <--- 32 bits ---> this is the poly, 4*8 bits
(figure 1)
這是一個你用來存放暫時CRC結果的記存器,現在我稱它為CRC記存器或者記存器.你從右
至左移動位串,當從左邊移出的位是1,則整個記存器被與poly的低W位進行XOR運算.(此例
中為32).事實上,我們精確的完成了上面除法所做的事情.
移動前記存器值為:10110100
當從右邊移入4位時,左邊的高4位將被移出,此例中1011將被移出,而1101被移入.
情況如下:
當前8位CRC記存器 : 01001101
剛剛被移出的高4位 : 1011
我們用此poly : 101011100, 寬度 W=8
現在我們用如前介紹的方法來計算記存器的新值.
頂部 記存器
---- --------
1011 01001101 高四位和當前記存器值
1010 11100 + (*1) Poly 放在頂部最高位進行XOR運算 (因為那里是1)
-------------
0001 10101101 運算結果
現在我們仍有一位1在高4位:
0001 10101101 上一步結果
1 01011100+ (*2) Poly 放在頂部的最低位進行XOR運算 (因為那里是1)
-------------
0000 11110001 第二步運算結果
^^^^
現在頂部所有位均為0,所以我們不需要在與poly進行XOR運算
你可以得到相同的結果如果你先將(*1)與(*2)做XOR然后將結果與記存器值做XOR.
這就是標准XOR運算的特性:
(a XOR b) XOR c = a XOR (b XOR c) 由此,推出如下的運算順序也是正確的.
1010 11100 poly (*1) 放在頂部最高位
1 01011100+ polys (*2) 放在頂部最低位
-------------
1011 10111100 (*3) XOR運算結果
The result (*3) 將(*3)與記存器的值做XOR運算
1011 10111100
1011 01001101+ 如右:
-------------
0000 11110001
你看到了嗎?得到一樣的結果!現在(*3)變的重要了,因為頂部為1010則(3)的值總是等於
10111100(當然是在一定的條件之下)這意味着你可以預先計算出任意頂部位結合的XOR值.
注意,頂部結果總是0,這就是組合XOR操作導致的結果.(翻譯不准確,保留原文)
現在我們回到figure 1,對每一個頂部字節的值都做移出操作,我們可以預先計算出一個值.
此例中,它將是一個包含256個double word(32 bit)雙字的表.(附錄中CRC-32的表).
(翻譯不准確,保留原文)
用偽語言表示我們的算法如下:
While (byte string is not exhausted)
Begin
Top = top_byte of register ;
Register = Register shifted 8 bits left ORred with a new byte from string ;
Register = Register XORred by value from precomputedTable at position Top ;
End
direct table算法:
上面提到的算法可以被優化.字節串中的字節在被用到之前沒有必要經過整個記村器.用
這個新的算法,我們可以直接用一個字節去XOR一個字節串通過將此字節移出記存器.結果
指向預先計算的表中的一個值,這個值是用來被記存器的值做XOR運算的.
我不十分確切的知道為什么這會得到同樣的結果(這需要了解XOR運算的特性),但是這又
極為便利,因為你無須在你的字節串后填充0字節/位.(如果你知道原理,請告訴我:)
讓我們來實現這個算法:
+---- < byte string (or file) 字節串,(或是文件)
|
v 3 2 1 0 byte 字節
| +---+---+---+---+
XOR--- <| | | | | Register 記存器
| +---+---+---+---+
| |
| XOR
| ^
v +---+---|---+---+
| | | | | | Precomputed table 值表(用來進行操作)
| +---+---+---+---+
+---> -: : : : :
+---+---+---+---+
| | | | |
+---+---+---+---+
(figure 2)
'reflected ' direct Table 算法:
由於這里有這樣一個與之相對應的 '反射 '算法,事情顯得復雜了.一個反射的值/記存器
就是將它的每一位以此串的中心位為標准對調形成的.例如:0111011001就是1001101110
的反射串.
他們提出 '反射 '是因為UART(一種操作IO的芯片)發送每一個字節時是先發最沒用的0位,
最后再發最有意義的第七位.這與正常的位置是相逆的.
除了信息串不做反射以外,在進行下一步操作前,要將其於的數據都做反射處理.所以在
計算值表時,位向右移,且poly也是作過反射處理的.當然,在計算CRC時,記存器也要向右
移,而且值表也必須是反射過的.
byte string (or file) --> ---+
| 1. 表中每一個入口都是反射的.
byte 3 2 1 0 V 2. 初始化記存器也是反射的.
+---+---+---+---+ | 3. 但是byte string中的數據不是反射的,
| | | | |> ---XOR 因為其他的都做過反射處理了.
+---+---+---+---+ |
| |
XOR V
^ |
+---+---|---+---+ |
| | | | | | 值表
+---+---+---+---+ |
: : : : : <---+
+---+---+---+---+
| | | | |
+---+---+---+---+
(figure 3)
我們的算法如下:
1. 將記存器向右移動一個字節.
2. 將剛移出的哪個字節與byte string中的新字節做XOR運算,
得出一個指向值表table[0..255]的索引
3. 將索引所指的表值與記存器做XOR運算.
4. 如數據沒有全部處理完,則跳到步驟1.
下面是這個算法的簡單的可執行匯編源碼:
完整的CRC-32標准所包含的內容:
Name : "CRC-32 "
Width : 32
Poly : 04C11DB7
Initial value : FFFFFFFF
Reflected : True
XOR out with : FFFFFFFF
作為對你好奇心的獎勵, 這里是CRC-16標准: :)
Name : "CRC-16 "
Width : 16
Poly : 8005
Initial value : 0000
Reflected : True
XOR out with : 0000
'XOR out with ' 是為了最終得到CRC而用來與記存器最后結果做XOR運算的值.
假如你想了解一些關於 'reversed '逆向CRC poly的話,請看我的參考文章.
我是在16位DOS模式下用的32位編碼,因此你會在這個程序中看到很多32位與16位混合
的編碼...當然這是很容易轉換成純32位編碼的.注意這個程序是經過完整測試並且能夠
正常運行的.下面的Java 和 C 代碼都是由這個匯編代碼而來的.
底下的這段程序就是用來計算CRC-32 table的:
xor ebx, ebx ;ebx=0, 將被用做一個指針.
InitTableLoop:
xor eax, eax ;eax=0 為計算新的entry.
mov al, bl ;al <-bl
;生成入口.
xor cx, cx
entryLoop:
test eax, 1
jz no_topbit
shr eax, 1
xor eax, poly
jmp entrygoon
no_topbit:
shr eax, 1
entrygoon:
inc cx
test cx, 8
jz entryLoop
mov dword ptr[ebx*4 + crctable], eax
inc bx
test bx, 256
jz InitTableLoop
注釋: - crctable 是一個包含256個dword的數組.
- 由於使用反射算法,EAX被向右移.
- 因此最低的8位被處理了.
用Java和C寫的代碼如下(int is 32 bit):
for (int bx=0; bx <256; bx++){
int eax=0;
eax=eax&0xFFFFFF00+bx&0xFF; // 就是 'mov al,bl ' 指令
for (int cx=0; cx <8; cx++){
if (eax&&0x1) {
eax> > =1;
eax^=poly;
}
else eax> > =1;
}
crctable[bx]=eax;
}
下面的匯編代碼是用來計算CRC-32的:
computeLoop:
xor ebx, ebx
xor al, [si]
mov bl, al
shr eax, 8
xor eax, dword ptr[4*ebx+crctable]
inc si
loop computeLoop
xor eax, 0FFFFFFFFh
注釋: - ds:si 指向將要被處理的byte string信息流.
- cx 信息流的長度.
- eax 是當前的CRC.
- crctable是用來計算CRC的值表.
- 此例中記存器的初始值為: FFFFFFFF.
- 要將中間值與FFFFFFFFh做XOR才能得到CRC
下面是Java和C寫的代碼:
for (int cx=0; cx> =8;
eax^=crcTable[ebx];
}
eax^=0xFFFFFFFF;
現在我們已經完成了本文的第一部分:CRC原理部分,所以如果你希望能夠對CRC做更深
的研究,那么我建議你去讀在本文最后給出連接上的資料,我讀了.好了,終於到了本文最
有意思的部分,CRC的逆向分析!