From 0cb13b9d387dfd2cf5aaadc27be3b159bea1ccda Mon Sep 17 00:00:00 2001 From: melvyn2 Date: Mon, 20 Jun 2022 23:15:09 -0700 Subject: [PATCH] Move all win32 logic to wine executable Actions like reading the registry and serial number were being done in python even though the final decryption was done in wine. This commit moves all windows logic except architecture detection into the exe ran under wine to simplify the architecture. --- calibre-plugin/getEncryptionKeyLinux.py | 353 ++---------------------- calibre-plugin/keyextract/Makefile | 4 +- calibre-plugin/keyextract/main.c | 284 +++++++------------ 3 files changed, 120 insertions(+), 521 deletions(-) diff --git a/calibre-plugin/getEncryptionKeyLinux.py b/calibre-plugin/getEncryptionKeyLinux.py index de0e619..ee46351 100644 --- a/calibre-plugin/getEncryptionKeyLinux.py +++ b/calibre-plugin/getEncryptionKeyLinux.py @@ -5,307 +5,7 @@ import sys, binascii -def unfuck(user): - # Wine uses a pretty nonstandard encoding in their registry file. - # I haven't found any existing Python implementation for that, - # so I looked at the C code and wrote my own. - # This implementation doesn't support multi-byte UTF-8 chars, - # but a standard-conforming Wine registry won't contain - # these anyways, so who cares. - - hex_char_list = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70, 97, 98, 99, 100, 101, 102] - - # Remove the quotation marks at beginning and end: - user = user.strip()[1:-1] - user_new = bytearray() - i = 0 - while i < len(user): - # Convert string of len 1 to a byte - char = user[i][0].encode("latin-1")[0] - if sys.version_info[0] == 2: - char = ord(char) - - if char == ord('\\'): - # Get next char: - i += 1 - char = user[i][0].encode("latin-1")[0] - if sys.version_info[0] == 2: - char = ord(char) - - if (char == ord('a')): - user_new.append(0x07) - elif (char == ord('b')): - user_new.append(0x08) - elif (char == ord('e')): - user_new.append(0x1b) - elif (char == ord('f')): - user_new.append(0x0c) - elif (char == ord('n')): - user_new.append(0x0a) - elif (char == ord('r')): - user_new.append(0x0d) - elif (char == ord('t')): - user_new.append(0x09) - elif (char == ord('v')): - user_new.append(0x0b) - elif (char == ord('x')): - # Get next char - i += 1 - char = user[i][0].encode("latin-1")[0] - if sys.version_info[0] == 2: - char = ord(char) - if char not in hex_char_list: - user_new.append(ord('x')) - # This seems to be fallback code. - # Subtract 1 so the next char (the one that's not a hex char) - # is handled normally. - i -= 1 - else: - ival = "" + chr(char) - - # Read up to 3 more chars - next = user[i + 1][0].encode("latin-1")[0] - if sys.version_info[0] == 2: - next = ord(next) - - if next in hex_char_list: - ival += chr(next) - i += 1 - - next = user[i + 1][0].encode("latin-1")[0] - if sys.version_info[0] == 2: - next = ord(next) - - if next in hex_char_list: - ival += chr(next) - i += 1 - - next = user[i + 1][0].encode("latin-1")[0] - if sys.version_info[0] == 2: - next = ord(next) - - if next in hex_char_list: - ival += chr(next) - i += 1 - - # ival now contains "00e9". Convert to int ... - ival = int(ival, 16) - # then drop everything except the lowest byte - ival = ival & 0xFF - # then add it to the array - user_new.append(ival) - elif (char >= ord('0') and char <= ord('9') ): - - octal = char - ord('0') - - # Read up to 2 more chars - next = user[i + 1][0].encode("latin-1")[0] - if sys.version_info[0] == 2: - next = ord(next) - - if next >= ord('0') and next <= ord('9'): - octal = (octal * 8) + (next - ord('0')) - i += 1 - - next = user[i + 1][0].encode("latin-1")[0] - if sys.version_info[0] == 2: - next = ord(next) - - if next >= ord('0') and next <= ord('9'): - octal = (octal * 8) + (next - ord('0')) - i += 1 - - else: - - if (char < 0x80): - user_new.append(char) - else: - print("Multi-Byte UTF-8, not supported") - print("This should never happen in a standard-conform Wine registry ...") - return False - - # Parse next char - i += 1 - - return user_new - -def GetMasterKey(path_to_wine_prefix): - - import os - - if os.name == 'nt': - print("Hey! This is for Linux!") - return - - verbose_logging = False - try: - import calibre_plugins.deacsm.prefs as prefs - deacsmprefs = prefs.DeACSM_Prefs() - verbose_logging = deacsmprefs["detailed_logging"] - except: - pass - - - import cpuid - import struct - - try: - # Linux / Wine code just assumes that the system drive is C:\ - serial_file = open(os.path.join(path_to_wine_prefix, "drive_c", ".windows-serial"), "r") - serial = serial_file.read() - serial_file.close() - serial = int(serial, 16) - except: - # If this file is not present, Wine will usually use a default serial number of "0". - # There are some edge cases where Wine uses a different serial number even when that - # .windows-serial file is not present. - serial = 0 - - if (verbose_logging): - print("Serial: {}".format(serial)) - - cpu = cpuid.CPUID() - _, b, c, d = cpu(0) - vendor = struct.pack("III", b, d, c) - - if (verbose_logging): - print("Vendor: {}".format(vendor)) - - signature, _, _, _ = cpu(1) - signature = struct.pack('>I', signature)[1:] - - if (verbose_logging): - print("Signature: {}".format(binascii.hexlify(signature))) - - # Search for the username in the registry: - user = None - - - # Linux - loop through the Wine registry file to find the "username" attribute - try: - registry_file = open(os.path.join(path_to_wine_prefix, "user.reg")) - waiting_for_username = False - while True: - line = registry_file.readline() - if not line: - break - - if waiting_for_username: - if (not line.lower().startswith("\"username\"=")): - continue - - # If we end up here, we have the username. - user = line.split('=', 1)[1].strip() - user = unfuck(user) - break - - else: - if (line.startswith("[Software\\\\Adobe\\\\Adept\\\\Device]")): - waiting_for_username = True - - if (line.startswith("[Volatile Environment]")): - waiting_for_username = True - - registry_file.close() - except: - # There was an error hunting through the registry. - raise - pass - - if (user is None): - print("Error while determining username ...") - exit() - - # Comes as bytearray - if sys.version_info[0] == 3: - user = bytes(user) - else: - user = str(user) - - if verbose_logging: - print("Username: {}".format(user)) - - # Find the value we want to decrypt from the registry. loop through the Wine registry file to find the "key" attribute - try: - registry_file = open(os.path.join(path_to_wine_prefix, "user.reg")) - waiting_for_key = False - key_line = None - while True: - line = registry_file.readline() - if not line: - break - - if waiting_for_key: - if (not line.lower().startswith("\"key\"=")): - continue - - # If we end up here, we have the key. - key_line = line - while (key_line.strip().endswith('\\')): - key_line = key_line.strip()[:-1] + registry_file.readline() - - # Now parse ... - key_line = key_line.split(':', 1)[1] - key_line = key_line.replace('\t', '').replace('\r', '').replace('\n', '').replace(' ', '').replace(',', '') - key_line = binascii.unhexlify(key_line) - - else: - if (line.startswith("[Software\\\\Adobe\\\\Adept\\\\Device]")): - waiting_for_key = True - - - - - registry_file.close() - except: - # There was an error hunting through the registry. - raise - pass - - if key_line is None: - print("No ADE activation found ...") - return None - - if verbose_logging: - print("Encrypted key: {}".format(binascii.hexlify(key_line))) - - # These should all be "bytes" (Py3) or "str" (Py2) - # print(type(vendor)) - # print(type(signature)) - # print(type(user)) - - entropy = struct.pack('>I12s3s13s', serial, vendor, signature, user) - - if verbose_logging: - print("Entropy: {}".format(binascii.hexlify(entropy))) - - # We would now call CryptUnprotectData to decrypt the stuff, - # but unfortunately there's no working Linux implementation - # for that. - # - # The plan was to handle everything in Python so we don't have - # to interact with Wine - that's why we're doing all the registry - # handling ourselves. - # Unfortunately, that doesn't work for the actual decryption. - # - # This means we have to call a Windows binary through - # Wine just for this one single decryption call ... - - success, data = CryptUnprotectDataExecuteWine(path_to_wine_prefix, key_line, entropy) - if (success): - keykey = data - if verbose_logging: - print("Key: {}".format(binascii.hexlify(keykey))) - return keykey - - else: - print("Error number: {}".format(data)) - if data == 13: # WINError ERROR_INVALID_DATA - print("Could not decrypt data with the given key. Did the entropy change?") - return None - - -def CryptUnprotectDataExecuteWine(wineprefix, data, entropy): +def GetMasterKey(wineprefix): import subprocess, os, re verbose_logging = False @@ -319,7 +19,7 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy): print("Asking WINE to decrypt encrypted key for us ...") if wineprefix == "" or not os.path.exists(wineprefix): - print("Wineprefix not found!!") + print("Wineprefix not found!") return None @@ -334,27 +34,21 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy): if not line: break - stuff = re.match(r'#arch=(win32|win64)', line) - if (stuff): - winearch = stuff.groups()[0] + archkey = re.match(r'#arch=(win32|win64)', line) + if (archkey): + winearch = archkey.groups()[0] break regfile.close() except: pass - # Execute! - env_dict = os.environ env_dict["PYTHONPATH"] = "" env_dict["WINEPREFIX"] = wineprefix #env_dict["WINEDEBUG"] = "-all,+crypt" env_dict["WINEDEBUG"] = "+err,+fixme" - # Use environment variables to get the input data to the application. - env_dict["X_DECRYPT_DATA"] = binascii.hexlify(data).decode("utf-8") - env_dict["X_DECRYPT_ENTROPY"] = binascii.hexlify(entropy).decode("utf-8") - try: from calibre.utils.config import config_dir pluginsdir = os.path.join(config_dir,"plugins") @@ -366,37 +60,26 @@ def CryptUnprotectDataExecuteWine(wineprefix, data, entropy): # calls decrypt_win32.exe or decrypt_win64.exe proc = subprocess.Popen(["wine", "decrypt_" + winearch + ".exe"], shell=False, cwd=moddir, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - prog_output, prog_stderr = proc.communicate() - stdout = prog_output.decode("utf-8") - stderr = prog_stderr.decode("utf-8") + prog_stdout, prog_stderr = proc.communicate() - if stdout.startswith("PROGOUTPUT:0:"): - key_string = stdout.split(':')[2] + if verbose_logging: + print("Stderr log:\n{}".format(prog_stderr.decode("utf-8"))) + print("Stdout log: {}".format(prog_stdout.decode("utf-8"))) + print("Exit code: {}".format(proc.returncode)) + + if proc.returncode == 0: if verbose_logging: - print("Successfully got encryption key from WINE: {}".format(key_string)) + print("Successfully got encryption key from WINE: {}".format(prog_stdout.decode("utf-8"))) else: print("Successfully got encryption key from WINE.") - master_key = binascii.unhexlify(key_string) - return True, master_key - - + master_key = binascii.unhexlify(prog_stdout) + return master_key else: - print("Huh. That didn't work. ") - try: - err = int(stdout.split(':')[1]) - if err == -4: - err = int(stdout.split(':')[2]) - new_serial = int(stdout.split(':')[3]) - if verbose_logging: - print("New serial: {}".format(new_serial)) - except: - err = None + print("Failed to extract encryption key from WINE.") + print("Exit code: {}".format(proc.returncode)) - if verbose_logging: - # print("Stderr log:\n{}".format(stderr)) - print("Program output: {}".format(stdout)) - return False, err + return None if __name__ == "__main__": diff --git a/calibre-plugin/keyextract/Makefile b/calibre-plugin/keyextract/Makefile index f25d6b8..f982222 100644 --- a/calibre-plugin/keyextract/Makefile +++ b/calibre-plugin/keyextract/Makefile @@ -5,10 +5,10 @@ clean: rm decrypt_win32.exe decrypt_win64.exe 2>/dev/null || /bin/true decrypt_win32.exe: main.c Makefile - i686-w64-mingw32-gcc main.c -Os -o decrypt_win32.exe -lcrypt32 + i686-w64-mingw32-gcc main.c -Os -o decrypt_win32.exe -lcrypt32 -lwsock32 i686-w64-mingw32-strip decrypt_win32.exe decrypt_win64.exe: main.c Makefile - x86_64-w64-mingw32-gcc main.c -Os -o decrypt_win64.exe -lcrypt32 + x86_64-w64-mingw32-gcc main.c -Os -o decrypt_win64.exe -lcrypt32 -lwsock32 x86_64-w64-mingw32-strip decrypt_win64.exe diff --git a/calibre-plugin/keyextract/main.c b/calibre-plugin/keyextract/main.c index 7870114..aa0e181 100644 --- a/calibre-plugin/keyextract/main.c +++ b/calibre-plugin/keyextract/main.c @@ -1,196 +1,112 @@ #include -#include -#include -#include -#include -#include -#include -#include +#include +#include -#ifdef DEBUG -#undef DEBUG -#endif +union CPUIDVendor { + unsigned int reg[3]; + char vendor[13]; +}; -int char2int(char input) { - if (input >= '0' && input <= '9') - return input - '0'; - if (input >= 'A' && input <= 'F') - return input - 'A' + 10; - if (input >= 'a' && input <= 'f') - return input - 'a' + 10; - - fputs("PROGOUTPUT:-3", stdout); - exit(-3); -} - -void hex2bin(const char * src, char * dst) { - while (*src && src[1]) { - *(dst++) = char2int(*src) * 16 + char2int(src[1]); - src += 2; - } -} - -#ifdef DEBUG -void hexDump ( - const char * desc, - const void * addr, - const int len, - int perLine -) { - // Silently ignore silly per-line values. - - if (perLine < 4 || perLine > 64) perLine = 16; - - int i; - unsigned char buff[perLine+1]; - const unsigned char * pc = (const unsigned char *)addr; - - // Output description if given. - - if (desc != NULL) fprintf (stderr, "%s:\n", desc); - - // Length checks. - - if (len == 0) { - fprintf(stderr, " ZERO LENGTH\n"); - return; - } - if (len < 0) { - fprintf(stderr, " NEGATIVE LENGTH: %d\n", len); - return; - } - - // Process every byte in the data. - - for (i = 0; i < len; i++) { - // Multiple of perLine means new or first line (with line offset). - - if ((i % perLine) == 0) { - // Only print previous-line ASCII buffer for lines beyond first. - - if (i != 0) fprintf (stderr, " %s\n", buff); - - // Output the offset of current line. - - fprintf (stderr, " %04x ", i); - } - - // Now the hex code for the specific character. - - fprintf (stderr, " %02x", pc[i]); - - // And buffer a printable ASCII character for later. - - if ((pc[i] < 0x20) || (pc[i] > 0x7e)) // isprint() may be better. - buff[i % perLine] = '.'; - else - buff[i % perLine] = pc[i]; - buff[(i % perLine) + 1] = '\0'; - } - - // Pad out last line if not exactly perLine characters. - - while ((i % perLine) != 0) { - fprintf (stderr, " "); - i++; - } - - // And print the final ASCII buffer. - - fprintf (stderr, " %s\n", buff); -} -#endif - -int get_serial() { - DWORD serial = 0; - int retval = GetVolumeInformation("c:\\\\", NULL, 0, &serial, NULL, NULL, NULL, 0); - if (retval == 0) { - fprintf(stderr, "Error with GetVolumeInformation: %d\n", GetLastError()); - return 0; - } - return serial; -} +struct EncEntropy { + unsigned int serial; + char vendor[12]; + char signature[3]; + char user[13]; +}; int main() { - char * var_data = "X_DECRYPT_DATA"; - char * var_entropy = "X_DECRYPT_ENTROPY"; - - char * data_hex = getenv(var_data); - char * entropy_hex = getenv(var_entropy); - - if (data_hex == NULL || entropy_hex == NULL) { - fputs("PROGOUTPUT:-1", stdout); - exit(-1); + // Get disk serial + DWORD serial; + if (GetVolumeInformation("c:\\\\", NULL, 0, &serial, NULL, NULL, NULL, 0) == 0) { + DWORD err = GetLastError(); + fprintf(stderr, "Error with GetVolumeInformation: %ld\n", err); + return err; } + DWORD be_serial = htonl(serial); + fprintf(stderr, "Disk serial (hex): %08lx\n", serial); - char * data_bytes = malloc((strlen(data_hex) / 2)); - char * entropy_bytes = malloc((strlen(entropy_hex) / 2)); - if (data_bytes == NULL || entropy_bytes == NULL) { - fputs("PROGOUTPUT:-2", stdout); - exit(-2); + unsigned int eax, ebx, ecx, edx; + // Get CPUID vendor string + union CPUIDVendor cpu_vendor; + if (__get_cpuid(0, &eax, &cpu_vendor.reg[0], &cpu_vendor.reg[2], &cpu_vendor.reg[1]) == 0) { + fprintf(stderr, "Error: cpuid(0) not supported"); + return 1; } - - hex2bin(data_hex, data_bytes); - hex2bin(entropy_hex, entropy_bytes); - -#ifdef DEBUG - hexDump("data", data_bytes, strlen(data_hex)/2, 16); - hexDump("entropy", entropy_bytes, strlen(entropy_hex)/2, 16); -#endif - - - -DATA_BLOB input_data; -DATA_BLOB entropy_data; - - -DATA_BLOB output_data; - -input_data.pbData = data_bytes; -input_data.cbData = strlen(data_hex)/2; - -entropy_data.pbData = entropy_bytes; -entropy_data.cbData = strlen(entropy_hex)/2; - - -int ret = CryptUnprotectData( - &input_data, - NULL, - &entropy_data, - NULL, - NULL, - 0, - &output_data); - - - -if (ret) { - if (output_data.cbData != 16) { - printf("PROGOUTPUT:-5:%d", output_data.cbData); - exit(-5); + cpu_vendor.vendor[12] = '\0'; + fprintf(stderr, "CPUID Vendor: %s\n", cpu_vendor.vendor); + // Get CPUID "signature" (eax of CPUID(1)) + unsigned int signature; + if (__get_cpuid(1, &signature, &ebx, &ecx, &edx) == 0) { + fprintf(stderr, "Error: cpuid(1) not supported"); + return 1; } - // Success! Return decrypted data - fputs("PROGOUTPUT:0:", stdout); + unsigned int be_signature = htonl(signature); + fprintf(stderr, "CPUID Signature (hex): %08x\n", signature); + + + // Get windows user + #define USERBUFSIZE 512 + TCHAR user[USERBUFSIZE]; + memset(&user, 0, sizeof(user)); // GetUserName only sets bytes as needed for length of username, but we need null bytes to fill the rest + DWORD bufsize = USERBUFSIZE ; + if (GetUserName(user, &bufsize) == 0) { + DWORD err = GetLastError(); + fprintf(stderr, "Error with GetUserName: %ld\n", err); + return err; + } + fprintf(stderr, "Username: %s\n", user); + + + // Get Encrypted adobe key + #define KEYBUFSIZE 180 // As measured + BYTE key[KEYBUFSIZE]; + DWORD regkeysize = KEYBUFSIZE; + LSTATUS retval = RegGetValue(HKEY_CURRENT_USER, "Software\\Adobe\\Adept\\Device", "key", RRF_RT_REG_BINARY, NULL, &key, ®keysize); + if (retval != ERROR_SUCCESS) { + fprintf(stderr, "Error with RegGetValue: %ld\n", retval); + fprintf(stderr, "regkeysize: %ld\n", regkeysize); + return retval; + } + fprintf(stderr, "Encrypted key (hex): "); + for (size_t i = 0; i < KEYBUFSIZE; i++ ) + { + fprintf(stderr, "%02x", key[i]); + } + fprintf(stderr, "\n"); + + + // Assemble "entropy" (passphrase) for key + struct EncEntropy entropy; + memcpy(&entropy.serial, &be_serial, sizeof(entropy.serial)); + memcpy(&entropy.vendor, &cpu_vendor.vendor, sizeof(entropy.vendor)); + memcpy(&entropy.signature, ((char*)(&be_signature))+1, sizeof(entropy.signature)); // Only the last 3 bytes are needed, hence the +1 ptr + memcpy(&entropy.user, &user, sizeof(entropy.user)); + + // Print entropy byte by byte in hex + fprintf(stderr, "Entropy: "); + for (size_t i = 0; i < sizeof(entropy); i++ ) + { + fprintf(stderr, "%02x", ((unsigned char*)&entropy)[i]); + } + fprintf(stderr, "\n"); + + + // Run decryption API + DATA_BLOB ciphertext_data, entropy_data, plaintext_data; + ciphertext_data.pbData = key; + ciphertext_data.cbData = sizeof(key); + entropy_data.pbData = (BYTE*)(&entropy); + entropy_data.cbData = sizeof(entropy); + if (CryptUnprotectData(&ciphertext_data, NULL, &entropy_data, NULL, NULL, 0, &plaintext_data) != TRUE) { + DWORD err = GetLastError(); + fprintf(stderr, "Error with CryptUnprotectData: %ld\n", err); + return err; + } + fprintf(stderr, "Decrypted key length: %lu\n", plaintext_data.cbData); + + // Print decrypted key to stdout for (int i = 0; i < 16; i++) { - printf("%02x", output_data.pbData[i]); + printf("%02x", plaintext_data.pbData[i]); } - exit(0); -} -else { - - // Apparently Wine has issues with the volume serial code sometimes - // so the code on the Linux side detects the wrong serial number. - // Thus, if the decryption fails, we read the serial number that Wine - // (and ADE) sees back to the Linux side for another attempt. - - int err = GetLastError(); - - printf("PROGOUTPUT:-4:%d:%08x", err, get_serial()); - - exit(-4); -} - - -} - +} \ No newline at end of file