CDecryptPwd(一)——Navicat

 

0x00 前言

本篇是CDecryptPwd系列的第一篇,笔者将介绍Navicat加密过程、其使用的加密算法以及如何使用C语言来实现其加密过程,文章最后是笔者自己编写的工具(解密Navicat保存在注册表中的数据库密码)。

 

0x01 环境

  • 平台:Windows 10(64 bit)
  • 编译环境:Visual Studio Community 2019
  • Navicat Premium 版本 12.1(64 bit)

 

0x02 Blowfish

Navicat使用加密算法是Blowfish Cipher(河豚加密),所以在介绍Navicat加密过程之前笔者要先介绍一下Blowfish Cipher。

0x02.0 Introduction

Blowfish是一种对称密钥分组密码,Bruce Schneier在1993年设计并公布。它没有专利,任何人均可自由使用。

0x02.1 Detail

  • 密钥长度:32-448位
  • 分组长度:64位
  • 16轮循环的Feistel结构

0x02.2 Feistel Structure

在介绍Blowfish之前先来搞清楚Feistel Structure(下图来自Wikipedia):

  1. 将原数据分成左右两部分
  2. 原数据的右侧不变,直接变成下次循环的左侧
  3. 将原数据的右侧与子密钥传递给轮函数F
  4. 轮函数返回值与原数据左侧进行异或运算,变成下次循环的右侧

上述4个步骤是一轮循环的工作过程,Feistel Structure是进行了16轮循环方才完成一次加密。需要说明一点,在最后一轮循环中左右数据不对调。解密过程是加密过程的反向,具体可参照上图,不再赘述。

0x02.3 Source Code

笔者分析的源码是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_PORIG_S取自圆周率的小数位,每4个字节赋值给其中的一个元素。

0x02.3-3 void Blowfish_Init(BLOWFISH_CTX *ctx, unsigned char *key, int keyLen)

该函数用来初始化S-Box与P-Box。传递参数中的key即密钥,keyLen是密钥长度。

  1. BLOWFISH_CTX *ctx中S-Box进行初始化,直接将ORIG_S中的每个元素逐一赋值给S-Box
  2. BLOWFISH_CTX *ctx中P-Box进行初始化,具体过程如下:
    a. data=0x00000000;
    b. 如果参数中的字符数组key长度不足4,则循环使用key中字符(当使用到key中最后一个字符时,下一个字符是key中第一个字符)与data << 8进行或运算
    上述过程总结起来就是将参数中的字符数组key转换为ASCII码形式(e.g.:key[3]="abc"——>>0x61626361并存储于data中)
    c. 将ORIG_P中的每个元素与data作异或运算后逐一赋值给P-Box
  3. datal=0x00000000;
    datar=0x00000000;
  4. 将上面经过变换后的ctxdataldatar传递给Blowfish_Encrypt
  5. 将加密后的dataldatar赋值给P-Box中的元素
  6. 重复9次步骤4-5
  7. 与步骤4类似,不过这次传递的是上面过程中已经加密后的dataldatar
  8. 将加密后的dataldatar赋值给S-Box中的元素
  9. 重复512次步骤7-8

    步骤5、8中提到的赋值过程是这样的(以步骤5来举例):
    第一次 P[0]=datalP[1]=datar
    第二次 P[2]=datalP[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指向原数据的右半部分。

  1. 左侧数据与P-Box中第i个元素作异或运算(i=n-1,其中n是轮数)
  2. 将左侧数据与ctx传递给轮函数F
  3. 右侧数据与轮函数F返回值作异或运算
  4. 交换运算后的左右两侧数据
  5. 重复16次步骤1-5
  6. 将最后一轮运算互换的左右两侧数据换回来
  7. 右侧数据与P-Box中第16个元素作异或运算
  8. 左侧数据与P-Box中第17个元素作异或运算

上述过程即一次完整的加密过程,可参照下图来理解(来自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)

轮函数工作过程:

  1. 将参数x逐字节分割,赋值给a,b,c,d四个变量(e.g.:x=0x21564378,则a=0x21,b=0x56,c=0x43,d=0x78)
  2. y = ((ctx->S[0][a] + ctx->S[1][b]) ^ ctx->S[2][c]) + ctx->S[3][d]
  3. 返回y的值

0x02.4 Demo

此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种:

  • ECB(Electronic Codebook, 电子密码本)模式
  • CBC(Cipher Block Chaining, 密码分组链接)模式
  • CFB(Cipher Feedback, 密码反馈)模式
  • OFB(Output Feedback, 输出反馈)模式
  • CTR(Counter, 计数器)模式

Navicat并没有使用上述的任何一种加密模式,但其采用的加密模式与CBC相似,故简单介绍CBC模式(下图来自Wikipedia),如果对于其他4种加密模式感兴趣,可自行百度。

Plaintext是明文按照分组密码的分组长度分割而成,初始化向量IV、密钥Key在加密时自行决定,Block Cipher Encryption是使用的加密算法,最终将每个密文分组Ciphertext连接起来即密文。

 

0x04 Navicat 加密过程

0x04.1 新建连接

首先,打开Navicat,新建一个MySQL连接:

连接名为Test,用户名默认root,密码123456:

Navicat将主机(Host),端口(Port),用户名(UserName)与加密后的密码(Pwd)保存到注册表中,不同的数据库连接对应的注册表路径不同,具体路径如下:

Win+R之后键入regedit打开注册表,按照上述路径去查找,可以看到刚刚我们建立的MySQL连接的相关键值对:

0x04.2 Navicat如何加密数据库密码

0x04.2-1 Key

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
};

