15 Commits

Author SHA1 Message Date
hyugogirubato
9779db14fe clean base repo 2024-04-01 18:07:58 +02:00
hyugogirubato
09391e8dac Release 1.0.3
- Fix target analyse
- Fix function arg count
- Fix selected functions by name
- Show loaded script
- Add extracted function for SDK 34
- Add simple arg
2024-04-01 18:04:51 +02:00
hyugogirubato
c9b2f8975c Update version
- Fix version number
- Fix import functions text
- Remove auto-select for imported functions (already done with python)
- Add extracted functions for 15.0.0 arm64
- Show script version
- Add clear command
2024-04-01 17:46:39 +02:00
hyugogirubato
00ecd4e1c7 Update README.md
https://github.com/sinsukehlab/NOTE-test/blob/main/NOTE.md
2024-04-01 16:37:03 +02:00
hyugogirubato
4caf035425 Update README.md 2024-04-01 16:35:39 +02:00
hyugogirubato
24c07aae4c Fix set update 2024-04-01 15:55:46 +02:00
hyugogirubato
1bccbcc284 Use functions instead of symbols
- Merge OEM Crypto API to set
- Use functions instead of symbols
2024-04-01 15:53:18 +02:00
hyugogirubato
b59551c708 Clean import 2024-04-01 13:17:28 +02:00
hyugogirubato
539a895541 Update README usage 2024-04-01 13:10:21 +02:00
hyugogirubato
a9b86aa2c1 Avoid frida library hook error 2024-04-01 13:00:44 +02:00
hyugogirubato
262efe8c0c Add ADB env check + auto start 2024-04-01 12:24:00 +02:00
hyugogirubato
3910b2b531 Update SYMBOLS.md 2024-03-31 15:37:26 +02:00
hyugogirubato
7bba298df9 Release v1.0.2 2024-03-31 15:27:10 +02:00
hyugogirubato
bbaa7c67f4 rename symbols file 2024-03-31 13:45:30 +02:00
hyugogirubato
e5b04e917f hook optimization 2024-03-31 13:24:18 +02:00
15 changed files with 156853 additions and 13966 deletions

3
.gitignore vendored
View File

@@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# KeyDive
device/

View File

@@ -2,8 +2,7 @@
KeyDive is a sophisticated Python script designed for the precise extraction of Widevine L3 DRM (Digital Rights Management) keys from Android devices. This tool leverages the capabilities of the Widevine CDM (Content Decryption Module) to facilitate the recovery of DRM keys, enabling a deeper understanding and analysis of the Widevine L3 DRM implementation across various Android SDK versions.
> **Warning**
>
> [!IMPORTANT]
> Support for Android 14+ (SDK > 33) is currently under development. Some features may not function as expected on these newer versions.
## Features
@@ -33,27 +32,34 @@ Follow these steps to set up KeyDive:
## Usage
To use KeyDive, follow these steps:
1. Play a DRM-protected video on the target device.
2. Launch the KeyDive script.
3. Reload the DRM-protected video on your device.
4. The script will automatically extract the Widevine L3 keys, saving them as follows:
- `client_id.bin` - This file contains device identification information.
- `private_key.pem` - This file contains the RSA private key.
1. Launch the KeyDive script.
2. Play a DRM-protected video on the target device.
3. The script will automatically extract the Widevine L3 keys, saving them in the following format:
- `client_id.bin` - Contains device identification information.
- `private_key.pem` - Contains the RSA private key.
This sequence ensures that the DRM-protected content is active and ready for key extraction by the time the KeyDive script is initiated, optimizing the extraction process.
### Command-Line Options
```sh
usage: keydive.py [-h] [--device DEVICE]
```shell
usage: keydive.py [-h] [--device DEVICE] [--functions FUNCTIONS]
Extract Widevine L3 keys from an Android device.
options:
-h, --help show this help message and exit
--device DEVICE Target Android device ID.
-h, --help show this help message and exit
--device DEVICE Target Android device ID.
--functions FUNCTIONS
Ghidra XML functions file.
```
### Extracting Functions for Advanced Usage
For advanced users looking to use custom functions with KeyDive, a comprehensive guide on extracting functions from Widevine libraries using Ghidra is available. Please refer to our [Functions Extraction Guide](./docs/FUNCTIONS.md) for detailed instructions.
## Temporary Disabling L1 for L3 Extraction
Some manufacturers (e.g., Xiaomi) allow the use of L1 keyboxes even after unlocking the bootloader. In such cases, it's necessary to install a Magisk module called [liboemcrypto-disabler](https://github.com/Magisk-Modules-Repo/liboemcryptodisabler) to temporarily disable L1, thereby facilitating L3 key extraction.

44
docs/FUNCTIONS.md Normal file
View File

@@ -0,0 +1,44 @@
# Functions
To utilize custom functions with KeyDive, particularly when extracting Widevine L3 DRM keys from Android devices, you might need to generate a `functions.xml` file using Ghidra. This file helps KeyDive accurately identify necessary functions within the Widevine library, facilitating a more efficient extraction process. Below is a step-by-step guide on how to create a `functions.xml` file using Ghidra:
### Extracting functions with Ghidra
#### 1. Preparing Ghidra
Ensure you have Ghidra installed on your system. If not, download it from the [Ghidra project page](https://ghidra-sre.org/) and follow the installation instructions.
#### 2. Importing the ELF Binary
- Launch Ghidra and start a new project (or open an existing one).
- Import the ELF binary file (e.g., the Widevine CDM library from the Android device) by navigating to `File` > `Import File` and selecting the binary.
- Choose the default options for the import settings, unless you have specific requirements.
#### 3. Analyzing the Binary
- Once the binary is imported, double-click on it in the project window to open it in the CodeBrowser tool.
- Begin the analysis by navigating to `Analysis` > `Auto Analyze` from the top menu.
- In the "Auto Analysis" window, ensure all relevant analyzers are selected, especially those related to symbol and function discovery. Click "Analyze" to start the process.
- Wait for the analysis to complete, which may take some time depending on the binary's size and complexity.
#### 4. Exporting Functions as XML
- After analysis, navigate to `File` > `Export Program...`.
- In the "Export Program" window, choose the "XML" format from the dropdown menu.
- Click "Options" and ensure that only the "Functions" option is selected. This step is crucial as it filters the export to include only the functions necessary for KeyDive, making the XML file more manageable and relevant.
- Choose a destination for the `functions.xml` file and confirm the export.
#### 5. Using the Functions with KeyDive
Once you have the `functions.xml` file:
- Ensure KeyDive is set up according to its documentation.
- When running KeyDive, use the `--functions` argument to specify the path to your `functions.xml` file. For example:
```shell
python keydive.py --device <DEVICE_ID> --functions /path/to/functions_x86.xml
```
- Proceed with the key extraction process as detailed in KeyDive's usage instructions.
### Additional Tips
- **Understanding Functions:** The `functions.xml` file maps function names and variables within the Widevine CDM library, enabling KeyDive to correctly identify and hook into specific processes for key extraction.
- **Ghidra Compatibility:** Ensure your version of Ghidra supports the binary format you're analyzing. Newer versions of Ghidra typically offer better support for a wide range of binary formats.
- **Analysis Depth:** While a full analysis is recommended, you can customize the analysis options based on your understanding of the binary and the functions you are specifically interested in. This can significantly reduce analysis time.
- **Security Considerations:** Be aware of the security implications of extracting and handling DRM keys. Always comply with legal restrictions and ethical guidelines when using tools like KeyDive and Ghidra for reverse engineering.
By following these steps, you can generate a `functions.xml` file that aids in the effective use of KeyDive for educational, research, or security analysis purposes.

View File

@@ -1,4 +1,4 @@
# Packages
# Package
This document provides an overview of the external libraries, tools, and applications utilized within the KeyDive project. Each package plays a crucial role in enabling the project to efficiently extract Widevine L3 keys from Android devices for educational and research purposes.

View File

@@ -15,3 +15,5 @@ tree() {
path=${1:-.}
find ${path} -print | sort | sed 's;[^/]*/;|---;g;s;---|; |;g'
}
clear

File diff suppressed because it is too large Load Diff

View File

@@ -1,28 +0,0 @@
[
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=android.hardware.drm-service.widevine, base=0x5e990b799000, size=24576, path=/vendor/bin/hw/android.hardware.drm-service.widevine)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=linker64, base=0x74822b449000, size=278528, path=/system/bin/linker64)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=android.hardware.drm-V1-ndk.so, base=0x748225906000, size=196608, path=/apex/com.android.vndk.v33/lib64/android.hardware.drm-V1-ndk.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libbase.so, base=0x7482258b3000, size=274432, path=/apex/com.android.vndk.v33/lib64/libbase.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=liblog.so, base=0x74822585e000, size=73728, path=/system/lib64/liblog.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libutils.so, base=0x7482279c9000, size=131072, path=/apex/com.android.vndk.v33/lib64/libutils.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libwvaidl.so, base=0x74822766d000, size=3072000, path=/vendor/lib64/libwvaidl.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libbinder_ndk.so, base=0x74822aca2000, size=98304, path=/system/lib64/libbinder_ndk.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libc++.so, base=0x74822630b000, size=696320, path=/apex/com.android.vndk.v33/lib64/libc++.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libc.so, base=0x748225c08000, size=6520832, path=/apex/com.android.runtime/lib64/bionic/libc.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libm.so, base=0x74822ae11000, size=335872, path=/apex/com.android.runtime/lib64/bionic/libm.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libdl.so, base=0x748225a1f000, size=20480, path=/apex/com.android.runtime/lib64/bionic/libdl.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=android.hardware.common-V2-ndk.so, base=0x74822ac4e000, size=24576, path=/apex/com.android.vndk.v33/lib64/android.hardware.common-V2-ndk.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libc++.so, base=0x748225953000, size=696320, path=/system/lib64/libc++.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libcutils.so, base=0x7482263e1000, size=98304, path=/apex/com.android.vndk.v33/lib64/libcutils.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libvndksupport.so, base=0x74822adc2000, size=16384, path=/system/lib64/libvndksupport.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libcrypto.so, base=0x748225a4d000, size=1634304, path=/apex/com.android.vndk.v33/lib64/libcrypto.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libprotobuf-cpp-lite-3.9.1.so, base=0x74822acc2000, size=503808, path=/vendor/lib64/libprotobuf-cpp-lite-3.9.1.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libandroid_runtime_lazy.so, base=0x74822aff9000, size=16384, path=/system/lib64/libandroid_runtime_lazy.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libbase.so, base=0x74822628f000, size=274432, path=/system/lib64/libbase.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libbinder.so, base=0x74822aea6000, size=860160, path=/system/lib64/libbinder.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libutils.so, base=0x74822ad52000, size=131072, path=/system/lib64/libutils.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libdl_android.so, base=0x74822ad98000, size=12288, path=/apex/com.android.runtime/lib64/bionic/libdl_android.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libcutils.so, base=0x74822af8a000, size=98304, path=/system/lib64/libcutils.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libnetd_client.so, base=0x748015810000, size=40960, path=/system/lib64/libnetd_client.so)",
"Library(process=Process(pid=404, name=\"android.hardware.drm-service.widevine\", parameters={}), name=linux-vdso.so.1, base=0x7fff0bde6000, size=4096, path=linux-vdso.so.1)"
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
from .cdm import *
from .vendor import *
__version__ = '1.0.1'
__version__ = '1.0.3'

View File

@@ -1,11 +1,13 @@
import json
import logging
import re
import subprocess
from pathlib import Path
import xmltodict
import frida
from _frida import Process
from frida.core import Device, Session, Script, RPCException
from frida.core import Device, Session, Script
from Cryptodome.PublicKey import RSA
from extractor.license_protocol_pb2 import SignedMessage, LicenseRequest, ClientIdentification, DrmCertificate, SignedDrmCertificate
@@ -18,8 +20,15 @@ class Cdm:
"""
Manages the capture and processing of DRM keys from a specified device using Frida to inject custom hooks.
"""
OEM_CRYPTO_API = {
# Mapping of function names across different API levels (obfuscated names may vary).
'rnmsglvj', 'polorucp', 'kqzqahjq', 'pldrclfq', 'kgaitijd',
'cwkfcplc', 'crhqcdet', 'ulns', 'dnvffnze', 'ygjiljer',
'qbjxtubz', 'qkfrcjtw', 'rbhjspoh'
# Add more as needed for different versions.
}
def __init__(self, device: str = None):
def __init__(self, device: str = None, functions: Path = None):
self.logger = logging.getLogger('Cdm')
self.running = True
self.keys = {}
@@ -33,7 +42,8 @@ class Cdm:
self.logger.info('ABI CPU: %s', self.properties['ro.product.cpu.abi'])
# Determine vendor based on SDK API
self.script = self._prepare_hook_script()
self.script = self._prepare_hook_script(functions)
self.logger.info('Successfully loaded script')
self.vendor = self._prepare_vendor()
def _fetch_device_properties(self) -> dict:
@@ -55,12 +65,76 @@ class Cdm:
properties[key] = value
return properties
def _prepare_hook_script(self) -> str:
def _prepare_hook_script(self, path: Path) -> str:
"""
Prepares and returns the hook script with the SDK API version replaced.
Prepares and returns the hook script by replacing placeholders with actual values, including
SDK API version and selected functions from a given XML file.
"""
script_content = SCRIPT_PATH.read_text(encoding='utf-8')
return script_content.replace("'${SDK_API}'", str(self.sdk_api))
selected = {}
if path:
# Verify symbols file path
if not path.is_file():
raise FileNotFoundError('Symbols file not found')
try:
# Parse the XML file
program = xmltodict.parse(path.read_bytes())['PROGRAM']
addr_base = int(program['@IMAGE_BASE'], 16)
functions = program['FUNCTIONS']['FUNCTION']
# Find a target function from a predefined list
target = next((f['@NAME'] for f in functions if f['@NAME'] in self.OEM_CRYPTO_API), None)
# Extract relevant functions
for func in functions:
name = func['@NAME']
args = len(func.get('REGISTER_VAR', []))
# Add function if it matches specific criteria
if name not in selected and (
name == target
or any(keyword in name for keyword in ['UsePrivacyMode', 'PrepareKeyRequest'])
or (not target and re.match(r'^[a-z]+$', name) and args >= 6)
):
selected[name] = {'name': name, 'address': hex(int(func['@ENTRY_POINT'], 16) - addr_base)}
except Exception:
raise ValueError('Failed to extract functions from Ghidra')
# Read and prepare the hook script content
content = SCRIPT_PATH.read_text(encoding='utf-8')
# Replace placeholders with actual values
content = content.replace('${SDK_API}', str(self.sdk_api))
content = content.replace('${OEM_CRYPTO_API}', json.dumps(list(self.OEM_CRYPTO_API)))
content = content.replace('${SYMBOLS}', json.dumps(list(selected.values())))
return content
def _prepare_vendor(self) -> Vendor:
"""
Prepares and selects the most compatible vendor version based on the device's processes.
"""
details: [int] = []
for p in self.device.enumerate_processes():
for k, v in Vendor.SDK_VERSIONS.items():
if p.name == v[2]:
session: Session = self.device.attach(p.name)
script: Script = session.create_script(self.script)
script.load()
if script.exports_sync.getlibrary(v[3]):
details.append(k)
session.detach()
if not details:
return Vendor.from_sdk_api(self.sdk_api)
# Find the closest SDK version to the current one, preferring lower matches in case of a tie.
sdk_api = min(details, key=lambda x: abs(x - self.sdk_api))
if sdk_api == Vendor.SDK_MAX and self.sdk_api > Vendor.SDK_MAX:
sdk_api = self.sdk_api
elif sdk_api != self.sdk_api:
self.logger.warning('Non-default Widevine version for SDK %s', sdk_api)
return Vendor.from_sdk_api(sdk_api)
def _process_message(self, message: dict, data: bytes) -> None:
"""
@@ -132,36 +206,6 @@ class Cdm:
else:
self.logger.warning('Failed to intercept the private key')
def _prepare_vendor(self) -> Vendor:
"""
Prepares and selects the most compatible vendor version based on the device's processes.
"""
details: [int] = []
for p in self.device.enumerate_processes():
for k, v in Vendor.SDK_VERSIONS.items():
if p.name == v[2]:
session: Session = self.device.attach(p.name)
script: Script = session.create_script(self.script)
script.load()
try:
script.exports_sync.getlibrary(v[3])
details.append(k)
except RPCException:
pass
session.detach()
if not details:
return Vendor.from_sdk_api(self.sdk_api)
# Find the closest SDK version to the current one, preferring lower matches in case of a tie.
sdk_api = min(details, key=lambda x: abs(x - self.sdk_api))
if sdk_api == Vendor.SDK_MAX and self.sdk_api > Vendor.SDK_MAX:
sdk_api = self.sdk_api
elif sdk_api != self.sdk_api:
self.logger.warning('Non-default Widevine version for SDK %s', sdk_api)
return Vendor.from_sdk_api(sdk_api)
def hook_process(self, process: Process) -> bool:
"""
Hooks into the specified process to intercept DRM keys.
@@ -171,9 +215,8 @@ class Cdm:
script.on('message', self._process_message)
script.load()
try:
library_info = script.exports_sync.getlibrary(self.vendor.library)
library_info = script.exports_sync.getlibrary(self.vendor.library)
if library_info:
self.logger.info('Library: %s (%s)', library_info['name'], library_info['path'])
return script.exports_sync.hooklibrary(library_info['name'])
except RPCException:
return False
return False

View File

@@ -4,14 +4,10 @@
* Source: https://github.com/hyugogirubato/KeyDive
*/
const SDK_API = '${SDK_API}'; // Dynamically replaced with the actual SDK API level.
const OEM_CRYPTO_API = [
// Mapping of function names across different API levels (obfuscated names may vary).
'rnmsglvj', 'polorucp', 'kqzqahjq', 'pldrclfq', 'kgaitijd',
'cwkfcplc', 'crhqcdet', 'ulns', 'dnvffnze', 'ygjiljer',
'qbjxtubz', 'qkfrcjtw', 'rbhjspoh'
// Add more as needed for different versions.
];
// Placeholder values dynamically replaced at runtime.
const SDK_API = parseInt('${SDK_API}', 10);
const OEM_CRYPTO_API = JSON.parse('${OEM_CRYPTO_API}');
const SYMBOLS = JSON.parse('${SYMBOLS}');
// Logging levels to synchronize with Python's logging module.
@@ -65,17 +61,37 @@ const print = (level, message) => {
}
// Identifies and returns the specified library.
const getLibrary = (name) => Process.getModuleByName(name);
const getLibrary = (name) => {
try {
return Process.getModuleByName(name);
} catch (e) {
return undefined;
}
};
// Hooks into specified functions within a library, aiming to extract keys and disable privacy mode.
const hookLibrary = (name) => {
// https://github.com/poxyran/misc/blob/master/frida-enumerate-imports.py
const library = getLibrary(name);
const functions = [...library.enumerateExports(), ...library.enumerateImports()];
const targetFunction = functions.find(func => OEM_CRYPTO_API.includes(func.name));
if (!library) return false;
let functions, target;
if (SYMBOLS.length > 0) {
functions = SYMBOLS.map(symbol => ({
'type': 'function',
'name': symbol.name,
'address': ptr(parseInt(symbol.address, 16) + parseInt(library.base, 16))
}));
print(Level.INFO, 'Successfully imported functions');
} else {
functions = [...library.enumerateExports(), ...library.enumerateImports()];
target = functions.find(func => OEM_CRYPTO_API.includes(func.name));
}
let hookedCount = 0;
functions.forEach((func) => {
if (func.type !== 'function') return;
const funcName = func.name;
const funcAddr = func.address;
@@ -85,7 +101,7 @@ const hookLibrary = (name) => {
disablePrivacyMode(funcAddr);
} else if (funcName.includes('PrepareKeyRequest')) {
prepareKeyRequest(funcAddr);
} else if (targetFunction === func || (!targetFunction && funcName.match(/^[a-z]+$/))) {
} else if (target === func || (!target && funcName.match(/^[a-z]+$/))) {
getPrivateKey(funcAddr);
} else {
funcHooked = false;
@@ -96,7 +112,7 @@ const hookLibrary = (name) => {
print(Level.DEBUG, `Hooked (${funcAddr}): ${funcName}`);
}
} catch (e) {
print(Level.ERROR, `${funcName} (${funcAddr}): ${e.message}`);
print(Level.ERROR, `${e.message} for ${funcName}`);
}
});

View File

@@ -1,10 +1,13 @@
import argparse
import logging
import subprocess
import time
import coloredlogs
from _frida import Process
from pathlib import Path
import extractor
from extractor.cdm import Cdm
coloredlogs.install(
@@ -17,12 +20,20 @@ if __name__ == '__main__':
# Parse command line arguments for device ID
parser = argparse.ArgumentParser(description='Extract Widevine L3 keys from an Android device.')
parser.add_argument('--device', type=str, help='Target Android device ID.')
parser.add_argument('-d', '--device', required=False, type=str, help='Target Android device ID.')
parser.add_argument('-f', '--functions', required=False, type=Path, help='Ghidra XML functions file.')
args = parser.parse_args()
try:
logger.info('Version: %s', extractor.__version__)
# Start ADB server
exitcode, _ = subprocess.getstatusoutput('adb start-server')
if exitcode != 0:
raise EnvironmentError('ADB is not recognized as an environment variable')
# Initialize CDM handler with given device
cdm = Cdm(device=args.device)
cdm = Cdm(device=args.device, functions=args.functions)
# Find Widevine process on the device
process: Process = next((p for p in cdm.device.enumerate_processes() if cdm.vendor.process == p.name), None)

View File

@@ -2,4 +2,5 @@ frida~=16.1.4
pathlib~=1.0.1
coloredlogs~=15.0.1
pycryptodomex~=3.20.0
protobuf~=4.25.1
protobuf~=4.25.1
xmltodict~=0.13.0