密碼學基礎:Base64編碼

文章概述:

本文目的:寫這篇文章的目的主要是整理下密碼學中Base64的知識點,並把它們分享出來。並且幫助初探密碼學的壇友們一步一步的用C語言將Base64的編碼實現出來。
閱讀方法:希望大家在瀏覽完本片文章後可以自己去實現一下,相信一定會對你的編程技術有所提高。(附件中提供參考代碼)
具備基礎:
(1)熟練掌握C語言
學習環境:任意C語言開發環境, Base64加解密工具

Base64簡介:

  雖然這篇文章發佈在密碼算法區,但希望大家不要誤解,Base64並不是一種加密的方法,而是一種編碼的方式。雖然用Base64加密(暫且說是加密)的字符串看起來有一種被加密的感覺,但是這只是感覺。因爲如果用標準的Base64進行加密會發現很多Base64的特徵,比如在Base64字符串中會出現'+'和'\'兩種字符,在字符串的末尾經常會有一個到兩個連續的'='。只要發現了這些特徵,就可以肯定這個字符串被Base64加密過,只要通過相應的解密小程序就可以輕鬆得到加密前的樣子(非標準的除外)。
  那麼有爲什麼說Base64是一中編碼方式呢?這是因爲Base64可以把所有的二進制數據都轉換成ASCII碼可打印字符串的形式。以便在只支持文本的環境中也能夠順利地傳輸二進制數據。當然有時在CTF的題目中摻雜上非標準的Base64編碼表也會有加密的效果,但是如果找到這個表那就編程明文了,所以在CTF題目中只會起到輔助的作用。

Base64的編碼原理:
  Base64編碼的核心原理是將二進制數據進行分組,每24Bit(3字節)爲一個大組,再把一個大組的數據分成4個6Bit的小分組。由於6Bit數據只能表示64個不同的字符(2^6=64),所以這也是Base64的名字由來。這64個字符分別對應ASCII碼錶中的'A'-'Z','a'-'z','0''9','+'和'/'。他們的對應關係是由Base64字符集決定的。因爲小分組中的6Bit數據表示起來並不方便,所以要把每個小分組進行高位補零操作,這樣每個小分組就構成了一個8Bit(字節)的數據。在補零操作完成後接下來的工作就簡單多了,那就是將小分組的內容作爲Base64字符集的下標,然後一一替換成對應的ASCII字符。加密工作完成。

  Base64解密的工作原理也非常的簡單,只要操作方式和加密步驟相反即可。首先將Base64編碼根據其對應的字符集轉換成下標,這就是補完零後的8Bit(一字節)數據。既然有補零操作那自然會有去零操作了,我們要將這些8Bit數據的最高位上的兩個0抹去形成6Bit數據,這也就是前面我們提到過的小分組。最後就是將每4個6Bit數據進行合併形成24Bit的大分組,然後將這些大分組按照每組8Bit進行拆分就會得到3個8Bit的數據,這寫8Bit數據就是加密前的數據了。解密工作完成。

  重點:別看前面說的Base64工作流程這麼簡單,實際上裏面還是有很多坑的,那在我們瞭解了編碼原理後現在就來填坑了:
  我們在Base64編碼前是無法保證準備編碼的字符串長度是3的倍數,所以爲了讓編碼能夠順利進行就必須在獲取編碼字符串的同時判斷字符串的長度是否是3的倍數,如果是3的倍數編碼就可以正常進行,如果不是那麼就要進行額外的操作——補零,就是要在不足3的倍數的字符串末尾用0x00進行填充。
  這樣就是解決了字符串長度不足的問題了,但是同時也引進了另一個新的問題,那就是末尾補充上的0在進行Base64字符集替換的時候會與字符集中的'A'字符發生衝突。因爲字符集中的下標0對應的字符是'A',而末尾填充上的0x00在分組補零後同樣是下標0x00,這樣就無法分辨出到底是末尾填充的0x00還是二進制數據中的0x00。爲了解決這個問題我們就必須引入Base64字符集外的新字符來區分末尾補充上的0x00,這就是'='字符不在Base64字符集中,但是也出現在Base64編碼的原因了,'='字符在一個Base64編碼的末尾中最多會出現兩個,如果不符合這以規則那麼這個Base64就可能被人做了手腳。

Base64的編碼圖解:

我們以輸入字符串"6666"爲例: 

