Skip to content

Instantly share code, notes, and snippets.

@D4stiny
Created June 12, 2025 02:59
Show Gist options
  • Save D4stiny/412157ca10d0201105e41b340af389ec to your computer and use it in GitHub Desktop.
Save D4stiny/412157ca10d0201105e41b340af389ec to your computer and use it in GitHub Desktop.
Tool to decrypt and encrypt Megarac-based configuration backups.
"""
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