0x04.2-2 IV

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
};

0x04.2-3 Mode

0x03部分中已经提到Navicat采用的分组密码模式并非5种主要加密模式之一,其采用的分组密码模式工作过程如下所示:

  • 每个PlaintextBlock长度为8字节;在Blowfish_Cipher环节不需要提供密钥Key,密钥Key在调用Blowfish_Init()已经提供,Blowfish Cipher在加解密过程使用已经初始化的ctx进行。
  • 只有剩余分组大小不足8字节时,才会进行上图中的最后一步。否则,一切照旧。

0x04.2-4 密文存储

按照分组密码模式,最终的密文应与明文长度一致,可注册表中保存的是”15057D7BA390”。这是因为Navicat在向注册表中写入的并非密文,而是十六进制表示的密文ASCII码。

 

0x05 Navicat Part of CDecryptPwd

由于此工具目前处于测试阶段,仍有许多有待完善之处,故暂时不公开源码,下面介绍的只是各环节的核心部分。

0x05.1 blowfish.c & blowfish.h

这两个文件直接使用的是Paul Kocher版本源码。

0x05.2 NavicatPartHeader.h

#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.cNavicatPartMain.c文件中要使用到的头文件;定义了两个全局符号常量BLOCK_SIZEKEYLENGTH,分别是分组长度与最大键值长度;以及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中并未使用。

  1. 初始化IV上面已经介绍,不再赘述,需提示一点:这一部分不能作为函数独立,然后在Navicat_Encrypt()中调用该函数,因为下面的Blowfish Cipher加密要使用其初始化的ctx。
  2. 第二个for循环部分完成0x04.2-3图中给出的前(n-1)步的过程。
  3. if部分是处理剩余分组大小不足8字节的情况,即第n步。
  4. 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写入注册表时创建路径的规律来进行拼接:

DataNavicatPremium子项均与存储数据库相关信息无关,不包含Servers,而存储数据库相关信息的NavicatNavicatPG子项均包含Servers,所以可进行这一判断来决定是否要用RegEnumKeyEx()遍历Servers下的子项。

  • 使用RegEnumValue()遍历Servers子项中的键值对,主要是Host、UserName、Pwd、Port四项。在读取Pwd值之后传递给Navicat_Decrypt()进行解密。需要说明一点:在读取Port之前,读取类型要从REG_SZ转为REG_DWORD,否则读出的值无意义。

运行效果图:

0x06 参考

(完)