1、判斷字符串長度,不足3的倍數用0x00填充:
密碼學基礎:Base64編碼
2、將補零後的字符串進行8Bit分組:
密碼學基礎:Base64編碼
3、把每個大分組進行6Bit分組:
密碼學基礎:Base64編碼
4、將6Bit組轉換成Base64字符集的下標:
(注:由於是進行圖片解說,所以省區了6Bit組高位補零操作!)
密碼學基礎:Base64編碼
5、把字符集的下標替換成Base64字符:
密碼學基礎:Base64編碼
6、修正末尾的符號,得到Base64編碼結果
密碼學基礎:Base64編碼
解密操作和加密操作相反!

Base64核心代碼講解(C語言):

Base64加密部分:

1、將長度補全後的字符串轉換成6Bit分組:

int TransitionSixBitGroup(unsigned char *BitPlainText, unsigned char* SixBitGroup, unsigned int SixBitGroupSize)
{
    int ret = 0;

    //1、每4個6Bit組一個循環
    for (int i = 0, j = 0; i < SixBitGroupSize; i += 4, j += 3)
    {
        SixBitGroup[i]        = ((BitPlainText[j] & 0xFC) >> 2);
        SixBitGroup[i + 1]    = ((BitPlainText[j] & 0x03) << 4) + ((BitPlainText[j + 1] & 0xF0) >> 4);
        SixBitGroup[i + 2]    = ((BitPlainText[j + 1] & 0x0F) << 2) + ((BitPlainText[j + 2] & 0xC0) >> 6);
        SixBitGroup[i + 3]    = (BitPlainText[j + 2] & 0x3F);
    }

    return ret;
}

這一段代碼的功能是將已經補足長度的16進制數據轉變成6Bit分組,每一個分組用8Bit(一個字節)表示,所以也就自動完成了6Bit組的高位補零操作。這裏用到了一個for循環,其目的是爲了達到前面所說的分組,這裏分了兩個部分,一部分是將16進制數據分成一個38Bit的大分組,另一部分是將大分組中的數據分割成46Bit的小分組,分割的過程用到了很多位操作,大大降低了分組的複雜性,只不過需要注意運算法的優先級。
  語句剖析:
  SixBitGroup[i] = ((BitPlainText[j] & 0xFC) >> 2); //在38Bit數據中的第一個數據取6Bit內容右移兩位得到第一個6Bit數據。
密碼學基礎:Base64編碼
SixBitGroup[i + 1] = ((BitPlainText[j] & 0x03) << 4) + ((BitPlainText[j + 1] & 0xF0) >> 4);//在38Bit數據中的第一個數據取2Bit內容後左移4位,加上38Bit數據中的第二個數據取4Bit數據構成第二個6Bit數據。
密碼學基礎:Base64編碼
SixBitGroup[i + 2] = ((BitPlainText[j + 1] & 0x0F) << 2) + ((BitPlainText[j + 2] & 0xC0) >> 6);//在38Bit數據中的第二個數據取4Bit內容後左移2位,加上38Bit數據中的第三個數據取2Bit數據右移6位構成第三個6Bit數據。
密碼學基礎:Base64編碼
SixBitGroup[i + 3] = (BitPlainText[j + 2] & 0x3F);//在38Bit數據中的第三個數據取6Bit數據得到最後一個6Bit數據。
密碼學基礎:Base64編碼

2、根據6Bit組獲取字符串:

unsigned char Base64Table[64] =
{
    'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
    'I', 'G', 'K', 'L', 'M', 'N', 'O', 'P',
    'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
    'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
    'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
    'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
    'w', 'x', 'y', 'z', '0', '1', '2', '3',
    '4', '5', '6', '7', '8', '9', '+', '/'
};

int GetBase64String(unsigned char *CipherGroup, unsigned char *SixBitGroup, unsigned int SixBitGroupSize)
{
    int ret = 0;

    for (int i = 0; i < SixBitGroupSize; i++)
    {
        CipherGroup[i] = Base64Table[SixBitGroup[i]];
    }

    return ret;
}

通過第一步的處理,我們得到了Base64的高位補零後的6Bit分組也就是Base64字符集的下標。通過下標獲取Base64字符集中的內容也就非常簡單了,利用for循環進行查表賦值操作就可以初步得到Base64的編碼值了。

3、將初步得到Base64的編碼值末尾補充的字符轉換成'=':

