From 2899fc93a2d9ab968ee076412951277e455b9213 Mon Sep 17 00:00:00 2001 From: Diazole Date: Sun, 30 Oct 2022 02:23:28 +0000 Subject: [PATCH] Allow the CDM version and fn to be passed via args --- Helpers/Device.py | 20 ++++++++++---------- Helpers/script.js | 48 ++++++++++++++++++++++++++++++++++++++++------- README.md | 29 +++++++++++++++++++++++++--- dump_keys.py | 12 ++++++++++-- 4 files changed, 87 insertions(+), 22 deletions(-) diff --git a/Helpers/Device.py b/Helpers/Device.py index 725e238..efd8d1e 100644 --- a/Helpers/Device.py +++ b/Helpers/Device.py @@ -7,20 +7,18 @@ from Helpers.wv_proto2_pb2 import SignedLicenseRequest class Device: - def __init__(self): + def __init__(self, dynamic_function_name, cdm_version): self.logger = logging.getLogger(__name__) self.saved_keys = {} - self.frida_script = open( - './Helpers/script.js', - 'r', - encoding="utf_8" - ).read() - self.widevine_libraries = [ - 'libwvhidl.so' - ] + self.widevine_libraries = ['libwvhidl.so'] self.usb_device = frida.get_usb_device() self.name = self.usb_device.name + with open('./Helpers/script.js', 'r', encoding="utf_8") as script: + self.frida_script = script.read() + self.frida_script = self.frida_script.replace(r'${DYNAMIC_FUNCTION_NAME}', dynamic_function_name) + self.frida_script = self.frida_script.replace(r'${CDM_VERSION}', cdm_version) + def export_key(self, key, client_id): save_dir = os.path.join( 'key_dumps', @@ -65,7 +63,9 @@ class Device: public_key = root.Msg.ClientId.Token._DeviceCertificate.PublicKey key = RSA.importKey(public_key) cur = self.saved_keys.get(key.n) - self.export_key(cur, root.Msg.ClientId) + + if cur is not None: + self.export_key(cur, root.Msg.ClientId) def find_widevine_process(self, process_name): process = self.usb_device.attach(process_name) diff --git a/Helpers/script.js b/Helpers/script.js index 44e6a33..ca562f0 100644 --- a/Helpers/script.js +++ b/Helpers/script.js @@ -1,4 +1,16 @@ -const CDM_VERSION = '' +const DYNAMIC_FUNCTION_NAME = '${DYNAMIC_FUNCTION_NAME}'; +const CDM_VERSION = '${CDM_VERSION}'; + +// These strings are function names that have been succesfully dumped. +const KNOWN_DYNAMIC_FUNCTION_NAMES = [ + 'rnmsglvj', + 'polorucp', + 'kqzqahjq', + 'pldrclfq', + 'kgaitijd', + 'dnvffnze', + 'cwkfcplc' +]; // The TextEncoder/Decoder API isn't supported so it has to be polyfilled. // Taken from https://gist.github.com/Yaffle/5458286#file-textencodertextdecoder-js @@ -85,6 +97,8 @@ function prepareKeyRequest(address) { this.ret = args[5]; break; default: + const message = 'Defaulting to args[4] for PrepareKeyRequest.' + send('message_info', new TextEncoder().encode(message)); this.ret = args[4]; break; } @@ -102,22 +116,29 @@ function prepareKeyRequest(address) { function hookLibFunctions(lib) { const name = lib['name']; const baseAddr = lib['base']; - const message = 'Hooking ' + name + ' at ' + baseAddr; + let message = 'Hooking ' + name + ' at ' + baseAddr; + let hookedProvidedModule = false; + let funcNames = []; - send('message_info', new TextEncoder().encode(message)) + send('message_info', new TextEncoder().encode(message)); Module.enumerateExportsSync(name).forEach(function (module) { try { let hookedModule; if (module.name.includes('UsePrivacyMode')) { disablePrivacyMode(module.address); - hookedModule = module.name + hookedModule = module.name; } else if (module.name.includes('PrepareKeyRequest')) { prepareKeyRequest(module.address); - hookedModule = module.name - } else if (module.name.match(/^[a-z]+$/)) { + hookedModule = module.name; + } else if (DYNAMIC_FUNCTION_NAME !== '' && module.name.includes(DYNAMIC_FUNCTION_NAME)) { getPrivateKey(module.address); - hookedModule = module.name + hookedModule = module.name; + hookedProvidedModule = true; + } else if (DYNAMIC_FUNCTION_NAME === '' && module.name.match(/^[a-z]+$/)) { + getPrivateKey(module.address); + hookedModule = module.name; + funcNames.push(hookedModule); } if (hookedModule) { @@ -128,6 +149,19 @@ function hookLibFunctions(lib) { console.log("Error: " + e + " at F: " + module.name); } }); + + if (DYNAMIC_FUNCTION_NAME !== '' && !hookedProvidedModule) { + const message = "Unable to find '" + DYNAMIC_FUNCTION_NAME + "'"; + send('message_info', new TextEncoder().encode(message)); + } + + if (DYNAMIC_FUNCTION_NAME === '') { + const possibleFuncNames = KNOWN_DYNAMIC_FUNCTION_NAMES.filter(x => funcNames.includes(x)); + if (possibleFuncNames.length) { + message = "Your function name is most likely: " + "'" + possibleFuncNames.join('\', \'') + "'"; + send('message_info', new TextEncoder().encode(message)); + } + } } function getModuleByName(lib) { diff --git a/README.md b/README.md index 34f5852..c3d7bfd 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,42 @@ The function parameters can differ between CDM versions. The default is [4] but * `CDM_VERSION` can be retrieved using a DRM Info app. -## Requirements +## Requirements: Use pip to install the dependencies: `pip3 install -r requirements.txt` -## Usage +## Usage: * Enable USB debugging * Start frida-server on the device * Execute dump_keys.py * Start streaming some DRM-protected content -## Known Working Versions +The script will hook every function in your 'libwvhidl.so' module by default, effectively brute forcing the private key function name. +``` +python3 .\dump_keys.py [OPTIONS] +``` + +You can pass the function name to hook using the `--function-name` argument. +``` +python3 .\dump_keys.py --function-name 'polorucp' +``` + +The script defaults to `args[4]` if no `--cdm-version` is provided. This will only have an effect if your version is `16.1.0`. + +``` +python3 .\dump_keys.py --cdm-version '16.1.0' +``` + +## Options: +``` + -h, --help Print this help text and exit. + --cdm-version The CDM version of the device e.g. '16.1.0'. + --function-name The name of the function to hook to retrieve the private key. +``` + +## Known Working Versions: * Android 9 * CDM 14.0.0 * Android 10 diff --git a/dump_keys.py b/dump_keys.py index e0cbdc3..9ca9e54 100644 --- a/dump_keys.py +++ b/dump_keys.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import argparse import time import logging from Helpers.Device import Device @@ -10,10 +11,17 @@ logging.basicConfig( level=logging.DEBUG, ) - def main(): + parser = argparse.ArgumentParser(description='Android Widevine L3 dumper.') + parser.add_argument('--cdm-version', help='The CDM version of the device e.g. \'14.0.0\'', default='14.0.0') + parser.add_argument('--function-name', help='The name of the function to hook to retrieve the private key.', default='') + args = parser.parse_args() + + dynamic_function_name = args.function_name + cdm_version = args.cdm_version + logger = logging.getLogger("main") - device = Device() + device = Device(dynamic_function_name, cdm_version) logger.info('Connected to %s', device.name) logger.info('Scanning all processes')