Python中的Pycrypto重复Windows加密服务提供程序问题

4 投票
1 回答
1291 浏览
提问于 2025-04-17 19:49

编辑和更新

2013年3月24日:
我在Python中得到的输出哈希现在和C++中的哈希匹配了,前提是我把它转换成utf-16格式,并在遇到任何'e'或'm'字节之前停止。然而,解密的结果却不一致。我知道我的SHA1哈希是20个字节(160位),而RC4的密钥长度可以从40位到2048位不等,所以可能在WinCrypt中有一些默认的盐值处理,我需要模仿一下。CryptGetKeyParam KP_LENGTH或KP_SALT

2013年3月24日:
CryptGetKeyParam KP_LENGTH告诉我我的密钥长度是128位。我给它输入的是160位的哈希。所以可能它只是丢弃了最后32位……或者4个字节。现在正在测试。

2013年3月24日:
没错,就是这样。如果我在Python中丢弃SHA1哈希的最后4个字节……我得到的解密结果就一致了。

快速信息:

我有一个C++程序用来解密数据块。它使用Windows加密服务提供者,所以只能在Windows上运行。我希望它能在其他平台上也能工作。

方法概述:

在Windows Crypto API中,ASCII编码的密码字节被转换为宽字符表示,然后用SHA1进行哈希,以生成RC4流密码的密钥。

在Python PyCrypto中,ASCII编码的字节字符串被解码为Python字符串。根据经验观察到的字节进行截断,这些字节导致mbctowcs在C++中停止转换。这个截断后的字符串然后被编码为utf-16,实际上在字符之间填充了0x00字节。这个新的截断、填充后的字节字符串被传递给SHA1哈希,哈希结果的前128位被传递给PyCrypto的RC4对象。

问题 [已解决]
我似乎无法在Python 3.x中使用PyCrypto得到相同的结果。

C++代码框架:

HCRYPTPROV hProv      = 0x00;
HCRYPTHASH hHash      = 0x00;
HCRYPTKEY  hKey       = 0x00;
wchar_t    sBuf[256]  = {0};

CryptAcquireContextW(&hProv, L"FileContainer", L"Microsoft Enhanced RSA and AES Cryptographic Provider", 0x18u, 0);

CryptCreateHash(hProv, 0x8004u, 0, 0, &hHash);
//0x8004u is SHA1 flag

int len = mbstowcs(sBuf, iRec->desc, sizeof(sBuf));
//iRec is my "Record" class
//iRec->desc is 33 bytes within header of my encrypted file
//this will be used to create the hash key. (So this is the password)

CryptHashData(hHash, (const BYTE*)sBuf, len, 0);

CryptDeriveKey(hProv, 0x6801, hHash, 0, &hKey);

DWORD dataLen = iRec->compLen;  
//iRec->compLen is the length of encrypted datablock
//it's also compressed that's why it's called compLen

CryptDecrypt(hKey, 0, 0, 0, (BYTE*)iRec->decrypt, &dataLen);
// iRec is my record that i'm decrypting
// iRec->decrypt is where I store the decrypted data
//&dataLen is how long the encrypted data block is.
//I get this from file header info

Python代码框架:

from Crypto.Cipher import ARC4
from Crypto.Hash import SHA

#this is the Decipher method from my record class
def Decipher(self):

    #get string representation of 33byte password
    key_string= self.desc.decode('ASCII')

    #so far, these characters fail, possibly others but
    #for now I will make it a list
    stop_chars = ['e','m']

    #slice off anything beyond where mbstowcs will stop
    for char in stop_chars:
        wc_stop = key_string.find(char)
        if wc_stop != -1:
            #slice operation
            key_string = key_string[:wc_stop]

    #make "wide character"
    #this is equivalent to padding bytes with 0x00

    #Slice off the two byte "Byte Order Mark" 0xff 0xfe 
    wc_byte_string = key_string.encode('utf-16')[2:]

    #slice off the trailing 0x00
    wc_byte_string = wc_byte_string[:len(wc_byte_string)-1] 

    #hash the "wchar" byte string
    #this is the equivalent to sBuf in c++ code above
    #as determined by writing sBuf to file in tests
    my_key = SHA.new(wc_byte_string).digest()

    #create a PyCrypto cipher object
    RC4_Cipher = ARC4.new(my_key[:16])

    #store the decrypted data..these results NOW MATCH
    self.decrypt = RC4_Cipher.decrypt(self.datablock)