for (int i = SixBitGroupSize - 1; i > SixBitGroupSize - 3; i--)
{
    if (CipherGroup[i] == 'A')
    {
        CipherGroup[i] = '=';
    }
}

因爲Base64編碼最多隻可能出現兩個'='字符,所以判斷條件爲i > SixBitGroupSize-3,並且在循環中判斷末尾是否是補充字符。經過這一過程,也就獲取了Base64加密後的結果。加密完成!

Base64解密部分:

1、將Base64密文轉換成Base64下標:

int GetBase64Index(unsigned char *CipherText, unsigned char *Base64Index, unsigned int Base64IndexSize)
{
    int ret = 0;

    for (int i = 0; i < Base64IndexSize; i++)
    {
        //計算下標
        if (CipherText[i] >= 'A' && CipherText[i] <= 'Z')    //'A'-'Z'
        {
            Base64Index[i] = CipherText[i] - 'A';
        }
        else if (CipherText[i] >= 'a' && CipherText[i] <= 'z')    //'a'-'z'
        {
            Base64Index[i] = CipherText[i] - 'a' + 26;
        }
        else if (CipherText[i] >= '0' && CipherText[i] <= '9')    //'0'-'9'
        {
            Base64Index[i] = CipherText[i] - '0' + 52;
        }
        else if (CipherText[i] == '+')
        {
            Base64Index[i] = 62;
        }
        else if (CipherText[i] == '/')
        {
            Base64Index[i] = 63;
        }
        else    //處理字符串末尾是'='的情況
        {
            Base64Index[i] = 0;
        }
    }

    return ret;
}```
 **由於Base64字符串是用ASCII碼錶示的,所以要想獲取其對應的索引號就需要減去每一段ASCII第一個字符後加上相應的偏移,最後應該注意的是不要忘記還有一個不在Base64字符集的字符。**
 2、將Base64下標(6Bit組)轉換爲明文字符串的8Bit組形式
 ```C
 int TransitionEightBitGroup(unsigned char *BitPlainText, unsigned char *Base64Index, unsigned int Base64IndexSize)
{
    int ret = 0;
    for (int i = 0, j = 0; j < Base64IndexSize; i += 3, j += 4)
    {
        BitPlainText[i]        = (Base64Index[j] << 2) + ((Base64Index[j + 1] & 0xF0) >> 4);
        BitPlainText[i + 1]    = ((Base64Index[j + 1] & 0x0F) << 4) + ((Base64Index[j + 2] & 0xFC) >> 2);
        BitPlainText[i + 2]    = ((Base64Index[j + 2] & 0x03) << 6) + Base64Index[j + 3];
    }
F
    return ret;
}

這裏進行的位操作有點不容易理解,但是它的作用就是把46Bit組中高位填充的0x00去掉後合併成38Bit的明文數據。需要留心下位操作的運算符優先級和處理的數據位。最後得到的結果就是16進制數據了。解密完成!
  語句剖析:
  BitPlainText[i] = (Base64Index[j] << 2) + ((Base64Index[j + 1] & 0xF0) >> 4);//將第一個46Bit組數據左移2位去除高位補的0x00得到6個有效Bit位,從第二個46Bit組取得4Bit數據右移4位(包含兩個有效位),兩個部分相加得到第一個38Bit組數據。
密碼學基礎:Base64編碼
BitPlainText[i + 1] = ((Base64Index[j + 1] & 0x0F) << 4) + ((Base64Index[j + 2] & 0x3C) >> 2);//從第二個46Bit組取得4Bit數據左移4位得到4個有效Bit位,從第三個46Bit組取得6Bit數據右移2位得到4個有效Bit位,兩個部分相加得到第二個38Bit組數據。
密碼學基礎:Base64編碼
BitPlainText[i + 2] = ((Base64Index[j + 2] & 0x03) << 6) + Base64Index[j + 3];//從第三個46Bit組取得2Bit數據左移6位得到2個有效Bit位,從第四個46Bit組取得8Bit數據得到6個有效Bit位,兩個部分相加得到第三個3*8Bit組數據。
密碼學基礎:Base64編碼

總結:

寫到這裏,我對Base64算法的理解也就分享完了。其實Base64的原理非常簡單,但是實現它的過程卻十分的有意思,它展現了C語言中位操作的魅力。我感覺只有對位操作有着深入的理解,才能更好的進行密碼學的學習。

原文鏈接:密碼學基礎:Base64編碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章