Files
KeyDive/keydive/keydive.js
T

963 lines
34 KiB
JavaScript

/**
* Date: 2026-04-28
* Description: DRM key extraction for research and educational purposes.
* Source: https://github.com/hyugogirubato/KeyDive
*/
// Placeholder values dynamically replaced at runtime.
const OEM_CRYPTO_API = JSON.parse('${OEM_CRYPTO_API}');
const NATIVE_C_API = JSON.parse('${NATIVE_C_API}');
const SYMBOLS = JSON.parse('${SYMBOLS}');
const DETECT = '${DETECT}' === 'True';
const DISABLER = '${DISABLER}' === 'True';
const UNENCRYPT = '${UNENCRYPT}' === 'True';
// Logging levels to synchronize with Python's logging module.
const Level = {
NOTSET: 0,
DEBUG: 10,
INFO: 20,
// WARN: WARNING,
WARNING: 30,
ERROR: 40,
// FATAL: CRITICAL,
CRITICAL: 50
};
// Backward compatibility with the modern equivalent implemented since frida 17
// https://frida.re/news/2025/05/17/frida-17-0-0-released/
/*
readS8 = readShort
readU8 = readUShort
readS16 = readInt
readU16 = readUInt
readS32 = readFloat
readU32 = readDouble
console.log(hexdump(address, {
offset: 0,
length: 128,
header: true,
ansi: true
}));
*/
Memory = typeof Memory === 'undefined' ? {} : Memory;
Memory.readByteArray ??= (address, length) => address.readByteArray(length);
Memory.readPointer ??= (address) => address.readPointer();
Memory.readU16 ??= (address) => address.readU16();
// libc++ std::string (SSO flag at bit 0 of byte 0):
// short: [size<<1 : 1B][inline data @+1 ...]
// long: [bit0=1 : 1B][...][size_t length @+pSize][char* data ptr @+2*pSize]
// Bit 0 of the first byte selects short vs long; length read as U16.
// https://learnfrida.info/intermediate_usage/#stdstring
Memory.readStdString = function (address) {
const length = Memory.readU16(address.add(Process.pointerSize));
const LSB = address.readU8() & 1;
if (LSB === 0) {
// SSO: data is stored inline starting at address + 1
return Memory.readByteArray(address.add(1), length);
} else {
// Non-SSO: pointer to data is stored at address
return Memory.readByteArray(address.add(Process.pointerSize * 2).readPointer(), length);
}
}
// libstdc++ C++11 ABI:
// [char* _M_p][size_t length][SSO buf 16B or capacity]
// _M_p always points to the data (heap or local SSO buffer).
Memory.readStdStringGnuCxx11 = function (address) {
const dataPtr = address.readPointer();
if (dataPtr.isNull() || dataPtr.compare(ptr(0x1000)) < 0) return null;
const length = address.add(Process.pointerSize).readU32();
if (length === 0 || length > 0x10000000) return null;
return Memory.readByteArray(dataPtr, length);
}
// libstdc++ classic (pre-C++11) COW std::string:
// [char* _M_p @+0] + _Rep header before data: [length][capacity][refcount]
// Container is a single pointer; length sits 3*pSize before the data.
Memory.readStdStringGnuCow = function (address) {
const dataPtr = address.readPointer();
if (dataPtr.isNull() || dataPtr.compare(ptr(0x1000)) < 0) return null;
const length = dataPtr.sub(3 * Process.pointerSize).readU32();
if (length === 0 || length > 0x10000000) return null;
return Memory.readByteArray(dataPtr, length);
}
// 24-byte vector-style std::string (old MediaTek/ARMv7 Widevine):
// [SSO buf 12B][cap/alloc ptr][char* _M_finish][char* _M_start] (offsets 0/12/16/20)
// Length = _M_finish - _M_start (matches IDA *((_DWORD*)s+5)==*(s+4) empty check).
Memory.readStdStringRange = function (address) {
const finish = address.add(16).readPointer();
const start = address.add(20).readPointer();
if (start.isNull() || start.compare(ptr(0x1000)) < 0) return null;
if (finish.compare(start) < 0) return null;
const length = finish.sub(start).toInt32();
if (length === 0 || length > 0x10000000) return null;
return Memory.readByteArray(start, length);
}
// Try every known std::string ABI and return the first plausible result.
Memory.readStdStringAny = function (address) {
const readers = [
Memory.readStdStringRange, // 24B vector-style (old MediaTek/ARMv7 Widevine)
Memory.readStdString, // libc++ (modern Android)
Memory.readStdStringGnuCxx11, // libstdc++ C++11 (old NDK GCC, _GLIBCXX_USE_CXX11_ABI=1)
Memory.readStdStringGnuCow, // libstdc++ pre-C++11 (very old NDK GCC)
];
for (const reader of readers) {
try {
const data = reader(address);
if (data && data.byteLength > 0) return data;
} catch (e) { /* try next */
}
}
return null;
}
// Legacy compact vector layout (learnfrida.info):
// [data ptr @+0][...][U16 size @+8]
// Doesn't match any current STL; kept as a last-resort fallback.
// https://learnfrida.info/intermediate_usage/#stdvector
Memory.readStdVector = function (address) {
let length = Memory.readU16(address.add(8));
const data = Memory.readByteArray(address.readPointer(), length);
/*
const buffer = new Uint8Array(data);
// Trim trailing null bytes (0x00) from the end
while (size > 0 && buffer[size - 1] === 0x00) {
size--;
}
// Return the trimmed buffer
return buffer.slice(0, size);
*/
return data;
}
// android::Vector<T> from utils/Vector.h (vptr-prefixed because VectorImpl has a virtual dtor):
// [vptr][void* mStorage][size_t mCount][uint32_t mFlags][size_t mItemSize]
// Data ptr = mStorage @+pSize, byte count = mCount @+2*pSize (T = uint8_t).
Memory.readAndroidVector = function (address) {
const dataPtr = address.add(Process.pointerSize).readPointer();
if (dataPtr.isNull() || dataPtr.compare(ptr(0x1000)) < 0) return null;
const count = address.add(2 * Process.pointerSize).readU32();
if (count === 0 || count > 0x10000000) return null;
return Memory.readByteArray(dataPtr, count);
}
// std::vector<T> (libc++ and libstdc++ C++11):
// [pointer begin][pointer end][pointer capacity_end]
// For vector<uint8_t> the byte count is end - begin.
Memory.readStdVectorRange = function (address) {
const begin = address.readPointer();
const end = address.add(Process.pointerSize).readPointer();
if (begin.isNull() || begin.compare(ptr(0x1000)) < 0) return null;
if (end.compare(begin) < 0) return null;
const length = end.sub(begin).toInt32();
if (length === 0 || length > 0x10000000) return null;
return Memory.readByteArray(begin, length);
}
// Try every known vector layout and return the first plausible result.
Memory.readStdVectorAny = function (address) {
const readers = [
Memory.readAndroidVector, // android::Vector<uint8_t>
Memory.readStdVectorRange, // std::vector<uint8_t>
Memory.readStdVector, // legacy U16-at-offset-8 layout
];
for (const reader of readers) {
try {
const data = reader(address);
if (data && data.byteLength > 0) return data;
} catch (e) { /* try next */
}
}
return null;
}
// Utility for encoding strings into byte arrays (UTF-8).
// https://gist.github.com/Yaffle/5458286#file-textencodertextdecoder-js
class TextEncoder {
encode(string) {
const octets = [];
let i = 0;
while (i < string.length) {
const codePoint = string.codePointAt(i);
let c = 0;
let bits = 0;
if (codePoint <= 0x007F) {
c = 0;
bits = 0x00;
} else if (codePoint <= 0x07FF) {
c = 6;
bits = 0xC0;
} else if (codePoint <= 0xFFFF) {
c = 12;
bits = 0xE0;
} else if (codePoint <= 0x1FFFFF) {
c = 18;
bits = 0xF0;
}
octets.push(bits | (codePoint >> c));
while (c >= 6) {
c -= 6;
octets.push(0x80 | ((codePoint >> c) & 0x3F));
}
i += codePoint >= 0x10000 ? 2 : 1;
}
return octets;
}
}
// Simplified log function to handle messages and encode them for transport.
const print = (level, message) => {
message = message instanceof Object ? JSON.stringify(message) : message;
message = message ? new TextEncoder().encode(message) : message;
send(level, message);
}
const getVersion = () => Frida.version;
// @Utils
function getLibraries() {
// https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792
try {
return Process.enumerateModules();
} catch (e) {
print(Level.CRITICAL, e.message);
return [];
}
}
function getLibrary(name) {
return getLibraries().find(l => l.name === name);
}
function getFunctions(library, dynamic = false) {
try {
// https://frida.re/news/2025/01/09/frida-16-6-0-released/
const functions = dynamic
? library.enumerateSymbols().map(item => ({
type: item.type,
name: item.name,
address: item.address
})) : [];
return functions.concat(library.enumerateExports());
} catch (e) {
print(Level.CRITICAL, e.message);
return [];
}
}
function getDerLength(buffer) {
let pos = 1; // Skip the tag byte (usually 0x30 for SEQUENCE)
let lengthByte = buffer[pos++]; // Read length descriptor
if (lengthByte < 0x80) {
// Short form: length is in this byte
return pos + lengthByte;
}
// Long form: next N bytes encode the length
const numLenBytes = lengthByte & 0x7F;
if (numLenBytes + pos > buffer.length) {
throw new Error('DER length bytes exceed buffer size');
}
let lengthValue = 0;
for (let i = 0; i < numLenBytes; i++) {
lengthValue = (lengthValue << 8) + buffer[pos++]; // Accumulate length
}
// Total length = current pos (after length bytes) + value length
return pos + lengthValue;
}
function readDerKey(address, size) {
// Read initial bytes from memory
const data = Memory.readByteArray(address, size);
const buffer = new Uint8Array(data);
const tag = buffer[0]; // Usually 0x30 for SEQUENCE
// Check if tag indicates a DER SEQUENCE
if (tag !== 0x30) return;
try {
// Adjust size based on DER length field
size = getDerLength(buffer); // ASN.1 DER
} catch (e) {
// Ignore error, keep initial size
}
// Return the DER key slice
return buffer.slice(0, size);
}
function disableLibrary(name) {
// Disables all functions in the specified library by replacing their implementations.
const library = getLibrary(name);
if (library) {
// https://github.com/hyugogirubato/KeyDive/issues/23#issuecomment-2230374415
const functions = getFunctions(library, false);
const disabled = new Set();
functions.forEach(({name: funcName, address: funcAddr, type}) => {
if (type !== 'function' || disabled.has(funcAddr)) return;
try {
Interceptor.replace(funcAddr, new NativeCallback(() => ptr(0), 'pointer', []));
disabled.add(funcAddr);
} catch (e) {
print(Level.DEBUG, `${e.message} for ${funcName}`);
}
});
print(Level.INFO, `Library ${library.name} (${library.path}) has been disabled`);
} else {
print(Level.DEBUG, `Library ${name} was not found`);
}
}
// @Libraries
function Properties_UsePrivacyMode(address) {
/*
wvcdm::Properties::UsePrivacyMode
Args:
args[1]: const CdmSessionId& session_id
Return:
retval: bool
*/
Interceptor.replace(address, new NativeCallback(() => 0, 'bool', []));
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: Properties::UsePrivacyMode');
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: Properties::UsePrivacyMode');
}
});
}
function Properties_GetCdmClientPropertySet(address) {
/*
wvcdm::Properties::GetCdmClientPropertySet
Args:
args[1]: const CdmSessionId& session_id
Return:
retval: wvcdm::CdmClientPropertySet*
*/
Interceptor.replace(address, new NativeCallback(() => ptr(0), 'pointer', []));
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: Properties::GetCdmClientPropertySet');
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: Properties::GetCdmClientPropertySet');
}
});
}
function CdmLicense_PrepareKeyRequest(address) {
/*
wvcdm::CdmLicense::PrepareKeyRequest
Args:
args[1]: const InitializationData& init_data
args[2]: const std::string& client_token
args[3]: CdmLicenseType license_type
args[4]: const CdmAppParameterMap& app_parameters
args[5]: CdmKeyMessage* signed_request
args[6]: std::string* server_url
Return:
retval: wvcdm::CdmResponseType
*/
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: CdmLicense::PrepareKeyRequest');
// https://github.com/hyugogirubato/KeyDive/issues/13#issue-2327487249
this.params = [];
for (let i = 0; i < 8; i++) {
this.params.push(args[i]);
}
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: CdmLicense::PrepareKeyRequest');
let dumped = false;
// Extract and dump the relevant arguments
for (let i = 0; i < this.params.length; i++) {
// Extract the signed_request data (CdmKeyMessage*)
try {
const signedRequestData = Memory.readStdStringAny(this.params[i]);
if (signedRequestData) {
dumped = true;
send('challenge', signedRequestData);
}
} catch (e) {
// print(Level.WARNING, `Failed to extract signed_request data from args[${i}]`);
}
}
!dumped && print(Level.ERROR, 'Failed to dump challenge data');
}
});
}
function CdmEngine_GenerateKeyRequest(address) {
/*
wvcdm::CdmEngine::GenerateKeyRequest
Args:
args[1]: const CdmSessionId& session_id
args[2]: const CdmKeySetId& key_set_id
args[3]: const InitializationData& init_data
args[4]: const CdmLicenseType license_type
args[5]: CdmAppParameterMap& app_parameters
args[6]: CdmKeyRequest* key_request
Return:
retval: wvcdm::CdmResponseType
*/
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: CdmEngine::GenerateKeyRequest');
// https://github.com/hyugogirubato/KeyDive/issues/13#issue-2327487249
this.params = [];
for (let i = 0; i < 8; i++) {
this.params.push(args[i]);
}
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: CdmEngine::GenerateKeyRequest');
let dumped = false;
// Extract and dump the relevant arguments
for (let i = 0; i < this.params.length; i++) {
// Extract the signed_request data (CdmKeyMessage*)
try {
const signedRequestData = Memory.readStdStringAny(this.params[i]);
if (signedRequestData) {
dumped = true;
send('challenge', signedRequestData);
}
} catch (e) {
// print(Level.WARNING, `Failed to extract signed_request data from args[${i}]`);
}
}
!dumped && print(Level.ERROR, 'Failed to dump challenge data');
}
});
}
function AesCbcKey_Encrypt(address) {
/*
wvcdm::AesCbcKey::Encrypt
Args:
args[1]: const std::string& in
args[2]: std::string* out
args[3]: std::string* iv
Return:
retval: bool
*/
Interceptor.attach(address, {
onEnter: function (args) {
const inData = Memory.readStdStringAny(args[1]);
if (inData) {
print(Level.DEBUG, '[*] AesCbcKey::Encrypt');
send('client_id', inData);
}
}
});
}
function FileSystem_Read(address) {
/*
wvoec3::OEMCrypto_Level3AndroidFileSystem::Read
Args:
args[1]: const char *filename
args[2]: void *buffer
args[3]: size_t size
Return:
retval: ssize_t
*/
Interceptor.attach(address, {
onEnter: function (args) {
const bufferPtr = args[2];
const size = args[3].toInt32();
const data = Memory.readByteArray(bufferPtr, size);
// TODO: check compatibility for SDK 36
// Check if the size matches known keybox sizes (128 or 132 bytes)
if ([128, 132].includes(size) && data) {
print(Level.DEBUG, '[*] FileSystem::Read');
send('keybox', data);
}
}
});
}
function File_Read(address) {
/*
wvcdm::File::Read
Args:
args[1]: char* buffer
args[2]: size_t bytes
Return:
retval: ssize_t
*/
/*
_x1c36
Args:
args[0]: std::string* filename
args[1]: char* buffer
args[2]: size_t bytes
Return:
retval: ssize_t
*/
Interceptor.attach(address, {
onEnter: function (args) {
const bufferPtr = args[1];
const size = args[2].toInt32();
const data = Memory.readByteArray(bufferPtr, size);
// Check if the size matches known keybox sizes (128 or 132 bytes)
if ([128, 132].includes(size) && data) {
print(Level.DEBUG, '[*] File::Read');
send('keybox', data);
}
}
});
}
function RunningCRC(address) {
/*
wvoec::wvrunningcrc32
Args:
args[0]: const uint8_t* p_begin
args[1]: int i_count
args[2]: uint32_t i_crc
Return:
retval: uint32_t
*/
Interceptor.attach(address, {
onEnter: function (args) {
const size = args[1].toInt32();
const data = Memory.readByteArray(args[0], 128);
// Check if size matches keybox length excluding 4-byte magic/tag fields
if (size === 124 && data) {
print(Level.DEBUG, '[*] RunningCRC');
send('keybox', data);
}
}
});
}
function OEMCrypto_GetDeviceID(address) {
/*
wvcdm::OEMCrypto_GetDeviceID
Args:
args[0]: uint8_t* deviceID
args[1]: size_t* idLength
args[2]: SecurityLevel level
Return:
retval: OEMCryptoResult
*/
Interceptor.attach(address, {
onEnter: function (args) {
this.deviceIdPtr = args[0];
this.idLengthPtr = args[1];
},
onLeave: function (retval) {
const idLength = Memory.readPointer(this.idLengthPtr).toInt32();
const deviceIdData = Memory.readByteArray(this.deviceIdPtr, idLength);
if (deviceIdData) {
print(Level.DEBUG, '[*] OEMCrypto_GetDeviceID');
send('stable_id', deviceIdData);
}
}
});
}
function OEMCrypto_GetKeyData(address) {
/*
wvcdm::OEMCrypto_GetKeyData
Args:
args[0]: uint8_t* keyData
args[1]: size_t* keyDataLength
args[2]: SecurityLevel level
Return:
retval: OEMCryptoResult
*/
Interceptor.attach(address, {
onEnter: function (args) {
this.keyDataPtr = args[0];
this.keyDataLengthPtr = args[1];
},
onLeave: function (retval) {
const keyDataLength = Memory.readPointer(this.keyDataLengthPtr).toInt32();
const keyData = Memory.readByteArray(this.keyDataPtr, keyDataLength);
if (keyData) {
print(Level.DEBUG, '[*] OEMCrypto_GetKeyData');
send('device_id', keyData);
}
}
});
}
function CryptoSession_LoadCertificatePrivateKey(address) {
/*
wvcdm::CryptoSession::LoadCertificatePrivateKey
Args:
args[1]: const CryptoWrappedKey& private_key
Return:
retval: wvcdm::CdmResponseType
*/
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: CryptoSession::LoadCertificatePrivateKey');
this.privateKeyPtr = args[1];
// Call OEMCrypto_GetOEMPublicCertificate before OEMCrypto_LoadDeviceRSAKey (old)
// Call OEMCrypto_GetOEMPublicCertificate before OEMCrypto_LoadDRMPrivateKey (new)
// Called from LoadPrivateOrLegacyKey
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: CryptoSession::LoadCertificatePrivateKey');
console.log(hexdump(this.privateKeyPtr, {
offset: 0,
length: 128,
header: true,
ansi: true
}));
}
});
}
function OEMCrypto_ProvisioningMethod(address) {
/*
wvcdm::OEMCrypto_GetProvisioningMethod
Args:
args[0]: SecurityLevel level
Return:
retval: OEMCrypto_ProvisioningMethod
*/
Interceptor.attach(address, {
onLeave: function (retval) {
// https://github.com/fox0618/dumper/blob/main/Helpers/script.js#L784
print(Level.DEBUG, '[*] OEMCrypto_ProvisioningMethod');
send('provisioning_method', new TextEncoder().encode(`${retval.toInt32()}`));
}
});
}
function OEMCrypto_GenerateDerivedKeys(address) {
/*
wvcdm::OEMCrypto_GenerateDerivedKeys
Args:
args[0]: OEMCrypto_SESSION session
args[1]: const uint8_t* mac_key_context
args[2]: uint32_t mac_key_context_length
args[3]: const uint8_t* enc_key_context,
args[4]: uint32_t enc_key_context_length
Return:
retval: OEMCryptoResult
*/
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[*] OEMCrypto_GenerateDerivedKeys');
// https://github.com/Avalonswanderer/widevinel3_Android_PoC/blob/main/PoCs/content_key_recovery.py#L103C55-L103C72
// const macKeyContext = Memory.readByteArray(args[1], args[2].toInt32());
// console.log('macKeyContext:', macKeyContext);
/*
macKeyContext: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 41 55 54 48 45 4e 54 49 43 41 54 49 4f 4e 00 12 AUTHENTICATION..
00000010 04 0a d5 8e c0 1a 04 08 00 12 00 2a 98 06 0a 0c ...........*....
00000020 77 69 64 65 76 69 6e 65 2e 63 6f 6d 12 10 51 43 widevine.com..QC
00000030 4f e2 a4 4c 76 3b cc 2c 82 6a 2d 6e f9 a7 1a e0 O..Lv;.,.j-n....
*/
const encKeyContext = Memory.readByteArray(args[3], args[4].toInt32());
// console.log('encKeyContext:', encKeyContext);
/*
encKeyContext: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 45 4e 43 52 59 50 54 49 4f 4e 00 12 04 0a d5 8e ENCRYPTION......
00000010 c0 1a 04 08 00 12 00 2a 98 06 0a 0c 77 69 64 65 .......*....wide
00000020 76 69 6e 65 2e 63 6f 6d 12 10 51 43 4f e2 a4 4c vine.com..QCO..L
00000030 76 3b cc 2c 82 6a 2d 6e f9 a7 1a e0 03 ec 2a c5 v;.,.j-n......*.
*/
if (encKeyContext) {
send('encryption_context', encKeyContext);
}
}
});
}
function OEMCrypto_DeriveKeysFromSessionKey(address) {
/*
wvcdm::OEMCrypto_DeriveKeysFromSessionKey
Args:
args[0]: OEMCrypto_SESSION session
args[1]: const uint8_t* enc_session_key
args[2]: size_t enc_session_key_length
args[3]: const uint8_t* mac_key_context
args[4]: size_t mac_key_context_length
args[5]: const uint8_t* enc_key_context
args[6]: size_t enc_key_context_length
Return:
retval: OEMCryptoResult
*/
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[+] onEnter: OEMCrypto_DeriveKeysFromSessionKey');
const encSessionKey = Memory.readByteArray(args[1], args[2].toInt32());
const macKeyContext = Memory.readByteArray(args[3], args[4].toInt32());
const encKeyContext = Memory.readByteArray(args[5], args[6].toInt32());
// console.log('encSessionKey:', encSessionKey);
/*
encSessionKey: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 c4 9d ef cf 5f 0b 98 9c 03 46 93 89 14 8f 08 e2 ...._....F......
00000010 12 da 13 39 ad 31 75 f7 b5 32 94 ee 2f 7f bf 6a ...9.1u..2../..j
00000020 d7 45 c0 50 22 9a 6c 36 76 a7 78 d8 9f 76 b5 45 .E.P".l6v.x..v.E
00000030 f3 5c 6f 25 91 08 cf de a3 d9 90 08 cb e1 d4 55 .\o%...........U
*/
// console.log('macKeyContext:', macKeyContext);
/*
macKeyContext: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 41 55 54 48 45 4e 54 49 43 41 54 49 4f 4e 00 0a AUTHENTICATION..
00000010 c3 0f 08 01 12 aa 0b 0a ed 03 08 02 12 20 8d a4 ............. ..
00000020 21 77 04 fb 58 ff d6 58 80 8c d2 32 b5 81 01 5a !w..X..X...2...Z
00000030 6a d6 97 29 97 51 ac 92 95 de 81 fe b3 13 18 f7 j..).Q..........
*/
// console.log('encKeyContext:', encKeyContext);
/*
encKeyContext: 0 1 2 3 4 5 6 7 8 9 A B C D E F 0123456789ABCDEF
00000000 45 4e 43 52 59 50 54 49 4f 4e 00 0a c3 0f 08 01 ENCRYPTION......
00000010 12 aa 0b 0a ed 03 08 02 12 20 8d a4 21 77 04 fb ......... ..!w..
00000020 58 ff d6 58 80 8c d2 32 b5 81 01 5a 6a d6 97 29 X..X...2...Zj..)
00000030 97 51 ac 92 95 de 81 fe b3 13 18 f7 f6 ad c1 06 .Q..............
*/
},
onLeave: function (retval) {
print(Level.DEBUG, '[-] onLeave: OEMCrypto_DeriveKeysFromSessionKey');
}
});
}
function WVDrmPlugin_provideProvisionResponse(address) {
/*
wvdrm::WVDrmPlugin::provideProvisionResponse
Args:
args[1]: const Vector<uint8_t>& response
args[2]: Vector<uint8_t>& certificate,
args[3]: Vector<uint8_t>& wrapped_key
Return:
retval: wvcdm::CdmResponseType
*/
Interceptor.attach(address, {
onEnter: function (args) {
print(Level.DEBUG, '[*] WVDrmPlugin::provideProvisionResponse');
let dumped = false;
// Extract and dump the relevant arguments
for (let i = 0; i < 4; i++) {
try {
const responseData = Memory.readStdVectorAny(args[i]);
if (responseData) {
dumped = true;
send('provisioning_response', responseData);
}
} catch (e) {
// print(Level.WARNING, `Failed to extract provisioning response data from args[${i}]`);
}
}
!dumped && print(Level.ERROR, 'Failed to dump provisioning response data');
}
});
}
function Level3_RewrapDeviceRSAKey30(address, name) {
/*
wvcdm::Level3_RewrapDeviceRSAKey30
Args:
args[1]: OEMCrypto_SESSION session
args[2]: const uint32_t* nonce
args[3]: const uint8_t* encrypted_message_key
args[4]: size_t encrypted_message_key_length
args[5]: const uint8_t* enc_rsa_key
args[6]: size_t enc_rsa_key_length
args[7]: const uint8_t* enc_rsa_key_iv
args[8]: uint8_t* wrapped_rsa_key
args[9]: size_t* wrapped_rsa_key_length
Return:
retval: OEMCryptoResult
*/
Interceptor.attach(address, {
onEnter: function (args) {
try {
const bufferPtr = args[5];
const sizePtr = args[6];
if (sizePtr.isNull() || bufferPtr.isNull()) return;
if (bufferPtr.compare(ptr(0x10000)) < 0) return;
// Check if the size matches a potential 2048 or 4096-bit RSA private key (range 1190-2350)
const size = sizePtr.toInt32();
if (size < 1000 || size > 2400) return;
const bufferData = readDerKey(bufferPtr, size);
if (bufferData) {
print(Level.DEBUG, `[*] Level3_RewrapDeviceRSAKey30: ${name}`);
send({'private_key': name}, bufferData);
}
} catch (e) { /* not a real RewrapDeviceRSAKey30 call */ }
}
});
}
// @Hooks
const hookLibrary = (name, dynamic) => {
// https://github.com/poxyran/misc/blob/master/frida-enumerate-imports.py
let library = getLibrary(name);
if (!library) return false;
let functions;
if (Object.keys(SYMBOLS).length) {
// https://github.com/hyugogirubato/KeyDive/issues/13#issuecomment-2143741896
functions = Object.entries(SYMBOLS).map(([key, value]) => ({
type: 'function',
name: value,
address: library.base.add(ptr(key))
}));
} else {
// https://github.com/hyugogirubato/KeyDive/issues/50
functions = getFunctions(library, dynamic);
}
functions = functions.filter(f => !NATIVE_C_API.includes(f.name));
let targets = DETECT ? functions.filter(f => OEM_CRYPTO_API.includes(f.name)).map(f => f.name) : [];
const required = new Set();
const hooked = new Set();
functions.forEach(({name: funcName, address: funcAddr, type}) => {
if (type !== 'function' || required.has(funcAddr)) return;
try {
// Interception of client ID via challenge or in clear text
if (['AesCbcKey', 'Encrypt'].every(n => funcName.includes(n))) {
AesCbcKey_Encrypt(funcAddr);
required.add(funcAddr);
// Calling this method is further down in the challenge request execution flow
// Using the GenerateKeyRequest function at a higher level
//} else if (['CdmLicense', 'PrepareKeyRequest'].every(n => funcName.includes(n))) {
// CdmLicense_PrepareKeyRequest(funcAddr);
// required.add(funcAddr);
} else if (['CdmEngine', 'GenerateKeyRequest'].every(n => funcName.includes(n))) {
CdmEngine_GenerateKeyRequest(funcAddr);
required.add(funcAddr);
// Full and block keybox interception
} else if (['FileSystem', 'Read'].every(n => funcName.includes(n))) {
FileSystem_Read(funcAddr);
} else if (['File', 'Read'].every(n => funcName.includes(n)) || funcName.includes('_x1c36')) {
File_Read(funcAddr);
} else if (['runningcrc'].every(n => funcName.includes(n))) {
// https://github.com/Avalonswanderer/widevinel3_Android_PoC/blob/main/PoCs/recover_l3keybox.py#L50
RunningCRC(funcAddr);
} else if (['_oecc07', '_lcc07'].some(n => funcName === n)) {
OEMCrypto_GetDeviceID(funcAddr);
} else if (['_oecc04', '_lcc04'].some(n => funcName === n)) {
OEMCrypto_GetKeyData(funcAddr);
// TODO: Intercept the private key of OEM device certificate
//} else if (['CryptoSession', 'LoadCertificatePrivateKey'].every(n => funcName.includes(n))) {
// CryptoSession_LoadCertificatePrivateKey(funcAddr);
// Provisioning Interception
} else if (['_oecc49', '_lcc49'].some(n => funcName === n)) {
OEMCrypto_ProvisioningMethod(funcAddr);
} else if (['_oecc12', '_lcc12', '_oecc95', '_lcc95'].some(n => funcName === n)) {
// Key derivation via keybox provisioning
OEMCrypto_GenerateDerivedKeys(funcAddr);
//} else if (['_oecc21', '_lcc21'].some(n => funcName === n)) {
// Key derivation via session key for license request
// OEMCrypto_DeriveKeysFromSessionKey(funcAddr);
} else if (['WVDrmPlugin', 'provideProvisionResponse'].every(n => funcName.includes(n))) {
WVDrmPlugin_provideProvisionResponse(funcAddr);
// Disable encrypted client id for license request (deprecated)
} else if (UNENCRYPT && ['Properties', 'GetCdmClientPropertySet'].every(n => funcName.includes(n))) {
Properties_GetCdmClientPropertySet(funcAddr);
} else if (UNENCRYPT && ['Properties', 'UsePrivacyMode'].every(n => funcName.includes(n))) {
// Calling this function usually returns a boolean handled by the GetCdmClientPropertySet subcall
// Replacing this function is quite unstable, causing library crashes
Properties_UsePrivacyMode(funcAddr);
// OEM private key interruption from obfuscated functions
} else if (targets.includes(funcName) || (!targets.length && funcName.match(/^[a-z]+$/))) {
Level3_RewrapDeviceRSAKey30(funcAddr, funcName);
required.add(funcAddr);
} else {
return;
}
hooked.add(funcAddr);
print(Level.DEBUG, `Hooked (${funcAddr}): ${funcName}`);
} catch (e) {
print(Level.ERROR, `${e.message} for ${funcName}`);
}
});
if (required.size < 3) {
print(Level.CRITICAL, 'Insufficient functions hooked');
return false;
}
if (DISABLER) {
// TODO: Disable old L1 libraries? (https://github.com/wvdumper/dumper/blob/main/Helpers/Scanner.py#L23)
// https://github.com/hzy132/liboemcryptodisabler/blob/master/customize.sh#L33
disableLibrary('liboemcrypto.so');
}
return true;
}
// RPC interfaces exposed to external calls.
rpc.exports = {
getversion: getVersion,
getlibraries: getLibraries,
hooklibrary: hookLibrary
};