Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da97eca0e1 | ||
|
|
a09f3c5590 | ||
|
|
24d400e7ff | ||
|
|
1d2d1b356d | ||
|
|
56a2e0ea45 | ||
|
|
6fbafa9fb0 | ||
|
|
276208c472 | ||
|
|
80da61e0bb | ||
|
|
da3b1f95d9 | ||
|
|
9554598970 | ||
|
|
d8a8273db6 | ||
|
|
305d861f11 | ||
|
|
12365bd00e | ||
|
|
d6f81e8259 | ||
|
|
73dd21db8d | ||
|
|
4942c95ee3 | ||
|
|
20dc3aceae | ||
|
|
782b105182 | ||
|
|
39ba4e8f70 | ||
|
|
9779db14fe | ||
|
|
09391e8dac | ||
|
|
c9b2f8975c | ||
|
|
00ecd4e1c7 | ||
|
|
4caf035425 | ||
|
|
24c07aae4c | ||
|
|
1bccbcc284 | ||
|
|
b59551c708 | ||
|
|
539a895541 | ||
|
|
a9b86aa2c1 | ||
|
|
262efe8c0c | ||
|
|
3910b2b531 |
103
CHANGELOG.md
Normal file
103
CHANGELOG.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.6] - 2024-04-26
|
||||
|
||||
### Added
|
||||
|
||||
- Added `mksrc` script to manually improve Android shell interaction.
|
||||
- Added `editor` script for a text editor within the Android shell.
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed example from the XML functions to prevent misunderstandings.
|
||||
- Enhanced the Android shell functionality.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed encoding issues with ADB commands, addressing issue [#3](https://github.com/hyugogirubato/KeyDive/issues/3).
|
||||
|
||||
## [1.0.5] - 2024-04-08
|
||||
|
||||
### Added
|
||||
|
||||
- Added a function known from SDK 33 (arm64-v8a).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed import analysis that was causing the JavaScript script to crash.
|
||||
|
||||
## [1.0.4] - 2024-04-06
|
||||
|
||||
### Added
|
||||
|
||||
- Added the `--force` option to use the default vendor, bypassing analysis.
|
||||
- Progress information for analysis stages.
|
||||
- Support for Android 14.
|
||||
- Error message for using SDK version 34 and above without an XML functions file.
|
||||
- Documentation links for certain error messages.
|
||||
|
||||
### Changed
|
||||
|
||||
- Switched from Frida to ADB for listing processes due to a [Frida issue](https://github.com/frida/frida/issues/1225#issuecomment-604181822).
|
||||
- Optimized process search to improve performance.
|
||||
- Improved error reporting when the Widevine process is not detected.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed compatibility with buggy `frida-server` versions by using direct PID attachment.
|
||||
- Updated the script handling for non-standard version scenarios.
|
||||
|
||||
## [1.0.3] - 2024-04-01
|
||||
|
||||
### Added
|
||||
|
||||
- Environment check for ADB and automatic start if not running.
|
||||
- Extraction function support for SDK version 34 and above.
|
||||
- Simplified command-line argument processing.
|
||||
|
||||
### Changed
|
||||
|
||||
- Enhanced error handling to avoid Frida library hook errors.
|
||||
- Transitioned from using symbols to functions for better clarity and efficiency.
|
||||
- Display of loaded script for improved debugging and verification.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolved target analysis issues, ensuring accurate process targeting.
|
||||
- Corrected function argument count errors for more robust script execution.
|
||||
- Fixed function selection by name to accurately identify and use the correct functions.
|
||||
|
||||
## [1.0.2] - 2024-03-31
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for interpreting and using symbols, enhancing analysis capabilities.
|
||||
|
||||
### Changed
|
||||
|
||||
- Optimized analysis logic during the hook process for increased efficiency.
|
||||
- Improved script generation process for more reliable and effective hooking.
|
||||
|
||||
## [1.0.1] - 2024-03-31
|
||||
|
||||
### Added
|
||||
|
||||
- Introduced support for non-standard version handling, accommodating a wider range of target applications.
|
||||
|
||||
## [1.0.0] - 2024-03-30
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release of the project, laying the foundation for future enhancements and features.
|
||||
|
||||
[1.0.6]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.6
|
||||
[1.0.5]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.5
|
||||
[1.0.4]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.4
|
||||
[1.0.3]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.3
|
||||
[1.0.2]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.2
|
||||
[1.0.1]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.1
|
||||
[1.0.0]: https://github.com/hyugogirubato/KeyDive/releases/tag/v1.0.0
|
||||
32
README.md
32
README.md
@@ -2,9 +2,9 @@
|
||||
|
||||
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.
|
||||
> Support for Android 14+ (SDK > 33) require the use of functions extracted from Ghidra.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -33,31 +33,35 @@ 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
|
||||
|
||||
```shell
|
||||
usage: keydive.py [-h] [--device DEVICE] [--symbols SYMBOLS]
|
||||
usage: keydive.py [-h] [-d DEVICE] [-f FUNCTIONS] [--force]
|
||||
|
||||
Extract Widevine L3 keys from an Android device.
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--device DEVICE Target Android device ID.
|
||||
--symbols SYMBOLS Ghidra XML symbols file.
|
||||
-h, --help show this help message and exit
|
||||
-d DEVICE, --device DEVICE
|
||||
Target Android device ID.
|
||||
-f FUNCTIONS, --functions FUNCTIONS
|
||||
Path to Ghidra XML functions file.
|
||||
--force Force using the default vendor (skipping analysis).
|
||||
|
||||
```
|
||||
|
||||
### Extracting Symbols for Advanced Usage
|
||||
### Extracting Functions for Advanced Usage
|
||||
|
||||
For advanced users looking to use custom symbols with KeyDive, a comprehensive guide on extracting symbols from Widevine libraries using Ghidra is available. Please refer to our [Symbols Extraction Guide](./docs/SYMBOLS.md) for detailed instructions.
|
||||
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
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# Symbols
|
||||
# Functions
|
||||
|
||||
To utilize custom symbols with KeyDive, particularly when extracting Widevine L3 DRM keys from Android devices, you might need to generate a `symbols.xml` file using Ghidra. This file helps KeyDive accurately identify necessary symbols within the Widevine library, facilitating a more efficient extraction process. Below is a step-by-step guide on how to create a `symbols.xml` file using Ghidra:
|
||||
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 Symbols with 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.
|
||||
@@ -18,29 +18,27 @@ Ensure you have Ghidra installed on your system. If not, download it from the [G
|
||||
- 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 Symbols as XML
|
||||
#### 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
|
||||
- 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.
|
||||
|
||||
" and ensure that only the "Symbols" option is selected. This step is crucial as it filters the export to include only the symbols necessary for KeyDive, making the XML file more manageable and relevant.
|
||||
- Choose a destination for the `symbols.xml` file and confirm the export.
|
||||
|
||||
#### 5. Using the Symbols with KeyDive
|
||||
Once you have the `symbols.xml` file:
|
||||
#### 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 `--symbols` argument to specify the path to your `symbols.xml` file. For example:
|
||||
- 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> --symbols /path/to/symbols_x86.xml
|
||||
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 Symbols:** The `symbols.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.
|
||||
- **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 symbols you are specifically interested in. This can significantly reduce analysis time.
|
||||
- **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 `symbols.xml` file that aids in the effective use of KeyDive for educational, research, or security analysis purposes.
|
||||
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.
|
||||
@@ -1,17 +0,0 @@
|
||||
|
||||
alias ls='ls --color=auto'
|
||||
alias grep='grep --color=auto'
|
||||
alias fgrep='fgrep --color=auto'
|
||||
alias egrep='egrep --color=auto'
|
||||
alias logcat='logcat -v color'
|
||||
|
||||
alias ll='ls -alF'
|
||||
alias la='ls -A'
|
||||
alias l='ls -CF'
|
||||
alias ipa='ip -c a'
|
||||
alias rm='rm -rf'
|
||||
|
||||
tree() {
|
||||
path=${1:-.}
|
||||
find ${path} -print | sort | sed 's;[^/]*/;|---;g;s;---|; |;g'
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -1,19 +0,0 @@
|
||||
[
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=android.hardware.drm-service.widevine, base=0x5ac8c61f0000, size=2727936, path=/apex/com.google.android.widevine/bin/hw/android.hardware.drm-service.widevine)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=linker64, base=0x7a1e2ec4d000, size=290816, path=/system/bin/linker64)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libcrypto.so, base=0x7a1e29844000, size=1617920, path=/apex/com.google.android.widevine/lib64/libcrypto.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=liblog.so, base=0x7a1e2b2ad000, size=73728, path=/system/lib64/liblog.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libbinder_ndk.so, base=0x7a1e2e8c7000, size=118784, path=/system/lib64/libbinder_ndk.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libc.so, base=0x7a1e2820c000, size=5476352, path=/apex/com.android.runtime/lib64/bionic/libc.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libm.so, base=0x7a1e2d489000, size=278528, path=/apex/com.android.runtime/lib64/bionic/libm.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libdl.so, base=0x7a1e29820000, size=20480, path=/apex/com.android.runtime/lib64/bionic/libdl.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libc++.so, base=0x7a1e2b2e3000, size=749568, path=/system/lib64/libc++.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libbinder.so, base=0x7a1e2d512000, size=888832, path=/system/lib64/libbinder.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libutils.so, base=0x7a1e2b24b000, size=126976, path=/system/lib64/libutils.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libcutils.so, base=0x7a1e2878a000, size=102400, path=/system/lib64/libcutils.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libvndksupport.so, base=0x7a1e2d477000, size=16384, path=/system/lib64/libvndksupport.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libbase.so, base=0x7a1e2e860000, size=274432, path=/system/lib64/libbase.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libdl_android.so, base=0x7a1e287d1000, size=12288, path=/apex/com.android.runtime/lib64/bionic/libdl_android.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=libnetd_client.so, base=0x7a1c180e3000, size=45056, path=/system/lib64/libnetd_client.so)",
|
||||
"Library(process=Process(pid=5399, name=\"android.hardware.drm-service.widevine\", parameters={}), name=linux-vdso.so.1, base=0x7fff16bf7000, size=4096, path=linux-vdso.so.1)"
|
||||
]
|
||||
@@ -1,39 +0,0 @@
|
||||
/data_mirror/misc_de/null/0/apexdata/com.google.android.widevine
|
||||
/data_mirror/misc_ce/null/0/apexdata/com.google.android.widevine
|
||||
/vendor/apex/com.google.android.widevine.nonupdatable.apex
|
||||
/linkerconfig/com.google.android.widevine
|
||||
/linkerconfig/com.google.android.widevine/ld.config.txt
|
||||
/dev/f3o/.magisk/mirror/data/misc/apexdata/com.google.android.widevine
|
||||
/dev/f3o/.magisk/mirror/data/system/package_cache/d499bb796bfde9867e4d86a4c6309a64a05dfb0d/com.google.android.widevine.nonupdatable.apex-16--1978776024
|
||||
/dev/f3o/.magisk/mirror/data/misc_ce/0/apexdata/com.google.android.widevine
|
||||
/dev/f3o/.magisk/mirror/data/misc_de/0/apexdata/com.google.android.widevine
|
||||
/dev/f3o/.magisk/mirror/vendor/apex/com.google.android.widevine.nonupdatable.apex
|
||||
find: '/dev/f3o/.magisk/mirror/system_root': loop detected
|
||||
/data/misc/apexdata/com.google.android.widevine
|
||||
/data/system/package_cache/d499bb796bfde9867e4d86a4c6309a64a05dfb0d/com.google.android.widevine.nonupdatable.apex-16--1978776024
|
||||
/data/misc_ce/0/apexdata/com.google.android.widevine
|
||||
/data/misc_de/0/apexdata/com.google.android.widevine
|
||||
/apex/com.google.android.widevine
|
||||
/apex/com.google.android.widevine/lost+found
|
||||
/apex/com.google.android.widevine/bin
|
||||
/apex/com.google.android.widevine/bin/hw
|
||||
/apex/com.google.android.widevine/bin/hw/android.hardware.drm-service.widevine
|
||||
/apex/com.google.android.widevine/lib64
|
||||
/apex/com.google.android.widevine/lib64/libcrypto.so
|
||||
/apex/com.google.android.widevine/apex_manifest.pb
|
||||
/apex/com.google.android.widevine/etc
|
||||
/apex/com.google.android.widevine/etc/com.google.android.widevine.rc
|
||||
/apex/com.google.android.widevine/etc/vintf
|
||||
/apex/com.google.android.widevine/etc/vintf/com.google.android.widevine.xml
|
||||
/apex/com.google.android.widevine@340720000
|
||||
/apex/com.google.android.widevine@340720000/lost+found
|
||||
/apex/com.google.android.widevine@340720000/bin
|
||||
/apex/com.google.android.widevine@340720000/bin/hw
|
||||
/apex/com.google.android.widevine@340720000/bin/hw/android.hardware.drm-service.widevine
|
||||
/apex/com.google.android.widevine@340720000/lib64
|
||||
/apex/com.google.android.widevine@340720000/lib64/libcrypto.so
|
||||
/apex/com.google.android.widevine@340720000/apex_manifest.pb
|
||||
/apex/com.google.android.widevine@340720000/etc
|
||||
/apex/com.google.android.widevine@340720000/etc/com.google.android.widevine.rc
|
||||
/apex/com.google.android.widevine@340720000/etc/vintf
|
||||
/apex/com.google.android.widevine@340720000/etc/vintf/com.google.android.widevine.xml
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
from .cdm import *
|
||||
from .vendor import *
|
||||
|
||||
__version__ = '1.0.1'
|
||||
__version__ = '1.0.7'
|
||||
|
||||
204
extractor/cdm.py
204
extractor/cdm.py
@@ -4,10 +4,9 @@ import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
import xmltodict
|
||||
from _frida import Process
|
||||
from frida.core import Device, Session, Script, RPCException
|
||||
import frida
|
||||
from frida.core import Device, Session, Script
|
||||
from Cryptodome.PublicKey import RSA
|
||||
|
||||
from extractor.license_protocol_pb2 import SignedMessage, LicenseRequest, ClientIdentification, DrmCertificate, SignedDrmCertificate
|
||||
@@ -20,30 +19,43 @@ class Cdm:
|
||||
"""
|
||||
Manages the capture and processing of DRM keys from a specified device using Frida to inject custom hooks.
|
||||
"""
|
||||
OEM_CRYPTO_API = [
|
||||
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'
|
||||
'qbjxtubz', 'qkfrcjtw', 'rbhjspoh', 'zgtjmxko', 'igrqajte'
|
||||
# Add more as needed for different versions.
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, device: str = None, symbols: Path = None):
|
||||
def __init__(self, device: str = None, functions: Path = None, force: bool = False):
|
||||
self.logger = logging.getLogger('Cdm')
|
||||
self.functions = functions
|
||||
self.running = True
|
||||
self.keys = {}
|
||||
# Select device based on provided ID or default to the first USB device.
|
||||
self.device: Device = frida.get_device(id=device, timeout=5) if device else frida.get_usb_device(timeout=5)
|
||||
self.logger.info('Device: %s (%s)', self.device.name, self.device.id)
|
||||
|
||||
# Fetch and log device properties
|
||||
# Obtain device properties
|
||||
self.properties = self._fetch_device_properties()
|
||||
|
||||
self.sdk_api = self.properties['ro.build.version.sdk']
|
||||
self.logger.info('SDK API: %s', self.sdk_api)
|
||||
self.logger.info('ABI CPU: %s', self.properties['ro.product.cpu.abi'])
|
||||
|
||||
# Determine vendor based on SDK API
|
||||
self.script = self._prepare_hook_script(symbols)
|
||||
self.vendor = self._prepare_vendor()
|
||||
# Load the hook scrip
|
||||
self.script = self._prepare_hook_script()
|
||||
self.logger.info('Script loaded successfully')
|
||||
|
||||
# Determine vendor based on device SDK API
|
||||
vendor_api = self._prepare_vendor_api(force=force)
|
||||
self.vendor = Vendor.from_sdk_api(vendor_api)
|
||||
|
||||
# Update script for specific vendor API, if necessary
|
||||
if vendor_api != self.sdk_api:
|
||||
self.sdk_api = vendor_api
|
||||
self.script = self._prepare_hook_script()
|
||||
self.logger.info('Script updated for vendor API')
|
||||
|
||||
def _fetch_device_properties(self) -> dict:
|
||||
"""
|
||||
@@ -51,7 +63,8 @@ class Cdm:
|
||||
"""
|
||||
# https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands
|
||||
properties = {}
|
||||
for line in subprocess.getoutput(f'adb -s "{self.device.id}" shell getprop').splitlines():
|
||||
sp = subprocess.run(f'adb -s "{self.device.id}" shell getprop', capture_output=True)
|
||||
for line in sp.stdout.decode('utf-8').splitlines():
|
||||
match = re.match(r'\[(.*?)\]: \[(.*?)\]', line)
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
@@ -64,71 +77,113 @@ class Cdm:
|
||||
properties[key] = value
|
||||
return properties
|
||||
|
||||
def _prepare_hook_script(self, path: Path) -> str:
|
||||
def _prepare_hook_script(self) -> str:
|
||||
"""
|
||||
Prepares and returns the hook script by replacing placeholders with actual values, including
|
||||
SDK API version and selected symbols from a given XML file.
|
||||
Prepares the Frida hook script, injecting dynamic content like SDK API and selected functions.
|
||||
"""
|
||||
symbols = {}
|
||||
if path:
|
||||
try:
|
||||
# Parse the XML file
|
||||
program = xmltodict.parse(path.read_bytes())['PROGRAM']
|
||||
base_addr = int(program['@IMAGE_BASE'], 16)
|
||||
functions: [dict] = program['SYMBOL_TABLE']['SYMBOL']
|
||||
|
||||
# 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 symbols
|
||||
for func in functions:
|
||||
name = func['@NAME']
|
||||
|
||||
# Add symbol if it matches specific criteria
|
||||
if any(keyword in name for keyword in ['UsePrivacyMode', 'PrepareKeyRequest']) or name == target or (not target and re.match(r'^[a-z]+$', name)):
|
||||
addr = hex(int(func['@ADDRESS'], 16) - base_addr)
|
||||
symbols[addr] = {'name': name, 'address': addr}
|
||||
except Exception:
|
||||
raise ValueError('Failed to extract symbols 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(self.OEM_CRYPTO_API))
|
||||
content = content.replace('${SYMBOLS}', json.dumps(list(symbols.values())))
|
||||
selected = self._select_functions() if self.functions else {}
|
||||
|
||||
# Replace placeholders in script template
|
||||
replacements = {
|
||||
'${SDK_API}': str(self.sdk_api),
|
||||
'${OEM_CRYPTO_API}': json.dumps(list(self.OEM_CRYPTO_API)),
|
||||
'${SYMBOLS}': json.dumps(list(selected.values())),
|
||||
}
|
||||
|
||||
for placeholder, real_value in replacements.items():
|
||||
content = content.replace(placeholder, real_value)
|
||||
|
||||
return content
|
||||
|
||||
def _prepare_vendor(self) -> Vendor:
|
||||
def _select_functions(self) -> dict:
|
||||
"""
|
||||
Prepares and selects the most compatible vendor version based on the device's processes.
|
||||
Parses the provided XML functions file to select relevant functions.
|
||||
"""
|
||||
if not self.functions.is_file():
|
||||
raise FileNotFoundError('Functions file not found')
|
||||
|
||||
try:
|
||||
program = xmltodict.parse(self.functions.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
|
||||
selected = {}
|
||||
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)}
|
||||
return selected
|
||||
except Exception:
|
||||
pass
|
||||
raise ValueError('Failed to extract functions from Ghidra')
|
||||
|
||||
def enumerate_processes(self) -> dict:
|
||||
"""
|
||||
Lists processes running on the device, returning a mapping of process names to PIDs.
|
||||
"""
|
||||
# https://github.com/frida/frida/issues/1225#issuecomment-604181822
|
||||
# Iterate through lines starting from the second line (skipping header)
|
||||
processes = {}
|
||||
sp = subprocess.run(f'adb -s "{self.device.id}" shell ps', capture_output=True)
|
||||
for line in sp.stdout.decode('utf-8').splitlines()[1:]:
|
||||
try:
|
||||
line = line.split() # USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME
|
||||
name = ' '.join(line[8:]).strip()
|
||||
name = name if name.startswith('[') else Path(name).name
|
||||
processes[name] = int(line[1])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return processes
|
||||
|
||||
def _prepare_vendor_api(self, force: bool = False) -> int:
|
||||
"""
|
||||
Determines the most compatible vendor API version based on device processes.
|
||||
"""
|
||||
if force:
|
||||
self.logger.warning('Using default vendor due to force flag')
|
||||
return self.sdk_api
|
||||
|
||||
# Check if forcing is not enabled and enumerate 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()
|
||||
processes = self.enumerate_processes()
|
||||
for k, v in Vendor.SDK_VERSIONS.items():
|
||||
pid = processes.get(v[2])
|
||||
if pid:
|
||||
self.logger.debug('Analysing... (%s)', v[2])
|
||||
session: Session = self.device.attach(pid)
|
||||
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)
|
||||
# If no compatible versions found
|
||||
if details:
|
||||
# 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))
|
||||
|
||||
# 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)
|
||||
# Adjust SDK version if it exceeds the maximum supported version
|
||||
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('Using non-default Widevine version for SDK %s', sdk_api)
|
||||
|
||||
return Vendor.from_sdk_api(sdk_api)
|
||||
return sdk_api
|
||||
|
||||
raise EnvironmentError('Unable to detect Widevine, see: https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#drm-info')
|
||||
|
||||
def _process_message(self, message: dict, data: bytes) -> None:
|
||||
"""
|
||||
@@ -200,18 +255,25 @@ class Cdm:
|
||||
else:
|
||||
self.logger.warning('Failed to intercept the private key')
|
||||
|
||||
def hook_process(self, process: Process) -> bool:
|
||||
def hook_process(self, pid: int) -> bool:
|
||||
"""
|
||||
Hooks into the specified process to intercept DRM keys.
|
||||
"""
|
||||
session: Session = self.device.attach(process.name)
|
||||
session: Session = self.device.attach(pid)
|
||||
script: Script = session.create_script(self.script)
|
||||
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'])
|
||||
|
||||
# Check if Ghidra XML functions loaded
|
||||
if self.sdk_api > 33:
|
||||
if not self.functions:
|
||||
raise AttributeError('For SDK API > 33, specifying "functions" is required, see: https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md')
|
||||
elif self.functions:
|
||||
self.logger.warning('The "functions" attribute is deprecated for SDK API < 34')
|
||||
|
||||
return script.exports_sync.hooklibrary(library_info['name'])
|
||||
except RPCException:
|
||||
return False
|
||||
return False
|
||||
|
||||
@@ -61,7 +61,13 @@ 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) => {
|
||||
@@ -69,20 +75,19 @@ const hookLibrary = (name) => {
|
||||
const library = getLibrary(name);
|
||||
if (!library) return false;
|
||||
|
||||
let functions;
|
||||
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 symbols');
|
||||
} else {
|
||||
functions = [...library.enumerateExports(), ...library.enumerateImports()];
|
||||
functions = library.enumerateExports();
|
||||
// functions = [...library.enumerateExports(), ...library.enumerateImports()];
|
||||
target = functions.find(func => OEM_CRYPTO_API.includes(func.name));
|
||||
}
|
||||
|
||||
const targetFunction = functions.find(func => OEM_CRYPTO_API.includes(func.name));
|
||||
|
||||
let hookedCount = 0;
|
||||
functions.forEach((func) => {
|
||||
if (func.type !== 'function') return;
|
||||
@@ -96,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;
|
||||
@@ -107,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}`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -134,8 +139,10 @@ const prepareKeyRequest = (address) => {
|
||||
index = 5;
|
||||
} else if ([24, 25, 26, 27, 28, 29, 30].includes(SDK_API)) {
|
||||
index = 4;
|
||||
} else if ([34].includes(SDK_API)) {
|
||||
index = 6;
|
||||
} else {
|
||||
index = 5; // Default index assignment
|
||||
index = 6; // Default index assignment
|
||||
print(Level.WARNING, 'SDK API not implemented');
|
||||
print(Level.WARNING, `Defaulting to args[${index}] for PrepareKeyRequest`);
|
||||
}
|
||||
|
||||
38
keydive.py
38
keydive.py
@@ -1,11 +1,12 @@
|
||||
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(
|
||||
@@ -18,27 +19,32 @@ 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', required=False, type=str, help='Target Android device ID.')
|
||||
parser.add_argument('--symbols', required=False, type=Path, help='Ghidra XML symbols file.')
|
||||
parser.add_argument('-d', '--device', required=False, type=str, help='Target Android device ID.')
|
||||
parser.add_argument('-f', '--functions', required=False, type=Path, help='Path to Ghidra XML functions file.')
|
||||
parser.add_argument('--force', required=False, action='store_true', help='Force using the default vendor (skipping analysis).')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
symbols = args.symbols
|
||||
if symbols and not symbols.is_file():
|
||||
raise FileNotFoundError('Symbols file not found')
|
||||
logger.info('Version: %s', extractor.__version__)
|
||||
|
||||
# Initialize CDM handler with given device
|
||||
cdm = Cdm(device=args.device, symbols=symbols)
|
||||
# Ensure the ADB server is running
|
||||
sp = subprocess.run('adb start-server', capture_output=True)
|
||||
if sp.returncode != 0:
|
||||
raise EnvironmentError('ADB is not recognized as an environment variable, see https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge')
|
||||
|
||||
# Find Widevine process on the device
|
||||
process: Process = next((p for p in cdm.device.enumerate_processes() if cdm.vendor.process == p.name), None)
|
||||
if not process:
|
||||
raise Exception('Failed to find Widevine process')
|
||||
logger.info('Process: %s (%s)', process.pid, process.name)
|
||||
# Initialize the CDM handler with the specified or default device
|
||||
cdm = Cdm(device=args.device, functions=args.functions, force=args.force)
|
||||
|
||||
# Hook into the process to extract DRM keys
|
||||
if not cdm.hook_process(process):
|
||||
raise Exception('Failed to hook into the process')
|
||||
# Attempt to locate and identify the Widevine process on the target device
|
||||
pid = cdm.enumerate_processes().get(cdm.vendor.process)
|
||||
if not pid:
|
||||
raise EnvironmentError('Widevine process not found on the device')
|
||||
logger.info('Process: %s (%s)', pid, cdm.vendor.process)
|
||||
|
||||
# Hook into the identified process for DRM key extraction
|
||||
if not cdm.hook_process(pid=pid):
|
||||
raise Exception('Failed to hook into the Widevine process')
|
||||
logger.info('Successfully hooked. To test, play a DRM-protected video: https://bitmovin.com/demos/drm')
|
||||
|
||||
# Keep script running while extracting keys
|
||||
|
||||
Reference in New Issue
Block a user