Created
June 12, 2025 02:59
-
-
Save D4stiny/412157ca10d0201105e41b340af389ec to your computer and use it in GitHub Desktop.
Tool to decrypt and encrypt Megarac-based configuration backups.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
COPYRIGHT Bill Demirkapi 2025 | |
Small utility I wrote with some help from Gemini Pro 2.5 to decrypt/encrypt BMC config backups from my ASUS ASMB11-iKVM AST2600 BMC. | |
Fed the model a bunch of decompiled code from BMC firmware libaries like libaes and libBackupConf. | |
Probably works with other Megarac-based BMCs too. | |
Expects an AESKey and AESIV file in script directory. This varies by OEM, often requires unpacking firmware/flash image. | |
For my ASUS BMC, however, the Key/IV for config are just NULL keys. AESKey file content is "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", AESIV's is "AAAAAAAAAAAAAAAAAAAAAA==". | |
""" | |
import argparse | |
import base64 | |
import hashlib | |
import os | |
import re | |
import sys | |
from pathlib import Path | |
try: | |
from Crypto.Cipher import AES | |
except ImportError: | |
print("Error: PyCryptodome is not installed. Please install it with 'pip install pycryptodome'", file=sys.stderr) | |
sys.exit(1) | |
# --- Constants --- | |
# Based on a definitive reading of the C code's processing loop | |
PLAINTEXT_CHUNK_SIZE = 255 | |
ENCRYPTED_CHUNK_SIZE = 256 | |
BASE64_CHUNK_SIZE = 344 | |
SIGNATURE_LENGTH = 40 | |
# Based on reverse-engineering the GetCheckSumKey function's array. | |
# The index from the file maps directly to a key in this dictionary. | |
CHECKSUM_KEYS = { | |
0: "megarac", | |
1: "megaracsp", | |
2: "megaracsp2", | |
3: "megaracspx", | |
4: "megarac1", | |
5: "megarac3", | |
6: "megarac4", | |
7: "megarac5", | |
8: "megarac6", | |
} | |
AES_BLOCK_SIZE = 16 | |
class BmcBackupTool: | |
"""A tool to decrypt and encrypt ASUS BMC configuration backup files.""" | |
def __init__(self, verbose=False): | |
self.key = None | |
self.iv = None | |
self.verbose = verbose | |
self.active_checksum_key = None | |
def _vprint(self, *args, **kwargs): | |
if self.verbose: | |
print("[VERBOSE]", *args, **kwargs) | |
def _load_keys(self): | |
try: | |
with open("AESKey", "r") as f: | |
key_b64 = f.read().strip() | |
with open("AESIV", "r") as f: | |
iv_b64 = f.read().strip() | |
self.key = base64.b64decode(key_b64) | |
self.iv = base64.b64decode(iv_b64) | |
self._vprint(f"Loaded AESKey (b64): {key_b64}") | |
self._vprint(f"Loaded AESIV (b64): {iv_b64}") | |
except FileNotFoundError as e: | |
print(f"Error: Could not find key file '{e.filename}'.", file=sys.stderr) | |
sys.exit(1) | |
def decrypt(self, input_path: Path, output_path: Path): | |
print(f"--> Loading backup file: {input_path}") | |
parsed_content = self._parse_backup_file(input_path) | |
if not parsed_content: | |
print("Error: Failed to parse backup file. File may be empty or malformed.", file=sys.stderr) | |
return | |
print("--> Verifying SHA1 signature...") | |
if not self._verify_signature(parsed_content): | |
print("Warning: SHA1 signature verification FAILED.", file=sys.stderr) | |
else: | |
print("--> Signature OK.") | |
print("--> Loading AES key and IV...") | |
self._load_keys() | |
print("--> Decrypting data...") | |
decrypted_archive = self._decrypt_data_blob(parsed_content["data_blob"]) | |
if decrypted_archive is None: return | |
print(f"--> Extracting files to: {output_path}") | |
self._extract_archive(decrypted_archive, output_path) | |
print("\nDecryption complete.") | |
def encrypt(self, input_dir: Path, output_file: Path): | |
unencrypted_archive = self._build_unencrypted_archive(input_dir) | |
self._load_keys() | |
encrypted_blob = self._encrypt_archive(unencrypted_archive) | |
final_content = self._build_final_file(encrypted_blob) | |
output_file.write_bytes(final_content) | |
print(f"\nEncryption complete. File saved to: {output_file}") | |
def _parse_backup_file(self, path: Path) -> dict | None: | |
"""Parses the backup file by strictly adhering to the C code's slicing method.""" | |
try: | |
raw_content = path.read_bytes() | |
if len(raw_content) < SIGNATURE_LENGTH: return None | |
content_for_hashing = raw_content[:-SIGNATURE_LENGTH] | |
signature_bytes = raw_content[-SIGNATURE_LENGTH:] | |
data_header_marker = b'$$$Data=' | |
data_start_pos = content_for_hashing.find(data_header_marker) | |
if data_start_pos == -1: return None | |
blob_start_offset = data_start_pos + len(data_header_marker) | |
data_blob_bytes = content_for_hashing[blob_start_offset:] | |
match_key_index = re.search(rb"\$\$\$CheckSumKeyIndex=(\d+)\$", content_for_hashing) | |
if not match_key_index: return None | |
checksum_key_index = int(match_key_index.group(1)) | |
return { | |
"content_for_hashing": content_for_hashing, | |
"signature": signature_bytes.decode('ascii').strip(), | |
"checksum_key_index": checksum_key_index, | |
"data_blob": data_blob_bytes.decode('ascii'), | |
} | |
except Exception as e: | |
self._vprint(f"Parsing failed with exception: {e}") | |
return None | |
def _verify_signature(self, parsed_content: dict) -> bool: | |
"""Verifies the SHA1 signature by using the correct checksum key.""" | |
key_index = parsed_content["checksum_key_index"] | |
content_before_hash = parsed_content["content_for_hashing"] | |
signature = parsed_content["signature"] | |
checksum_key = CHECKSUM_KEYS.get(key_index) | |
if not checksum_key: | |
print(f"Error: CheckSumKeyIndex '{key_index}' is unknown.", file=sys.stderr) | |
return False | |
self._vprint(f"Using Checksum Key: '{checksum_key}' for index {key_index}") | |
data_to_hash = content_before_hash + f"\nKEY={checksum_key}".encode("ascii") | |
calculated_hash = hashlib.sha1(data_to_hash).hexdigest() | |
self._vprint(f"Calculated SHA1: {calculated_hash}") | |
self._vprint(f"Expected SHA1: {signature}") | |
is_valid = calculated_hash.lower() == signature.lower() | |
if is_valid: | |
self.active_checksum_key = checksum_key | |
return is_valid | |
def _decrypt_data_blob(self, data_blob_str: str) -> bytes | None: | |
decrypted_chunks = [] | |
sanitized_blob = "".join(data_blob_str.split()) | |
for i in range(0, len(sanitized_blob), BASE64_CHUNK_SIZE): | |
cipher = AES.new(self.key, AES.MODE_CBC, self.iv) | |
b64_chunk = sanitized_blob[i : i + BASE64_CHUNK_SIZE] | |
if len(b64_chunk) != BASE64_CHUNK_SIZE: continue | |
encrypted_chunk_full = base64.b64decode(b64_chunk) | |
encrypted_chunk_for_decryption = encrypted_chunk_full[:ENCRYPTED_CHUNK_SIZE] | |
if len(encrypted_chunk_for_decryption) != ENCRYPTED_CHUNK_SIZE: return None | |
decrypted_padded_chunk = cipher.decrypt(encrypted_chunk_for_decryption) | |
decrypted_chunks.append(decrypted_padded_chunk[:PLAINTEXT_CHUNK_SIZE]) | |
return b"".join(decrypted_chunks).rstrip(b'\x00') | |
def _extract_archive(self, decrypted_data: bytes, output_dir: Path): | |
output_dir.mkdir(parents=True, exist_ok=True) | |
file_entries = re.split(rb"\n?\[\$\$\$", decrypted_data) | |
for entry in file_entries: | |
if not entry: continue | |
full_entry = b"[$$$" + entry | |
header_match = re.match(rb'\[\$\$\$([^\]]+)\]\s*\$\$\$DataLength=(\d+)\s*', full_entry, re.DOTALL) | |
if not header_match: continue | |
original_path_str = header_match.group(1).decode("utf-8", "ignore") | |
file_content = full_entry[header_match.end() : header_match.end() + int(header_match.group(2))] | |
target_path = output_dir / Path(original_path_str.lstrip('/\\')) | |
target_path.parent.mkdir(parents=True, exist_ok=True) | |
print(f" - Writing {target_path} ({len(file_content)} bytes)") | |
target_path.write_bytes(file_content) | |
def _encrypt_archive(self, plaintext_archive: bytes) -> str: | |
final_base64_blob = '' | |
for i in range(0, len(plaintext_archive), PLAINTEXT_CHUNK_SIZE): | |
cipher = AES.new(self.key, AES.MODE_CBC, self.iv) | |
plaintext_chunk = plaintext_archive[i : i + PLAINTEXT_CHUNK_SIZE] | |
padded_chunk = plaintext_chunk.ljust(ENCRYPTED_CHUNK_SIZE, b'\x00') | |
encrypted_chunk = cipher.encrypt(padded_chunk) | |
final_base64_blob += base64.b64encode(encrypted_chunk).decode('ascii') | |
return final_base64_blob | |
def _build_unencrypted_archive(self, input_dir: Path) -> bytes: | |
archive_parts = [] | |
for root, _, files in os.walk(input_dir): | |
for filename in files: | |
file_path = Path(root) / filename | |
relative_path = file_path.relative_to(input_dir) | |
archive_path = "/" + str(relative_path).replace("\\", "/") | |
file_content = file_path.read_bytes() | |
header = f"\n[$$${archive_path}]\n$$$DataLength={len(file_content)}$\n".encode() | |
archive_parts.append(header) | |
archive_parts.append(file_content) | |
return b"".join(archive_parts).lstrip() | |
def _build_final_file(self, encrypted_blob: str) -> bytes: | |
# For creating new files, we will use the same index and key that was validated. | |
# Default to the one from the sample file if no validation was run. | |
checksum_key_index = 1 | |
checksum_key = self.active_checksum_key or CHECKSUM_KEYS.get(checksum_key_index) | |
self._vprint(f"Using key '{checksum_key}' for new backup signature.") | |
version = 1 | |
headers_part = ( | |
f"$$$Version={version}$\n" | |
f"$$$CheckSumKeyIndex={checksum_key_index}$\n\n" | |
f"[$$$/tmp/conf.bak]\n" | |
f"$$$IsEncrypted=1$\n" | |
f"$$$DataLength={len(encrypted_blob)}$\n" | |
f"$$$Data=" | |
).encode('ascii') | |
content_before_hash = headers_part + encrypted_blob.encode('ascii') | |
data_to_hash = content_before_hash + f"\nKEY={checksum_key}".encode("ascii") | |
signature = hashlib.sha1(data_to_hash).hexdigest() | |
return content_before_hash + signature.encode('ascii') | |
def main(): | |
parser = argparse.ArgumentParser(description="A tool to decrypt and encrypt ASUS ASMBM11-iKVM BMC configuration backups.") | |
parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose/debug output.") | |
subparsers = parser.add_subparsers(dest="command", required=True) | |
parser_decrypt = subparsers.add_parser("decrypt") | |
parser_decrypt.add_argument("input_file", type=Path) | |
parser_decrypt.add_argument("output_dir", type=Path) | |
parser_encrypt = subparsers.add_parser("encrypt") | |
parser_encrypt.add_argument("input_dir", type=Path) | |
parser_encrypt.add_argument("output_file", type=Path) | |
args = parser.parse_args() | |
tool = BmcBackupTool(verbose=args.verbose) | |
if args.command == "decrypt": | |
tool.decrypt(args.input_file, args.output_dir) | |
elif args.command == "encrypt": | |
tool.encrypt(args.input_dir, args.output_file) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment