From 6f2a34ad0102c027184746055585bf75f2eb9e3c Mon Sep 17 00:00:00 2001 From: Diazole Date: Tue, 4 Oct 2022 23:23:53 +0100 Subject: [PATCH] Update script to support Android 10, 11, 12 --- Helpers/Device.py | 83 ++++ Helpers/Keybox.py | 90 ---- Helpers/Scanner.py | 187 -------- Helpers/script.js | 1014 +++++--------------------------------------- README.md | 20 +- dump_keys.py | 35 +- requirements.txt | 2 +- 7 files changed, 233 insertions(+), 1198 deletions(-) create mode 100644 Helpers/Device.py delete mode 100644 Helpers/Keybox.py delete mode 100644 Helpers/Scanner.py diff --git a/Helpers/Device.py b/Helpers/Device.py new file mode 100644 index 0000000..868fad5 --- /dev/null +++ b/Helpers/Device.py @@ -0,0 +1,83 @@ +import os +import logging +import base64 +import frida +from Crypto.PublicKey import RSA +from Helpers.wv_proto2_pb2 import SignedLicenseRequest + + +class Device: + def __init__(self): + 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.usb_device = frida.get_usb_device() + self.name = self.usb_device.name + + def export_key(self, key, client_id): + system_id = client_id.Token._DeviceCertificate.SystemId + save_dir = os.path.join( + 'key_dumps', + f'{self.name}/private_keys/{system_id}/{str(key.n)[:10]}' + ) + + if not os.path.exists(save_dir): + os.makedirs(save_dir) + + with open(os.path.join(save_dir, 'client_id.bin'), 'wb+') as writer: + writer.write(client_id.SerializeToString()) + + with open(os.path.join(save_dir, 'private_key.pem'), 'wb+') as writer: + writer.write(key.exportKey('PEM')) + self.logger.info('Key pairs saved at %s', save_dir) + + def on_message(self, msg, data): + if msg['payload'] == 'private_key': + key = RSA.import_key(data) + if key.n not in self.saved_keys: + encoded_key = base64.b64encode(data).decode('utf-8') + self.logger.debug('Retrieved key: %s', encoded_key) + self.saved_keys[key.n] = key + elif msg['payload'] == 'device_info': + self.license_request_message(data) + elif msg['payload'] == 'message_info': + self.logger.info(data.decode()) + + def license_request_message(self, data): + root = SignedLicenseRequest() + root.ParseFromString(data) + public_key = root.Msg.ClientId.Token._DeviceCertificate.PublicKey + self.logger.debug( + 'Retrieved key: %s', + base64.b64encode(public_key).decode('utf-8') + ) + key = RSA.importKey(public_key) + cur = self.saved_keys.get(key.n) + self.export_key(cur, root.Msg.ClientId) + + def find_widevine_process(self, process_name): + process = self.usb_device.attach(process_name) + script = process.create_script(self.frida_script) + script.load() + loaded_modules = [] + try: + for lib in self.widevine_libraries: + loaded_modules.append(script.exports.getmodulebyname(lib)) + finally: + process.detach() + return loaded_modules + + def hook_to_process(self, process, library): + session = self.usb_device.attach(process) + script = session.create_script(self.frida_script) + script.on('message', self.on_message) + script.load() + script.exports.hooklibfunctions(library) + return session diff --git a/Helpers/Keybox.py b/Helpers/Keybox.py deleted file mode 100644 index 7a58dbd..0000000 --- a/Helpers/Keybox.py +++ /dev/null @@ -1,90 +0,0 @@ -import io -import json -import struct -from base64 import b64decode, b64encode -from binascii import hexlify - - -def create_table(): - a = [] - for i in range(256): - k = i << 24 - for _ in range(8): - k = (k << 1) ^ 0x4c11db7 if k & 0x80000000 else k << 1 - a.append(k & 0xffffffff) - return a - - -def crc32_mpeg(data, length): - crc_val = 0xFFFFFFFF - crctab = create_table() - for i in range(length): - crc_val = (crctab[(data[i] & 0xFF) ^ (crc_val >> 24)] ^ (crc_val << 8)) & 0xFFFFFFFF - return crc_val - - -class Keybox: - def __init__(self, keybox_data: any): - if isinstance(keybox_data, str): - self.__keybox = b64decode(keybox_data) - elif isinstance(keybox_data, io.BufferedReader): - self.__keybox = keybox_data.read() - elif isinstance(keybox_data, dict): - self.__keybox = self.__generate_crc(keybox_data) - else: - print(type(keybox_data)) - raise ValueError('unable to read the file/string, etc') - - self.__parse() - - @staticmethod - def __generate_crc(keybox) -> bytes: - device_id = keybox['device_id'] - device_token = keybox['device_token'] - device_key = keybox['device_key'] - key_box = bytes.fromhex(device_id) + bytes.fromhex(device_key) + bytes.fromhex(device_token) + b'kbox' - crc = crc32_mpeg(key_box, len(key_box)) - key_box += struct.pack('>I', crc) - key_box += keybox['security_level'].encode() - return key_box - - def __parse(self): - self.device_id = self.__keybox[0:32] - # this is the aes key - self.device_key = self.__keybox[32:48] - self.device_token = self.__keybox[48:120] - self.keybox_tag = self.__keybox[120:124] - self.crc32 = struct.unpack('>I', self.__keybox[124:128])[0] - self.crc32_raw = hexlify(self.__keybox[124:128]) - # this is optional, most likely not required - self.level_tag = self.__keybox[128:132] - self.flags = struct.unpack(">L", self.__keybox[48:120][0:4])[0] - self.version = struct.unpack(">I", self.__keybox[48:52])[0] - self.system_id = struct.unpack(">I", self.__keybox[52:56])[0] - # or unique_id as in wv pdf, encrypted by pre-provisioning key - self.provisioning_id = self.__keybox[56:72] - # encrypted with unique id, contains device key, device key hash, and flags - self.encrypted_bits = self.__keybox[72:120] - - def __repr__(self): - return json.dumps({ - 'device_id': b64encode(self.device_id).decode(), - 'device_id_size': len(self.device_id), - 'device_key': b64encode(self.device_key).decode(), - 'device_token': b64encode(self.device_token).decode(), - 'device_token_size': len(self.device_token), - 'kbox_tag': self.keybox_tag.decode(), - 'crc32': self.crc32, - 'crc32_raw': self.crc32_raw.decode(), - 'lvl1_tag': self.level_tag.decode(), - 'flags': self.flags, - 'released': True if self.flags & 2 == 2 else False, - 'version': self.version, - 'system_id': self.system_id, - 'provisioning_id': b64encode(self.provisioning_id).decode(), - 'encrypted_bits': b64encode(self.encrypted_bits).decode(), - 'keybox': b64encode(self.__keybox).decode() - }, indent=4) - - def get_keybox(self): - return self.__keybox diff --git a/Helpers/Scanner.py b/Helpers/Scanner.py deleted file mode 100644 index 4211ac6..0000000 --- a/Helpers/Scanner.py +++ /dev/null @@ -1,187 +0,0 @@ -import os -import json -from Crypto.PublicKey import RSA -from google.protobuf import message -import logging -from Helpers.Keybox import Keybox -from Helpers.wv_proto2_pb2 import SignedLicenseRequest - - -class Scan: - def __init__(self, device_name): - self.logger = logging.getLogger(__name__) - self.KEY_DUMP_LOC = 'keydump/' - self.device_name = device_name - self.saved_keys = {} - self.frida_script = open('Helpers/script.js', 'r').read() - self.device = { - 'device_id': None, - 'device_token': None, - 'device_key': os.urandom(16).hex(), - 'security_level': '' - } - self.widevine_libraries = [ - 'libwvhidl.so', - 'libwvdrmengine.so', - 'liboemcrypto.so', - 'libmediadrm.so', - 'libwvdrm_L1.so', - 'libWVStreamControlAPI_L1.so', - 'libdrmwvmplugin.so', - 'libwvm.so' - ] - - def export_key(self, k): - root = SignedLicenseRequest() - root.ParseFromString(k['id']) - cid = root.Msg.ClientId - system_id = cid.Token._DeviceCertificate.SystemId - save_dir = os.path.join('key_dumps', f'{self.device_name}/private_keys/{system_id}/{str(k["key"].n)[:10]}') - - if not os.path.exists(save_dir): - os.makedirs(save_dir) - - with open(os.path.join(save_dir, 'client_id.bin'), 'wb+') as writer: - writer.write(cid.SerializeToString()) - - with open(os.path.join(save_dir, 'private_key.pem'), 'wb+') as writer: - writer.write(k['key'].exportKey('PEM')) - self.logger.info('Key pairs saved at ' + save_dir) - - def on_message(self, msg, data): - try: - if msg['payload'] == 'priv': - self.logger.debug('processing private key') - self.private_key_message(msg, data) - elif msg['payload'] == 'id': - self.logger.debug('processing id') - self.license_request_message(data) - elif msg['payload'] == 'device_id': - self.logger.debug('processing device id') - self.device_id_message(data) - elif msg['payload'] == 'device_token': - self.logger.debug('processing device token') - self.device_token_message(data) - elif msg['payload'] == 'security_level': - tag = data.decode() - if tag == 'L1': - self.device['security_level'] = 'LVL1' - else: - self.device['security_level'] = 'LVL3' - elif msg['payload'] == 'aes_key': - self.aes_key_message(data) - elif msg['payload'] == 'message': - payload = json.loads(data.decode()) - self.logger.debug( - json.dumps( - payload, - indent=4 - ) - ) - elif msg['payload'] == 'message_info': - self.logger.info(data.decode()) - - except: - self.logger.error('unable to process the message') - self.logger.error(msg) - self.logger.error(data) - - def private_key_message(self, private_key_message, data): - try: - try: - key = RSA.importKey(data) - cur = self.saved_keys.get(key.n, {}) - if 'id' in cur: - if 'key' not in cur: - cur['key'] = key - self.saved_keys[key.n] = cur - self.export_key(cur) - else: - self.saved_keys[key.n] = {'key': key} - except: - self.logger.error('unable to load private key') - self.logger.error(data) - pass - except: - self.logger.error('payload of type priv failed') - self.logger.error(private_key_message) - - def license_request_message(self, data): - with open('license_request.bin', 'wb+') as f: - f.write(data) - root = SignedLicenseRequest() - try: - root.ParseFromString(data) - except message.DecodeError: - return - try: - key = RSA.importKey(root.Msg.ClientId.Token._DeviceCertificate.PublicKey) - cur = self.saved_keys.get(key.n, {}) - if 'key' in cur: - if 'id' not in cur: - cur['id'] = data - self.saved_keys[key.n] = cur - self.export_key(cur) - else: - self.saved_keys[key.n] = {'id': data} - except Exception as error: - self.logger.error(error) - - def device_id_message(self, data_buffer): - if not self.device['device_id']: - self.device['device_id'] = data_buffer.hex() - if self.device['device_id'] and self.device['device_token'] and self.device['device_key']: - self.save_key_box() - - def device_token_message(self, data_buffer): - if not self.device['device_token']: - self.device['device_token'] = data_buffer.hex() - if self.device['device_id'] and self.device['device_token']: - self.save_key_box() - - def aes_key_message(self, data_buffer): - if not self.device['device_key']: - self.device['device_key'] = data_buffer.hex() - if self.device['device_id'] and self.device['device_token']: - self.save_key_box() - - def find_widevine_process(self, dev, process_name): - process = dev.attach(process_name) - script = process.create_script(self.frida_script) - script.load() - loaded = [] - try: - for lib in self.widevine_libraries: - try: - loaded.append(script.exports.widevinelibrary(lib)) - except: - pass - finally: - process.detach() - return loaded - - def hook_to_process(self, device, process, library): - session = device.attach(process) - script = session.create_script(self.frida_script) - script.on('message', self.on_message) - script.load() - script.exports.inject(library, process) - return session - - def save_key_box(self): - try: - if self.device['device_id'] is not None and self.device['device_token'] is not None: - self.logger.info('saving key box') - keybox = Keybox(self.device) - box = os.path.join('key_dumps', f'{self.device_name}/key_boxes/{keybox.system_id}') - self.logger.debug(f'saving to {box}') - if not os.path.exists(box): - os.makedirs(box) - with open(os.path.join(box, f'{keybox.system_id}.bin'), 'wb') as writer: - writer.write(keybox.get_keybox()) - with open(os.path.join(box, f'{keybox.system_id}.json'), 'w') as writer: - writer.write(keybox.__repr__()) - self.logger.info(f'saved keybox to {box}') - except Exception as error: - self.logger.error('unable to save keybox') - self.logger.error(error) \ No newline at end of file diff --git a/Helpers/script.js b/Helpers/script.js index 24ff28b..5f1d03b 100644 --- a/Helpers/script.js +++ b/Helpers/script.js @@ -1,940 +1,154 @@ -const KNOWN_DYNAMIC_FUNC = ['ulns', 'cwkfcplc', 'dnvffnze', 'kgaitijd', 'polorucp']; +const DYNAMIC_FUNCTION_NAME = 'CHANGE_ME' +const CDM_VERSION = 'CHANGE_ME' +// The TextEncoder/Decoder API isn't supported so it has to be polyfilled. +// Taken from https://gist.github.com/Yaffle/5458286#file-textencodertextdecoder-js function TextEncoder() { } TextEncoder.prototype.encode = function (string) { - var octets = []; - var length = string.length; - var i = 0; - while (i < length) { - var codePoint = string.codePointAt(i); - var c = 0; - var bits = 0; - if (codePoint <= 0x0000007F) { - c = 0; - bits = 0x00; - } else if (codePoint <= 0x000007FF) { - c = 6; - bits = 0xC0; - } else if (codePoint <= 0x0000FFFF) { - c = 12; - bits = 0xE0; - } else if (codePoint <= 0x001FFFFF) { - c = 18; - bits = 0xF0; + var octets = []; + var length = string.length; + var i = 0; + while (i < length) { + var codePoint = string.codePointAt(i); + var c = 0; + var bits = 0; + if (codePoint <= 0x0000007F) { + c = 0; + bits = 0x00; + } else if (codePoint <= 0x000007FF) { + c = 6; + bits = 0xC0; + } else if (codePoint <= 0x0000FFFF) { + c = 12; + bits = 0xE0; + } else if (codePoint <= 0x001FFFFF) { + c = 18; + bits = 0xF0; + } + octets.push(bits | (codePoint >> c)); + c -= 6; + while (c >= 0) { + octets.push(0x80 | ((codePoint >> c) & 0x3F)); + c -= 6; + } + i += codePoint >= 0x10000 ? 2 : 1; } - octets.push(bits | (codePoint >> c)); - c -= 6; - while (c >= 0) { - octets.push(0x80 | ((codePoint >> c) & 0x3F)); - c -= 6; - } - i += codePoint >= 0x10000 ? 2 : 1; - } - return octets; + return octets; } -function TextDecoder() { -} - -TextDecoder.prototype.decode = function (octets) { - var string = ""; - var i = 0; - while (i < octets.length) { - var octet = octets[i]; - var bytesNeeded = 0; - var codePoint = 0; - if (octet <= 0x7F) { - bytesNeeded = 0; - codePoint = octet & 0xFF; - } else if (octet <= 0xDF) { - bytesNeeded = 1; - codePoint = octet & 0x1F; - } else if (octet <= 0xEF) { - bytesNeeded = 2; - codePoint = octet & 0x0F; - } else if (octet <= 0xF4) { - bytesNeeded = 3; - codePoint = octet & 0x07; - } - if (octets.length - i - bytesNeeded > 0) { - var k = 0; - while (k < bytesNeeded) { - octet = octets[i + k + 1]; - codePoint = (codePoint << 6) | (octet & 0x3F); - k += 1; - } - } else { - codePoint = 0xFFFD; - bytesNeeded = octets.length - i; - } - string += String.fromCodePoint(codePoint); - i += bytesNeeded + 1; - } - return string -} - -function containsLib(library){ - return Process.getModuleByName(library); -} - -function containsFunction(name, address) { - var result = false; - for (var i = 0; i < KNOWN_DYNAMIC_FUNC.length; i++) { - result = KNOWN_DYNAMIC_FUNC[i] === name; - if (result) { - sender_payload({ - from: 'Dynamic Function', - message: 'L3 RSA Key export function found: ' + name - }); - return result; - } - } - - return result; - -} - - -function inject(lib, process_name){ - // printer('Running ' + lib['name'] + ' at ' + lib['base'], 'Hook'); - sender_payload_info( - 'Running ' + lib['name'] + ' at ' + lib['base'] - ); - Hooker(lib, process_name) -} - -function Hooker(lib, process_name) { - const name = lib['name']; - Module.enumerateExportsSync(name).forEach(function(exp){ - try { - var module_address = exp.address; - if (exp.name === '_lcc00' || exp.name === '_oecc00') { - GetLevel3_IsInApp(module_address, process_name); - } else if (exp.name === '_lcc01' || exp.name === '_oecc01') { - GetLevel3_Initialize(module_address, process_name); - } else if (exp.name === '_lcc49' || exp.name === '_oecc49') { - GetLevel3_GetProvisioningMethod(module_address, process_name); - } else if (exp.name === '_lcc38' || exp.name === '_oecc38') { - GetLevel3_GetNumberOfOpenSessions(module_address, process_name); - } else if (exp.name === '_lcc37' || exp.name === '_oecc37') { - GetLevel3_GetMaxNumberOfSessions(module_address, process_name); - } else if (exp.name === '_lcc22' || exp.name === '_oecc22') { - GetApiVersion(module_address, process_name); - } else if (exp.name === '_lcc46' || exp.name === '_oecc46') { - GetSecurityPatchLevel(module_address, process_name); - } else if (exp.name === '_lcc23' || exp.name === '_oecc23') { - GetSecurityLevel(module_address, process_name); - } else if (exp.name === '_lcc90' || exp.name === '_oecc90') { - GetLevel3_BuildInformation(module_address, process_name); - } else if (exp.name === '_lcc52' || exp.name === '_oecc52') { - GetSupportedCertificates(module_address, process_name); - } else if (exp.name === '_lcc02' || exp.name === '_oecc02') { - GetLevel3_Terminate_Status(module_address, process_name); - } else if (exp.name === '_lcc07' || exp.name === '_oecc07') { - GetLevel3_GetDeviceID(module_address, process_name); - } else if (exp.name === '_lcc04' || exp.name === '_oecc04') { - GetLevel3_GetKeyData(module_address, process_name) - } else if (exp.name === 'OEMCrypto_LoadKeys_Back_Compat') { - GetLevel3_LoadKeys(module_address, process_name); - } else if (exp.name === '_lcc12' || exp.name === '_oecc12') { - GetLevel3_GenerateDerivedKeys(module_address, process_name); - } else if (exp.name === '_lcc13' || exp.name === '_oecc13') { - GetLevel3_GenerateSignature(module_address, process_name); - } else if (exp.name === '_lcc50' || exp.name === '_oecc50') { - GetLevel3_GetOEMPublicCertificate(module_address, process_name); - } else if (exp.name === '_lcc19' || exp.name === '_oecc19') { - GetLevel3_LoadDeviceRSAKey(module_address, process_name) - } else if (exp.name === '_lcc18' || exp.name === '_oecc18') { - GetLevel3_RewrapDeviceRSAKey(module_address, process_name); - } else if (exp.name === 'AES_unwrap_key') { - AES_unwrap_key(module_address, process_name) - } else if (containsFunction(exp.name, exp.address)) { - polorucp(module_address, process_name); - } else if (exp.name.includes('UsePrivacyMode')) { - UsePrivacyMode(module_address, process_name); - } else if (exp.name === 'CdmInfo') { - CdmInfo(module_address, process_name); - } else if (exp.name.includes('PrepareKeyRequest')) { - PrepareKeyRequest(module_address, process_name); - } else if (exp.name.includes("_ZN14video_widevine25SignedProvisioningMessageC2Ev")) { - SignedProvisioningMessage(module_address, process_name) - } else if (exp.name === 'AES_set_encrypt_key') { - AES_set_encrypt_key(module_address, process_name) - } else if (exp.name.includes('jnyxqs')) { - // this needs to be changed to an array of methods since they all differ between oemcryptos for l1 and l3 - jnyxqs(module_address) - } else if (exp.name === 'fwemrknr') { - fwemrknr(module_address, process_name) - } else if (exp.name === 'pbntpypb') { - pbntpypb(module_address, process_name) - } - } catch (e) { - console.log("Error: " + e + " at F: " + exp.name); - } -}); -} - -function pbntpypb(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function(args) { - this.data = { - '1': args[0] - } - }, - onLeave: function(returnResult) { - console.log(hexdump(returnResult)); - // console.log('onleave') - // console.log('first parameter'); - // const data = Memory.readPointer(this.data['1']); - // const param1 = hexdump(data); - // console.log(param1); - // console.log('ended') - } - }); -} - -function fwemrknr(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function(args) { - this.data = { - '0': args[0], - '1': args[1], - '2': args[2], - '3': args[3], - '4': args[4], - '5': args[5], - '6': args[6], - '7': args[7], - '8': args[8], - '9': args[9], - '10': args[10], - '11': args[11], - '12': args[12], - '13': args[13], - '14': args[14], - '15': args[15], - '16': args[16] - } - }, - onLeave: function(returnResult) { - // console.log('onleave') - // console.log('first parameter'); - // const data = Memory.readPointer(this.data['1']); - // const param1 = hexdump(data); - // console.log(param1); - // console.log('ended') - } - }); -} - -function jnyxqs(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function(args) { - this.data = { - '1': args[0], - '2': args[1] - } - }, - onLeave: function(returnResult) { - printer('jnyxqs', process_name); - console.log(hexdump(returnResult)); - console.log(Memory.readByteArray(this.data['1'], this.data['2'].toInt32())); - console.log(this.data['2'].toInt32()); - send('aes_key', Memory.readByteArray(this.data['1'], this.data['2'].toInt32())) - } - }); -} - -function ithomqf(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function(args) { - this.data = { - '1': args[0], - '2': args[1] - } - }, - onLeave: function(returnResult) { - printer('ithomqf', process_name); - console.log(hexdump(returnResult)); - } - }); -} - -function AES_set_encrypt_key(address, process_name) { +function getPrivateKey(address) { Interceptor.attach(ptr(address), { onEnter: function (args) { - // both of these are pointers - this.data = { - "userKey": args[0], - "bits": args[1], - 'key': args[2] - - } - }, - onLeave: function (returnResult) { - const size = this.data['bits'].toInt32() / 8; - const userKey = Memory.readByteArray(this.data['userKey'], size); - const key = Memory.readByteArray(this.data['key'], size); - printer('return result: ' + returnResult); - const data = { - from: process_name, - message: 'AES_set_encrypt_key', - payload: { - 'size': size, - 'user_key': byteArrayToHex(userKey), - 'key': byteArrayToHex(key) - + if (!args[6].isNull()) { + const size = args[6].toInt32(); + if (size >= 1000 && size <= 2000 && !args[5].isNull()) { + const buf = args[5].readByteArray(size); + const bytes = new Uint8Array(buf); + // The first two bytes of the DER encoding are 0x30 and 0x82 (MII). + if (bytes[0] === 0x30 && bytes[1] === 0x82) { + const binaryString = a2bs(bytes) + const keyLength = getKeyLength(binaryString); + const key = bytes.slice(0, keyLength); + send('private_key', key); + } } } - sender_payload(data) } }); } - -function SignedProvisioningMessage(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.data = args[0] - }, - onLeave: function () { - printer('SignedProvisioningMessage', process_name); - console.log(this.data); - console.log(hexdump(this.data)); - console.log(hexdump(Memory.readPointer(this.data))); - console.log(hexdump(Memory.readPointer(Memory.readPointer(this.data)))); - console.log(Memory.readByteArray(Memory.readPointer(Memory.readPointer(this.data)), 2000)); - - } - }); -} - -function readStdString(str) { - const size = str.add(Process.pointerSize).readUInt(); - return str.add(Process.pointerSize * 2).readPointer().readByteArray(size); -} - -function printer(message, origination){ - console.log('['+origination+']:[INFO]:', message) -} - -function CdmInfo(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function(args) { - console.log('CdmInfo'); - console.log(JSON.stringify(args)) - } - }); -} - -function polorucp(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function(args) { - if (!args[6].isNull()) { - const size = args[6].toInt32(); - if (size >= 1000 && size <= 2000 && !args[5].isNull()) { - const k = args[5].readByteArray(size); - const view = new Uint8Array(k); - if (view[0] === 0x30 && view[1] === 0x82) { - const data = { - from: process_name, - data: 'Captured Private Key' - }; - sender_payload(data); - send('priv', k); - } - } - } - } - }); -} - -function PrepareKeyRequest(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.ret = args[4]; - }, - onLeave: function () { - if (this.ret) { - const message = readStdString(this.ret); - const data = { - from: process_name, - message: 'PrepareKeyRequest, Captured License Request' - }; - sender_payload(data); - send('id', message); - } - } - }); -} - -function UsePrivacyMode(address, process_name) { +// nop privacy mode. +// PrivacyMode encrypts the payload with the public key returned by the license server which we don't want. +function disablePrivacyMode(address) { Interceptor.attach(address, { onLeave: function (retval) { - const data = { - from: process_name, - message: 'Replacing PrivacyMode' - }; - sender_payload(data); retval.replace(ptr(0)); } }); } -function AES_unwrap_key(address, process_name) { +function prepareKeyRequest(address) { Interceptor.attach(ptr(address), { onEnter: function (args) { - console.log('entering aes unwrap key') - } - }) -} - -function GetLevel3_Initialize(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function(args) { - sender_payload( - { - from: process_name, - message: 'OEMCrypto_Initialize' - } - ) - } - }) -} - -function GetApiVersion(address, process_name) { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - // const message = 'OEMCryptoVersion: ' + retval.toInt32(); - const data = { - from: process_name, - message: 'OEMCryptoVersion', - payload: { - 'Version': retval.toInt32() - } - }; - sender_payload(data) - } - }); -} - -function GetSecurityPatchLevel(address, process_name) { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - sender_payload({ - from: process_name, - message: 'OEMSecurityPatchLevel', - payload:{ - 'Patch_Level': retval.toInt32() - } - }) - } - }); -} - -function GetSecurityLevel(address, process_name) { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - const level = Memory.readUtf8String(retval); - sender_payload({ - from: process_name, - message: 'OEMSecurityLevel', - payload: { - 'Level': level - } - }); - send('security_level', new TextEncoder().encode(level)) - } - }); -} - -function GetLevel3_BuildInformation(address, process_name) { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - const message = 'OEMCrypto_BuildInformation: ' + Memory.readUtf8String(retval); - sender_payload({ - from: process_name, - message: message - }); - } - }); -} - -function GetSupportedCertificates(address, process_name) { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - const message = 'OEMSupportedCertificates: ' + OEMCrypto_RSA_Support[retval.toInt32()]; - sender_payload({ - from: process_name, - message: message - }); - } - }); -} - -function GetLevel3_IsInApp(address, process_name) { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - sender_payload({ - from: process_name, - message: 'OEMCrypto_IsInApp', - payload: { - 'in_app': Boolean(retval) - } - }); - } - }); -} - -function GetLevel3_GetProvisioningMethod(address, process_name) { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - sender_payload({ - from: process_name, - message: 'OEMCrypto_GetProvisioningMethod', - payload: { - 'Method': OEMCrypto_ProvisioningMethod[retval.toInt32()] - } - }); - } - }); -} - -function GetLevel3_GetNumberOfOpenSessions(address, process_name) { - Interceptor.attach(ptr(address), { - onLeave: function (retval) { - const message = 'OEMCrypto_GetNumberOfOpenSessions: ' + retval.toInt32(); - sender_payload({ - from: process_name, - message: message - }); - } - }); -} - -function GetLevel3_GetMaxNumberOfSessions(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.maximum = args[0] + switch (CDM_VERSION) { + case '15.0.0': + case '16.0.0': + this.ret = args[4]; + break; + case '16.1.0': + this.ret = args[5]; + break; + default: + this.ret = args[4]; + break; + } }, onLeave: function () { - const message = 'OEMCrypto_GetMaxNumberOfSessions: ' + Memory.readPointer(this.maximum).toInt32(); - sender_payload({ - from: process_name, - message: message - }); - } - }); -} - -function GetLevel3_Terminate_Status(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.maximum = args[0] - }, - onLeave: function (retvalue) { - const message = 'OEMCrypto_Terminate_Status: ' + OEMCryptoResult[retvalue.toInt32()]; - sender_payload({ - from: process_name, - message: message - }); - } - }); -} - -function GetLevel3_GetDeviceID(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function(args) { - this.deviceId = args[0]; - this.idLength = args[1] - }, - onLeave: function (retval) { - var idLength = Memory.readPointer(this.idLength).toInt32(); - const deviceIdArray = Memory.readByteArray(this.deviceId, idLength); - const deviceId = byteArrayToHex(deviceIdArray); - const data = { - from: process_name, - message: 'OEMCrypto_GetDeviceID', - payload: { - 'Status': OEMCryptoResult[retval.toInt32()], - 'Length': idLength, - 'DeviceId': deviceId - } - }; - sender_payload(data); - send('device_id', deviceIdArray) - } - }); -} - - - -function GetLevel3_GetKeyData(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.keyData = args[0]; - this.keyDataLength = args[1]; - }, - onLeave: function (retvalue) { - const keyDataLength = Memory.readPointer(this.keyDataLength).toInt32(); - const keyDataArray = Memory.readByteArray(this.keyData, keyDataLength); - const device_token = byteArrayToHex(keyDataArray); - const data = { - from: process_name, - message: 'OEMCrypto_GetKeyData', - payload: { - 'Status': OEMCryptoResult[retvalue.toInt32()], - 'Size': keyDataLength, - 'Device_Token': device_token - } - }; - sender_payload(data); - send('device_token', keyDataArray) - } - }); -} - -function GetLevel3_LoadKeys(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.data = { - 'session': args[0], - 'message': args[1], - 'message_length': args[2], - 'signature': args[3], - 'signature_length': args[4], - 'ivs': args[5], - 'keys': args[6], - 'num_keys': args[7], - 'key_array': args[8], - 'pst': args[9], - 'srm_restriction_data': args[10], - 'license_type': args[11] - } - }, - onLeave: function (retvalue) { - const message_length = this.data['message_length'].toInt32(); - const message = Memory.readByteArray(this.data['message'], message_length); - const signature_length = this.data['signature_length'].toInt32(); - const signature = Memory.readByteArray(this.data['signature'], signature_length); - // const ivs = this.data['ivs']; - // const keys = Memory.readPointer(this.data['keys']); - // const num_keys = this.data['num_keys'].toInt32(); - // const key_array = Memory.readPointer(this.data['key_array']); - // const pst = this.data['pst']; - // const srm_restriction_data = this.data['srm_restriction_data']; - const license_type = OEMCrypto_LicenseType[this.data['license_type'].toInt32()]; - const data = { - from: process_name, - message: 'OEMCrypto_LoadKeys', - payload: { - 'Status': OEMCryptoResult[retvalue.toInt32()], - 'Type': license_type, - 'Message': byteArrayToHex(message), - 'Signature': byteArrayToHex(signature) - } - }; - sender_payload(data) - } - }); -} - - -function GetLevel3_GenerateDerivedKeys(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.data = { - 'session': args[0], - 'mac_key_context': args[1], - 'mac_key_context_length': args[2], - 'enc_key_context': args[3], - 'enc_key_context_length': args[4] - } - }, - onLeave: function (retvalue) { - const mac_length = this.data['mac_key_context_length'].toInt32(); - const mac_context = Memory.readByteArray(this.data['mac_key_context'], mac_length); - const enc_length = this.data['enc_key_context_length'].toInt32(); - const enc_context = Memory.readByteArray(this.data['enc_key_context'], enc_length); - const data = { - from: process_name, - message: 'GetLevel3_GenerateDerivedKeys', - payload: { - 'Status': OEMCryptoResult[retvalue.toInt32()], - 'Session': this.data['session'].toInt32(), - 'Mac_Length': mac_length, - 'Mac_Context': mac_context, - 'Enc_Length': enc_length, - 'Enc_Context': enc_context - } - }; - sender_payload(data) - - } - }); -} - -function GetLevel3_GenerateSignature(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.data = { - 'session': args[0], - 'message': args[1], - 'message_length': args[2], - 'signature': args[3], - 'signature_lenght': args[4] - } - }, - onLeave: function (retvalue) { - const message_length = this.data['message_length'].toInt32(); - const message = Memory.readByteArray(this.data['message'], message_length); - const signature_lenght = Memory.readPointer(this.data['signature_lenght']).toInt32(); - const signature = Memory.readByteArray(this.data['signature'], signature_lenght); - const data = { - from: process_name, - message: 'GetLevel3_GenerateSignature', - payload: { - 'Status': OEMCryptoResult[retvalue.toInt32()], - 'Session': this.data['session'].toInt32(), - message: { - 'length': message_length, - 'context': byteArrayToHex(message) - }, - signature: { - 'length': signature_lenght, - 'context': byteArrayToHex(signature) - } - } - }; - sender_payload(data) - - } - }); -} - - -function GetLevel3_GetOEMPublicCertificate(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.data = { - 'session': args[0], - 'public_cert': args[1], - 'public_cert_length': args[2] - } - }, - onLeave: function (retvalue) { - const result = OEMCryptoResult[retvalue.toInt32()]; - const data = { - from: process_name, - message: 'GetLevel3_GetOEMPublicCertificate', - payload: { - 'Status': result - } - }; - sender_payload(data); - if (result === OEMCryptoResult["0"]) { - const public_cert_length = Memory.readPointer(this.data['public_cert_length']).toInt32(); - const public_cert = Memory.readByteArray(this.data['public_cert'], public_cert_length); - const data2 = { - from: process_name, - message: 'GetLevel3_GetOEMPublicCertificate', - payload: { - 'Session': this.data['session'].toInt32(), - 'Public_Cert_Length': public_cert_length, - 'Cert': public_cert - } - }; - sender_payload(data2); + if (this.ret) { + const size = Memory.readU32(ptr(this.ret).add(Process.pointerSize)) + const arr = Memory.readByteArray(this.ret.add(Process.pointerSize * 2).readPointer(), size) + send('device_info', arr); } } }); } -function GetLevel3_LoadDeviceRSAKey(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.data = { - 'session': args[0], - 'wrapped_rsa_key': args[1], - 'wrapped_rsa_key_length': args[2] - } - }, - onLeave: function (retvalue) { - const wrapped_rsa_key_length = this.data['wrapped_rsa_key_length'].toInt32(); - const wrapped_rsa_key = Memory.readByteArray(this.data['wrapped_rsa_key'], wrapped_rsa_key_length); - const data = { - from: process_name, - message: 'GetLevel3_LoadDeviceRSAKey', - payload: { - 'Status': OEMCryptoResult[retvalue.toInt32()], - 'Session': this.data['session'].toInt32(), - 'Length': wrapped_rsa_key_length, - 'Context': byteArrayToHex(wrapped_rsa_key) - } - }; - sender_payload(data) - } - }); -} +function hookLibFunctions(lib) { + const name = lib['name']; + const baseAddr = lib['base']; + const message = 'Hooking ' + name + ' at ' + baseAddr; - -function GetLevel3_RewrapDeviceRSAKey(address, process_name) { - Interceptor.attach(ptr(address), { - onEnter: function (args) { - this.data = { - 'session': args[0], - 'message': args[1], - 'message_length': args[2], - 'signature': args[3], - 'signature_length': args[4], - 'nonce': args[5], - 'enc_rsa_key': args[6], - 'enc_rsa_key_length': args[7], - 'enc_rsa_key_iv': args[8], - 'wrapped_rsa_key': args[9], - 'wrapped_rsa_key_length': args[10] - } - }, - onLeave: function (retvalue) { - const status = OEMCryptoResult[retvalue.toInt32()]; - if (status === OEMCryptoResult["0"]) { - const message_length = this.data['message_length'].toInt32(); - const message = Memory.readByteArray(this.data['message'], message_length); - const signature_length = this.data['signature_length'].toInt32(); - const signature = Memory.readByteArray(this.data['signature'], signature_length); - const enc_rsa_key_length = this.data['enc_rsa_key_length'].toInt32(); - const enc_rsa_key = Memory.readByteArray(this.data['enc_rsa_key'], enc_rsa_key_length); - const wrapped_rsa_key_length = Memory.readPointer(this.data['wrapped_rsa_key_length']).toInt32(); - const wrapped_rsa_key = Memory.readByteArray(this.data['wrapped_rsa_key'], wrapped_rsa_key_length); - const data = { - from: process_name, - message: 'GetLevel3_RewrapDeviceRSAKey', - status: status, - session: this.data['session'].toInt32(), - payload: { - enc_rsa_key: { - 'length': enc_rsa_key_length, - 'key': enc_rsa_key - }, - wrapped_rsa_key: { - 'length': wrapped_rsa_key_length, - 'key': wrapped_rsa_key - }, - signature: { - 'length': signature_length, - 'signature': signature - }, - message: { - 'lenght': message_length, - 'message': message - } - } - }; - sender_payload(data) - } - - } - }); -} - -function byteArrayToHex(data) { - var array = new Uint8Array(data); - var result = ''; - for (var i = 0; i < array.length; ++i) - result += ('0' + (array[i] & 0xFF).toString(16)).slice(-2); - return result; -} - -function sender_payload(data) { - var encoded = new TextEncoder().encode(JSON.stringify(data)); - send('message', encoded); -} - -function sender_payload_info(message) { send('message_info', new TextEncoder().encode(message)) + + Module.enumerateExportsSync(name).forEach(function (module) { + const privacy_mode = 'UsePrivacyMode' + const prepare_key_request = 'PrepareKeyRequest' + try { + let hookedModule; + if (module.name.includes(DYNAMIC_FUNCTION_NAME)) { + getPrivateKey(module.address); + hookedModule = DYNAMIC_FUNCTION_NAME + } else if (module.name.includes(privacy_mode)) { + disablePrivacyMode(module.address); + hookedModule = privacy_mode + } else if (module.name.includes(prepare_key_request)) { + prepareKeyRequest(module.address); + hookedModule = prepare_key_request + } + + if (hookedModule) { + const message = 'Hooked ' + hookedModule + ' at ' + module.address; + send('message_info', new TextEncoder().encode(message)); + } + } catch (e) { + console.log("Error: " + e + " at F: " + module.name); + } + }); } -const OEMCrypto_ProvisioningMethod = { - 0: 'OEMCrypto_ProvisioningError', // Device cannot be provisioned. - 1: 'OEMCrypto_DrmCertificate', // Device has baked in DRM certificate - // (level 3 only) - 2: 'OEMCrypto_Keybox', // Device has factory installed unique keybox. - 3: 'OEMCrypto_OEMCertificate' // Device has factory installed OEM certificate. -}; +function getModuleByName(lib) { + return Process.getModuleByName(lib); +} -const OEMCrypto_RSA_Support = { - 1: 'OEMCrypto_Supports_RSA_2048bit', - 2: 'OEMCrypto_Supports_RSA_3072bit', - 10: 'OEMCrypto_Supports_RSA_CAST' -}; +function a2bs(bytes) { + let b = ''; + for (let i = 0; i < bytes.byteLength; i++) + b += String.fromCharCode(bytes[i]); + return b +} -const OEMCryptoResult = { - 0: 'OEMCrypto_SUCCESS', - 1: 'OEMCrypto_ERROR_INIT_FAILED', - 2: 'OEMCrypto_ERROR_TERMINATE_FAILED', - 3: 'OEMCrypto_ERROR_OPEN_FAILURE', - 4: 'OEMCrypto_ERROR_CLOSE_FAILURE', - 5: 'OEMCrypto_ERROR_ENTER_SECURE_PLAYBACK_FAILED', // deprecated - 6: 'OEMCrypto_ERROR_EXIT_SECURE_PLAYBACK_FAILED', // deprecated - 7: 'OEMCrypto_ERROR_SHORT_BUFFER', - 8: 'OEMCrypto_ERROR_NO_DEVICE_KEY', // no keybox device key. - 9: 'OEMCrypto_ERROR_NO_ASSET_KEY', - 10: 'OEMCrypto_ERROR_KEYBOX_INVALID', - 11: 'OEMCrypto_ERROR_NO_KEYDATA', - 12: 'OEMCrypto_ERROR_NO_CW', - 13: 'OEMCrypto_ERROR_DECRYPT_FAILED', - 14: 'OEMCrypto_ERROR_WRITE_KEYBOX', - 15: 'OEMCrypto_ERROR_WRAP_KEYBOX', - 16: 'OEMCrypto_ERROR_BAD_MAGIC', - 17: 'OEMCrypto_ERROR_BAD_CRC', - 18: 'OEMCrypto_ERROR_NO_DEVICEID', - 19: 'OEMCrypto_ERROR_RNG_FAILED', - 20: 'OEMCrypto_ERROR_RNG_NOT_SUPPORTED', - 21: 'OEMCrypto_ERROR_SETUP', - 22: 'OEMCrypto_ERROR_OPEN_SESSION_FAILED', - 23: 'OEMCrypto_ERROR_CLOSE_SESSION_FAILED', - 24: 'OEMCrypto_ERROR_INVALID_SESSION', - 25: 'OEMCrypto_ERROR_NOT_IMPLEMENTED', - 26: 'OEMCrypto_ERROR_NO_CONTENT_KEY', - 27: 'OEMCrypto_ERROR_CONTROL_INVALID', - 28: 'OEMCrypto_ERROR_UNKNOWN_FAILURE', - 29: 'OEMCrypto_ERROR_INVALID_CONTEXT', - 30: 'OEMCrypto_ERROR_SIGNATURE_FAILURE', - 31: 'OEMCrypto_ERROR_TOO_MANY_SESSIONS', - 32: 'OEMCrypto_ERROR_INVALID_NONCE', - 33: 'OEMCrypto_ERROR_TOO_MANY_KEYS', - 34: 'OEMCrypto_ERROR_DEVICE_NOT_RSA_PROVISIONED', - 35: 'OEMCrypto_ERROR_INVALID_RSA_KEY', - 36: 'OEMCrypto_ERROR_KEY_EXPIRED', - 37: 'OEMCrypto_ERROR_INSUFFICIENT_RESOURCES', - 38: 'OEMCrypto_ERROR_INSUFFICIENT_HDCP', - 39: 'OEMCrypto_ERROR_BUFFER_TOO_LARGE', - 40: 'OEMCrypto_WARNING_GENERATION_SKEW', // Warning, not an error. - 41: 'OEMCrypto_ERROR_GENERATION_SKEW', - 42: 'OEMCrypto_LOCAL_DISPLAY_ONLY', - 43: 'OEMCrypto_ERROR_ANALOG_OUTPUT', - 44: 'OEMCrypto_ERROR_WRONG_PST', - 45: 'OEMCrypto_ERROR_WRONG_KEYS', - 46: 'OEMCrypto_ERROR_MISSING_MASTER', - 47: 'OEMCrypto_ERROR_LICENSE_INACTIVE', - 48: 'OEMCrypto_ERROR_ENTRY_NEEDS_UPDATE', - 49: 'OEMCrypto_ERROR_ENTRY_IN_USE', - 50: 'OEMCrypto_ERROR_USAGE_TABLE_UNRECOVERABLE', // Reserved. Do not use. - 51: 'OEMCrypto_KEY_NOT_LOADED', // obsolete. use error 26. - 52: 'OEMCrypto_KEY_NOT_ENTITLED', - 53: 'OEMCrypto_ERROR_BAD_HASH', - 54: 'OEMCrypto_ERROR_OUTPUT_TOO_LARGE', - 55: 'OEMCrypto_ERROR_SESSION_LOST_STATE', - 56:'OEMCrypto_ERROR_SYSTEM_INVALIDATED', -}; +function getKeyLength(key) { + let pos = 1 // Skip the tag + let buf = key.charCodeAt(pos++); + let len = buf & 0x7F; // Short tag length -const OEMCrypto_LicenseType = { - 0: 'OEMCrypto_ContentLicense', - 1: 'OEMCrypto_EntitlementLicense' -}; + buf = 0; + for (let i = 0; i < len; ++i) + buf = (buf * 256) + key.charCodeAt(pos++); + return pos + Math.abs(buf); +} -rpc.exports.inject = inject; - -rpc.exports.widevinelibrary = containsLib; +rpc.exports.hooklibfunctions = hookLibFunctions; +rpc.exports.getmodulebyname = getModuleByName; diff --git a/README.md b/README.md index 2c0f706..10a0b3b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,15 @@ Dumper is a Frida script to dump L3 CDMs from any Android device. -## Dependencies +## ** IMPORTANT ** +You MUST update `DYNAMIC_FUNCTION_NAME` and `CDM_VERSION` in `script.js` to the relevant values for your device. +* `CDM_VERSION` can be retrieved using a DRM Info app. +* `DYNAMIC_FUNCTION_NAME` value is unique to your device and can be found in the file `libwvhidl.so` on your device. + +If you've managed to get as far as updating `DYNAMIC_FUNCTION_NAME` but can't find your function name, create an issue and provide me with your `libwvhidl.so` file and I will give you the function name you need. + +## Requirements Use pip to install the dependencies: `pip3 install -r requirements.txt` @@ -15,11 +22,16 @@ Use pip to install the dependencies: * Execute dump_keys.py * Start streaming some DRM-protected content +## Known Working Versions +* Android 10 + * CDM 15.0.0 +* Android 11 + * CDM 16.0.0 +* Android 12 + * CDM 16.1.0 + ## Temporary disabling L1 to use L3 instead A few phone brands let us use the L1 keybox even after unlocking the bootloader (like Xiaomi). In this case, installation of a Magisk module called [liboemcrypto-disabler](https://github.com/umylive/liboemcrypto-disabler) is necessary. -## Known issues -It seems like Google made some changes in their OEMCrypto library and it broke the script. Further investigation is needed to make it work on Android 11+, feel free to open PRs. - ## Credits Thanks to the original author of the code. diff --git a/dump_keys.py b/dump_keys.py index 62afc95..e0cbdc3 100644 --- a/dump_keys.py +++ b/dump_keys.py @@ -1,9 +1,8 @@ #!/usr/bin/env python3 import time -import frida import logging -from Helpers.Scanner import Scan +from Helpers.Device import Device logging.basicConfig( format='%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s', @@ -11,17 +10,21 @@ logging.basicConfig( level=logging.DEBUG, ) -device = frida.get_usb_device() -scanner = Scan(device.name) -logging.info(f'Connected to {device.name}') -logging.info('scanning all processes for the following libraries') -for process in device.enumerate_processes(): - logging.debug(process) - if 'drm' in process.name: - libraries = scanner.find_widevine_process(device, process.name) - if libraries: - for library in libraries: - scanner.hook_to_process(device, process.name, library) -logging.info('Hooks completed') -while True: - time.sleep(1000) + +def main(): + logger = logging.getLogger("main") + device = Device() + logger.info('Connected to %s', device.name) + logger.info('Scanning all processes') + + for process in device.usb_device.enumerate_processes(): + if 'drm' in process.name: + for library in device.find_widevine_process(process.name): + device.hook_to_process(process.name, library) + logger.info('Functions Hooked, load the DRM stream test on Bitmovin!') + + +if __name__ == '__main__': + main() + while True: + time.sleep(1000) diff --git a/requirements.txt b/requirements.txt index 3abb28b..bf15ecd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ frida -protobuf +protobuf == 3.19.3 pycryptodome