#!/usr/bin/env python3 import json import gzip import base64 import argparse from hashlib import md5 from Crypto.Cipher import AES from struct import pack, unpack """ Author: bashis (2021) Contributors: iTuneDVR, February 2023: XVR, tested: DHI-XVR5104HS-I3 [Update] January 2022: Added support for NVR; Use "--key nvr" February 2023: Added support for XVR; Use "--key xvr" Tested: IPC/VTO/SD/NVR/XVR -[Get _possible_ key with 'DahuaConsole' (https://github.com/mcw0/DahuaConsole)]- Note: As usual with Dahua (sigh), their rules has always exceptions, and with my "SD" the key is simply "SD" [Console]# uboot getenv HWID { "id": 14, "params": { "table": { "HWID": "IPC-G42P-IMOU:01:02:03:04:05:06:07:08:09:10:11:123:00:00:00:00:00:00:00:00:000" } }, "result": true, "session": 1178419111 } [Console]# -[Demo]- $ ./DahuaConfigBackupDecEnc.py --infile configFileExport.backup --key IPC-G42P-IMOU [*] Dahua Config Backup Decrypt/Encrypt by bashis (2021) [*] Decrypt "configFileExport.backup", key: IPC-G42P-IMOU Version: 1 Config size : 73356 Provided MD5 : B218592EA84B55319A40C064D91FD6C2 Calculated MD5: B218592EA84B55319A40C064D91FD6C2 Saved decrypted "configFileExport.backup.dec" (73356 bytes) $ ./DahuaConfigBackupDecEnc.py --infile configFileExport.backup.dec --key IPC-G42P-IMOU [*] Dahua Config Backup Decrypt/Encrypt by bashis (2021) [*] Encrypt "configFileExport.backup.dec", key: IPC-G42P-IMOU Version: 1 Config size : 73356 Calculated MD5: B218592EA84B55319A40C064D91FD6C2 Saved encrypted "configFileExport.backup.enc" (73417) """ """Might have other gzip/base64 in the future""" dh_gzip = ['nvr', 'xvr'] class AESCipher: def __init__(self, key): self.key = key.encode('UTF-8') def pad_zero(self, dh_data): dh_data += bytes(AES.block_size - (len(dh_data) % AES.block_size)) return dh_data def ecb_encrypt(self, raw): cipher = AES.new(key=self.key, mode=AES.MODE_ECB) return cipher.encrypt(raw) def ecb_decrypt(self, raw): cipher = AES.new(key=self.key, mode=AES.MODE_ECB) return cipher.decrypt(raw) def read_file(dh_name, dh_size=0): try: with open(dh_name, 'rb') as fd: if dh_size: return fd.read(dh_size) return fd.read() except IOError as e: print(f'[read_file] error ({e})') return None def write_file(dh_name, dh_data): try: with open(dh_name, 'wb') as fd: return fd.write(dh_data) except IOError as e: print(f'[write_file] error ({e})') return None def generate_key(clear_text): key = '' num = 0 """Dahua obfuscation""" for char in range(0, len(clear_text)): num += 1 key += chr(ord(clear_text[char]) ^ num) return md5(key.encode('latin-1')).hexdigest() def generate_backup_key(clear_text, dh_char_string): if clear_text == 'nvr': return generate_key('DahuaNVR') elif clear_text == 'xvr': return generate_key('DahuaHCVR') dh_string = "yaojinfucrang,yixitgchuanfei.vhuanglaiwaerqingfemgsheng,qiangeningerbaiyune.tuiyuanlvzbu," \ "qilingxengzezhizfn;yeshuilhuhua,guakgzhaolinqhuanzhibi.zimeiju,ebnanbing." key = clear_text.encode('UTF-8') key += b'\x00' * 16 key = md5(key).digest() """dh_char_string looks fixed to '1', assuming it means 1'st char in the 'dh_string'""" key += dh_string[dh_char_string].encode('UTF-8') key = md5(key).digest() key = md5(key).hexdigest() key = key[8:].encode('UTF-8') key += b'\x00' * (32 - (len(key) % 32)) return key.decode('UTF-8') def dh_backup(mode, file_name, key): offset = 9 file = read_file(file_name) try: if mode == 'decrypt': """dh_char_string looks fixed to '1', assuming it means 1'st char in the 'dh_string'""" gen_key = generate_backup_key(key, dh_char_string=file[8] - 1) out = AESCipher(gen_key).ecb_decrypt(base64.b64decode(file[9:]) if key in dh_gzip else file[offset:]) dh_size = unpack('I', out[1:5])[0] md5sum = out[dh_size + 5:].strip(b"\x00").decode('UTF-8').lower() # including 5 bytes header if not md5sum == md5(out[:dh_size + 5]).hexdigest().lower(): print(f'[!] MD5 mismatch, decryption failed (correct key?)') return False if not file_name.rfind('.enc') == -1: file_name = file_name[:file_name.rfind('.enc')] if not file_name.rfind('.tgz') == -1: file_name = file_name[:file_name.rfind('.tgz')] written = write_file(file_name + '.tgz' if key in dh_gzip else file_name + '.dec', out[5:dh_size + 5]) print(f'Version: {out[0]}') print(f'Config size : {dh_size}') print(f'Provided MD5 : {md5sum}') print(f'Calculated MD5: {md5(out[:dh_size + 5]).hexdigest().lower()}') print(f'Saved decrypted "{file_name + ".tgz" if key in dh_gzip else file_name + ".dec"}" ({written} bytes)') return True elif mode == 'encrypt': version = 1 dh_size = len(file) """dh_char_string looks fixed to '1', assuming it means 1'st char in the 'dh_string'""" gen_key = generate_backup_key(key, dh_char_string=version - 1) out = AESCipher(gen_key) config = pack('B', 1) config += pack('I', dh_size) config += file if key in dh_gzip: md5sum = md5(config).hexdigest().lower().encode('latin-1') else: md5sum = md5(config).hexdigest().upper().encode('latin-1') config += md5sum config = out.pad_zero(config) out = out.ecb_encrypt(config) if key in dh_gzip: out = base64.b64encode(out) + b'\x00' out = b'MWPZWJGS' + (str(version).encode() if key in dh_gzip else pack('B', version)) + out if not file_name.rfind('.dec') == -1: file_name = file_name[:file_name.rfind('.dec')] written = write_file(file_name + '.backup' if key in dh_gzip else file_name + '.enc', out) print(f'Version: {version}') print(f'Config size : {dh_size}') md5sum = md5sum.decode('latin-1').lower() print(f'Calculated MD5: {md5sum}') print(f'Saved encrypted "{file_name + ".backup" if key in dh_gzip else file_name + ".enc"}" ({written})') return True except ValueError as e: print(f'Error: {e}') return False def main(): print('[*] Dahua Config Backup Decrypt/Encrypt by bashis (2021) [*]\n') parser = argparse.ArgumentParser() parser.add_argument( '--infile', metavar='\b', required=True, type=str, default=None, help='Encrypted or JSON unencrypted filename') parser.add_argument( '--key', metavar='\b', required=True, type=str, default=None, help='Encrypt/Decrypt key') args = parser.parse_args() file = read_file(args.infile, 25) if file is None: return False elif not len(file): print(f'[!] File "{args.infile}" is empty') return False if args.key.lower() == 'nvr': args.key = 'nvr' elif args.key.lower() == 'xvr': args.key = 'xvr' # bVar1 = std::operator==(param_4,"VTHRemoteIPCInfo"); # if (bVar1 == false) { # pcVar11 = "MWPZWJGS"; # } # else { # pcVar11 = "DHRDENFR"; # } if file[0:8] == b'MWPZWJGS' or file[0:8] == b'DHRDENFR': print(f'Decrypt "{args.infile}", key: {args.key}') return dh_backup('decrypt', args.infile, args.key) else: if args.key in dh_gzip: try: gzip.decompress(read_file(args.infile)) except gzip.BadGzipFile as e: print(e) print('[!] Input not valid GZIP file') return False else: try: json.loads(read_file(args.infile)) except ValueError as e: print(e) print('[!] Input not valid JSON file') return False print(f'Encrypt "{args.infile}", key: {args.key}') return dh_backup('encrypt', args.infile, args.key) if __name__ == '__main__': main()