#!/usr/bin/env python3 """Chrome Widevine CDM RSA Struct Scanner v2 BoringSSL RSA struct (384 bytes) のレイアウトに基づき、 CDM プロセスのメモリからRSA秘密鍵を検出する。 struct rsa_st layout (Chrome ~2025, macOS arm64): +0x00: RSA_METHOD *meth +0x08: BIGNUM *n +0x10: BIGNUM *e +0x18: BIGNUM *d +0x20: BIGNUM *p +0x28: BIGNUM *q +0x30: BIGNUM *dmp1 +0x38: BIGNUM *dmq1 +0x40: BIGNUM *iqmp +0x48: CRYPTO_EX_DATA (8 bytes) +0x50: CRYPTO_refcount_t (4) + flags (4) +0x58: CRYPTO_MUTEX (200 bytes = pthread_rwlock_t) ... 使い方: sudo uv run python dump_rsa_struct.py """ import ctypes import ctypes.util import json import os import struct import subprocess import sys from datetime import datetime from pathlib import Path # ─── Mach VM API ─── libc = ctypes.CDLL(ctypes.util.find_library("c")) KERN_SUCCESS = 0 VM_PROT_READ = 0x01 libc.task_for_pid.restype = ctypes.c_int32 libc.task_for_pid.argtypes = [ ctypes.c_uint32, ctypes.c_int32, ctypes.POINTER(ctypes.c_uint32), ] libc.mach_task_self.restype = ctypes.c_uint32 libc.mach_task_self.argtypes = [] libc.mach_vm_read_overwrite.restype = ctypes.c_int32 libc.mach_vm_read_overwrite.argtypes = [ ctypes.c_uint32, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_uint64, ctypes.POINTER(ctypes.c_uint64), ] libc.mach_vm_region.restype = ctypes.c_int32 libc.mach_vm_region.argtypes = [ ctypes.c_uint32, ctypes.POINTER(ctypes.c_uint64), ctypes.POINTER(ctypes.c_uint64), ctypes.c_int32, ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32), ctypes.POINTER(ctypes.c_uint32), ] VM_REGION_BASIC_INFO_64 = 9 VM_REGION_BASIC_INFO_COUNT_64 = 9 class VMRegionBasicInfo64(ctypes.Structure): _fields_ = [ ("protection", ctypes.c_int32), ("max_protection", ctypes.c_int32), ("inheritance", ctypes.c_uint32), ("shared", ctypes.c_uint32), ("reserved", ctypes.c_uint32), ("offset", ctypes.c_uint64), ("behavior", ctypes.c_int32), ("user_wired_count", ctypes.c_uint16), ] def get_task_port(pid): task = ctypes.c_uint32(0) kr = libc.task_for_pid(libc.mach_task_self(), pid, ctypes.byref(task)) if kr != KERN_SUCCESS: print("[!] task_for_pid failed. Run with sudo.") sys.exit(1) return task.value def read_memory(task, address, size): buf = (ctypes.c_char * size)() out_size = ctypes.c_uint64(0) buf_ptr = ctypes.cast(buf, ctypes.c_void_p).value kr = libc.mach_vm_read_overwrite( task, ctypes.c_uint64(address), ctypes.c_uint64(size), ctypes.c_uint64(buf_ptr), ctypes.byref(out_size), ) if kr != KERN_SUCCESS: return None return bytes(buf[: out_size.value]) def enumerate_regions(task): regions = [] address = ctypes.c_uint64(0) while True: size = ctypes.c_uint64(0) info = VMRegionBasicInfo64() info_count = ctypes.c_uint32(VM_REGION_BASIC_INFO_COUNT_64) obj_name = ctypes.c_uint32(0) kr = libc.mach_vm_region( task, ctypes.byref(address), ctypes.byref(size), VM_REGION_BASIC_INFO_64, ctypes.byref(info), ctypes.byref(info_count), ctypes.byref(obj_name), ) if kr != KERN_SUCCESS: break if info.protection & VM_PROT_READ: regions.append((address.value, size.value)) address.value += size.value return regions # ─── BIGNUM reader ─── def read_ptr_at(data, offset): if offset + 8 > len(data): return None return struct.unpack_from(" 128: return None, 0 words_data = read_memory(task, d_ptr, width * 8) if not words_data or len(words_data) < width * 8: return None, 0 value = 0 for i in range(width): word = struct.unpack_from(" 0: read_size = min(remaining, chunk_size) data = read_memory(task, addr, read_size) if data is None: addr += read_size remaining -= read_size continue # BIGNUM を探す: { BN_ULONG *d; int width; int dmax; int neg; int flags } # width=1, value=3 or 65537 のパターン # d -> [3] or d -> [65537], width=1 for off in range(0, len(data) - 24, 8): d_ptr = read_ptr_at(data, off) if not d_ptr or not is_plausible_ptr(d_ptr): continue width = ( struct.unpack_from(" 64: continue # d_ptr が指す値を読む val_data = read_memory(task, d_ptr, 8) if val_data is None: continue val = struct.unpack_from("= 2: try: return int(parts[1]) except ValueError: continue return None def main(): print("=" * 60) print("[*] Chrome Widevine CDM RSA Struct Scanner v2") print("[*] %s" % datetime.now().isoformat()) print("[*] Strategy: Find BIGNUM(e=3|65537) → trace back to RSA struct") print("=" * 60) print() if os.geteuid() != 0: print("[!] Run with: sudo uv run python dump_rsa_struct.py") sys.exit(1) pid = find_cdm_pid() if not pid: print("[!] CDM process not found. Play DRM content first.") sys.exit(1) print("[+] CDM process: PID %d" % pid) task = get_task_port(pid) print("[+] Task port: %d" % task) regions = enumerate_regions(task) print("[*] %d readable regions" % len(regions)) output_dir = Path("logs") / ( "rsastruct_%s" % datetime.now().strftime("%Y%m%d_%H%M%S") ) output_dir.mkdir(parents=True, exist_ok=True) results = scan_for_rsa(task, regions, output_dir) if not results: print() print("[-] No RSA structs found in CDM process heap.") sys.exit(0) saved = 0 for i, r in enumerate(results): if not (r["d"] and r["p"] and r["q"]): print("[%d] Incomplete (missing d/p/q)" % i) continue if not r["verified"]: print("[%d] n != p*q — false positive" % i) continue try: der = build_der(r["n"], r["e"], r["d"], r["p"], r["q"]) path = output_dir / ("private_key_%d.der" % i) path.write_bytes(der) print( "[+] Key #%d: %s (%d bytes, RSA-%d e=%d)" % (i, path, len(der), r["n_bits"], r["e"]) ) saved += 1 jpath = output_dir / ("rsa_struct_%d.json" % i) jpath.write_text( json.dumps( { "struct_addr": hex(r["struct_addr"]), "e_offset": hex(r["e_offset"]), "n_bits": r["n_bits"], "e": r["e"], "n": hex(r["n"]), "d": hex(r["d"]), "p": hex(r["p"]), "q": hex(r["q"]), }, indent=2, ) ) except Exception as ex: print("[%d] DER build failed: %s" % (i, ex)) print() if saved: print("=" * 60) print("[+] %d key(s) extracted to %s" % (saved, output_dir)) print("=" * 60) print( "[*] Verify: uv run python verify_key.py %s/private_key_0.der" % output_dir ) else: print("[-] No complete verified keys.") if __name__ == "__main__": main()