0x00 前言
本篇是CDecryptPwd
系列的第一篇,筆者將介紹Navicat加密過程、其使用的加密算法以及如何使用C語言來實現其加密過程,文章最后是筆者自己編寫的工具(解密Navicat保存在注冊表中的數據庫密碼)。
0x01 環境
0x02 Blowfish
Navicat使用加密算法是Blowfish Cipher(河豚加密),所以在介紹Navicat加密過程之前筆者要先介紹一下Blowfish Cipher。
Blowfish是一種對稱密鑰分組密碼,Bruce Schneier在1993年設計并公布。它沒有專利,任何人均可自由使用。
在介紹Blowfish之前先來搞清楚Feistel Structure(下圖來自Wikipedia):
上述4個步驟是一輪循環的工作過程,Feistel Structure是進行了16輪循環方才完成一次加密。需要說明一點,在最后一輪循環中左右數據不對調。解密過程是加密過程的反向,具體可參照上圖,不再贅述。
筆者分析的源碼是Paul Kocher版本,并沒有使用Bruce Schneier版本。Schneier版本中的子密鑰來源是
BLOWFISH.DAT
文件,而Kocher版本是直接在源文件中定義子密鑰,分析起來較為直觀。如需其他版本可到此網站下載
0x02.3-1?BLOWFISH_CTX
在blowfish.h
頭文件中有如下定義:
typedef struct {
unsigned long P[16 + 2];
unsigned long S[4][256];
} BLOWFISH_CTX;
為結構體定義別名,結構體中的兩數組即P-Box與S-Box。
0x02.3-2?ORIG_P[16 + 2]
與ORIG_S[4][256]
ORIG_P
與ORIG_S
取自圓周率的小數位,每4個字節賦值給其中的一個元素。
0x02.3-3?void Blowfish_Init(BLOWFISH_CTX *ctx, unsigned char *key, int keyLen)
該函數用來初始化S-Box與P-Box。傳遞參數中的key
即密鑰,keyLen
是密鑰長度。
BLOWFISH_CTX *ctx
中S-Box進行初始化,直接將ORIG_S
中的每個元素逐一賦值給S-BoxBLOWFISH_CTX *ctx
中P-Box進行初始化,具體過程如下:data=0x00000000;
key
長度不足4,則循環使用key
中字符(當使用到key
中最后一個字符時,下一個字符是key
中第一個字符)與data << 8
進行或運算key
轉換為ASCII碼形式(e.g.:key[3]="abc"
——>>0x61626361
并存儲于data
中)ORIG_P
中的每個元素與data
作異或運算后逐一賦值給P-Boxdatal=0x00000000;
datar=0x00000000;
ctx
,datal
與datar
傳遞給Blowfish_Encrypt
datal
與datar
賦值給P-Box中的元素datal
與datar
datal
與datar
賦值給S-Box中的元素步驟5、8中提到的賦值過程是這樣的(以步驟5來舉例):
第一次?P[0]=datal
,P[1]=datar
第二次?P[2]=datal
,P[3]=datar
……
0x02.3-4?void Blowfish_Encrypt(BLOWFISH_CTX *ctx, unsigned long *xl, unsigned long *xr)
該函數是Blowfish的加密部分。傳遞參數中的ctx
應該已經初始化過S-Box與P-Box,xl
指向原數據的左半部分,xr
指向原數據的右半部分。
ctx
傳遞給輪函數F上述過程即一次完整的加密過程,可參照下圖來理解(來自Wikipedia,其中輪函數F的工作過程見0x02.3-6
):
0x02.3-5?void Blowfish_Decrypt(BLOWFISH_CTX *ctx, unsigned long *xl, unsigned long *xr)
解密過程是加密過程的逆過程,如有困惑,可參照源碼理解,不再贅述。
0x02.3-6?static unsigned long F(BLOWFISH_CTX *ctx, unsigned long x)
輪函數工作過程:
x
逐字節分割,賦值給a
,b
,c
,d
四個變量(e.g.:x=0x21564378
,則a=0x21
,b=0x56
,c=0x43
,d=0x78
)y = ((ctx->S[0][a] + ctx->S[1][b]) ^ ctx->S[2][c]) + ctx->S[3][d]
此Demo來自Paul Kocher版本根目錄下的blowfish_test.c
:
#include <stdio.h>
#include "blowfish.h"
void main(void) {
unsigned long L = 1, R = 2;
BLOWFISH_CTX ctx;
Blowfish_Init (&ctx, (unsigned char*)"TESTKEY", 7);
Blowfish_Encrypt(&ctx, &L, &R);
printf("%08lX %08lXn", L, R);
if (L == 0xDF333FD2L && R == 0x30A71BB4L)
printf("Test encryption OK.n");
else
printf("Test encryption failed.n");
Blowfish_Decrypt(&ctx, &L, &R);
if (L == 1 && R == 2)
printf("Test decryption OK.n");
else
printf("Test decryption failed.n");
}
需要說明的一點是Paul Kocher這個版本并沒有考慮到小端序的情況,它均按大端序來處理,所以如果在Linux平臺運行此Demo會像下圖所示:
可以看到加密結果并非源碼中給出的結果,而在Windows平臺下運行,正常顯示:
0x03 CBC模式
如果對于分組密碼模式已經有所了解的讀者可直接跳過此節
Blowfish Cipher與DES、AES一樣,都是分組密碼,Blowfish Cipher每次只能處理64位(即分組長度為8字節)的數據,可是通常我們在加密時輸入的明文長度都會大于8字節,這時如何去處理每個明文分組就成為一個應當考慮的問題。分組密碼的模式是這個問題的解決方案,常見模式有5種:
Navicat并沒有使用上述的任何一種加密模式,但其采用的加密模式與CBC相似,故簡單介紹CBC模式(下圖來自Wikipedia),如果對于其他4種加密模式感興趣,可自行百度。
Plaintext
是明文按照分組密碼的分組長度分割而成,初始化向量IV
、密鑰Key
在加密時自行決定,Block Cipher Encryption
是使用的加密算法,最終將每個密文分組Ciphertext
連接起來即密文。
0x04 Navicat 加密過程
首先,打開Navicat,新建一個MySQL連接:
連接名為Test,用戶名默認root,密碼123456:
Navicat將主機(Host),端口(Port),用戶名(UserName)與加密后的密碼(Pwd)保存到注冊表中,不同的數據庫連接對應的注冊表路徑不同,具體路徑如下:
Win+R
之后鍵入regedit
打開注冊表,按照上述路徑去查找,可以看到剛剛我們建立的MySQL連接的相關鍵值對:
Navicat使用SHA-1生成160位長度的密鑰:
存放于無符號字符數組中:
unsigned char Key[20] = {
0x42, 0xCE, 0xB2, 0x71, 0xA5, 0xE4, 0x58, 0xB7,
0x4A, 0xEA, 0x93, 0x94, 0x79, 0x22, 0x35, 0x43,
0x91, 0x87, 0x33, 0x40
};
Navicat在加解密過程中用到的IV是通過Blowfish Cipher加密8字節長度的0xFFFFFFFFFFFFFFFF
得到,代碼細節如下:
unsigned long l,r;
unsigned char IV[BLOCK_SIZE] = "";
int i;
BLOWFISH_CTX ctx;
//Initialize Initial Vector
l=0xFFFFFFFF;
r=0xFFFFFFFF;
Blowfish_Init(&ctx,Key,20);
Blowfish_Encrypt(&ctx,&l,&r);
for(i=3; i>=0; --i)
{
IV[i]=(unsigned char)(l & 0xFF);
l >>=8;
IV[i+4]=(unsigned char)(r & 0xFF);
r >>=8;
}
Blowfish_Encrypt(&ctx,&l,&r)
之后的for
循環是要將8字節長度密文逐字節賦值給IV數組中的每個元素,IV數組中每個元素值具體如下:
unsigned char IV[8] = {
0xD9, 0xC7, 0xC3, 0xC8, 0x87, 0x0D, 0x64, 0xBD
};
0x03
部分中已經提到Navicat采用的分組密碼模式并非5種主要加密模式之一,其采用的分組密碼模式工作過程如下所示:
PlaintextBlock
長度為8字節;在Blowfish_Cipher
環節不需要提供密鑰Key
,密鑰Key
在調用Blowfish_Init()
已經提供,Blowfish Cipher在加解密過程使用已經初始化的ctx
進行。按照分組密碼模式,最終的密文應與明文長度一致,可注冊表中保存的是”15057D7BA390”。這是因為Navicat在向注冊表中寫入的并非密文,而是十六進制表示的密文ASCII碼。
0x05 Navicat Part of CDecryptPwd
由于此工具目前處于測試階段,仍有許多有待完善之處,故暫時不公開源碼,下面介紹的只是各環節的核心部分。
0x05.1 blowfish.c & blowfish.h
這兩個文件直接使用的是Paul Kocher版本源碼。
#include <stdio.h>
#include <string.h>
#include "blowfish.h"
#include <Windows.h>
#include <tchar.h>
#include <locale.h>
#define BLOCK_SIZE 8
#define KEYLENGTH 256
void XorText(unsigned char left[],unsigned char right[],unsigned char result[],unsigned long r_length);
unsigned long CharConvertLong(unsigned char Text[],short Len);
void LongConvertChar(unsigned long num,unsigned char Text[],short Len);
void Navicat_Encrypt(unsigned char PlainText[],unsigned char CipherText[]);
void Navicat_Decrypt(unsigned char CipherText[],unsigned char PlainText[]);
該文件包含了NavicatPart.c
與NavicatPartMain.c
文件中要使用到的頭文件;定義了兩個全局符號常量BLOCK_SIZE
和KEYLENGTH
,分別是分組長度與最大鍵值長度;以及NavicatPart.c
中的函數原型聲明。
0x05.3 XorText() & CharConvertLong() & LongConvertChar() of?NavicatPart.c
void XorText(unsigned char left[],unsigned char right[],unsigned char result[],unsigned long r_length)
接受4個參數:左操作字符串、右操作字符串、結果字符串、右操作字符串長度,功能是左操作字符串與右操作字符串的異或運算。之所以使用右操作字符串長度控制何時結束,是因為考慮到模式的剩余分組可能會小于分組長度(8字節)。
void XorText(unsigned char left[],unsigned char right[],unsigned char result[],unsigned long r_length)
{
int i;
for(i=0;i<r_length;++i)
result[i]=left[i]^right[i];
}
unsigned long CharConvertLong(unsigned char Text[],short Len)
接受2個參數:轉換字符串、其長度,功能是用十六進制ASCII碼表示字符串,即該函數返回值。(e.g.:unsigned char Test[5] = {0xA4,0x37,0x98,0x56,''}
——>>unsigned long T = CharConvertLong(Test, 4)= 0xA4379856
)
unsigned long CharConvertLong(unsigned char Text[],short Len)
{
unsigned long result=0;
short i;
for(i=0;i<Len;++i)
{
result <<=8;
result |=Text[i];
}
return result;
}
void LongConvertChar(unsigned long num,unsigned char Text[],short Len)
接受3個參數:轉換數、存儲字符串、長度,功能與上一函數相反,是將數值的十六進制表示逐字節分割后賦給存儲字符串中每個元素。(e.g.:unsigned long T = 0xA4379856
——>>unsigned char Test[5] = LongConvertChar(T,Test,4) = {0xA4,0x37,0x98,0x56,''};
)
void LongConvertChar(unsigned long num,unsigned char Text[],short Len)
{
short i;
for(i=Len-1;i>=0;--i)
{
Text[i]=(unsigned char)(num & 0xFF);
num >>=8;
}
}
0x05.4 Navicat_Encrypt() of?NavicatPart.c
void Navicat_Encrypt(unsigned char PlainText[],unsigned char CipherText[])
{
unsigned long l,r,TextLength,block,remain,l_temp,r_temp;
unsigned char IV[BLOCK_SIZE] = "";
unsigned char c_temp[BLOCK_SIZE + 1] = "";
int i;
BLOWFISH_CTX ctx;
//Initialize Initial Vector
l=0xFFFFFFFF;
r=0xFFFFFFFF;
Blowfish_Init(&ctx,Key,20);
Blowfish_Encrypt(&ctx,&l,&r);
for(i=3; i>=0; --i)
{
IV[i]=(unsigned char)(l & 0xFF);
l >>=8;
IV[i+4]=(unsigned char)(r & 0xFF);
r >>=8;
}
//Encrypt PlainText
TextLength=strlen(PlainText);
block=TextLength/BLOCK_SIZE;
remain=TextLength%BLOCK_SIZE;
for(i=0;i<block;++i)
{
memcpy(c_temp, PlainText + i * BLOCK_SIZE, BLOCK_SIZE);
c_temp[BLOCK_SIZE] = '';
XorText(IV,c_temp,c_temp,BLOCK_SIZE);
l_temp=CharConvertLong(c_temp,4);
r_temp=CharConvertLong(c_temp+4,4);
Blowfish_Encrypt(&ctx,&l_temp,&r_temp);
LongConvertChar(l_temp,c_temp,4);
LongConvertChar(r_temp,c_temp+4,4);
memcpy(CipherText + i * BLOCK_SIZE, c_temp, BLOCK_SIZE);
XorText(IV,c_temp,IV,BLOCK_SIZE);
}
if(remain)
{
l_temp=CharConvertLong(IV,4);
r_temp=CharConvertLong(IV+4,4);
Blowfish_Encrypt(&ctx,&l_temp,&r_temp);
LongConvertChar(l_temp,IV,4);
LongConvertChar(r_temp,IV+4,4);
memcpy(c_temp, PlainText + i * BLOCK_SIZE, remain);
c_temp[remain] = '';
XorText(IV,c_temp,c_temp,remain);
memcpy(CipherText + i * BLOCK_SIZE, c_temp, remain);
}
}
該函數在main
中并未使用。
Navicat_Encrypt()
中調用該函數,因為下面的Blowfish Cipher加密要使用其初始化的ctx。for
循環部分完成0x04.2-3
圖中給出的前(n-1)步的過程。if
部分是處理剩余分組大小不足8字節的情況,即第n步。CharConverLong()
與LongConverChar()
的存在是因Paul Kocher版本源只能處理無符號長整型。0x05.5 Navicat_Decrypt() of?NavicatPart.c
void Navicat_Decrypt(unsigned char CipherText[],unsigned char PlainText[])
{
unsigned long l,r,TextLength,block,remain,l_temp,r_temp;
unsigned char IV[BLOCK_SIZE] = "";
unsigned char c_temp1[BLOCK_SIZE+1] = "";
unsigned char c_temp2[BLOCK_SIZE+1] = "";
int i;
BLOWFISH_CTX ctx;
//Initialize Initial Vector
l=0xFFFFFFFF;
r=0xFFFFFFFF;
Blowfish_Init(&ctx,Key,20);
Blowfish_Encrypt(&ctx,&l,&r);
for(i=3; i>=0; --i)
{
IV[i]=(unsigned char)(l & 0xFF);
l >>=8;
IV[i+4]=(unsigned char)(r & 0xFF);
r >>=8;
}
//Decrypt CipherText
TextLength=strlen(CipherText);
block=TextLength/BLOCK_SIZE;
remain=TextLength%BLOCK_SIZE;
for(i=0;i<block;++i)
{
memcpy(c_temp1, CipherText + i * BLOCK_SIZE, BLOCK_SIZE);
c_temp1[BLOCK_SIZE] = '';
memcpy(c_temp2, CipherText + i * BLOCK_SIZE, BLOCK_SIZE);
c_temp2[BLOCK_SIZE] = '';
l_temp=CharConvertLong(c_temp1,4);
r_temp=CharConvertLong(c_temp1+4,4);
Blowfish_Decrypt(&ctx,&l_temp,&r_temp);
LongConvertChar(l_temp,c_temp1,4);
LongConvertChar(r_temp,c_temp1+4,4);
XorText(IV,c_temp1,c_temp1,BLOCK_SIZE);
memcpy(PlainText+i*BLOCK_SIZE, c_temp1, BLOCK_SIZE);
XorText(IV,c_temp2,IV,BLOCK_SIZE);
}
if(remain)
{
l_temp=CharConvertLong(IV,4);
r_temp=CharConvertLong(IV+4,4);
Blowfish_Encrypt(&ctx,&l_temp,&r_temp);
LongConvertChar(l_temp,IV,4);
LongConvertChar(r_temp,IV+4,4);
memcpy(c_temp1, CipherText + i * BLOCK_SIZE, remain);
c_temp1[remain] = '';
XorText(IV,c_temp1, c_temp1,remain);
memcpy(PlainText + i * BLOCK_SIZE, c_temp1, remain);
}
}
解密過程可參照下圖理解:
除了多一步密文分組的拷貝,其余都是加密過程的逆過程,不再贅述。
0x05.6 main() of?NavicatPartMain.c
主程序功能:
計算機HKEY_CURRENT_USERSoftwarePremiumSoft
不變,變化的是與其拼接的字符串,可根據Navicat寫入注冊表時創建路徑的規律來進行拼接:Data
與NavicatPremium
子項均與存儲數據庫相關信息無關,不包含Servers
,而存儲數據庫相關信息的Navicat
與NavicatPG
子項均包含Servers
,所以可進行這一判斷來決定是否要用RegEnumKeyEx()
遍歷Servers
下的子項。
RegEnumValue()
遍歷Servers
子項中的鍵值對,主要是Host、UserName、Pwd、Port四項。在讀取Pwd值之后傳遞給Navicat_Decrypt()
進行解密。需要說明一點:在讀取Port之前,讀取類型要從REG_SZ
轉為REG_DWORD
,否則讀出的值無意義。0x06 參考