怀疑的[编辑:确认]原因
1. mbstowcs转换密码时,输入到SHA1哈希的“原始数据”在Python和C++中并不相同。mbstowcs在0x65和0x6D字节处停止了转换。原始数据以宽字符编码结束,但只包含原始33字节密码的一部分。

  1. RC4的密钥长度可以变化。在增强的Win Crypt服务提供者中,默认长度是128位。未指定密钥长度时,取的是“原始数据”的160位SHA1哈希的前128位。

我如何调查的
编辑:根据我自己的实验和@RolandSmith的建议,我现在知道我的一个问题是mbctowcs的行为超出了我的预期。它似乎在“e”(0x65)和“m”(0x6D)处停止写入sBuf(可能还有其他字节)。所以我描述中的密码“Monkey”(ASCII编码字节),在sBuf中看起来像“M o n k”,因为mbstowcs在“e”处停止了,并根据我系统上的2字节wchar类型定义在字节之间放置了0x00。我是通过将转换结果写入文本文件发现这一点的。

BYTE pbHash[256];  //buffer we will store the hash digest in 
DWORD dwHashLen;  //store the length of the hash
DWORD dwCount;
dwCount = sizeof(DWORD);  //how big is a dword on this system?


//see above "len" is the return value from mbstowcs that tells how
//many multibyte characters were converted from the original
//iRec->desc an placed into sBuf.  In some cases it's 3, 7, 9
//and always seems to stop on "e" or "m"

fstream outFile4("C:/desc_mbstowcs.txt", ios::out | ios::trunc | ios::binary);
outFile4.write((const CHAR*)sBuf, int(len));
outFile4.close();

//now get the hash size from CryptGetHashParam
//an get the acutal hash from the hash object hHash
//write it to a file.
if(CryptGetHashParam(hHash, HP_HASHSIZE, (BYTE *)&dwHashLen, &dwCount, 0)) {
  if(CryptGetHashParam(hHash, 0x0002, pbHash, &dwHashLen,0)){

    fstream outFile3("C:/test_hash.txt", ios::out | ios::trunc | ios::binary);
    outFile3.write((const CHAR*)pbHash, int(dwHashLen));
    outFile3.close();
  }
}

参考资料:
宽字符在不同环境下会导致问题
VC++ 6.0和VS 2008之间的Windows加密服务差异

将utf-8转换为utf-16字符串
Python - 从二进制文件转换宽字符字符串为Python Unicode字符串

PyCrypto RC4示例
https://www.dlitz.net/software/pycrypto/api/current/Crypto.Cipher.ARC4-module.html

使用Sha256对字符串进行哈希

http://msdn.microsoft.com/en-us/library/windows/desktop/aa379916(v=vs.85).aspx

http://msdn.microsoft.com/en-us/library/windows/desktop/aa375599(v=vs.85).aspx

1 个回答

1

你可以用一个简单的测试程序(用C语言写)来检查 wchar_t 的大小:

#include <stdio.h> /* for printf */
#include <stddef.h> /* for wchar_t */

int main(int argc, char *argv[]) {
    printf("The size of wchar_t is %ld bytes.\n", sizeof(wchar_t));
    return 0;
}

如果你能从终端运行C++程序,也可以在你的C++代码里用 printf() 来输出,比如 iRec->descsbuf 中的哈希结果到屏幕上。否则,你可以用 fprintf() 把它们写入一个文件。

为了更好地模拟C++程序的行为,你甚至可以在Python代码中使用 ctypes 来调用 mbstowcs()

编辑:你写道:

一个问题肯定是出在 mbctowcs 上。它似乎把一个不可预测(对我来说)的字节数传输到我的缓冲区中进行哈希。

请记住,mbctowcs 返回的是转换后的宽字符数量。换句话说,在多字节编码中,一个33字节的缓冲区可以根据使用的编码包含从5个(UTF-8的6字节序列)到33个字符的任何数量。

编辑2:你在 CryptDeriveKeydwFlags 参数中使用了0。根据它的 文档,高16位应该包含密钥长度。你应该检查 CryptDeriveKey 的返回值,以确认调用是否成功。

编辑3:你可以在Python中测试 mbctowcs(我这里使用的是 IPython):

In [1]: from ctypes import *

In [2]: libc = CDLL('libc.so.7')

In [3]: monkey = c_char_p(u'Monkey')

In [4]: test = c_char_p(u'This is a test')

In [5]: wo = create_unicode_buffer(256)

In [6]: nref = c_size_t(250)

In [7]: libc.mbstowcs(wo, monkey, nref)
Out[7]: 6

In [8]: print wo.value
Monkey

In [9]: libc.mbstowcs(wo, test, nref)
Out[9]: 14

In [10]: print wo.value
This is a test

注意,在Windows上,你可能应该使用 libc = cdll.msvcrt 而不是 libc = CDLL('libc.so.7')

撰写回答