mirror of
https://github.com/hyugogirubato/KeyDive.git
synced 2025-04-30 00:24:25 +02:00
Compare commits
95 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d68dedc03a | ||
![]() |
3e4b9e54e7 | ||
![]() |
54457c8f0d | ||
![]() |
3628e42f45 | ||
![]() |
7c2f27df44 | ||
![]() |
480de09271 | ||
![]() |
fa86fd8392 | ||
![]() |
3c51fbbfa0 | ||
![]() |
fb8ae9d778 | ||
![]() |
9446f7dde8 | ||
![]() |
3f98c778d5 | ||
![]() |
9866784a9b | ||
![]() |
5ef8bf6f03 | ||
![]() |
f45cd6e681 | ||
![]() |
0f3ab497e8 | ||
![]() |
a9eb734b02 | ||
![]() |
5e53223e92 | ||
![]() |
73808d50c5 | ||
![]() |
bc50b55f5a | ||
![]() |
f0cff80f90 | ||
![]() |
dd83bf5d8a | ||
![]() |
bdb09fd16e | ||
![]() |
fcf893eeb8 | ||
![]() |
32ba8cd167 | ||
![]() |
79d1a0a41a | ||
![]() |
cae225c3db | ||
![]() |
c0943e101f | ||
![]() |
9f6ae602a4 | ||
![]() |
f2caf23f2c | ||
![]() |
61dd5849b7 | ||
![]() |
0c38979c95 | ||
![]() |
1bfcd52fd8 | ||
![]() |
8568eb6d99 | ||
![]() |
4e39a9956a | ||
![]() |
6737a9f9ee | ||
![]() |
57e349acca | ||
![]() |
2e86febb25 | ||
![]() |
52871ee505 | ||
![]() |
a5e7528842 | ||
![]() |
e2660a33dd | ||
![]() |
ec098e9aed | ||
![]() |
779c5a09e5 | ||
![]() |
97bee6f3ca | ||
![]() |
a8287299a5 | ||
![]() |
d0bc5ce68e | ||
![]() |
39f06d0ecd | ||
![]() |
bb4c479a77 | ||
![]() |
9cb9058ab8 | ||
![]() |
114a3eada1 | ||
![]() |
8b0fa92bfa | ||
![]() |
8f9b146a93 | ||
![]() |
f57cc87278 | ||
![]() |
3e42e642c7 | ||
![]() |
2246a641e6 | ||
![]() |
e47910819b | ||
![]() |
552b1b8e96 | ||
![]() |
c777e4e128 | ||
![]() |
c72d53e742 | ||
![]() |
ec0bd98436 | ||
![]() |
47b24c0d2c | ||
![]() |
ed043f20a0 | ||
![]() |
2e9bc0d9de | ||
![]() |
dcd5b38b3c | ||
![]() |
1a1f716f41 | ||
![]() |
9f51b3911b | ||
![]() |
228d2840c7 | ||
![]() |
e40dcc2e02 | ||
![]() |
6f4165cd59 | ||
![]() |
1f755f993d | ||
![]() |
bbea4d3467 | ||
![]() |
6f77ac89cf | ||
![]() |
6505ea9a5d | ||
![]() |
104d00b23c | ||
![]() |
1a6afdabd4 | ||
![]() |
a837cb0433 | ||
![]() |
f02162ddbe | ||
![]() |
736ff1c378 | ||
![]() |
0a619dfe4d | ||
![]() |
68428e06d5 | ||
![]() |
196ace40a4 | ||
![]() |
3f9ac64d20 | ||
![]() |
79a0d3fc3f | ||
![]() |
2fff361e79 | ||
![]() |
3316120552 | ||
![]() |
57e71699f1 | ||
![]() |
a3f0dac84c | ||
![]() |
372bb43f57 | ||
![]() |
9480d34151 | ||
![]() |
58d7d2a717 | ||
![]() |
123c7e726a | ||
![]() |
46e3ad9052 | ||
![]() |
08852fa082 | ||
![]() |
773efb8e30 | ||
![]() |
ff42c10db6 | ||
![]() |
d22cd7b858 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -171,5 +171,9 @@ poetry.toml
|
||||
pyrightconfig.json
|
||||
|
||||
### KeyDive ###
|
||||
*.xml
|
||||
device/
|
||||
logs/
|
||||
/docs/server/curl.txt
|
||||
/docs/server/provisioning.json
|
||||
/docs/debug.js
|
||||
|
121
CHANGELOG.md
121
CHANGELOG.md
@ -4,6 +4,120 @@ 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).
|
||||
|
||||
## [2.2.1] - 2025-03-01
|
||||
|
||||
### Added
|
||||
|
||||
- Added private key function.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Error extracting functions (symbols) for old libraries.
|
||||
|
||||
## [2.2.0] - 2025-01-19
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for dynamic interception without the need for Ghidra (available only for Frida server versions greater than 16.6.0).
|
||||
- Support for Android 16 developer version `Backlava` (SDK 36).
|
||||
|
||||
### Changed
|
||||
|
||||
- Added additional comments to help understand the script.
|
||||
- Optimized file path management in parameters.
|
||||
- Refactored the code globally.
|
||||
- Added glossary documentation for DRM/Widevine.
|
||||
- Restructured the documentation.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed inconsistency in logging messages for certain functions.
|
||||
- Fixed server-generated curl command issues.
|
||||
|
||||
## [2.1.5] - 2025-01-12
|
||||
|
||||
### Added
|
||||
|
||||
- Added private key function.
|
||||
|
||||
### Changed
|
||||
|
||||
- Searching for the library via pattern rather than by name.
|
||||
|
||||
## [2.1.4] - 2024-11-19
|
||||
|
||||
### Changed
|
||||
|
||||
- Library disabler error messages are now displayed in `DEBUG` mode for improved verbosity.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed errors in ADB shell messages.
|
||||
- Resolved issues with executing shell commands via `subprocess`.
|
||||
|
||||
## [2.1.3] - 2024-11-03
|
||||
|
||||
### Added
|
||||
|
||||
- Detection system for keybox data changes to prevent redundant exports.
|
||||
- Max API version available for plaintext keybox.
|
||||
|
||||
### Changed
|
||||
|
||||
- Encrypted keybox files are now exported with a `.enc` extension for clarity.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Issue with invalid keybox data preventing proper reception and export.
|
||||
- Device token encoding error for keybox data.
|
||||
|
||||
## [2.1.2] - 2024-11-02
|
||||
|
||||
#### Added
|
||||
|
||||
- Descriptions for functions used by the Frida script.
|
||||
- Support for dumping the keybox from older versions of CDM.
|
||||
|
||||
### Changed
|
||||
|
||||
- Replaced `libc`-based keybox interception with a native function.
|
||||
- Adjusted player/auto options to execute before DRM detection, enhancing detection on legacy devices.
|
||||
- Improved handling for displaying varying keybox contents based on the device ID.
|
||||
- Streamlined JS function detection for better performance.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resolved startup issue with the Widevine service when launching the script.
|
||||
- Addressed unsupported error with the new `ADB` class.
|
||||
- Fixed detection of the `MAIN` activity in applications.
|
||||
- Corrected parsing errors when listing applications.
|
||||
- Improved detection of minimum required functions.
|
||||
|
||||
## [2.1.1] - 2024-10-28
|
||||
|
||||
### Added
|
||||
|
||||
- Private key functionality for enhanced key extraction security.
|
||||
- Local DRM server for (almost) offline use.
|
||||
- Option to import the private key for easier management.
|
||||
- Automatic installation and usage of a local player.
|
||||
- New `Advanced` group for better argument organization.
|
||||
- Experimental keybox extraction from the device.
|
||||
- Added CDM details for SDK 35.
|
||||
|
||||
### Changed
|
||||
|
||||
- Device interactions migrated to the `ADB` class for better encapsulation.
|
||||
- Code comments added and optimizations made for clarity.
|
||||
- Displaying the `GetDeviceId` function name.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improved error handling for shell commands.
|
||||
- `xmltodict` is now a required dependency.
|
||||
- Enhanced formatting of `logging` messages for better readability.
|
||||
- Error in skip option for using a function file.
|
||||
|
||||
## [2.1.0] - 2024-10-20
|
||||
|
||||
### Added
|
||||
@ -332,6 +446,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
|
||||
- Initial release of the project, laying the foundation for future enhancements and features.
|
||||
|
||||
[2.2.1]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.2.1
|
||||
[2.2.0]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.2.0
|
||||
[2.1.5]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.1.5
|
||||
[2.1.4]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.1.4
|
||||
[2.1.3]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.1.3
|
||||
[2.1.2]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.1.2
|
||||
[2.1.1]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.1.1
|
||||
[2.1.0]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.1.0
|
||||
[2.0.9]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.0.9
|
||||
[2.0.8]: https://github.com/hyugogirubato/KeyDive/releases/tag/v2.0.8
|
||||
|
61
README.md
61
README.md
@ -3,7 +3,7 @@
|
||||
KeyDive is a sophisticated Python script designed for 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.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Support for OEM API 18+ (SDK > 33) requires the use of functions extracted from Ghidra.
|
||||
> A minimum version of `frida-server 16.6.0` is required for dynamic dumps on OEM API 18+ (SDK > 33). Otherwise, extracted functions from Ghidra are required.
|
||||
|
||||
## Features
|
||||
|
||||
@ -28,51 +28,63 @@ Before you begin, ensure you have the following prerequisites in place:
|
||||
Follow these steps to set up KeyDive:
|
||||
|
||||
1. Ensure all prerequisites are met (see above).
|
||||
2. Install KeyDive from PyPI using Poetry:
|
||||
2. Install KeyDive directly from PyPI:
|
||||
```shell
|
||||
pip install keydive
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
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 for decryption.
|
||||
KeyDive enables secure extraction of Widevine L3 keys in a straightforward sequence:
|
||||
|
||||
1. Run the KeyDive script:
|
||||
```bash
|
||||
keydive -kwp
|
||||
```
|
||||
2. The script will install and launch the [Kaltura](https://github.com/kaltura/kaltura-device-info-android) DRM test app (if not already installed).
|
||||
3. Follow these steps within the app:
|
||||
- **Provision Widevine** (if the device isn't provisioned).
|
||||
- **Refresh** to intercept the keybox or private key.
|
||||
- **Test DRM Playback** to extract the challenge.
|
||||
4. KeyDive automatically captures the Widevine keys, saving them as:
|
||||
- `client_id.bin` (device identification data).
|
||||
- `private_key.pem` (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 [-h] [-d <id>] [-v] [-l <dir>] [--delay <delay>] [--version] [-a] [-c <file>] [-w] [-o <dir>] [-f <file>]
|
||||
usage: keydive [-h] [-d <id>] [-v] [-l <dir>] [--delay <delay>] [--version] [-o <dir>] [-w] [-s] [-a] [-p] [-f <file>] [-k] [--challenge <file>] [--private-key <file>]
|
||||
|
||||
Extract Widevine L3 keys from an Android device.
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
|
||||
Global options:
|
||||
Global:
|
||||
-d <id>, --device <id>
|
||||
Specify the target Android device ID to connect with via ADB.
|
||||
Specify the target Android device ID for ADB connection.
|
||||
-v, --verbose Enable verbose logging for detailed debug output.
|
||||
-l <dir>, --log <dir>
|
||||
Directory to store log files.
|
||||
--delay <delay> Delay (in seconds) between process checks in the watcher.
|
||||
--delay <delay> Delay (in seconds) between process checks.
|
||||
--version Display KeyDive version information.
|
||||
|
||||
Cdm options:
|
||||
-a, --auto Automatically open Bitmovin's demo.
|
||||
-c <file>, --challenge <file>
|
||||
Path to unencrypted challenge for extracting client ID.
|
||||
-w, --wvd Generate a pywidevine WVD device file.
|
||||
Cdm:
|
||||
-o <dir>, --output <dir>
|
||||
Output directory path for extracted data.
|
||||
Output directory for extracted data.
|
||||
-w, --wvd Generate a pywidevine WVD device file.
|
||||
-s, --skip Skip auto-detection of the private function.
|
||||
-a, --auto Automatically start the Bitmovin web player.
|
||||
-p, --player Install and start the Kaltura app automatically.
|
||||
|
||||
Advanced:
|
||||
-f <file>, --functions <file>
|
||||
Path to Ghidra XML functions file.
|
||||
-s, --skip Skip auto-detect of private function.
|
||||
-k, --keybox Enable export of the Keybox data if it is available.
|
||||
--challenge <file> Path to unencrypted challenge for extracting client ID.
|
||||
--private-key <file> Path to private key for extracting client ID.
|
||||
|
||||
```
|
||||
|
||||
@ -80,18 +92,18 @@ Cdm options:
|
||||
|
||||
### Extracting Functions
|
||||
|
||||
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](https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.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](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/FUNCTIONS.md) for detailed instructions.
|
||||
|
||||
### Offline Extraction
|
||||
|
||||
KeyDive supports offline extraction mode for situations without internet access. This mode allows you to extract DRM keys directly from your Android device. Ensure all necessary dependencies are installed and follow the detailed [Offline Mode Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/Axinom/OFFLINE.md) for step-by-step instructions.
|
||||
KeyDive supports offline extraction mode for situations without internet access. This mode allows you to extract DRM keys directly from your Android device. Ensure all necessary dependencies are installed and follow the detailed [Offline Mode Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/OFFLINE.md) for step-by-step instructions.
|
||||
|
||||
### Obtaining Unencrypted Challenge Data
|
||||
|
||||
> [!NOTE]
|
||||
> Usage of unencrypted challenge is not required by default. It is only necessary when the script cannot extract the client id.
|
||||
|
||||
To extract the unencrypted challenge data required for KeyDive's advanced features, follow the steps outlined in our [Challenge Extraction Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/CHALLENGE.md). This data is crucial for analyzing DRM-protected content and enhancing your DRM key extraction capabilities.
|
||||
To extract the unencrypted challenge data required for KeyDive's advanced features, follow the steps outlined in our [Challenge Extraction Guide](https://github.com/hyugogirubato/KeyDive/blob/main/docs/advanced/CHALLENGE.md). This data is crucial for analyzing DRM-protected content and enhancing your DRM key extraction capabilities.
|
||||
|
||||
### Temporary Disabling L1 for L3 Extraction
|
||||
|
||||
@ -113,12 +125,11 @@ KeyDive is intended for educational and research purposes only. The use of this
|
||||
<a href="https://github.com/Nineteen93"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/107993263?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="Nineteen93"/></a>
|
||||
<a href="https://github.com/sn-o-w"><img src="https://images.weserv.nl/?url=avatars.githubusercontent.com/u/2406819?v=4&h=25&w=25&fit=cover&mask=circle&maxage=7d" alt="sn-o-w"/></a>
|
||||
|
||||
|
||||
## Licensing
|
||||
|
||||
This software is licensed under the terms of [MIT License](https://github.com/hyugogirubato/KeyDive/blob/main/LICENSE).
|
||||
You can find a copy of the license in the LICENSE file in the root folder.
|
||||
|
||||
* * *
|
||||
---
|
||||
|
||||
© hyugogirubato 2024
|
||||
© hyugogirubato 2025
|
Binary file not shown.
@ -1,47 +0,0 @@
|
||||
.method private isNetworkAvailable()Z
|
||||
.registers 2
|
||||
|
||||
# const-string v0, "connectivity"
|
||||
|
||||
# .line 139
|
||||
# invoke-virtual {p0, v0}, Lcom/axinom/drm/sample/activity/SampleChooserActivity;->getSystemService(Ljava/lang/String;)Ljava/lang/Object;
|
||||
|
||||
# move-result-object v0
|
||||
|
||||
# check-cast v0, Landroid/net/ConnectivityManager;
|
||||
|
||||
# if-eqz v0, :cond_f
|
||||
|
||||
# .line 142
|
||||
# invoke-virtual {v0}, Landroid/net/ConnectivityManager;->getActiveNetworkInfo()Landroid/net/NetworkInfo;
|
||||
|
||||
# move-result-object v0
|
||||
|
||||
# goto :goto_10
|
||||
|
||||
# :cond_f
|
||||
# const/4 v0, 0x0
|
||||
|
||||
# :goto_10
|
||||
# if-eqz v0, :cond_1a
|
||||
|
||||
# .line 144
|
||||
# invoke-virtual {v0}, Landroid/net/NetworkInfo;->isConnected()Z
|
||||
|
||||
# move-result v0
|
||||
|
||||
# if-eqz v0, :cond_1a
|
||||
|
||||
# const/4 v0, 0x1
|
||||
|
||||
# goto :goto_1b
|
||||
|
||||
# :cond_1a
|
||||
# const/4 v0, 0x0
|
||||
|
||||
# :goto_1b
|
||||
|
||||
const/4 v0, 0x1
|
||||
|
||||
return v0
|
||||
.end method
|
Binary file not shown.
Binary file not shown.
@ -1,47 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
# @Info: values to be patch
|
||||
PATCH = {
|
||||
"assets/samplelist.json": [
|
||||
[None, "samplelist.json"]
|
||||
],
|
||||
"com/axinom/drm/sample/activity/SampleChooserActivity.smali": [
|
||||
["SampleChooserActivity.smali", None]
|
||||
]
|
||||
}
|
||||
|
||||
# @Info: Keystore to sign the application
|
||||
KEYSTORE = {
|
||||
"algo": "RSA",
|
||||
"size": 2048,
|
||||
"sign": "SHA-256",
|
||||
"validity": 365 * 25,
|
||||
"password": "Axinom_PASSWORD",
|
||||
"alias": "Axinom_DRM_DEMO",
|
||||
"meta": {
|
||||
"common_name": "Axinom",
|
||||
"organizational_unit": "Front-End",
|
||||
"organization": "Axinom",
|
||||
"locality": "Tartu",
|
||||
"state": "Tartumaa",
|
||||
"country": "EE",
|
||||
}
|
||||
}
|
||||
|
||||
# @Info: Info about application
|
||||
METADATA = {
|
||||
# "name": "Axinom DRM Sample Player",
|
||||
"version": "202211021",
|
||||
"source": "https://github.com/Axinom/drm-sample-player-android",
|
||||
"input": "axinom.apk",
|
||||
"output": "axinom_signed.apk",
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
Path("config.yaml").write_text(yaml.dump({
|
||||
"metadata": METADATA,
|
||||
"keystore": KEYSTORE,
|
||||
"patch": PATCH
|
||||
}))
|
@ -1,26 +0,0 @@
|
||||
keystore:
|
||||
algo: RSA
|
||||
alias: Axinom_DRM_DEMO
|
||||
meta:
|
||||
common_name: Axinom
|
||||
country: EE
|
||||
locality: Tartu
|
||||
organization: Axinom
|
||||
organizational_unit: Front-End
|
||||
state: Tartumaa
|
||||
password: Axinom_PASSWORD
|
||||
sign: SHA-256
|
||||
size: 2048
|
||||
validity: 9125
|
||||
metadata:
|
||||
input: axinom.apk
|
||||
output: axinom_signed.apk
|
||||
source: https://github.com/Axinom/drm-sample-player-android
|
||||
version: '202211021'
|
||||
patch:
|
||||
assets/samplelist.json:
|
||||
- - null
|
||||
- samplelist.json
|
||||
com/axinom/drm/sample/activity/SampleChooserActivity.smali:
|
||||
- - SampleChooserActivity.smali
|
||||
- null
|
@ -1,260 +0,0 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import xmltodict
|
||||
import yaml
|
||||
|
||||
|
||||
def any2str(data: any) -> str:
|
||||
if isinstance(data, (bytes, bytearray)):
|
||||
data = data.decode("utf-8")
|
||||
|
||||
if isinstance(data, (dict, list)):
|
||||
data = json.dumps(data, indent=2, separators=(",", ":"))
|
||||
|
||||
return str(data)
|
||||
|
||||
|
||||
class Keystore:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
algo: str = "RSA",
|
||||
size: int = 2048,
|
||||
sign: str = "SHA-256",
|
||||
validity: int = 365,
|
||||
password: str = None,
|
||||
alias: str = None,
|
||||
meta: dict = None,
|
||||
path: Path = Path("..")
|
||||
):
|
||||
assert path.is_dir(), "Invalid Dir Path"
|
||||
assert algo in ["RSA", "EC", "DSA"], "Invalid Algorithm"
|
||||
assert sign in ["MD5", "SHA-1", "SHA-256", "SHA-512"], "Invalid Signature"
|
||||
|
||||
if algo == "RSA":
|
||||
assert size in [1024, 2048, 3072, 4096], "Invalid RSA Size"
|
||||
assert sign in ["MD5", "SHA-1", "SHA-256", "SHA-512"], "Invalid RSA Signature"
|
||||
elif algo == "EC":
|
||||
assert size in [192, 224, 256, 384, 521], "Invalid EC Size"
|
||||
assert sign in ["SHA-256", "SHA-512"], "Invalid EC Signature"
|
||||
elif algo == "DSA":
|
||||
assert size in [1024], "Invalid DSA Size"
|
||||
assert sign in ["SHA-1"], "Invalid DSA Signature"
|
||||
|
||||
self.algorithm = algo
|
||||
self.size = size
|
||||
self.signature = "{}with{}".format(
|
||||
sign.replace("-", ""),
|
||||
"ECDSA" if algo == "EC" else algo
|
||||
)
|
||||
self.digest = sign
|
||||
self.validity = validity
|
||||
meta = meta if meta else {}
|
||||
self.metadata = {
|
||||
"common_name": meta.get("common_name", "Unknown"),
|
||||
"organizational_unit": meta.get("organizational_unit", "Unknown"),
|
||||
"organization": meta.get("organization", "Unknown"),
|
||||
"locality": meta.get("locality", "Unknown"),
|
||||
"state": meta.get("state", "Unknown"),
|
||||
"country": meta.get("country", "Unknown"),
|
||||
}
|
||||
|
||||
match = re.search(r'[\s:]?([a-zA-Z]+)', self.metadata["common_name"])
|
||||
name = re.sub(r'[^A-Za-z0-9]', "", match.group(1)).lower() if match else "keystore"
|
||||
self.path = path / f"{name}_{algo.lower()}.p12"
|
||||
self.password = password or f"{name}_password"
|
||||
self.alias = alias or f"{name}_alias"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return json.dumps({
|
||||
"path": str(self.path),
|
||||
"algorithm": self.algorithm,
|
||||
"size": self.size,
|
||||
"signature": self.signature,
|
||||
"digest": self.digest,
|
||||
"validity": self.validity,
|
||||
"password": self.password,
|
||||
"alias": self.alias,
|
||||
"metadata": self.metadata
|
||||
}, indent=2)
|
||||
|
||||
def sign(self, path: Path) -> None:
|
||||
assert path.is_file() and path.suffix == ".apk", "Invalid APK Path"
|
||||
|
||||
if not self.path.is_file():
|
||||
tmp = Path("keystore.jks")
|
||||
os.system(
|
||||
'keytool -genkeypair -keystore "{}" -alias "{}" -keyalg "{}" -keysize "{}" -sigalg "{}" -validity "{}" -storepass "{}" -keypass "{}" -dname "CN=\\"{}\\", OU=\\"{}\\", O=\\"{}\\", L=\\"{}\\", ST=\\"{}\\", C=\\"{}\\"" -noprompt'.format(
|
||||
tmp, self.alias, self.algorithm, self.size, self.signature,
|
||||
self.validity, self.password, self.password, self.metadata["common_name"],
|
||||
self.metadata["organizational_unit"], self.metadata["organization"],
|
||||
self.metadata["locality"], self.metadata["state"], self.metadata["country"]
|
||||
))
|
||||
|
||||
os.system(
|
||||
'keytool -importkeystore -srckeystore "{}" -srcstorepass "{}" -destkeystore "{}" -deststoretype "PKCS12" -deststorepass "{}" -destkeypass "{}" -srcalias "{}"'.format(
|
||||
tmp, self.password, self.path, self.password, self.password, self.alias
|
||||
))
|
||||
tmp.unlink(missing_ok=True)
|
||||
os.system('apksigner sign --ks "{}" --ks-key-alias "{}" --ks-pass "pass:{}" --key-pass "pass:{}" "{}"'.format(
|
||||
self.path, self.alias, self.password, self.password, path
|
||||
))
|
||||
Path(str(path) + ".idsig").unlink(missing_ok=True)
|
||||
|
||||
def info(self, path: Path) -> None:
|
||||
assert path.is_file() and path.suffix == ".apk", "Invalid APK Path"
|
||||
os.system(f'apksigner verify --print-certs "{path}"')
|
||||
|
||||
|
||||
class ApkTool:
|
||||
|
||||
def __init__(self, instance: Path = Path(".apktool")):
|
||||
self.instance = instance
|
||||
|
||||
def decompile(self, path: Path) -> None:
|
||||
assert path.is_file() and path.suffix == ".apk", "Invalid APK Path"
|
||||
if not self.instance.is_dir():
|
||||
os.system(f'apktool d "{path}" -o "{self.instance}" -f --no-crunch --only-main-classes')
|
||||
|
||||
def compile(self, path: Path) -> None:
|
||||
assert path.suffix == ".apk", "Invalid APK Path"
|
||||
if not path.is_file():
|
||||
assert self.instance.is_dir(), "Invalid ApkTool Path"
|
||||
tmp = Path("unaligned.apk")
|
||||
|
||||
os.system(f'apktool b "{self.instance}" -o "{tmp}" -f --no-crunch')
|
||||
if tmp.is_file(): os.system(f'zipalign -f -p "4" "{tmp}" "{path}"')
|
||||
if path.is_file(): shutil.rmtree(self.instance, ignore_errors=True)
|
||||
tmp.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def rename_app(parent: Path, name: str) -> None:
|
||||
manifest_path = parent / "AndroidManifest.xml"
|
||||
if not manifest_path.is_file():
|
||||
raise FileNotFoundError(manifest_path)
|
||||
|
||||
manifest_dict = xmltodict.parse(manifest_path.read_bytes(), encoding="utf-8")
|
||||
value = str(manifest_dict["manifest"]["application"]["@android:label"])
|
||||
if value.startswith("@string/"):
|
||||
key = value.split("@string/")[1]
|
||||
|
||||
source = None
|
||||
for path in (parent / "res").iterdir():
|
||||
strings_path = path / "strings.xml"
|
||||
if "values" in str(path) and strings_path.is_file():
|
||||
strings_dict = xmltodict.parse(strings_path.read_bytes(), encoding="utf-8")
|
||||
|
||||
for item in strings_dict["resources"]["string"]:
|
||||
if isinstance(item, dict) and item["@name"] == key:
|
||||
source = item["#text"]
|
||||
print(f"I: Patching {strings_path.name} ({strings_path.parent})")
|
||||
if source != name:
|
||||
item["#text"] = name
|
||||
strings_path.write_bytes(
|
||||
xmltodict.unparse(strings_dict, encoding="utf-8", pretty=True).encode("utf-8"))
|
||||
break
|
||||
if not source:
|
||||
raise ImportError(value)
|
||||
else:
|
||||
manifest_dict["manifest"]["application"]["@android:label"] = name
|
||||
manifest_path.write_bytes(xmltodict.unparse(manifest_dict, encoding="utf-8", pretty=True).encode("utf-8"))
|
||||
print(f"I: Patching {manifest_path.name} ({manifest_path.parent})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
config = Path("config.yaml")
|
||||
if not config.is_file():
|
||||
config = Path(input("Config Path: "))
|
||||
if not config.is_file():
|
||||
raise FileNotFoundError(config)
|
||||
|
||||
content = yaml.safe_load(config.read_text())
|
||||
|
||||
apktool = ApkTool()
|
||||
jks = Keystore(**content["keystore"])
|
||||
src = Path(content["metadata"]["input"])
|
||||
opt = Path(content["metadata"]["output"])
|
||||
|
||||
for key, value in content["metadata"].items():
|
||||
print(f"I: {key.capitalize()}: {value}")
|
||||
|
||||
if not opt.is_file():
|
||||
apktool.decompile(src)
|
||||
|
||||
# @Info: Patch apk
|
||||
for key, value in content["patch"].items(): # {Path: list[tuple]}
|
||||
|
||||
path = apktool.instance / key
|
||||
if not path.is_file():
|
||||
exist = False
|
||||
for subp in apktool.instance.iterdir():
|
||||
path = subp / key
|
||||
if path.is_file():
|
||||
exist = True
|
||||
break
|
||||
|
||||
if not exist:
|
||||
raise FileNotFoundError(key)
|
||||
|
||||
src_data = path.read_text()
|
||||
for v in value:
|
||||
if v[0] is None:
|
||||
if isinstance(v[1], str):
|
||||
# @Info: Replace complet file using [None, Path]
|
||||
v[1] = Path(v[1])
|
||||
if not v[1].is_file():
|
||||
raise FileNotFoundError(v[1])
|
||||
src_data = v[1].read_text()
|
||||
elif v[1] is None:
|
||||
# @Info: Replace with empty file
|
||||
src_data = ""
|
||||
else:
|
||||
# @Info: Replace with custom char
|
||||
src_data = any2str(v[1])
|
||||
elif v[1] is None:
|
||||
# @Info: Replace functon using [Path, None]
|
||||
if not isinstance(v[0], str):
|
||||
raise ImportError(v[0])
|
||||
|
||||
v[0] = Path(v[0])
|
||||
if not v[0].is_file():
|
||||
raise FileNotFoundError(v[0])
|
||||
|
||||
opt_data = v[0].read_text()
|
||||
if opt_data not in src_data:
|
||||
try:
|
||||
keys = opt_data.split("\n")
|
||||
start = next(filter(None, keys), None)
|
||||
stop = next(filter(None, reversed(keys)), None)
|
||||
start_index = src_data.index(start)
|
||||
stop_index = src_data.index(stop, start_index) + len(stop)
|
||||
src_data = src_data.replace(src_data[start_index:stop_index], opt_data)
|
||||
except Exception as e:
|
||||
raise ValueError(v[0])
|
||||
else:
|
||||
# @Info: Replace char using [str, str]
|
||||
if not v[0] in src_data and not v[1] in src_data:
|
||||
raise ImportError(v[0])
|
||||
src_data = src_data.replace(*v)
|
||||
|
||||
path.write_text(src_data)
|
||||
print(f"I: Patching {path.name} ({path.parent})")
|
||||
|
||||
# @Info: Rename apk
|
||||
name = content["metadata"].get("name")
|
||||
if name:
|
||||
rename_app(apktool.instance, name)
|
||||
|
||||
apktool.compile(opt)
|
||||
print(f"I: Keystore: {jks.path}")
|
||||
print(f"I: Validity: {jks.validity}")
|
||||
jks.sign(opt)
|
||||
|
||||
jks.info(opt)
|
||||
|
||||
print(f'I: MD5: {hashlib.md5(opt.read_bytes()).hexdigest()}')
|
@ -1,9 +0,0 @@
|
||||
[
|
||||
{
|
||||
"title": "Axinom demo video - single key (DASH; cenc)",
|
||||
"videoUrl": "https://media.axprod.net/VTB/DrmQuickStart/AxinomDemoVideo-SingleKey/Encrypted_Cenc/Manifest.mpd",
|
||||
"drmScheme": "widevine",
|
||||
"licenseServer": "https://drm-widevine-licensing.axtest.net/AcquireLicense",
|
||||
"licenseToken": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ2ZXJzaW9uIjoxLCJjb21fa2V5X2lkIjoiNjllNTQwODgtZTllMC00NTMwLThjMWEtMWViNmRjZDBkMTRlIiwibWVzc2FnZSI6eyJ2ZXJzaW9uIjoyLCJ0eXBlIjoiZW50aXRsZW1lbnRfbWVzc2FnZSIsImxpY2Vuc2UiOnsiYWxsb3dfcGVyc2lzdGVuY2UiOnRydWV9LCJjb250ZW50X2tleXNfc291cmNlIjp7ImlubGluZSI6W3siaWQiOiIyMTFhYzFkYy1jOGEyLTQ1NzUtYmFmNy1mYTRiYTU2YzM4YWMiLCJ1c2FnZV9wb2xpY3kiOiJUaGVPbmVQb2xpY3kifV19LCJjb250ZW50X2tleV91c2FnZV9wb2xpY2llcyI6W3sibmFtZSI6IlRoZU9uZVBvbGljeSIsInBsYXlyZWFkeSI6eyJwbGF5X2VuYWJsZXJzIjpbIjc4NjYyN0Q4LUMyQTYtNDRCRS04Rjg4LTA4QUUyNTVCMDFBNyJdfX1dfX0.D9FM9sbTFxBmcCOC8yMHrEtTwm0zy6ejZUCrlJbHz_U"
|
||||
}
|
||||
]
|
File diff suppressed because one or more lines are too long
@ -1,44 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500S" type="static" mediaPresentationDuration="PT0H0M52.209S" maxSegmentDuration="PT0H0M4.011S" profiles="urn:mpeg:dash:profile:isoff-live:2011,http://dashif.org/guidelines/dash264" xmlns:cenc="urn:mpeg:cenc:2013">
|
||||
<Period duration="PT0H0M52.209S">
|
||||
<!--Axinom Makemedia v4.1.2-004677-9bc38e0 targeting General Purpose Media Format specification v10
|
||||
ffmpeg version N-90069-gdd8351b118-sherpya Copyright (c) 2000-2018 the FFmpeg developers
|
||||
x265 [info]: HEVC encoder version 2.4+14-bc0e9bd7c08f5ddc
|
||||
x264 0.150.2833 df79067
|
||||
MP4Box - GPAC version 0.7.2-DEV-rev539-gff59dfa0-master
|
||||
MediaInfoLib - v0.7.96
|
||||
-->
|
||||
<AdaptationSet segmentAlignment="true" maxWidth="1920" maxHeight="1080" maxFrameRate="24" par="16:9" lang="und" group="1" selectionPriority="0">
|
||||
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="211ac1dc-c8a2-4575-baf7-fa4ba56c38ac" />
|
||||
<ContentProtection value="MSPR 2.0" schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95">
|
||||
<cenc:pssh>AAAB5HBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAcTEAQAAAQABALoBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgAzAE0ARQBhAEkAYQBMAEkAZABVAFcANgA5AC8AcABMAHAAVwB3ADQAcgBBAD0APQA8AC8ASwBJAEQAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==</cenc:pssh>
|
||||
<pro xmlns="urn:microsoft:playready">xAEAAAEAAQC6ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AMwBNAEUAYQBJAGEATABJAGQAVQBXADYAOQAvAHAATABwAFcAdwA0AHIAQQA9AD0APAAvAEsASQBEAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA=</pro>
|
||||
</ContentProtection>
|
||||
<ContentProtection value="Widevine" schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
|
||||
<cenc:pssh>AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQIRrB3MiiRXW69/pLpWw4rA==</cenc:pssh>
|
||||
</ContentProtection>
|
||||
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
|
||||
<SegmentTemplate media="$RepresentationID$/$Number%04d$.m4s" timescale="24" startNumber="1" duration="96" initialization="$RepresentationID$/init.mp4" />
|
||||
<Representation id="1" mimeType="video/mp4" codecs="avc1.640015" width="512" height="288" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="358733"></Representation>
|
||||
<Representation id="2" mimeType="video/mp4" codecs="avc1.64001E" width="640" height="360" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="685122"></Representation>
|
||||
<Representation id="3" mimeType="video/mp4" codecs="avc1.64001E" width="852" height="480" frameRate="24" sar="640:639" startWithSAP="1" bandwidth="1015285"></Representation>
|
||||
<Representation id="4" mimeType="video/mp4" codecs="avc1.64001F" width="1280" height="720" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="1743704"></Representation>
|
||||
<Representation id="5" mimeType="video/mp4" codecs="avc1.640028" width="1920" height="1080" frameRate="24" sar="1:1" startWithSAP="1" bandwidth="2423111"></Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet segmentAlignment="true" lang="und" group="2" selectionPriority="0">
|
||||
<ContentProtection schemeIdUri="urn:mpeg:dash:mp4protection:2011" value="cenc" cenc:default_KID="211ac1dc-c8a2-4575-baf7-fa4ba56c38ac" />
|
||||
<ContentProtection value="MSPR 2.0" schemeIdUri="urn:uuid:9a04f079-9840-4286-ab92-e65be0885f95">
|
||||
<cenc:pssh>AAAB5HBzc2gAAAAAmgTweZhAQoarkuZb4IhflQAAAcTEAQAAAQABALoBPABXAFIATQBIAEUAQQBEAEUAUgAgAHgAbQBsAG4AcwA9ACIAaAB0AHQAcAA6AC8ALwBzAGMAaABlAG0AYQBzAC4AbQBpAGMAcgBvAHMAbwBmAHQALgBjAG8AbQAvAEQAUgBNAC8AMgAwADAANwAvADAAMwAvAFAAbABhAHkAUgBlAGEAZAB5AEgAZQBhAGQAZQByACIAIAB2AGUAcgBzAGkAbwBuAD0AIgA0AC4AMAAuADAALgAwACIAPgA8AEQAQQBUAEEAPgA8AFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBFAFkATABFAE4APgAxADYAPAAvAEsARQBZAEwARQBOAD4APABBAEwARwBJAEQAPgBBAEUAUwBDAFQAUgA8AC8AQQBMAEcASQBEAD4APAAvAFAAUgBPAFQARQBDAFQASQBOAEYATwA+ADwASwBJAEQAPgAzAE0ARQBhAEkAYQBMAEkAZABVAFcANgA5AC8AcABMAHAAVwB3ADQAcgBBAD0APQA8AC8ASwBJAEQAPgA8AC8ARABBAFQAQQA+ADwALwBXAFIATQBIAEUAQQBEAEUAUgA+AA==</cenc:pssh>
|
||||
<pro xmlns="urn:microsoft:playready">xAEAAAEAAQC6ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADAALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsARQBZAEwARQBOAD4AMQA2ADwALwBLAEUAWQBMAEUATgA+ADwAQQBMAEcASQBEAD4AQQBFAFMAQwBUAFIAPAAvAEEATABHAEkARAA+ADwALwBQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAD4AMwBNAEUAYQBJAGEATABJAGQAVQBXADYAOQAvAHAATABwAFcAdwA0AHIAQQA9AD0APAAvAEsASQBEAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA=</pro>
|
||||
</ContentProtection>
|
||||
<ContentProtection value="Widevine" schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
|
||||
<cenc:pssh>AAAANHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABQIARIQIRrB3MiiRXW69/pLpWw4rA==</cenc:pssh>
|
||||
</ContentProtection>
|
||||
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main" />
|
||||
<SegmentTemplate media="$RepresentationID$/$Number%04d$.m4s" timescale="48000" startNumber="1" duration="192000" initialization="$RepresentationID$/init.mp4" />
|
||||
<Representation id="6" mimeType="audio/mp4" codecs="mp4a.40.5" audioSamplingRate="48000" startWithSAP="1" bandwidth="67041">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2" />
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
</Period>
|
||||
</MPD>
|
@ -1,30 +0,0 @@
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, Response, redirect
|
||||
|
||||
RESPONSE_PATH = Path() / 'response.json'
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def read_file() -> bytes:
|
||||
return RESPONSE_PATH.read_bytes() if RESPONSE_PATH.is_file() else b''
|
||||
|
||||
|
||||
@app.route('/', defaults={'path': ''})
|
||||
@app.route('/<path:path>', methods=['GET', 'POST'])
|
||||
def catch_all(path):
|
||||
count = 0
|
||||
while count < 50:
|
||||
content = read_file()
|
||||
if content:
|
||||
return Response(status=200, content_type='application/json', response=content)
|
||||
time.sleep(1)
|
||||
count += 1
|
||||
|
||||
return redirect('https://www.googleapis.com/certificateprovisioning/v1/devicecertificates/create', code=302)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=9090, debug=True)
|
@ -1 +0,0 @@
|
||||
|
@ -8,7 +8,7 @@ This document provides an overview of the external libraries, tools, and applica
|
||||
|
||||
A tool designed to root Android Virtual Devices (AVDs). It enables users to gain superuser privileges on their AVDs, essential for accessing and modifying system-level files and settings that are otherwise restricted.
|
||||
|
||||
### [DRM Info](https://apkcombo.com/drm-info/com.androidfung.drminfo/download/phone-1.1.9.220313-apk)
|
||||
### [DRM Info](https://apkcombo.app/drm-info/com.androidfung.drminfo)
|
||||
|
||||
An Android application providing detailed information about the device's Digital Rights Management (DRM) modules, including Widevine. Useful for verifying the DRM support level (L1, L2, L3) on the target device.
|
||||
|
||||
|
272
docs/advanced/DCSL.txt
Normal file
272
docs/advanced/DCSL.txt
Normal file
@ -0,0 +1,272 @@
|
||||
drm_serial_number: "\341we\354N\267\362\353`\024;\002\277\025\312\267"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 6172
|
||||
soc: "HiSilicon 3798M"
|
||||
manufacturer: "Coolech"
|
||||
model: "K-Q"
|
||||
device_type: "tv"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: ""w\376\333\250|\273\003p\343\350\3533\334\013K"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 7999
|
||||
soc: "HiSilicon 3798MV100"
|
||||
manufacturer: "SmarDTV"
|
||||
model: "DIP4090"
|
||||
device_type: "stb"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\010\017P\315\237\r\315n\371\313\003\276\023R\247Z"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 9173
|
||||
soc: "HiSilicon 3798MV100"
|
||||
manufacturer: "SmarDTV"
|
||||
model: "DIP4090"
|
||||
device_type: "stb"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "F7\271J^\335EW\201\336\031\362\322\325\307\205"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 6053
|
||||
soc: "MTK8639"
|
||||
manufacturer: "MeeBoss"
|
||||
model: "M100"
|
||||
device_type: "tv"
|
||||
model_year: 2015
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\301f\217\310\273\243~\272\250$\327\264\205N\200["
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 6054
|
||||
soc: "MTK8639"
|
||||
manufacturer: "MeeBoss"
|
||||
model: "M150"
|
||||
device_type: "tv"
|
||||
model_year: 2015
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "9\320n=\330\203=\237\326\005\335\252M\317\271\364"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 5860
|
||||
soc: "Intel Z2520"
|
||||
manufacturer: "Micromax"
|
||||
model: "P666"
|
||||
device_type: "phone"
|
||||
model_year: 2014
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\223n5V\033\3040\266\025\212\223\025)\360\231\377"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 6765
|
||||
soc: "bcm"
|
||||
manufacturer: "Roku"
|
||||
model: "3600"
|
||||
device_type: "video dongle"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\222\322h*\003xo\310\240\367\356\034\021\3034\273"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 6950
|
||||
soc: "BCM"
|
||||
manufacturer: "Roku"
|
||||
model: "4200"
|
||||
device_type: "video dongle"
|
||||
model_year: 2015
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\007\351\241\014\017k\205^\0301\227l\352`f\353"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 8083
|
||||
soc: "BCM7218"
|
||||
manufacturer: "Roku"
|
||||
model: "2700"
|
||||
device_type: "video dongle"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\300\031\010\314\303\347R\316m\W\272\307,\307\350"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 8252
|
||||
soc: "BCM"
|
||||
manufacturer: "Roku"
|
||||
model: "4200"
|
||||
device_type: "video dongle"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\236\360\025d\302\344O\026W0\001BzIV\331"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 8253
|
||||
soc: "BCM"
|
||||
manufacturer: "Roku"
|
||||
model: "3500"
|
||||
device_type: "video dongle"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\315Q\220l\253\352\026\177\262\374\314\365nu\361D"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 8254
|
||||
soc: "BCM"
|
||||
manufacturer: "Roku"
|
||||
model: "2xxx"
|
||||
device_type: "video dongle"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "!cj<WP\344_\274\334\252\304\3474\003\215"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 8353
|
||||
soc: "bcm"
|
||||
manufacturer: "Roku"
|
||||
model: "x2400"
|
||||
device_type: "video dongle"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\345\364\353#\335q\342u\301\005\301t\274\374\035\372"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 8354
|
||||
soc: "bcm"
|
||||
manufacturer: "Roku"
|
||||
model: "3400"
|
||||
device_type: "video dongle"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\224\374s\220\304`U.A\231B\254TJ\306\335"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 6966
|
||||
soc: "MSD6485"
|
||||
manufacturer: "UMC"
|
||||
model: "BLA-43-134M-XX"
|
||||
device_type: "tv"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\026\337Z]\347f\274\253\371\2325\215\2212(("
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 6975
|
||||
soc: "MSD6486"
|
||||
manufacturer: "UMC"
|
||||
model: "SHARP-6486-S6-XX"
|
||||
device_type: "tv"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\2620\361Q\327\232\211a\240\001\332\313R\244sp"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 7321
|
||||
soc: "MSD6486"
|
||||
manufacturer: "UMC"
|
||||
model: "ETE-6486-S6-XX"
|
||||
device_type: "tv"
|
||||
security_level: LEVEL_2
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
||||
|
||||
|
||||
|
||||
drm_serial_number: "\226\300\340\001\365\000B\323\205\177\317\006&\341\334`"
|
||||
deprecated_status: DEPRECATED_VALID
|
||||
device_info {
|
||||
system_id: 1070321
|
||||
soc: "Qualcomm SM4350"
|
||||
manufacturer: "HMD Global"
|
||||
model: "TTG"
|
||||
device_type: "phone"
|
||||
model_year: 2021
|
||||
security_level: LEVEL_1
|
||||
provisioning_method: FACTORY_KEYBOX
|
||||
}
|
||||
status: STATUS_RELEASED
|
75
docs/advanced/DRM.md
Normal file
75
docs/advanced/DRM.md
Normal file
@ -0,0 +1,75 @@
|
||||
# Widevine and DRM Glossary
|
||||
|
||||
This document serves as a resource for understanding Widevine Digital Rights Management (DRM), its ecosystem, and associated terminology. The links and descriptions provided aim to help readers navigate the technical and procedural aspects of Widevine and DRM systems more effectively.
|
||||
|
||||
---
|
||||
|
||||
## **Widevine Digital Rights Management (DRM)**
|
||||
Widevine is a leading DRM technology developed by Google, designed to protect video content and ensure secure delivery across a wide range of devices. It supports various security levels, enabling seamless integration with content providers while safeguarding intellectual property.
|
||||
|
||||
---
|
||||
|
||||
### **Core Concepts and Terms**
|
||||
|
||||
#### **Certified Widevine Implementation Partner (CWIP)**
|
||||
The [CWIP program](https://support.google.com/widevine/answer/2938263?hl=en) ensures that individuals and organizations are equipped to install, configure, and troubleshoot Widevine DRM systems effectively. Key objectives of this program include:
|
||||
- Teaching candidates to implement Widevine systems with precision.
|
||||
- Enhancing satisfaction for integrators and end-users.
|
||||
- Ensuring trust among content owners.
|
||||
|
||||
#### **Widevine Device Certificate Status List (DCSL)**
|
||||
The [Widevine DCSL](https://developers.google.com/widevine/drm/overview) provides a detailed list of certified devices, including:
|
||||
- **System ID**: A unique identifier for devices using Widevine.
|
||||
- **Security Level**: Defines the degree of hardware-based protection (e.g., L1, L2, or L3).
|
||||
- **Provisioning Method**: How keys and certificates are deployed (e.g., Factory Keybox).
|
||||
- **Device Type**: Identifies whether a device is a phone, set-top box, TV, etc.
|
||||
|
||||
---
|
||||
|
||||
### **Key Technical Terms**
|
||||
|
||||
#### **Security Levels (L1, L2, L3)**
|
||||
Widevine security levels determine the degree of protection applied to content playback:
|
||||
- **L1**: Uses Trusted Execution Environment (TEE) for all decryption and processing.
|
||||
- **L2**: Partially relies on the TEE but may use additional secure layers.
|
||||
- **L3**: Relies entirely on software-based protection, typically for devices without TEE.
|
||||
|
||||
#### **Provisioning Methods**
|
||||
The way keys and certificates are securely delivered to devices:
|
||||
- **Factory Keybox**: Embedded during manufacturing to ensure hardware-level security.
|
||||
- **Device Provisioning**: Post-manufacturing certificate injection.
|
||||
|
||||
#### **Content Encryption**
|
||||
Widevine utilizes standardized encryption methods, typically AES (Advanced Encryption Standard), to secure video streams.
|
||||
|
||||
#### **DRM Key Types**
|
||||
- **Content Key**: Used to decrypt protected content.
|
||||
- **License Key**: Issued by the DRM license server to authorize content playback.
|
||||
|
||||
---
|
||||
|
||||
### **Security Vulnerabilities and Updates**
|
||||
|
||||
#### **CVE-2024-36971**
|
||||
[Learn More](https://thehackernews.com/2024/08/google-patches-new-android-kernel.html)
|
||||
A critical vulnerability in the Android kernel exploited in the wild. Google released a patch to address this issue, highlighting:
|
||||
- The importance of maintaining up-to-date systems.
|
||||
- Potential risks of targeted attacks using DRM components.
|
||||
|
||||
#### **Patching Processes**
|
||||
Widevine relies on regular updates to mitigate vulnerabilities. This includes:
|
||||
- Firmware updates for device security components.
|
||||
- Collaboration with OEMs to ensure ecosystem-wide fixes.
|
||||
|
||||
---
|
||||
|
||||
### **Further Resources**
|
||||
|
||||
- **Widevine Overview**: [Google Developers Documentation](https://developers.google.com/widevine/drm/overview)
|
||||
- **Understanding DRM**: [Wikipedia - Digital Rights Management](https://en.wikipedia.org/wiki/Digital_rights_management)
|
||||
- **Android Security Bulletins**: [Google Security Updates](https://source.android.com/security/bulletin)
|
||||
|
||||
---
|
||||
|
||||
### **Conclusion**
|
||||
This glossary aims to centralize essential Widevine and DRM-related terms, helping researchers, integrators, and developers understand the ecosystem. For those working with Widevine or exploring DRM technologies, these resources are foundational to a secure and efficient implementation.
|
BIN
docs/server/.assets/a-eng-0096k-libopus-2c.webm
Normal file
BIN
docs/server/.assets/a-eng-0096k-libopus-2c.webm
Normal file
Binary file not shown.
31
docs/server/.assets/dash.mpd
Normal file
31
docs/server/.assets/dash.mpd
Normal file
@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--Generated for https://github.com/hyugogirubato/KeyDive version release-->
|
||||
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT60S">
|
||||
<Period id="0">
|
||||
<AdaptationSet id="0" contentType="video" maxWidth="768" maxHeight="576" frameRate="12800/512" subsegmentAlignment="true" par="4:3">
|
||||
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="800aacaa-5229-58ae-8880-62b5695db6bf"/>
|
||||
<ContentProtection schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
|
||||
<cenc:pssh>AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnNoYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=</cenc:pssh>
|
||||
</ContentProtection>
|
||||
<Representation id="4" bandwidth="438388" codecs="avc1.42c01e" mimeType="video/mp4" sar="1:1" width="192" height="144">
|
||||
<BaseURL>v-0144p-0100k-libx264.mp4</BaseURL>
|
||||
<SegmentBase indexRange="1094-1305" timescale="12800">
|
||||
<Initialization range="0-1093"/>
|
||||
</SegmentBase>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
<AdaptationSet id="1" contentType="audio" lang="en" subsegmentAlignment="true">
|
||||
<ContentProtection schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed" cenc:default_KID="67b30c86-756f-57c5-a0a3-8a23ac8c9178">
|
||||
<cenc:pssh>AAAAPnBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAAB4iFnNoYWthX2NlYzJmNjRhYTc4OTBhMTFI49yVmwY=</cenc:pssh>
|
||||
</ContentProtection>
|
||||
<Role schemeIdUri="urn:mpeg:dash:role:2011" value="main"/>
|
||||
<Representation id="23" bandwidth="152381" codecs="opus" mimeType="audio/webm" audioSamplingRate="48000">
|
||||
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
|
||||
<BaseURL>a-eng-0096k-libopus-2c.webm</BaseURL>
|
||||
<SegmentBase indexRange="377-635" timescale="1000000">
|
||||
<Initialization range="0-376"/>
|
||||
</SegmentBase>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
</Period>
|
||||
</MPD>
|
BIN
docs/server/.assets/v-0144p-0100k-libx264.mp4
Normal file
BIN
docs/server/.assets/v-0144p-0100k-libx264.mp4
Normal file
Binary file not shown.
BIN
docs/server/httptoolkit_forward.png
Normal file
BIN
docs/server/httptoolkit_forward.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
BIN
docs/server/kaltura.apk
Normal file
BIN
docs/server/kaltura.apk
Normal file
Binary file not shown.
193
docs/server/server.py
Normal file
193
docs/server/server.py
Normal file
@ -0,0 +1,193 @@
|
||||
import argparse
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from flask import Flask, Response, request, redirect
|
||||
|
||||
from keydive.__main__ import configure_logging
|
||||
|
||||
# Suppress urllib3 warnings
|
||||
logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)
|
||||
|
||||
# Initialize Flask application
|
||||
app = Flask(__name__)
|
||||
|
||||
# Define paths, constants, and global flags
|
||||
PARENT = Path(__file__).parent
|
||||
VERSION = "1.0.1"
|
||||
KEYBOX = False
|
||||
DELAY = 10
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def health_check() -> Response:
|
||||
"""
|
||||
Health check endpoint to confirm the server is running.
|
||||
|
||||
Returns:
|
||||
Response: A simple "pong" message with a 200 OK status.
|
||||
"""
|
||||
return Response(response="pong", status=200, content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
@app.route('/shaka-demo-assets/angel-one-widevine/<path:file>', methods=['GET'])
|
||||
def shaka_demo_assets(file) -> Response:
|
||||
"""
|
||||
Serves cached assets for Widevine demo content. If the requested file is
|
||||
not available locally, it fetches it from a remote server and caches it.
|
||||
|
||||
Parameters:
|
||||
file (str): File path requested by the client.
|
||||
|
||||
Returns:
|
||||
Response: File content as a byte stream, or a 404 error if not found.
|
||||
"""
|
||||
logger = logging.getLogger("Shaka")
|
||||
logger.info("%s %s", request.method, request.path)
|
||||
|
||||
try:
|
||||
path = PARENT / ".assets" / file
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if path.is_file():
|
||||
# Serve cached file content if available
|
||||
content = path.read_bytes()
|
||||
else:
|
||||
# Fetch the file from remote storage if not cached locally
|
||||
r = requests.get(
|
||||
url=f"https://storage.googleapis.com/shaka-demo-assets/angel-one-widevine/{file}",
|
||||
headers={
|
||||
"Accept": "*/*",
|
||||
"User-Agent": "KalturaDeviceInfo/1.4.1 (Linux;Android 10) ExoPlayerLib/2.9.3"
|
||||
}
|
||||
)
|
||||
r.raise_for_status()
|
||||
path.write_bytes(r.content) # Cache the downloaded content
|
||||
content = r.content
|
||||
logger.debug("Downloaded assets: %s", path)
|
||||
|
||||
return Response(response=content, status=200, content_type="application/octet-stream")
|
||||
except Exception as e:
|
||||
return Response(response=str(e), status=404, content_type="text/html; charset=utf-8")
|
||||
|
||||
|
||||
@app.route('/certificateprovisioning/v1/devicecertificates/create', methods=['POST'])
|
||||
def certificate_provisioning() -> Response:
|
||||
"""
|
||||
Handles device certificate provisioning requests by intercepting the request,
|
||||
saving it as a curl command, and then responding based on cached data
|
||||
or redirecting if no cached response is available.
|
||||
|
||||
Returns:
|
||||
Response: JSON response if provisioning is complete, else a redirection.
|
||||
"""
|
||||
global KEYBOX, DELAY
|
||||
logger = logging.getLogger("Google")
|
||||
logger.info("%s %s", request.method, request.path)
|
||||
|
||||
if KEYBOX:
|
||||
logger.warning("Provisioning request aborted to prevent keybox spam")
|
||||
return Response(response="Internal Server Error", status=500, content_type="text/html; charset=utf-8")
|
||||
|
||||
# Generate a curl command from the incoming request for debugging or testing
|
||||
user_agent = request.headers.get("User-Agent", "Unknown")
|
||||
url = request.url.replace("http://", "https://")
|
||||
prompt = [
|
||||
'curl',
|
||||
'--request', 'POST',
|
||||
'--compressed',
|
||||
'--header', '"Accept-Encoding: gzip"',
|
||||
'--header', '"Connection: Keep-Alive"',
|
||||
'--header', '"Content-Type: application/x-www-form-urlencoded"',
|
||||
'--header', '"Host: www.googleapis.com"',
|
||||
'--header', f'"User-Agent: {user_agent}"'
|
||||
]
|
||||
|
||||
# Save the curl command for potential replay or inspection
|
||||
curl = PARENT / "curl.txt"
|
||||
curl.write_text(" \\\n ".join(prompt))
|
||||
logger.debug("Saved curl command to: %s", curl)
|
||||
|
||||
# Wait for provisioning response data with retries
|
||||
logger.warning("Waiting for provisioning response...")
|
||||
provision = PARENT / "provisioning.json"
|
||||
provision.unlink(missing_ok=True)
|
||||
provision.write_bytes(b"") # Create empty file for manual input if needed
|
||||
|
||||
# Poll for the presence of a response up to DELAY times with 1-second intervals
|
||||
for _ in range(DELAY):
|
||||
try:
|
||||
content = json.loads(provision.read_bytes())
|
||||
if content:
|
||||
# Cleanup after successful response
|
||||
curl.unlink(missing_ok=True)
|
||||
provision.unlink(missing_ok=True)
|
||||
return Response(response=content, status=200, content_type="application/json")
|
||||
except Exception as e:
|
||||
pass # Continue waiting if file is empty or not yet ready
|
||||
time.sleep(1)
|
||||
|
||||
# Redirect to the secure URL if response is not available
|
||||
logger.warning("Redirecting to avoid timeout")
|
||||
return redirect(url, code=302)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""
|
||||
Main entry point for the application. Parses command-line arguments
|
||||
to set global parameters and configures logging, then starts the Flask server.
|
||||
"""
|
||||
global VERSION, DELAY, KEYBOX
|
||||
parser = argparse.ArgumentParser(description="Local DRM provisioning video player.")
|
||||
|
||||
# Global arguments for the application
|
||||
group_global = parser.add_argument_group("Global")
|
||||
group_global.add_argument('--host', required=False, type=str, default="127.0.0.1", metavar="<host>", help="Host address for the server to bind to.")
|
||||
group_global.add_argument('--port', required=False, type=int, default=9090, metavar="<port>", help="Port number for the server to listen on.")
|
||||
group_global.add_argument('-v', '--verbose', required=False, action="store_true", help="Enable verbose logging for detailed debug output.")
|
||||
group_global.add_argument('-l', '--log', required=False, type=Path, metavar="<dir>", help="Directory to store log files.")
|
||||
group_global.add_argument('--version', required=False, action="store_true", help="Display Server version information.")
|
||||
|
||||
# Advanced options
|
||||
group_advanced = parser.add_argument_group("Advanced")
|
||||
group_advanced.add_argument('-d', '--delay', required=False, type=int, metavar="<delay>", default=10, help="Delay (in seconds) between successive checks for provisioning responses.")
|
||||
group_advanced.add_argument('-k', '--keybox', required=False, action="store_true", help="Enable keybox mode, which aborts provisioning requests to prevent spam.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Handle version display
|
||||
if args.version:
|
||||
print(f"Server {VERSION}")
|
||||
exit(0)
|
||||
|
||||
# Configure logging (file and console)
|
||||
log_path = configure_logging(path=args.log, verbose=args.verbose)
|
||||
logger = logging.getLogger("Server")
|
||||
logger.info("Version: %s", VERSION)
|
||||
|
||||
try:
|
||||
# Set global variables based on parsed arguments
|
||||
DELAY = args.delay
|
||||
KEYBOX = args.keybox
|
||||
|
||||
# Start Flask app with specified host, port, and debug mode
|
||||
logging.getLogger("werkzeug").setLevel(logging.INFO if args.verbose else logging.ERROR)
|
||||
app.run(host=args.host, port=args.port, debug=False)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.critical(e, exc_info=args.verbose)
|
||||
|
||||
# Final logging and exit
|
||||
if log_path:
|
||||
logger.info("Log file: %s" % log_path)
|
||||
logger.info("Exiting")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,5 +1,7 @@
|
||||
from .core import Core
|
||||
from .adb import ADB
|
||||
from .cdm import Cdm
|
||||
from .vendor import Vendor
|
||||
from .keybox import Keybox
|
||||
|
||||
__version__ = '2.1.0'
|
||||
__version__ = "2.2.1"
|
||||
|
@ -1,32 +1,33 @@
|
||||
import argparse
|
||||
import logging
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import coloredlogs
|
||||
|
||||
import keydive
|
||||
|
||||
from keydive.adb import ADB
|
||||
from keydive.cdm import Cdm
|
||||
from keydive.constants import CDM_VENDOR_API
|
||||
from keydive.constants import CDM_VENDOR_API, DRM_PLAYER
|
||||
from keydive.core import Core
|
||||
|
||||
|
||||
def configure_logging(path: Path, verbose: bool) -> Path:
|
||||
def configure_logging(path: Path = None, verbose: bool = False) -> Optional[Path]:
|
||||
"""
|
||||
Configures logging for the application.
|
||||
|
||||
Args:
|
||||
path (Path, optional): The path for log files.
|
||||
verbose (bool): Whether to enable verbose logging.
|
||||
Parameters:
|
||||
path (Path, optional): The directory to store log files.
|
||||
verbose (bool, optional): Flag to enable detailed debug logging.
|
||||
|
||||
Returns:
|
||||
Path: The path of log file.
|
||||
Path: The path of the log file, or None if no log file is created.
|
||||
"""
|
||||
# Get the root logger
|
||||
# Set up the root logger with the desired logging level
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(logging.DEBUG if verbose else logging.INFO)
|
||||
|
||||
@ -36,20 +37,21 @@ def configure_logging(path: Path, verbose: bool) -> Path:
|
||||
|
||||
file_path = None
|
||||
if path:
|
||||
# Ensure the log directory exists
|
||||
if path.is_file():
|
||||
path = path.parent
|
||||
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create a file handler
|
||||
file_path = path / ('keydive_%s.log' % datetime.now().strftime('%Y-%m-%d_%H-%M-%S'))
|
||||
file_path = path / ("keydive_%s.log" % datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
|
||||
file_path = file_path.resolve(strict=False)
|
||||
file_handler = logging.FileHandler(file_path)
|
||||
file_handler.setLevel(logging.DEBUG)
|
||||
|
||||
# Set log formatting
|
||||
formatter = logging.Formatter(
|
||||
fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
fmt="%(asctime)s [%(levelname).1s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
@ -58,8 +60,8 @@ def configure_logging(path: Path, verbose: bool) -> Path:
|
||||
|
||||
# Configure coloredlogs for console output
|
||||
coloredlogs.install(
|
||||
fmt='%(asctime)s [%(levelname).1s] %(name)s: %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
fmt="%(asctime)s [%(levelname).1s] %(name)s: %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
level=logging.DEBUG if verbose else logging.INFO,
|
||||
logger=root_logger
|
||||
)
|
||||
@ -67,97 +69,127 @@ def configure_logging(path: Path, verbose: bool) -> Path:
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description='Extract Widevine L3 keys from an Android device.')
|
||||
"""
|
||||
Main entry point for the KeyDive application.
|
||||
|
||||
# Global options
|
||||
opt_global = parser.add_argument_group('Global options')
|
||||
opt_global.add_argument('-d', '--device', required=False, type=str, metavar='<id>', help='Specify the target Android device ID to connect with via ADB.')
|
||||
opt_global.add_argument('-v', '--verbose', required=False, action='store_true', help='Enable verbose logging for detailed debug output.')
|
||||
opt_global.add_argument('-l', '--log', required=False, type=Path, metavar='<dir>', help='Directory to store log files.')
|
||||
opt_global.add_argument('--delay', required=False, type=float, metavar='<delay>', default=1, help='Delay (in seconds) between process checks in the watcher.')
|
||||
opt_global.add_argument('--version', required=False, action='store_true', help='Display KeyDive version information.')
|
||||
This application extracts Widevine L3 keys from an Android device.
|
||||
It supports device management via ADB and allows hooking into Widevine processes.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="Extract Widevine L3 keys from an Android device.")
|
||||
|
||||
# Global arguments for the application
|
||||
group_global = parser.add_argument_group("Global")
|
||||
group_global.add_argument('-d', '--device', required=False, type=str, metavar="<id>", help="Specify the target Android device ID for ADB connection.")
|
||||
group_global.add_argument('-v', '--verbose', required=False, action="store_true", help="Enable verbose logging for detailed debug output.")
|
||||
group_global.add_argument('-l', '--log', required=False, type=Path, metavar="<dir>", help="Directory to store log files.")
|
||||
group_global.add_argument('--delay', required=False, type=float, metavar="<delay>", default=1, help="Delay (in seconds) between process checks.")
|
||||
group_global.add_argument('--version', required=False, action="store_true", help="Display KeyDive version information.")
|
||||
|
||||
# Arguments specific to the CDM (Content Decryption Module)
|
||||
group_cdm = parser.add_argument_group("Cdm")
|
||||
group_cdm.add_argument('-o', '--output', required=False, type=Path, default=Path("device"), metavar="<dir>", help="Output directory for extracted data.")
|
||||
group_cdm.add_argument('-w', '--wvd', required=False, action="store_true", help="Generate a pywidevine WVD device file.")
|
||||
group_cdm.add_argument('-s', '--skip', required=False, action="store_true", help="Skip auto-detection of the private function.")
|
||||
group_cdm.add_argument('-a', '--auto', required=False, action="store_true", help="Automatically start the Bitmovin web player.")
|
||||
group_cdm.add_argument('-p', '--player', required=False, action="store_true", help="Install and start the Kaltura app automatically.")
|
||||
|
||||
# Advanced options
|
||||
group_advanced = parser.add_argument_group("Advanced")
|
||||
group_advanced.add_argument('-f', '--functions', required=False, type=Path, metavar="<file>", help="Path to Ghidra XML functions file.")
|
||||
group_advanced.add_argument('-k', '--keybox', required=False, action="store_true", help="Enable export of the Keybox data if it is available.")
|
||||
group_advanced.add_argument('--challenge', required=False, type=Path, metavar="<file>", help="Path to unencrypted challenge for extracting client ID.")
|
||||
group_advanced.add_argument('--private-key', required=False, type=Path, metavar="<file>", help="Path to private key for extracting client ID.")
|
||||
|
||||
# Cdm options
|
||||
opt_cdm = parser.add_argument_group('Cdm options')
|
||||
opt_cdm.add_argument('-a', '--auto', required=False, action='store_true', help='Automatically open Bitmovin\'s demo.')
|
||||
opt_cdm.add_argument('-c', '--challenge', required=False, type=Path, metavar='<file>', help='Path to unencrypted challenge for extracting client ID.')
|
||||
opt_cdm.add_argument('-w', '--wvd', required=False, action='store_true', help='Generate a pywidevine WVD device file.')
|
||||
opt_cdm.add_argument('-o', '--output', required=False, type=Path, default=Path('device'), metavar='<dir>', help='Output directory path for extracted data.')
|
||||
opt_cdm.add_argument('-f', '--functions', required=False, type=Path, metavar='<file>', help='Path to Ghidra XML functions file.')
|
||||
opt_cdm.add_argument('-s', '--skip', required=False, action='store_true', help='Skip auto-detect of private function.')
|
||||
args = parser.parse_args()
|
||||
|
||||
# Handle version display
|
||||
if args.version:
|
||||
print(f'KeyDive {keydive.__version__}')
|
||||
print(f"KeyDive {keydive.__version__}")
|
||||
exit(0)
|
||||
|
||||
# Configure logging
|
||||
# Configure logging (file and console)
|
||||
log_path = configure_logging(path=args.log, verbose=args.verbose)
|
||||
logger = logging.getLogger('KeyDive')
|
||||
logger.info('Version: %s', keydive.__version__)
|
||||
logger = logging.getLogger("KeyDive")
|
||||
logger.info("Version: %s", keydive.__version__)
|
||||
|
||||
try:
|
||||
# Start the ADB server if not already running
|
||||
sp = subprocess.run(['adb', 'start-server'], capture_output=True)
|
||||
if sp.returncode != 0:
|
||||
raise EnvironmentError('ADB is not recognized as an environment variable, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge')
|
||||
# Connect to the specified Android device
|
||||
adb = ADB(device=args.device)
|
||||
|
||||
# Initialize Cdm instance
|
||||
cdm = Cdm()
|
||||
# Initialize Cdm instance for content decryption module (with optional arguments)
|
||||
cdm = Cdm(keybox=args.keybox)
|
||||
if args.challenge:
|
||||
cdm.set_challenge(data=args.challenge)
|
||||
if args.private_key:
|
||||
cdm.set_private_key(data=args.private_key, name=None)
|
||||
|
||||
# Initialize Core instance for interacting with the device
|
||||
core = Core(cdm=cdm, device=args.device, functions=args.functions, skip=args.skip)
|
||||
core = Core(adb=adb, cdm=cdm, functions=args.functions, skip=args.skip)
|
||||
|
||||
# Process watcher loop
|
||||
logger.info('Watcher delay: %ss' % args.delay)
|
||||
current = None
|
||||
# Setup actions based on user arguments (for DRM player, Bitmovin player, etc.)
|
||||
if args.player:
|
||||
package = DRM_PLAYER["package"]
|
||||
|
||||
# Check if the application is already installed
|
||||
installed = package in adb.list_applications(user=True, system=False)
|
||||
if not installed:
|
||||
logger.debug("Application %s not found. Installing...", package)
|
||||
installed = adb.install_application(path=DRM_PLAYER["path"], url=DRM_PLAYER["url"])
|
||||
|
||||
# Skip starting the application if installation failed
|
||||
if installed:
|
||||
# Start the application
|
||||
logger.info("Starting application: %s", package)
|
||||
adb.start_application(package)
|
||||
elif args.auto:
|
||||
logger.info("Opening the Bitmovin web player...")
|
||||
adb.open_url("https://bitmovin.com/demos/drm")
|
||||
logger.info("Setup completed")
|
||||
|
||||
# Process watcher loop: continuously checks for Widevine processes
|
||||
logger.info("Watcher delay: %ss" % args.delay)
|
||||
current = None # Variable to track the current Widevine process
|
||||
while core.running:
|
||||
# Check if for current process data has been exported
|
||||
if current and cdm.export(args.output, args.wvd):
|
||||
raise KeyboardInterrupt
|
||||
raise KeyboardInterrupt # Stop if export is complete
|
||||
|
||||
# Get the currently running Widevine processes
|
||||
# https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792
|
||||
processes = {
|
||||
key: (name, pid)
|
||||
for name, pid in core.enumerate_processes().items()
|
||||
for name, pid in adb.enumerate_processes().items()
|
||||
for key in CDM_VENDOR_API.keys()
|
||||
if key in name or key.replace('-service', '-service-lazy') in name
|
||||
if key in name or key.replace("-service", "-service-lazy") in name
|
||||
}
|
||||
|
||||
if not processes:
|
||||
raise EnvironmentError('Unable to detect Widevine, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#drm-info')
|
||||
raise EnvironmentError("Unable to detect Widevine, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#drm-info")
|
||||
|
||||
# Check if the current process has changed
|
||||
if current and current not in [v[1] for v in processes.values()]:
|
||||
logger.warning('Widevine process has changed')
|
||||
logger.warning("Widevine process has changed")
|
||||
current = None
|
||||
|
||||
# If current process not found, attempt to hook into the detected processes
|
||||
if not current:
|
||||
logger.debug('Analysing...')
|
||||
logger.debug("Analysing...")
|
||||
|
||||
for key, (name, pid) in processes.items():
|
||||
if current:
|
||||
break
|
||||
for vendor in CDM_VENDOR_API[key]:
|
||||
if core.hook_process(pid=pid, vendor=vendor):
|
||||
logger.info('Process: %s (%s)', pid, name)
|
||||
logger.info("Process: %s (%s)", pid, name)
|
||||
current = pid
|
||||
break
|
||||
elif not core.running:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
if current:
|
||||
logger.info('Successfully hooked')
|
||||
if args.auto:
|
||||
logger.info('Starting DRM player launch process...')
|
||||
sp = subprocess.run(['adb', '-s', str(core.device.id), 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', 'https://bitmovin.com/demos/drm'], capture_output=True)
|
||||
if sp.returncode != 0:
|
||||
logger.error('Error launching DRM player: %s' % sp.stdout.decode('utf-8').strip())
|
||||
logger.info("Successfully hooked")
|
||||
else:
|
||||
logger.warning('Widevine library not found, searching...')
|
||||
logger.warning("Widevine library not found, searching...")
|
||||
|
||||
# Delay before next iteration
|
||||
time.sleep(args.delay)
|
||||
@ -168,9 +200,9 @@ def main() -> None:
|
||||
|
||||
# Final logging and exit
|
||||
if log_path:
|
||||
logger.info('Log file: %s' % log_path)
|
||||
logger.info('Exiting')
|
||||
logger.info("Log file: %s" % log_path)
|
||||
logger.info("Exiting")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
287
keydive/adb.py
Normal file
287
keydive/adb.py
Normal file
@ -0,0 +1,287 @@
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
import requests
|
||||
|
||||
from frida.core import Device
|
||||
|
||||
# Suppress urllib3 warnings
|
||||
logging.getLogger("urllib3.connectionpool").setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def shell(prompt: list) -> subprocess.CompletedProcess:
|
||||
"""
|
||||
Executes a shell command and returns the result.
|
||||
|
||||
Parameters:
|
||||
prompt (list): The command to execute as a list of strings.
|
||||
|
||||
Returns:
|
||||
subprocess.CompletedProcess: The result containing return code, stdout, and stderr.
|
||||
"""
|
||||
prompt = list(map(str, prompt)) # Ensure all command parts are strings
|
||||
# logging.getLogger("Shell").debug(" ".join(prompt))
|
||||
return subprocess.run(prompt, capture_output=True) # Run the command and capture output
|
||||
|
||||
|
||||
class ADB:
|
||||
"""
|
||||
Class for managing interactions with the Android device via ADB.
|
||||
"""
|
||||
|
||||
def __init__(self, device: str = None, timeout: int = 5):
|
||||
"""
|
||||
Initializes ADB connection to the device.
|
||||
|
||||
Parameters:
|
||||
device (str, optional): Device ID to connect to, defaults to the first USB device.
|
||||
timeout (int, optional): Timeout for connection in seconds. Defaults to 5.
|
||||
|
||||
Raises:
|
||||
EnvironmentError: If ADB is not found in the system path.
|
||||
Exception: If connection to the device fails.
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
# Ensure ADB is available
|
||||
if not shutil.which("adb"):
|
||||
raise EnvironmentError(
|
||||
"ADB is not recognized as an environment variable. "
|
||||
"Ensure ADB is installed and refer to the documentation: "
|
||||
"https://github.com/hyugogirubato/KeyDive/blob/main/docs/PACKAGE.md#adb-android-debug-bridge"
|
||||
)
|
||||
|
||||
# Start the ADB server if not already running
|
||||
sp = shell(['adb', 'start-server'])
|
||||
if sp.returncode != 0:
|
||||
self.logger.warning("ADB server startup failed (Error: %s)", sp.stdout.decode("utf-8").strip())
|
||||
|
||||
# Connect to device (or default to the first USB device)
|
||||
try:
|
||||
self.device: Device = frida.get_device(id=device, timeout=timeout) if device else frida.get_usb_device(timeout=timeout)
|
||||
self.logger.info("Connected to device: %s (%s)", self.device.name, self.device.id)
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to connect to device: %s", e)
|
||||
raise e
|
||||
|
||||
self.prompt = ['adb', '-s', self.device.id, 'shell']
|
||||
|
||||
# Retrieve and log device properties
|
||||
properties = self.device_properties()
|
||||
if properties:
|
||||
self.logger.info("SDK API: %s", properties.get("ro.build.version.sdk", "Unknown"))
|
||||
self.logger.info("ABI CPU: %s", properties.get("ro.product.cpu.abi", "Unknown"))
|
||||
else:
|
||||
self.logger.warning("No device properties retrieved")
|
||||
|
||||
def device_properties(self) -> dict:
|
||||
"""
|
||||
Retrieves system properties from the device.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping property keys to their corresponding values.
|
||||
"""
|
||||
# https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands
|
||||
properties = {}
|
||||
|
||||
# Execute the shell command to retrieve device properties
|
||||
sp = shell([*self.prompt, 'getprop'])
|
||||
if sp.returncode != 0:
|
||||
self.logger.error("Failed to retrieve device properties (Error: %s)", sp.stdout.decode("utf-8").strip())
|
||||
return properties
|
||||
|
||||
# Parse the output and cast values accordingly
|
||||
for line in sp.stdout.decode("utf-8").splitlines():
|
||||
match = re.match(r"\[(.*?)\]: \[(.*?)\]", line)
|
||||
if match:
|
||||
key, value = match.groups()
|
||||
|
||||
# Cast numeric and boolean values where appropriate
|
||||
if value.isdigit():
|
||||
value = int(value)
|
||||
elif value.lower() in ("true", "false"):
|
||||
value = value.lower() == "true"
|
||||
|
||||
properties[key] = value
|
||||
|
||||
return properties
|
||||
|
||||
def list_applications(self, user: bool = True, system: bool = False) -> dict:
|
||||
"""
|
||||
Lists installed applications on the device, with optional filters for user/system apps.
|
||||
|
||||
Parameters:
|
||||
user (bool, optional): Include user-installed apps. Defaults to True.
|
||||
system (bool, optional): Include system apps. Defaults to False.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of application packages and their file paths.
|
||||
"""
|
||||
applications = {}
|
||||
|
||||
# Validate input; return empty dict if no filter is set
|
||||
if not user and not system:
|
||||
return applications
|
||||
|
||||
# Set the appropriate shell command based on user/system filters
|
||||
prompt = [*self.prompt, 'pm', 'list', 'packages', '-f']
|
||||
if user and not system:
|
||||
prompt.append("-3")
|
||||
elif not user and system:
|
||||
prompt.append("-s")
|
||||
|
||||
# Execute the shell command to list applications
|
||||
sp = shell(prompt)
|
||||
if sp.returncode != 0:
|
||||
self.logger.error("Failed to retrieve app list (Error: %s)", sp.stdout.decode("utf-8").strip())
|
||||
return applications
|
||||
|
||||
# Parse and add applications to the dictionary
|
||||
for line in sp.stdout.decode("utf-8").splitlines():
|
||||
try:
|
||||
path, package = line.strip().split(":", 1)[1].rsplit("=", 1)
|
||||
applications[package] = path
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return applications
|
||||
|
||||
def start_application(self, package: str) -> bool:
|
||||
"""
|
||||
Starts an application by its package name.
|
||||
|
||||
Parameters:
|
||||
package (str): The package name of the application.
|
||||
|
||||
Returns:
|
||||
bool: True if the app was started successfully, False otherwise.
|
||||
"""
|
||||
# Get package information using dumpsys
|
||||
sp = shell([*self.prompt, 'dumpsys', 'package', package])
|
||||
lines = sp.stdout.decode("utf-8").splitlines()
|
||||
|
||||
# Remove empty lines to ensure backwards compatibility
|
||||
lines = [l.strip() for l in lines if l.strip()]
|
||||
|
||||
# Look for MAIN activity to identify entry point
|
||||
for i, line in enumerate(lines):
|
||||
if "android.intent.action.MAIN" in line:
|
||||
match = re.search(fr"({package}/[^ ]+)", lines[i + 1])
|
||||
if match:
|
||||
# Start the application by its main activity
|
||||
main_activity = match.group()
|
||||
sp = shell([*self.prompt, 'am', 'start', '-n', main_activity])
|
||||
if sp.returncode == 0:
|
||||
return True
|
||||
|
||||
self.logger.error("Failed to start app %s (Error: %s)", package, sp.stdout.decode("utf-8").strip())
|
||||
break
|
||||
|
||||
self.logger.error("Package %s not found or no MAIN intent", package)
|
||||
return False
|
||||
|
||||
def enumerate_processes(self) -> dict:
|
||||
"""
|
||||
Lists running processes and maps process names to their PIDs.
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of process names and corresponding PIDs.
|
||||
"""
|
||||
# https://github.com/frida/frida/issues/1225#issuecomment-604181822
|
||||
processes = {}
|
||||
|
||||
# Attempt to get the list of processes using the 'ps -A' command
|
||||
prompt = [*self.prompt, 'ps']
|
||||
sp = shell([*prompt, '-A'])
|
||||
lines = sp.stdout.decode("utf-8").splitlines()
|
||||
|
||||
# If the output has less than 10 lines, retry with a simpler 'ps' command
|
||||
if len(lines) < 10:
|
||||
sp = shell(prompt)
|
||||
if sp.returncode != 0:
|
||||
self.logger.error("Failed to execute ps command (Error: %s)", sp.stdout.decode("utf-8").strip())
|
||||
return processes
|
||||
lines = sp.stdout.decode("utf-8").splitlines()
|
||||
|
||||
# Iterate through lines starting from the second line (skipping header)
|
||||
for line in lines[1:]:
|
||||
try:
|
||||
parts = line.split() # USER,PID,PPID,VSZ,RSS,WCHAN,ADDR,S,NAME
|
||||
pid = int(parts[1]) # Extract PID
|
||||
name = " ".join(parts[8:]).strip() # Extract process name
|
||||
|
||||
# Handle cases where process name might be in brackets (e.g., kernel threads)
|
||||
name = name if name.startswith("[") else Path(name).name
|
||||
processes[name] = pid
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return processes
|
||||
|
||||
def install_application(self, path: Path = None, url: str = None) -> bool:
|
||||
"""
|
||||
Installs an application on the device either from a local file or by downloading from a URL.
|
||||
|
||||
Parameters:
|
||||
path (Path, optional): The local file path of the APK to install. Defaults to None.
|
||||
url (str, optional): The URL to download the APK from. Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: True if the installation was successful, False otherwise.
|
||||
"""
|
||||
# Prepare the shell command for installation
|
||||
prompt = [*self.prompt[:-1], 'install']
|
||||
|
||||
# Install from a local file path if a valid path is provided
|
||||
if path and path.is_file():
|
||||
sp = shell([*prompt, path]) # Run the installation command with the local file path
|
||||
if sp.returncode == 0:
|
||||
return True
|
||||
self.logger.error("Installation failed for local path: %s (Error: %s)", path, sp.stdout.decode("utf-8").strip())
|
||||
|
||||
# If URL is provided, attempt to download the APK and install it
|
||||
status = False
|
||||
if url:
|
||||
file = Path("tmp.apk") # Temporary file to store the downloaded APK
|
||||
try:
|
||||
# Download the APK from the provided URL
|
||||
r = requests.get(url, headers={"Accept": "*/*", "User-Agent": "KeyDive/ADB"})
|
||||
r.raise_for_status()
|
||||
|
||||
# Save the downloaded APK to a temporary file
|
||||
file.write_bytes(r.content)
|
||||
|
||||
# Attempt installation from the downloaded APK
|
||||
status = self.install_application(path=file)
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to download application from URL: %s (Error: %s)", url, e)
|
||||
file.unlink(missing_ok=True) # Clean up the temporary file, even if there was an error
|
||||
|
||||
return status
|
||||
|
||||
def open_url(self, url: str) -> bool:
|
||||
"""
|
||||
Opens a specified URL on the device.
|
||||
|
||||
Parameters:
|
||||
url (str): The URL to be opened on the device.
|
||||
|
||||
Returns:
|
||||
bool: True if the URL was successfully opened, False otherwise.
|
||||
"""
|
||||
# Execute the shell command to open the URL using the Android 'am' (Activity Manager) command.
|
||||
sp = shell([*self.prompt, 'am', 'start', '-a', 'android.intent.action.VIEW', '-d', url])
|
||||
|
||||
# Check the result of the command execution and log if there is an error
|
||||
if sp.returncode != 0:
|
||||
self.logger.error("URL open failed for: %s (Return: %s)", url, sp.stdout.decode("utf-8").strip())
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
__all__ = ("ADB",)
|
177
keydive/cdm.py
177
keydive/cdm.py
@ -2,19 +2,21 @@ import base64
|
||||
import json
|
||||
import logging
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
from zlib import crc32
|
||||
from unidecode import unidecode
|
||||
from pathlib import Path
|
||||
|
||||
from pathvalidate import sanitize_filepath, sanitize_filename
|
||||
from Cryptodome.PublicKey import RSA
|
||||
from Cryptodome.PublicKey.RSA import RsaKey
|
||||
from pywidevine.device import Device, DeviceTypes
|
||||
from pywidevine.license_protocol_pb2 import (SignedMessage, LicenseRequest, ClientIdentification, SignedDrmCertificate,
|
||||
DrmCertificate, EncryptedClientIdentification)
|
||||
from pywidevine.license_protocol_pb2 import (
|
||||
SignedMessage, LicenseRequest, ClientIdentification, SignedDrmCertificate, DrmCertificate,
|
||||
EncryptedClientIdentification)
|
||||
|
||||
from keydive.constants import OEM_CRYPTO_API
|
||||
from keydive.keybox import Keybox
|
||||
|
||||
|
||||
class Cdm:
|
||||
@ -23,25 +25,27 @@ class Cdm:
|
||||
extracting and storing private keys, and exporting device information.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, keybox: bool = False):
|
||||
"""
|
||||
Initializes the Cdm object, setting up a logger and containers for client IDs and private keys.
|
||||
|
||||
Attributes:
|
||||
client_id (dict[int, ClientIdentification]): Stores client identification info mapped by key modulus.
|
||||
private_key (dict[int, RsaKey]): Stores private keys mapped by key modulus.
|
||||
Parameters:
|
||||
keybox (bool, optional): Initializes a Keybox instance for secure key management.
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
# https://github.com/devine-dl/pywidevine
|
||||
self.client_id: dict[int, ClientIdentification] = {}
|
||||
self.private_key: dict[int, RsaKey] = {}
|
||||
|
||||
# Optionally initialize a Keybox instance for secure key management if 'keybox' is True
|
||||
self.keybox = Keybox() if keybox else None
|
||||
|
||||
@staticmethod
|
||||
def __client_info(client_id: ClientIdentification) -> dict:
|
||||
"""
|
||||
Converts client identification information to a dictionary.
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
client_id (ClientIdentification): The client identification.
|
||||
|
||||
Returns:
|
||||
@ -54,155 +58,222 @@ class Cdm:
|
||||
"""
|
||||
Converts encrypted client identification information to a dictionary.
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
encrypted_client_id (EncryptedClientIdentification): The encrypted client identification.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of encrypted client information.
|
||||
"""
|
||||
content = {
|
||||
'providerId': encrypted_client_id.provider_id,
|
||||
'serviceCertificateSerialNumber': encrypted_client_id.service_certificate_serial_number,
|
||||
'encryptedClientId': encrypted_client_id.encrypted_client_id,
|
||||
'encryptedClientIdIv': encrypted_client_id.encrypted_client_id_iv,
|
||||
'encryptedPrivacyKey': encrypted_client_id.encrypted_privacy_key
|
||||
"providerId": encrypted_client_id.provider_id,
|
||||
"serviceCertificateSerialNumber": encrypted_client_id.service_certificate_serial_number,
|
||||
"encryptedClientId": encrypted_client_id.encrypted_client_id,
|
||||
"encryptedClientIdIv": encrypted_client_id.encrypted_client_id_iv,
|
||||
"encryptedPrivacyKey": encrypted_client_id.encrypted_privacy_key
|
||||
}
|
||||
return {
|
||||
k: base64.b64encode(v).decode('utf-8') if isinstance(v, bytes) else v
|
||||
k: base64.b64encode(v).decode("utf-8") if isinstance(v, bytes) else v
|
||||
for k, v in content.items()
|
||||
}
|
||||
|
||||
def set_challenge(self, data: Union[Path, bytes]) -> None:
|
||||
"""
|
||||
Sets the challenge data by extracting device information.
|
||||
Sets the challenge data by extracting device information and client ID.
|
||||
|
||||
Args:
|
||||
data (Union[Path, bytes]): The challenge data as a file path or bytes.
|
||||
Parameters:
|
||||
data (Union[Path, bytes]): Challenge data as a file path or raw bytes.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the provided file path does not exist.
|
||||
FileNotFoundError: If the file path doesn't exist.
|
||||
Exception: Logs any other exceptions that occur.
|
||||
"""
|
||||
try:
|
||||
# Check if the data is a Path object, indicating it's a file path
|
||||
if isinstance(data, Path):
|
||||
if not data.is_file():
|
||||
raise FileNotFoundError(data)
|
||||
data = data.read_bytes()
|
||||
|
||||
try:
|
||||
# Parse the signed message from the data
|
||||
signed_message = SignedMessage()
|
||||
signed_message.ParseFromString(data)
|
||||
|
||||
# Parse the license request from the signed message
|
||||
license_request = LicenseRequest()
|
||||
license_request.ParseFromString(signed_message.msg)
|
||||
|
||||
# Extract the encrypted client ID, if available
|
||||
# https://integration.widevine.com/diagnostics
|
||||
encrypted_client_id: EncryptedClientIdentification = license_request.encrypted_client_id
|
||||
if encrypted_client_id.SerializeToString():
|
||||
self.logger.debug('Receive encrypted client id: \n\n%s\n', json.dumps(self.__encrypted_client_info(encrypted_client_id), indent=2))
|
||||
self.logger.warning('The client ID of the challenge is encrypted')
|
||||
# If encrypted, log the encrypted client ID and indicate encryption
|
||||
self.logger.info("Receive encrypted client id: \n\n%s\n", json.dumps(self.__encrypted_client_info(encrypted_client_id), indent=2))
|
||||
self.logger.warning("The client ID of the challenge is encrypted")
|
||||
else:
|
||||
# If unencrypted, extract and set the client ID
|
||||
client_id: ClientIdentification = license_request.client_id
|
||||
self.set_client_id(data=client_id)
|
||||
except Exception as e:
|
||||
self.logger.debug('Failed to set challenge data: %s', e)
|
||||
|
||||
def set_private_key(self, data: bytes, name: str) -> None:
|
||||
except FileNotFoundError as e:
|
||||
raise FileNotFoundError(f"Challenge file not found: {data}") from e
|
||||
except Exception as e:
|
||||
self.logger.debug("Failed to set challenge data: %s", e)
|
||||
|
||||
def set_private_key(self, data: Union[Path, bytes], name: str = None) -> None:
|
||||
"""
|
||||
Sets the private key from the provided data.
|
||||
|
||||
Args:
|
||||
data (bytes): The private key data.
|
||||
name (str): The name of the function.
|
||||
Parameters:
|
||||
data (Union[Path, bytes]): The private key data, either as a file path or byte data.
|
||||
name (str, optional): Function name for verification against known functions.
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If the file path doesn't exist.
|
||||
Exception: Logs any other exceptions that occur.
|
||||
"""
|
||||
try:
|
||||
# Check if the data is a Path object, indicating it's a file path
|
||||
if isinstance(data, Path):
|
||||
data = data.read_bytes()
|
||||
|
||||
# Import the private key using the RSA module
|
||||
key = RSA.import_key(data)
|
||||
|
||||
# Log the private key if it's not already in the dictionary
|
||||
if key.n not in self.private_key:
|
||||
self.logger.debug('Receive private key: \n\n%s\n', key.exportKey('PEM').decode('utf-8'))
|
||||
self.logger.info("Receive private key: \n\n%s\n", key.exportKey("PEM").decode("utf-8"))
|
||||
|
||||
if name not in OEM_CRYPTO_API:
|
||||
self.logger.warning(f'The function "{name}" does not belong to the referenced functions. Communicate it to the developer to improve the tool.')
|
||||
# If a function name is provided, verify it against known functions
|
||||
if name and name not in OEM_CRYPTO_API:
|
||||
self.logger.warning("The function '%s' does not belong to the referenced functions. Communicate it to the developer to improve the tool.",name)
|
||||
|
||||
# Store the private key in the dictionary, using the modulus (key.n) as the key
|
||||
self.private_key[key.n] = key
|
||||
except FileNotFoundError as e:
|
||||
raise FileNotFoundError(f"Private key file not found: {data}") from e
|
||||
except Exception as e:
|
||||
self.logger.debug('Failed to set private key: %s', e)
|
||||
self.logger.debug("Failed to set private key: %s", e)
|
||||
|
||||
def set_client_id(self, data: Union[ClientIdentification, bytes]) -> None:
|
||||
"""
|
||||
Sets the client ID from the provided data.
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
data (Union[ClientIdentification, bytes]): The client ID data.
|
||||
"""
|
||||
try:
|
||||
# Check if the provided data is already a `ClientIdentification` object
|
||||
if isinstance(data, ClientIdentification):
|
||||
client_id = data
|
||||
else:
|
||||
# Deserialize the byte data into a `ClientIdentification` object
|
||||
client_id = ClientIdentification()
|
||||
client_id.ParseFromString(data)
|
||||
|
||||
# Initialize objects for parsing the DRM certificate and signed certificate
|
||||
signed_drm_certificate = SignedDrmCertificate()
|
||||
drm_certificate = DrmCertificate()
|
||||
|
||||
# Parse the signed DRM certificate from the client ID token
|
||||
signed_drm_certificate.ParseFromString(client_id.token)
|
||||
drm_certificate.ParseFromString(signed_drm_certificate.drm_certificate)
|
||||
|
||||
# Extract the public key from the DRM certificate
|
||||
public_key = drm_certificate.public_key
|
||||
key = RSA.importKey(public_key)
|
||||
|
||||
# Check if this public key has already been recorded and log the client ID info
|
||||
if key.n not in self.client_id:
|
||||
self.logger.debug('Receive client id: \n\n%s\n', json.dumps(self.__client_info(client_id), indent=2))
|
||||
self.logger.info("Receive client id: \n\n%s\n", json.dumps(self.__client_info(client_id), indent=2))
|
||||
|
||||
# Store the client ID in the client_id dictionary, using the public key modulus (`key.n`) as the key
|
||||
self.client_id[key.n] = client_id
|
||||
except Exception as e:
|
||||
self.logger.debug('Failed to set client ID: %s', e)
|
||||
self.logger.debug("Failed to set client ID: %s", e)
|
||||
|
||||
def set_device_id(self, data: bytes) -> None:
|
||||
"""
|
||||
Sets the device ID in the keybox.
|
||||
|
||||
Parameters:
|
||||
data (bytes): The device ID to be stored in the keybox.
|
||||
"""
|
||||
if self.keybox:
|
||||
self.keybox.set_device_id(data=data)
|
||||
|
||||
def set_keybox(self, data: bytes) -> None:
|
||||
"""
|
||||
Sets the keybox data.
|
||||
|
||||
Parameters:
|
||||
data (bytes): The keybox data to be set.
|
||||
"""
|
||||
if self.keybox:
|
||||
self.keybox.set_keybox(data=data)
|
||||
|
||||
def export(self, parent: Path, wvd: bool = False) -> bool:
|
||||
"""
|
||||
Exports the client ID and private key to disk.
|
||||
Exports client ID, private key, and optionally WVD files to disk.
|
||||
|
||||
Args:
|
||||
parent (Path): The parent directory to export the files to.
|
||||
wvd (bool): Whether to export WVD files.
|
||||
Parameters:
|
||||
parent (Path): Directory to export the files to.
|
||||
wvd (bool, optional): Whether to export WVD files. Defaults to False.
|
||||
|
||||
Returns:
|
||||
bool: True if any keys were exported, otherwise False.
|
||||
"""
|
||||
# Find the intersection of client IDs and private keys
|
||||
keys = self.client_id.keys() & self.private_key.keys()
|
||||
|
||||
for k in keys:
|
||||
# Retrieve client information based on the client ID
|
||||
client_info = self.__client_info(self.client_id[k])
|
||||
|
||||
# https://github.com/devine-dl/pywidevine/blob/master/pywidevine/main.py#L211
|
||||
device = Device(
|
||||
client_id=self.client_id[k].SerializeToString(),
|
||||
private_key=self.private_key[k].exportKey('PEM'),
|
||||
private_key=self.private_key[k].exportKey("PEM"),
|
||||
type_=DeviceTypes.ANDROID,
|
||||
security_level=3,
|
||||
flags=None
|
||||
)
|
||||
|
||||
# Generate a sanitized file path for exporting the data
|
||||
# https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146958022
|
||||
parent = sanitize_filepath(parent / client_info['company_name'] / client_info['model_name'] / str(device.system_id) / str(k)[:10])
|
||||
parent = sanitize_filepath(parent / client_info["company_name"] / client_info["model_name"] / str(device.system_id) / str(k)[:10])
|
||||
parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
path_id_bin = parent / 'client_id.bin'
|
||||
# Export the client ID to a binary file
|
||||
path_id_bin = parent / "client_id.bin"
|
||||
path_id_bin.write_bytes(data=device.client_id.SerializeToString())
|
||||
self.logger.info('Exported client ID: %s', path_id_bin)
|
||||
self.logger.info("Exported client ID: %s", path_id_bin)
|
||||
|
||||
path_key_bin = parent / 'private_key.pem'
|
||||
path_key_bin.write_bytes(data=device.private_key.exportKey('PEM'))
|
||||
self.logger.info('Exported private key: %s', path_key_bin)
|
||||
# Export the private key to a PEM file
|
||||
path_key_bin = parent / "private_key.pem"
|
||||
path_key_bin.write_bytes(data=device.private_key.exportKey("PEM"))
|
||||
self.logger.info("Exported private key: %s", path_key_bin)
|
||||
|
||||
# If the WVD option is enabled, export the WVD file
|
||||
if wvd:
|
||||
# Serialize the device to WVD format
|
||||
wvd_bin = device.dumps()
|
||||
|
||||
# Generate a unique name for the WVD file using client and device details
|
||||
name = f"{client_info['company_name']} {client_info['model_name']}"
|
||||
if client_info.get('widevine_cdm_version'):
|
||||
if client_info.get("widevine_cdm_version"):
|
||||
name += f" {client_info['widevine_cdm_version']}"
|
||||
name += f" {crc32(wvd_bin).to_bytes(4, 'big').hex()}"
|
||||
name = unidecode(name.strip().lower().replace(' ', '_'))
|
||||
path_wvd = parent / sanitize_filename(f'{name}_{device.system_id}_l{device.security_level}.wvd')
|
||||
name = unidecode(name.strip().lower().replace(" ", "_"))
|
||||
path_wvd = parent / sanitize_filename(f"{name}_{device.system_id}_l{device.security_level}.wvd")
|
||||
|
||||
# Export the WVD file to disk
|
||||
path_wvd.write_bytes(data=wvd_bin)
|
||||
self.logger.info('Exported WVD: %s', path_wvd)
|
||||
self.logger.info("Exported WVD: %s", path_wvd)
|
||||
|
||||
# If keybox is available and hasn't been exported, issue a warning
|
||||
if self.keybox and not self.keybox.export(parent=parent.parent):
|
||||
self.logger.warning("The keybox has not been intercepted or decrypted")
|
||||
|
||||
# Return True if any keys were exported, otherwise return False
|
||||
return len(keys) > 0
|
||||
|
||||
|
||||
__all__ = ('Cdm',)
|
||||
__all__ = ("Cdm",)
|
||||
|
@ -1,123 +1,145 @@
|
||||
from pathlib import Path
|
||||
|
||||
from keydive.vendor import Vendor
|
||||
|
||||
# https://developer.android.com/ndk/guides/cpp-support
|
||||
# https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:bionic/libc/libc.map.txt
|
||||
NATIVE_C_API = {
|
||||
# BUILT-IN
|
||||
"main",
|
||||
# STDIO
|
||||
'fclose', 'fflush', 'fgetc', 'fgetpos', 'fgets', 'fopen', 'fprintf', 'fputc', 'fputs', 'fread', 'freopen',
|
||||
'fscanf', 'fseek', 'fsetpos', 'ftell', 'fwrite', 'getc', 'getchar', 'gets', 'perror', 'printf', 'putc',
|
||||
'putchar', 'puts', 'remove', 'rename', 'rewind', 'scanf', 'setbuf', 'setvbuf', 'sprintf', 'sscanf', 'tmpfile',
|
||||
'tmpnam', 'ungetc', 'vfprintf', 'vprintf', 'vsprintf', 'fileno', 'feof', 'ferror', 'snprintf',
|
||||
"fclose", "fflush", "fgetc", "fgetpos", "fgets", "fopen", "fprintf", "fputc", "fputs", "fread", "freopen",
|
||||
"fscanf", "fseek", "fsetpos", "ftell", "fwrite", "getc", "getchar", "gets", "perror", "printf", "putc",
|
||||
"putchar", "puts", "remove", "rename", "rewind", "scanf", "setbuf", "setvbuf", "sprintf", "sscanf", "tmpfile",
|
||||
"tmpnam", "ungetc", "vfprintf", "vprintf", "vsprintf", "fileno", "feof", "ferror", "snprintf",
|
||||
# STDLIB
|
||||
'abort', 'abs', 'atexit', 'atof', 'atoi', 'atol', 'bsearch', 'calloc', 'div', 'exit', 'free', 'getenv', 'labs',
|
||||
'ldiv', 'malloc', 'mblen', 'mbstowcs', 'mbtowc', 'qsort', 'rand', 'realloc', 'srand', 'strtod', 'strtol',
|
||||
'strtoul', 'system', 'wcstombs', 'wctomb',
|
||||
"abort", "abs", "atexit", "atof", "atoi", "atol", "bsearch", "calloc", "div", "exit", "free", "getenv", "labs",
|
||||
"ldiv", "malloc", "mblen", "mbstowcs", "mbtowc", "qsort", "rand", "realloc", "srand", "strtod", "strtol",
|
||||
"strtoul", "system", "wcstombs", "wctomb",
|
||||
# STRING
|
||||
'memchr', 'memcmp', 'memcpy', 'memmove', 'memset', 'strcat', 'strchr', 'strcmp', 'strcoll', 'strcpy', 'strcspn',
|
||||
'strerror', 'strlen', 'strncat', 'strncmp', 'strncpy', 'strpbrk', 'strrchr', 'strspn', 'strstr', 'strtok',
|
||||
'strxfrm', 'strncasecmp',
|
||||
"memchr", "memcmp", "memcpy", "memmove", "memset", "strcat", "strchr", "strcmp", "strcoll", "strcpy", "strcspn",
|
||||
"strerror", "strlen", "strncat", "strncmp", "strncpy", "strpbrk", "strrchr", "strspn", "strstr", "strtok",
|
||||
"strxfrm", "strncasecmp",
|
||||
# MATH
|
||||
'acos', 'asin', 'atan', 'atan2', 'cos', 'cosh', 'exp', 'fabs', 'floor', 'fmod', 'frexp', 'ldexp', 'log',
|
||||
'log10', 'modf', 'pow', 'sin', 'sinh', 'sqrt', 'tan', 'tanh',
|
||||
"acos", "asin", "atan", "atan2", "cos", "cosh", "exp", "fabs", "floor", "fmod", "frexp", "ldexp", "log",
|
||||
"log10", "modf", "pow", "sin", "sinh", "sqrt", "tan", "tanh",
|
||||
# CTYPE
|
||||
'isalnum', 'isalpha', 'iscntrl', 'isdigit', 'isgraph', 'islower', 'isprint', 'ispunct', 'isspace', 'isupper',
|
||||
'isxdigit', 'tolower', 'toupper',
|
||||
"isalnum", "isalpha", "iscntrl", "isdigit", "isgraph", "islower", "isprint", "ispunct", "isspace", "isupper",
|
||||
"isxdigit", "tolower", "toupper",
|
||||
# TIME
|
||||
'asctime', 'clock', 'ctime', 'difftime', 'gmtime', 'localtime', 'mktime', 'strftime', 'time',
|
||||
"asctime", "clock", "ctime", "difftime", "gmtime", "localtime", "mktime", "strftime", "time",
|
||||
# UNISTD
|
||||
'access', 'alarm', 'chdir', 'chown', 'close', 'dup', 'dup2', 'execle', 'execv', 'execve', 'execvp', 'fork',
|
||||
'fpathconf', 'getcwd', 'getegid', 'geteuid', 'getgid', 'getgroups', 'getlogin', 'getopt', 'getpgid', 'getpgrp',
|
||||
'getpid', 'getppid', 'getuid', 'isatty', 'lseek', 'pathconf', 'pause', 'pipe', 'read', 'rmdir', 'setgid',
|
||||
'setpgid', 'setsid', 'setuid', 'sleep', 'sysconf', 'tcgetpgrp', 'tcsetpgrp', 'ttyname', 'ttyname_r', 'write',
|
||||
'fsync', 'unlink', 'syscall', 'getpagesize',
|
||||
"access", "alarm", "chdir", "chown", "close", "dup", "dup2", "execle", "execv", "execve", "execvp", "fork",
|
||||
"fpathconf", "getcwd", "getegid", "geteuid", "getgid", "getgroups", "getlogin", "getopt", "getpgid", "getpgrp",
|
||||
"getpid", "getppid", "getuid", "isatty", "lseek", "pathconf", "pause", "pipe", "read", "rmdir", "setgid",
|
||||
"setpgid", "setsid", "setuid", "sleep", "sysconf", "tcgetpgrp", "tcsetpgrp", "ttyname", "ttyname_r", "write",
|
||||
"fsync", "unlink", "syscall", "getpagesize",
|
||||
# FCNTL
|
||||
'creat', 'fcntl', 'open',
|
||||
"creat", "fcntl", "open",
|
||||
# SYS_TYPE
|
||||
'fd_set', 'FD_CLR', 'FD_ISSET', 'FD_SET', 'FD_ZERO',
|
||||
"fd_set", "FD_CLR", "FD_ISSET", "FD_SET", "FD_ZERO",
|
||||
# SYS_STAT
|
||||
'chmod', 'fchmod', 'fstat', 'mkdir', 'mkfifo', 'stat', 'umask',
|
||||
"chmod", "fchmod", "fstat", "mkdir", "mkfifo", "stat", "umask",
|
||||
# SYS_TIME
|
||||
'gettimeofday', 'select', 'settimeofday',
|
||||
"gettimeofday", "select", "settimeofday",
|
||||
# SIGNAL
|
||||
'signal', 'raise', 'kill', 'sigaction', 'sigaddset', 'sigdelset', 'sigemptyset', 'sigfillset', 'sigismember',
|
||||
'sigpending', 'sigprocmask', 'sigsuspend', 'alarm', 'pause',
|
||||
"signal", "raise", "kill", "sigaction", "sigaddset", "sigdelset", "sigemptyset", "sigfillset", "sigismember",
|
||||
"sigpending", "sigprocmask", "sigsuspend", "alarm", "pause",
|
||||
# SETJMP
|
||||
'longjmp', 'setjmp',
|
||||
"longjmp", "setjmp",
|
||||
# ERRNO
|
||||
'errno', 'strerror', 'perror',
|
||||
"errno", "strerror", "perror",
|
||||
# ASSERT
|
||||
'assert',
|
||||
"assert",
|
||||
# LOCAL
|
||||
'localeconv', 'setlocale',
|
||||
"localeconv", "setlocale",
|
||||
# WCHAR
|
||||
'btowc', 'fgetwc', 'fgetws', 'fputwc', 'fputws', 'fwide', 'fwprintf', 'fwscanf', 'getwc', 'getwchar', 'mbrlen',
|
||||
'mbrtowc', 'mbsinit', 'mbsrtowcs', 'putwc', 'putwchar', 'swprintf', 'swscanf', 'ungetwc', 'vfwprintf',
|
||||
'vfwscanf', 'vwprintf', 'vwscanf', 'wcrtomb', 'wcscat', 'wcschr', 'wcscmp', 'wcscoll', 'wcscpy', 'wcscspn',
|
||||
'wcsftime', 'wcslen', 'wcsncat', 'wcsncmp', 'wcsncpy', 'wcspbrk', 'wcsrchr', 'wcsrtombs', 'wcsspn', 'wcsstr',
|
||||
'wcstod', 'wcstok', 'wcstol', 'wcstombs', 'wcstoul', 'wcsxfrm', 'wctob', 'wmemchr', 'wmemcmp', 'wmemcpy',
|
||||
'wmemmove', 'wmemset', 'wprintf', 'wscanf',
|
||||
"btowc", "fgetwc", "fgetws", "fputwc", "fputws", "fwide", "fwprintf", "fwscanf", "getwc", "getwchar", "mbrlen",
|
||||
"mbrtowc", "mbsinit", "mbsrtowcs", "putwc", "putwchar", "swprintf", "swscanf", "ungetwc", "vfwprintf",
|
||||
"vfwscanf", "vwprintf", "vwscanf", "wcrtomb", "wcscat", "wcschr", "wcscmp", "wcscoll", "wcscpy", "wcscspn",
|
||||
"wcsftime", "wcslen", "wcsncat", "wcsncmp", "wcsncpy", "wcspbrk", "wcsrchr", "wcsrtombs", "wcsspn", "wcsstr",
|
||||
"wcstod", "wcstok", "wcstol", "wcstombs", "wcstoul", "wcsxfrm", "wctob", "wmemchr", "wmemcmp", "wmemcpy",
|
||||
"wmemmove", "wmemset", "wprintf", "wscanf",
|
||||
# WCTYPE
|
||||
'iswalnum', 'iswalpha', 'iswcntrl', 'iswdigit', 'iswgraph', 'iswlower', 'iswprint', 'iswpunct', 'iswspace',
|
||||
'iswupper', 'iswxdigit', 'towlower', 'towupper', 'iswctype', 'wctype',
|
||||
"iswalnum", "iswalpha", "iswcntrl", "iswdigit", "iswgraph", "iswlower", "iswprint", "iswpunct", "iswspace",
|
||||
"iswupper", "iswxdigit", "towlower", "towupper", "iswctype", "wctype",
|
||||
# STDDEF
|
||||
'NULL', 'offsetof', 'ptrdiff_t', 'size_t', 'wchar_t',
|
||||
"NULL", "offsetof", "ptrdiff_t", "size_t", "wchar_t",
|
||||
# STDARG
|
||||
'va_arg', 'va_end', 'va_start',
|
||||
"va_arg", "va_end", "va_start",
|
||||
# DLFCN
|
||||
'dlclose', 'dlerror', 'dlopen', 'dlsym',
|
||||
"dlclose", "dlerror", "dlopen", "dlsym",
|
||||
# DIRENT
|
||||
'closedir', 'opendir', 'readdir',
|
||||
"closedir", "opendir", "readdir",
|
||||
# SYS_SENDFILE
|
||||
'sendfile',
|
||||
"sendfile",
|
||||
# SYS_MMAN
|
||||
'mmap', 'mprotect', 'munmap',
|
||||
"mmap", "mprotect", "munmap",
|
||||
# SYS_UTSNAME
|
||||
'uname',
|
||||
"uname",
|
||||
# LINK
|
||||
'dladdr'
|
||||
"dladdr"
|
||||
}
|
||||
|
||||
# https://cs.android.com/search?q=oemcrypto&sq=&ss=android%2Fplatform%2Fsuperproject
|
||||
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', 'zgtjmxko', 'igrqajte', 'ofskesua', 'qllcoacg', 'pukctkiv', 'ehdqmfmd',
|
||||
'xftzvkwx', 'gndskkuk', 'wcggmnnx', 'kaatohcz', 'ktmgdchz', 'jkcwonus', 'ehmduqyt'
|
||||
"rnmsglvj", "polorucp", "kqzqahjq", "pldrclfq", "kgaitijd", "cwkfcplc", "crhqcdet", "ulns", "dnvffnze", "ygjiljer",
|
||||
"qbjxtubz", "qkfrcjtw", "rbhjspoh", "zgtjmxko", "igrqajte", "ofskesua", "qllcoacg", "pukctkiv", "ehdqmfmd",
|
||||
"xftzvkwx", "gndskkuk", "wcggmnnx", "kaatohcz", "ktmgdchz", "jkcwonus", "ehmduqyt", "vewtuecx", "mxrbzntq",
|
||||
"isyowgmp", "flzfkhbc", "rtgejgqb", "sxxprljw", "ebxjbtxl", "pcmtpkrj"
|
||||
# Add more as needed for different versions.
|
||||
}
|
||||
|
||||
# https://developer.android.com/tools/releases/platforms
|
||||
CDM_VENDOR_API = {
|
||||
'mediaserver': [
|
||||
Vendor(22, 11, '1.0', 'libwvdrmengine.so'),
|
||||
Vendor(23, 11, '1.0', 'libwvdrmengine.so')
|
||||
"mediaserver": [
|
||||
Vendor(22, 11, "1.0", r"libwvdrmengine(?:@\S+)?\.so")
|
||||
],
|
||||
'mediadrmserver': [
|
||||
Vendor(24, 11, '1.0', 'libwvdrmengine.so')
|
||||
"mediadrmserver": [
|
||||
Vendor(24, 11, "1.0", r"libwvdrmengine(?:@\S+)?\.so")
|
||||
],
|
||||
'android.hardware.drm@1.0-service.widevine': [
|
||||
Vendor(26, 13, '5.1.0', 'libwvhidl.so')
|
||||
"android.hardware.drm@1.0-service.widevine": [
|
||||
Vendor(26, 13, "5.1.0", r"libwvhidl(?:@\S+)?\.so")
|
||||
],
|
||||
'android.hardware.drm@1.1-service.widevine': [
|
||||
Vendor(28, 14, '14.0.0', 'libwvhidl.so')
|
||||
"android.hardware.drm@1.1-service.widevine": [
|
||||
Vendor(28, 14, "14.0.0", r"libwvhidl(?:@\S+)?\.so")
|
||||
],
|
||||
'android.hardware.drm@1.2-service.widevine': [
|
||||
Vendor(29, 15, '15.0.0', 'libwvhidl.so')
|
||||
"android.hardware.drm@1.2-service.widevine": [
|
||||
Vendor(29, 15, "15.0.0", r"libwvhidl(?:@\S+)?\.so")
|
||||
],
|
||||
'android.hardware.drm@1.3-service.widevine': [
|
||||
Vendor(30, 16, '16.0.0', 'libwvhidl.so')
|
||||
"android.hardware.drm@1.3-service.widevine": [
|
||||
Vendor(30, 16, "16.0.0", r"libwvhidl(?:@\S+)?\.so")
|
||||
],
|
||||
'android.hardware.drm@1.4-service.widevine': [
|
||||
Vendor(31, 16, '16.1.0', 'libwvhidl.so')
|
||||
"android.hardware.drm@1.4-service.widevine": [
|
||||
Vendor(31, 16, "16.1.0", r"libwvhidl(?:@\S+)?\.so")
|
||||
],
|
||||
'android.hardware.drm-service.widevine': [
|
||||
Vendor(33, 17, '17.0.0', 'libwvaidl.so'),
|
||||
Vendor(34, 18, '18.0.0', 'android.hardware.drm-service.widevine')
|
||||
"android.hardware.drm-service.widevine": [
|
||||
Vendor(33, 17, "17.0.0", r"libwvaidl(?:@\S+)?\.so"),
|
||||
Vendor(34, 18, "18.0.0", r"android\.hardware\.drm-service(?:-lazy)?\.widevine(?:@\S+)?"),
|
||||
Vendor(35, 18, "19.0.1", r"android\.hardware\.drm-service(?:-lazy)?\.widevine(?:@\S+)?")
|
||||
]
|
||||
}
|
||||
|
||||
# https://developers.google.com/widevine
|
||||
CDM_FUNCTION_API = {
|
||||
'UsePrivacyMode',
|
||||
'GetCdmClientPropertySet',
|
||||
'PrepareKeyRequest',
|
||||
'getOemcryptoDeviceId'
|
||||
"UsePrivacyMode",
|
||||
"GetCdmClientPropertySet",
|
||||
"PrepareKeyRequest",
|
||||
"getOemcryptoDeviceId",
|
||||
"lcc07",
|
||||
"oecc07",
|
||||
"Read",
|
||||
"x1c36",
|
||||
"runningcrc"
|
||||
}
|
||||
|
||||
# Maximum clear API level for Keybox
|
||||
# https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:trusty/user/app/sample/hwcrypto/keybox/keybox.c
|
||||
KEYBOX_MAX_CLEAR_API = 28
|
||||
|
||||
# https://github.com/kaltura/kaltura-device-info-android
|
||||
DRM_PLAYER = {
|
||||
"package": "com.kaltura.kalturadeviceinfo",
|
||||
"path": Path(__file__).parent.parent / "docs" / "server" / "kaltura.apk",
|
||||
"url": "https://github.com/kaltura/kaltura-device-info-android/releases/download/t3/kaltura-device-info-release.apk"
|
||||
}
|
||||
|
234
keydive/core.py
234
keydive/core.py
@ -1,15 +1,15 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import frida
|
||||
import xmltodict
|
||||
|
||||
from frida.core import Device, Session, Script
|
||||
from frida.core import Session, Script
|
||||
|
||||
from keydive.adb import ADB
|
||||
from keydive.cdm import Cdm
|
||||
from keydive.constants import OEM_CRYPTO_API, NATIVE_C_API, CDM_FUNCTION_API
|
||||
from keydive.vendor import Vendor
|
||||
@ -17,69 +17,66 @@ from keydive.vendor import Vendor
|
||||
|
||||
class Core:
|
||||
"""
|
||||
Core class for handling DRM operations and device interactions.
|
||||
Core class for managing DRM operations and interactions with Android devices.
|
||||
"""
|
||||
|
||||
def __init__(self, cdm: Cdm, device: str = None, functions: Path = None, skip: bool = False):
|
||||
def __init__(self, adb: ADB, cdm: Cdm, functions: Path = None, skip: bool = False):
|
||||
"""
|
||||
Initializes a Core instance.
|
||||
|
||||
Args:
|
||||
cdm (Cdm): Instance of Cdm for managing DRM related operations.
|
||||
device (str, optional): ID of the Android device to connect to via ADB. Defaults to None (uses USB device).
|
||||
functions (Path, optional): Path to Ghidra XML functions file for symbol extraction. Defaults to None.
|
||||
skip (bool, optional): Flag to determine whether to skip predefined functions (e.g., OEM_CRYPTO_API).
|
||||
Parameters:
|
||||
adb (ADB): ADB instance for device communication.
|
||||
cdm (Cdm): Instance for handling DRM-related operations.
|
||||
functions (Path, optional): Path to Ghidra XML file for symbol extraction. Defaults to None.
|
||||
skip (bool, optional): Whether to skip predefined functions (e.g., OEM_CRYPTO_API). Defaults to False.
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
self.running = True
|
||||
self.cdm = cdm
|
||||
self.adb = adb
|
||||
|
||||
# Flag to skip predefined functions based on the vendor's API level
|
||||
# https://github.com/hyugogirubato/KeyDive/issues/38#issuecomment-2411932679
|
||||
self.skip = skip
|
||||
|
||||
# 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)
|
||||
|
||||
# Obtain device properties
|
||||
properties = self.device_properties()
|
||||
self.logger.info('SDK API: %s', properties['ro.build.version.sdk'])
|
||||
self.logger.info('ABI CPU: %s', properties['ro.product.cpu.abi'])
|
||||
|
||||
# Load the hook script
|
||||
# Load the hook script with relevant data and prepare for injection
|
||||
self.functions = functions
|
||||
self.script = self.__prepare_hook_script()
|
||||
self.logger.info('Script loaded successfully')
|
||||
self.logger.info("Hook script prepared successfully")
|
||||
|
||||
def __prepare_hook_script(self) -> str:
|
||||
"""
|
||||
Prepares the hook script content by injecting the library-specific scripts.
|
||||
Prepares the hook script by injecting library-specific data.
|
||||
|
||||
Returns:
|
||||
str: The prepared script content.
|
||||
str: The finalized hook script content with placeholders replaced.
|
||||
"""
|
||||
content = Path(__file__).with_name('keydive.js').read_text(encoding='utf-8')
|
||||
# Read the base JavaScript template file
|
||||
content = Path(__file__).with_name("keydive.js").read_text(encoding="utf-8")
|
||||
|
||||
# Generate the list of symbols from the functions file
|
||||
symbols = self.__prepare_symbols(self.functions)
|
||||
|
||||
# Replace placeholders in script template
|
||||
# Define the placeholder replacements
|
||||
replacements = {
|
||||
'${OEM_CRYPTO_API}': json.dumps(list(OEM_CRYPTO_API)),
|
||||
'${NATIVE_C_API}': json.dumps(list(NATIVE_C_API)),
|
||||
'${SYMBOLS}': json.dumps(symbols),
|
||||
'${SKIP}': str(self.skip)
|
||||
"${OEM_CRYPTO_API}": json.dumps(list(OEM_CRYPTO_API)),
|
||||
"${NATIVE_C_API}": json.dumps(list(NATIVE_C_API)),
|
||||
"${SYMBOLS}": json.dumps(symbols),
|
||||
"${SKIP}": str(self.skip)
|
||||
}
|
||||
|
||||
# Replace placeholders in the script content
|
||||
for placeholder, value in replacements.items():
|
||||
content = content.replace(placeholder, value)
|
||||
content = content.replace(placeholder, value, 1)
|
||||
|
||||
return content
|
||||
|
||||
def __prepare_symbols(self, path: Path) -> list:
|
||||
"""
|
||||
Parses the provided XML functions file to select relevant functions.
|
||||
Extracts relevant functions from a Ghidra XML file.
|
||||
|
||||
Args:
|
||||
path (Path): Path to Ghidra XML functions file.
|
||||
Parameters:
|
||||
path (Path): Path to the Ghidra XML functions file.
|
||||
|
||||
Returns:
|
||||
list: List of selected functions as dictionaries.
|
||||
@ -88,117 +85,83 @@ class Core:
|
||||
FileNotFoundError: If the functions file is not found.
|
||||
ValueError: If functions extraction fails.
|
||||
"""
|
||||
# Return an empty list if no path is provided
|
||||
if not path:
|
||||
return []
|
||||
elif not path.is_file():
|
||||
raise FileNotFoundError('Functions file not found')
|
||||
|
||||
try:
|
||||
program = xmltodict.parse(path.read_bytes())['PROGRAM']
|
||||
addr_base = int(program['@IMAGE_BASE'], 16)
|
||||
functions = program['FUNCTIONS']['FUNCTION']
|
||||
# Parse the XML file and extract program data
|
||||
program = xmltodict.parse(path.read_bytes())["PROGRAM"]
|
||||
addr_base = int(program["@IMAGE_BASE"], 16) # Base address for function addresses
|
||||
functions = program["FUNCTIONS"]["FUNCTION"] # List of functions in the XML
|
||||
|
||||
# Find a target function from a predefined list
|
||||
target = None if self.skip else next((f['@NAME'] for f in functions if f['@NAME'] in OEM_CRYPTO_API), None)
|
||||
# Identify a target function from the predefined OEM_CRYPTO_API list (if not skipped)
|
||||
target = next((f["@NAME"] for f in functions if f["@NAME"] in OEM_CRYPTO_API and not self.skip), None)
|
||||
|
||||
# Extract relevant functions
|
||||
# Prepare a dictionary to store selected functions
|
||||
selected = {}
|
||||
for func in functions:
|
||||
name = func['@NAME']
|
||||
args = len(func.get('REGISTER_VAR', []))
|
||||
name = func["@NAME"] # Function name
|
||||
args = len(func.get("REGISTER_VAR", [])) # Number of arguments
|
||||
|
||||
# Add function if it matches specific criteria
|
||||
"""
|
||||
Add the function if it matches specific criteria
|
||||
- Match the target function if identified
|
||||
- Match API keywords
|
||||
- Match unnamed functions with 6+ args
|
||||
"""
|
||||
if name not in selected and (
|
||||
name == target
|
||||
or any(None if self.skip else keyword in name for keyword in CDM_FUNCTION_API)
|
||||
or (not target and re.match(r'^[a-z]+$', name) and args >= 6)
|
||||
or any(True if self.skip else keyword in name for keyword in CDM_FUNCTION_API)
|
||||
or (not target and re.match(r"^[a-z]+$", name) and args >= 6)
|
||||
):
|
||||
selected[name] = {
|
||||
'type': 'function',
|
||||
'name': name,
|
||||
'address': hex(int(func['@ENTRY_POINT'], 16) - addr_base)
|
||||
"type": "function",
|
||||
"name": name,
|
||||
"address": hex(int(func["@ENTRY_POINT"], 16) - addr_base) # Calculate relative address
|
||||
}
|
||||
|
||||
# Return the list of selected functions
|
||||
return list(selected.values())
|
||||
except FileNotFoundError as e:
|
||||
raise FileNotFoundError(f"Functions file not found: {path}") from e
|
||||
except Exception as e:
|
||||
raise ValueError('Failed to extract functions from Ghidra') from e
|
||||
|
||||
def device_properties(self) -> dict:
|
||||
"""
|
||||
Retrieves system properties from the connected device using ADB shell commands.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary of device properties.
|
||||
"""
|
||||
# https://source.android.com/docs/core/architecture/configuration/add-system-properties?#shell-commands
|
||||
properties = {}
|
||||
sp = subprocess.run(['adb', '-s', str(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()
|
||||
# Attempt to cast numeric and boolean values to appropriate types
|
||||
try:
|
||||
value = int(value)
|
||||
except ValueError:
|
||||
if value.lower() in ('true', 'false'):
|
||||
value = value.lower() == 'true'
|
||||
properties[key] = value
|
||||
return properties
|
||||
|
||||
def enumerate_processes(self) -> dict:
|
||||
"""
|
||||
Lists processes running on the device, returning a mapping of process names to PIDs.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping process names to PIDs.
|
||||
"""
|
||||
processes = {}
|
||||
|
||||
# https://github.com/frida/frida/issues/1225#issuecomment-604181822
|
||||
prompt = ['adb', '-s', str(self.device.id), 'shell', 'ps']
|
||||
lines = subprocess.run([*prompt, '-A'], capture_output=True).stdout.decode('utf-8').splitlines()
|
||||
if len(lines) < 10:
|
||||
lines = subprocess.run(prompt, capture_output=True).stdout.decode('utf-8').splitlines()
|
||||
# Iterate through lines starting from the second line (skipping header)
|
||||
for line in lines[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
|
||||
raise ValueError("Failed to extract functions from Ghidra XML file") from e
|
||||
|
||||
def __process_message(self, message: dict, data: bytes) -> None:
|
||||
"""
|
||||
Handles messages received from the Frida script.
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
message (dict): The message payload.
|
||||
data (bytes): The raw data associated with the message.
|
||||
"""
|
||||
logger = logging.getLogger('Script')
|
||||
level = message.get('payload')
|
||||
logger = logging.getLogger("Script")
|
||||
level = message.get("payload")
|
||||
|
||||
if isinstance(level, int):
|
||||
# Process logging messages from Frida script
|
||||
logger.log(level=level, msg=data.decode('utf-8'))
|
||||
# Log the message based on its severity level
|
||||
logger.log(level=level, msg=data.decode("utf-8"))
|
||||
if level in (logging.FATAL, logging.CRITICAL):
|
||||
self.running = False
|
||||
elif isinstance(level, dict) and 'private_key' in level:
|
||||
self.cdm.set_private_key(data=data, name=level['private_key'])
|
||||
elif level == 'challenge':
|
||||
self.running = False # Stop the process on critical errors
|
||||
elif isinstance(level, dict) and "private_key" in level:
|
||||
# Set the private key in the DRM handler
|
||||
self.cdm.set_private_key(data=data, name=level["private_key"])
|
||||
elif level == "challenge":
|
||||
# Set the challenge data in the DRM handler
|
||||
self.cdm.set_challenge(data=data)
|
||||
elif level == 'client_id':
|
||||
self.cdm.set_client_id(data=data)
|
||||
elif level == "device_id":
|
||||
# Set the device ID in the DRM handler
|
||||
self.cdm.set_device_id(data)
|
||||
elif level == "keybox":
|
||||
# Set the keybox data in the DRM handler
|
||||
self.cdm.set_keybox(data)
|
||||
|
||||
def hook_process(self, pid: int, vendor: Vendor, timeout: int = 0) -> bool:
|
||||
"""
|
||||
Hooks into the specified process.
|
||||
|
||||
Args:
|
||||
Parameters:
|
||||
pid (int): The process ID to hook.
|
||||
vendor (Vendor): Instance of Vendor class representing the vendor information.
|
||||
timeout (int, optional): Timeout for attaching to the process. Defaults to 0.
|
||||
@ -207,36 +170,59 @@ class Core:
|
||||
bool: True if the process was successfully hooked, otherwise False.
|
||||
"""
|
||||
try:
|
||||
session: Session = self.device.attach(pid, persist_timeout=timeout)
|
||||
# Attach to the target process using the specified PID.
|
||||
# The 'persist_timeout' parameter ensures the session persists for the given duration.
|
||||
session: Session = self.adb.device.attach(pid, persist_timeout=timeout)
|
||||
except frida.ServerNotRunningError as e:
|
||||
raise EnvironmentError('Frida server is not running') from e
|
||||
# Handle the case where the Frida server is not running on the device.
|
||||
raise EnvironmentError("Frida server is not running") from e
|
||||
except Exception as e:
|
||||
# Log other exceptions and return False to indicate failure.
|
||||
self.logger.error(e)
|
||||
return False
|
||||
|
||||
# Define a callback to handle when the process is destroyed.
|
||||
def __process_destroyed() -> None:
|
||||
session.detach()
|
||||
|
||||
# Create a Frida script object using the prepared script content.
|
||||
script: Script = session.create_script(self.script)
|
||||
script.on('message', self.__process_message)
|
||||
script.on('destroyed', __process_destroyed)
|
||||
script.on("message", self.__process_message)
|
||||
script.on("destroyed", __process_destroyed)
|
||||
script.load()
|
||||
|
||||
library = script.exports_sync.getlibrary(vendor.name)
|
||||
# Fetch a list of libraries loaded by the target process.
|
||||
libraries = script.exports_sync.getlibraries()
|
||||
library = next((l for l in libraries if re.match(vendor.pattern, l["name"])), None)
|
||||
|
||||
if library:
|
||||
self.logger.info('Library: %s (%s)', library['name'], library['path'])
|
||||
# Log information about the library if it is found.
|
||||
self.logger.info("Library: %s (%s)", library["name"], library["path"])
|
||||
|
||||
# Check if Ghidra XML functions loaded
|
||||
if vendor.oem > 17 and not self.functions:
|
||||
self.logger.warning('For OEM API > 17, specifying "functions" is required, refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md')
|
||||
elif vendor.oem < 18 and self.functions:
|
||||
self.logger.warning('The "functions" attribute is deprecated for OEM API < 18')
|
||||
# Retrieve and log the version of the Frida server.
|
||||
version = script.exports_sync.getversion()
|
||||
self.logger.debug(f"Server: %s", version)
|
||||
|
||||
return script.exports_sync.hooklibrary(vendor.name)
|
||||
# Determine if the Frida server version is older than 16.6.0.
|
||||
code = tuple(map(int, version.split(".")))
|
||||
minimum = code[0] > 16 or (code[0] == 16 and code[1] >= 6)
|
||||
|
||||
# Warn the user if certain conditions related to the functions option are met.
|
||||
if minimum and self.functions:
|
||||
self.logger.warning("The '--functions' option is deprecated starting from Frida 16.6.0")
|
||||
elif not minimum and vendor.oem < 18 and self.functions:
|
||||
self.logger.warning("The '--functions' option is deprecated for OEM API < 18")
|
||||
elif not minimum and vendor.oem > 17 and not self.functions:
|
||||
self.logger.warning("For OEM API > 17, specifying '--functions' is required. Refer to https://github.com/hyugogirubato/KeyDive/blob/main/docs/FUNCTIONS.md")
|
||||
|
||||
# Enable dynamic analysis (symbols) only when necessary
|
||||
dynamic = minimum and vendor.oem > 17 and not self.functions
|
||||
return script.exports_sync.hooklibrary(library["name"], dynamic)
|
||||
|
||||
# Unload the script if the target library is not found.
|
||||
script.unload()
|
||||
self.logger.warning('Library not found: %s' % vendor.name)
|
||||
self.logger.warning("Library not found: %s" % vendor.pattern)
|
||||
return False
|
||||
|
||||
|
||||
__all__ = ('Core',)
|
||||
__all__ = ("Core",)
|
||||
|
180
keydive/keybox.py
Normal file
180
keydive/keybox.py
Normal file
@ -0,0 +1,180 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
|
||||
from json.encoder import encode_basestring_ascii
|
||||
from typing import Literal
|
||||
from uuid import UUID
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def bytes2int(value: bytes, byteorder: Literal["big", "little"] = "big", signed: bool = False) -> int:
|
||||
"""
|
||||
Convert a byte sequence to an integer.
|
||||
|
||||
Parameters:
|
||||
value (bytes): The byte sequence to convert.
|
||||
byteorder (str, optional): Byte order for conversion. 'big' or 'little'. Defaults to 'big'.
|
||||
signed (bool, optional): Whether the integer is signed. Defaults to False.
|
||||
|
||||
Returns:
|
||||
int: The integer representation of the byte sequence.
|
||||
"""
|
||||
return int.from_bytes(value, byteorder=byteorder, signed=signed)
|
||||
|
||||
|
||||
class Keybox:
|
||||
"""
|
||||
The Keybox class handles the storage and management of device IDs and keybox data.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initializes the Keybox object, setting up logger and containers for device IDs and keyboxes.
|
||||
"""
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
# https://github.com/kaltura/kaltura-device-info-android/blob/master/app/src/main/java/com/kaltura/kalturadeviceinfo/MainActivity.java#L203
|
||||
self.device_id = []
|
||||
self.keybox = {}
|
||||
|
||||
def set_device_id(self, data: bytes) -> None:
|
||||
"""
|
||||
Set the device ID from the provided data.
|
||||
|
||||
Parameters:
|
||||
data (bytes): The device ID, expected to be 32 bytes long.
|
||||
|
||||
Raises:
|
||||
AssertionError: If the data length is not 32 bytes.
|
||||
"""
|
||||
try:
|
||||
size = len(data)
|
||||
# Ensure the device ID is exactly 32 bytes long
|
||||
assert size == 32, f"Invalid device ID length: {size}. Should be 32 bytes"
|
||||
|
||||
# Add device ID to the list if it's not already present
|
||||
if data not in self.device_id:
|
||||
self.logger.info("Receive device id: \n\n%s\n", encode_basestring_ascii(data.decode("utf-8")))
|
||||
self.device_id.append(data)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.debug("Failed to set device id: %s", e)
|
||||
|
||||
def set_keybox(self, data: bytes) -> None:
|
||||
"""
|
||||
Set the keybox from the provided data.
|
||||
|
||||
Parameters:
|
||||
data (bytes): The keybox data, expected to be either 128 or 132 bytes long.
|
||||
|
||||
Raises:
|
||||
AssertionError: If the data length is not 128 or 132 bytes or does not meet other criteria.
|
||||
"""
|
||||
# https://github.com/zybpp/Python/tree/master/Python/keybox
|
||||
try:
|
||||
size = len(data)
|
||||
# Validate the keybox size (128 or 132 bytes)
|
||||
assert size in (128, 132), f"Invalid keybox length: {size}. Should be 128 or 132 bytes"
|
||||
|
||||
# Validate the QSEE-style keybox end
|
||||
assert size == 128 or data[128:132] == b"LVL1", "QSEE-style keybox must end with bytes 'LVL1'"
|
||||
|
||||
# Validate the keybox magic (should be 'kbox')
|
||||
assert data[120:124] == b"kbox", "Invalid keybox magic"
|
||||
|
||||
device_id = data[0:32] # Extract the device ID from the first 32 bytes
|
||||
|
||||
# Retrieve and log the structured keybox information
|
||||
infos = self.__keybox_info(data)
|
||||
encrypted = infos["flags"] > 10 # Check if the keybox is encrypted
|
||||
self.set_device_id(data=device_id) # Set the device ID
|
||||
|
||||
# Log and store the keybox data if it's a new keybox or the device ID is updated
|
||||
if (device_id in self.keybox and self.keybox[device_id] != (data, encrypted)) or device_id not in self.keybox:
|
||||
self.logger.info("Receive keybox: \n\n%s\n", json.dumps(infos, indent=2))
|
||||
|
||||
# Warn if keybox is encrypted and interception of plaintext device token is needed
|
||||
if encrypted:
|
||||
self.logger.warning("Keybox contains encrypted data. Interception of plaintext device token is needed")
|
||||
|
||||
# Store the keybox (encrypted or not) for the device ID
|
||||
if (device_id in self.keybox and not encrypted) or device_id not in self.keybox:
|
||||
self.keybox[device_id] = (data, encrypted)
|
||||
except Exception as e:
|
||||
self.logger.debug("Failed to set keybox: %s", e)
|
||||
|
||||
@staticmethod
|
||||
def __keybox_info(data: bytes) -> dict:
|
||||
"""
|
||||
Extract keybox information from the provided data.
|
||||
|
||||
Parameters:
|
||||
data (bytes): The keybox data.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing extracted keybox information.
|
||||
"""
|
||||
# https://github.com/wvdumper/dumper/blob/main/Helpers/Keybox.py#L51
|
||||
|
||||
# Extract device-specific information from the keybox data
|
||||
device_token = data[48:120]
|
||||
|
||||
# Prepare the keybox content dictionary
|
||||
content = {
|
||||
"device_id": data[0:32].decode("utf-8"), # Device's unique identifier (32 bytes)
|
||||
"device_key": data[32:48], # Device cryptographic key (16 bytes)
|
||||
"device_token": device_token, # Token for device authentication (72 bytes)
|
||||
"keybox_tag": data[120:124].decode("utf-8"), # Magic tag (4 bytes)
|
||||
"crc32": bytes2int(data[124:128]), # CRC32 checksum (4 bytes)
|
||||
"level_tag": data[128:132].decode("utf-8") or None, # Optional level tag (4 bytes)
|
||||
|
||||
# Extract metadata from the device token (Bytes 48–120)
|
||||
"flags": bytes2int(device_token[0:4]), # Device flags (4 bytes)
|
||||
"system_id": bytes2int(device_token[4:8]), # System identifier (4 bytes)
|
||||
"provisioning_id": UUID(bytes_le=device_token[8:24]), # Provisioning UUID (16 bytes)
|
||||
"encrypted_bits": device_token[24:72] # Encrypted device-specific information (48 bytes)
|
||||
}
|
||||
|
||||
# https://github.com/ThatNotEasy/Parser-DRM/blob/main/modules/widevine.py#L84
|
||||
# TODO: decrypt device token value
|
||||
|
||||
# Encode bytes as base64 and convert UUIDs to string
|
||||
return {
|
||||
k: base64.b64encode(v).decode("utf-8") if isinstance(v, bytes) else str(v) if isinstance(v, UUID) else v
|
||||
for k, v in content.items()
|
||||
}
|
||||
|
||||
def export(self, parent: Path) -> bool:
|
||||
"""
|
||||
Export the keybox data to a file in the specified parent directory.
|
||||
|
||||
Parameters:
|
||||
parent (Path): The parent directory where the keybox data will be saved.
|
||||
|
||||
Returns:
|
||||
bool: True if any keybox were exported, otherwise False.
|
||||
"""
|
||||
# Find matching keyboxes based on the device_id
|
||||
keys = self.device_id & self.keybox.keys()
|
||||
|
||||
for k in keys:
|
||||
# Create the parent directory if it doesn't exist
|
||||
parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Define the export file path and extension (encrypted or binary)
|
||||
path_keybox_bin = parent / ("keybox." + ("enc" if self.keybox[k][1] else "bin"))
|
||||
|
||||
# Write the keybox data to the file
|
||||
path_keybox_bin.write_bytes(self.keybox[k][0])
|
||||
|
||||
# Log export status based on whether the keybox is encrypted
|
||||
if self.keybox[k][1]:
|
||||
self.logger.warning("Exported encrypted keybox: %s", path_keybox_bin)
|
||||
else:
|
||||
self.logger.info("Exported keybox: %s", path_keybox_bin)
|
||||
|
||||
# Return True if any keyboxes were exported, otherwise False
|
||||
return len(keys) > 0
|
||||
|
||||
|
||||
__all__ = ("Keybox",)
|
@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Date: 2024-10-20
|
||||
* Date: 2025-03-01
|
||||
* Description: DRM key extraction for research and educational purposes.
|
||||
* Source: https://github.com/hyugogirubato/KeyDive
|
||||
*/
|
||||
@ -65,27 +65,41 @@ const print = (level, message) => {
|
||||
send(level, message);
|
||||
}
|
||||
|
||||
const getVersion = () => Frida.version;
|
||||
|
||||
|
||||
// @Utils
|
||||
const getLibraries = (name) => {
|
||||
const getLibraries = () => {
|
||||
// https://github.com/hyugogirubato/KeyDive/issues/14#issuecomment-2146788792
|
||||
try {
|
||||
const libraries = Process.enumerateModules();
|
||||
return libraries.filter(l => l.name.includes(name));
|
||||
return Process.enumerateModules();
|
||||
} catch (e) {
|
||||
print(Level.CRITICAL, e.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const getLibrary = (name) => {
|
||||
const libraries = getLibraries(name);
|
||||
const libraries = getLibraries().filter(l => l.name === name);
|
||||
return libraries.length === 1 ? libraries[0] : undefined;
|
||||
}
|
||||
|
||||
const getFunctions = (library) => {
|
||||
const getFunctions = (library, dynamic) => {
|
||||
try {
|
||||
return library.enumerateExports();
|
||||
// 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
|
||||
})) : [];
|
||||
|
||||
library.enumerateExports().forEach(item => {
|
||||
if (!functions.includes(item)) {
|
||||
functions.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
return functions;
|
||||
} catch (e) {
|
||||
print(Level.CRITICAL, e.message);
|
||||
return [];
|
||||
@ -97,7 +111,7 @@ const disableLibrary = (name) => {
|
||||
const library = getLibrary(name);
|
||||
if (library) {
|
||||
// https://github.com/hyugogirubato/KeyDive/issues/23#issuecomment-2230374415
|
||||
const functions = getFunctions(library);
|
||||
const functions = getFunctions(library, false);
|
||||
const disabled = [];
|
||||
|
||||
functions.forEach(func => {
|
||||
@ -116,14 +130,19 @@ const disableLibrary = (name) => {
|
||||
});
|
||||
print(Level.INFO, `Library ${library.name} (${library.path}) has been disabled`);
|
||||
} else {
|
||||
print(Level.INFO, `Library ${name} was not found`);
|
||||
print(Level.DEBUG, `Library ${name} was not found`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// @Libraries
|
||||
const UsePrivacyMode = (address) => {
|
||||
// wvcdm::Properties::UsePrivacyMode
|
||||
/*
|
||||
wvcdm::Properties::UsePrivacyMode
|
||||
|
||||
Args:
|
||||
args[0]: std::string const&
|
||||
*/
|
||||
Interceptor.replace(address, new NativeCallback(function () {
|
||||
return 0;
|
||||
}, 'int', []));
|
||||
@ -139,7 +158,12 @@ const UsePrivacyMode = (address) => {
|
||||
}
|
||||
|
||||
const GetCdmClientPropertySet = (address) => {
|
||||
// wvcdm::Properties::GetCdmClientPropertySet
|
||||
/*
|
||||
wvcdm::Properties::GetCdmClientPropertySet
|
||||
|
||||
Args:
|
||||
args[0]: std::string const&
|
||||
*/
|
||||
Interceptor.replace(address, new NativeCallback(function () {
|
||||
return 0;
|
||||
}, 'int', []));
|
||||
@ -155,7 +179,18 @@ const GetCdmClientPropertySet = (address) => {
|
||||
}
|
||||
|
||||
const PrepareKeyRequest = (address) => {
|
||||
// wvcdm::CdmLicense::PrepareKeyRequest
|
||||
/*
|
||||
wvcdm::CdmLicense::PrepareKeyRequest
|
||||
|
||||
Args:
|
||||
args[0]: wvcdm::CdmLicense *this
|
||||
args[1]: wvcdm::InitializationData const&
|
||||
args[2]: wvcdm::CdmLicenseType
|
||||
args[3]: std::map<std::string
|
||||
args[4]: std::string> const&
|
||||
args[5]: std::string*
|
||||
args[6]: std::string*
|
||||
*/
|
||||
Interceptor.attach(address, {
|
||||
onEnter: function (args) {
|
||||
print(Level.DEBUG, '[+] onEnter: PrepareKeyRequest');
|
||||
@ -183,7 +218,7 @@ const PrepareKeyRequest = (address) => {
|
||||
// print(Level.WARNING, `Failed to dump data for arg ${i}`);
|
||||
}
|
||||
}
|
||||
!dumped && print(Level.ERROR, 'Failed to dump challenge.');
|
||||
!dumped && print(Level.ERROR, 'Failed to dump challenge');
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -240,32 +275,129 @@ const getKeyLength = (key) => {
|
||||
return pos + lengthValue;
|
||||
}
|
||||
|
||||
const GetDeviceID = (address) => {
|
||||
// wvdash::OEMCrypto::GetDeviceID
|
||||
const GetDeviceId = (address, name) => {
|
||||
/*
|
||||
wvcdm::_oecc07
|
||||
|
||||
Args:
|
||||
args[0]: uchar *
|
||||
args[1]: ulong *
|
||||
args[3]: wvcdm::SecurityLevel
|
||||
*/
|
||||
Interceptor.attach(address, {
|
||||
onEnter: function (args) {
|
||||
print(Level.DEBUG, '[+] onEnter: GetDeviceID');
|
||||
// print(Level.DEBUG, '[+] onEnter: GetDeviceId');
|
||||
this.data = args[0];
|
||||
this.size = args[1];
|
||||
},
|
||||
onLeave: function (retval) {
|
||||
print(Level.DEBUG, '[-] onLeave: GetDeviceID');
|
||||
try {
|
||||
// print(Level.DEBUG, '[-] onLeave: GetDeviceId');
|
||||
const size = Memory.readPointer(this.size).toInt32();
|
||||
const data = Memory.readByteArray(this.data, size);
|
||||
data && send('client_id', data);
|
||||
} catch (e) {
|
||||
print(Level.ERROR, `Failed to dump device ID.`);
|
||||
|
||||
if (data) {
|
||||
print(Level.DEBUG, `[*] GetDeviceId: ${name}`);
|
||||
send('device_id', data);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const FileSystemRead = (address) => {
|
||||
/*
|
||||
wvoec3::OEMCrypto_Level3AndroidFileSystem::Read
|
||||
|
||||
Args:
|
||||
args[0]: wvoec3::OEMCrypto_Level3AndroidFileSystem *this
|
||||
args[1]: char const*
|
||||
args[2]: void *
|
||||
args[3]: ulong
|
||||
*/
|
||||
Interceptor.attach(address, {
|
||||
onEnter: function (args) {
|
||||
// print(Level.DEBUG, '[+] onEnter: FileSystemRead');
|
||||
const size = args[3].toInt32();
|
||||
const data = Memory.readByteArray(args[2], size);
|
||||
|
||||
// Check if the size matches known keybox sizes (128 or 132 bytes)
|
||||
if ([128, 132].includes(size) && data) {
|
||||
print(Level.DEBUG, '[*] FileSystemRead');
|
||||
send('keybox', data);
|
||||
}
|
||||
},
|
||||
onLeave: function (retval) {
|
||||
// print(Level.DEBUG, '[-] onLeave: FileSystemRead');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const FileRead = (address, name) => {
|
||||
/*
|
||||
wvcdm::File::Read
|
||||
|
||||
Args:
|
||||
args[0]: wvcdm::File *this
|
||||
args[1]: char *
|
||||
args[2]: uint
|
||||
*/
|
||||
/*
|
||||
_x1c36
|
||||
|
||||
Args:
|
||||
args[0]: char *filename
|
||||
args[1]: void *ptr
|
||||
args[2]: size_t n
|
||||
*/
|
||||
Interceptor.attach(address, {
|
||||
onEnter: function (args) {
|
||||
// print(Level.DEBUG, `[+] onEnter: FileRead: ${name}`);
|
||||
const size = args[2].toInt32();
|
||||
const data = Memory.readByteArray(args[1], size);
|
||||
|
||||
// Check if the size matches known keybox sizes (128 or 132 bytes)
|
||||
if ([128, 132].includes(size) && data) {
|
||||
print(Level.DEBUG, `[*] FileRead: ${name}`);
|
||||
send('keybox', data);
|
||||
}
|
||||
},
|
||||
onLeave: function (retval) {
|
||||
// print(Level.DEBUG, `[-] onLeave: FileRead: ${name}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const RunningCRC = (address) => {
|
||||
/*
|
||||
wvrunningcrc32
|
||||
|
||||
Args:
|
||||
args[0]: uchar const*
|
||||
args[1]: int
|
||||
args[2]: uint
|
||||
*/
|
||||
Interceptor.attach(address, {
|
||||
onEnter: function (args) {
|
||||
// print(Level.DEBUG, '[+] onEnter: RunningCRC');
|
||||
const size = args[1].toInt32();
|
||||
|
||||
// Check if size matches keybox length excluding 4-byte magic/tag fields
|
||||
if (size === 124) {
|
||||
const data = Memory.readByteArray(args[0], 128);
|
||||
print(Level.DEBUG, '[*] RunningCRC');
|
||||
send('keybox', data);
|
||||
}
|
||||
},
|
||||
onLeave: function (retval) {
|
||||
// print(Level.DEBUG, '[-] onLeave: RunningCRC');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// @Hooks
|
||||
const hookLibrary = (name) => {
|
||||
const hookLibrary = (name, dynamic) => {
|
||||
// https://github.com/poxyran/misc/blob/master/frida-enumerate-imports.py
|
||||
const library = getLibrary(name);
|
||||
let library = getLibrary(name);
|
||||
if (!library) return false;
|
||||
|
||||
let functions;
|
||||
@ -277,33 +409,46 @@ const hookLibrary = (name) => {
|
||||
address: library.base.add(s.address)
|
||||
}));
|
||||
} else {
|
||||
functions = getFunctions(library);
|
||||
// https://github.com/hyugogirubato/KeyDive/issues/50
|
||||
functions = getFunctions(library, dynamic);
|
||||
}
|
||||
|
||||
functions = functions.filter(f => !NATIVE_C_API.includes(f.name));
|
||||
const targets = SKIP ? [] : functions.filter(f => OEM_CRYPTO_API.includes(f.name)).map(f => f.name);
|
||||
let targets = SKIP ? [] : functions.filter(f => OEM_CRYPTO_API.includes(f.name)).map(f => f.name);
|
||||
const hooked = [];
|
||||
|
||||
functions.forEach(func => {
|
||||
let required = false;
|
||||
const {name: funcName, address: funcAddr} = func;
|
||||
if (func.type !== 'function' || hooked.includes(funcAddr)) return;
|
||||
|
||||
try {
|
||||
if (funcName.includes('UsePrivacyMode')) {
|
||||
UsePrivacyMode(funcAddr);
|
||||
required = true;
|
||||
} else if (funcName.includes('GetCdmClientPropertySet')) {
|
||||
GetCdmClientPropertySet(funcAddr);
|
||||
required = true;
|
||||
} else if (funcName.includes('PrepareKeyRequest')) {
|
||||
PrepareKeyRequest(funcAddr);
|
||||
} else if (funcName.includes('getOemcryptoDeviceId')) {
|
||||
GetDeviceID(funcAddr);
|
||||
required = true;
|
||||
} else if (targets.includes(funcName) || (!targets.length && funcName.match(/^[a-z]+$/))) {
|
||||
LoadDeviceRSAKey(funcAddr, funcName);
|
||||
required = true;
|
||||
} else if (['lcc07', 'oecc07', 'getOemcryptoDeviceId'].some(n => funcName.includes(n))) {
|
||||
GetDeviceId(funcAddr, funcName);
|
||||
} else if (['FileSystem', 'Read'].every(n => funcName.includes(n))) {
|
||||
FileSystemRead(funcAddr);
|
||||
} else if (['File', 'Read'].every(n => funcName.includes(n)) || funcName.includes('x1c36')) {
|
||||
FileRead(funcAddr, funcName);
|
||||
} else if (funcName.includes('runningcrc')) {
|
||||
// https://github.com/Avalonswanderer/widevinel3_Android_PoC/blob/main/PoCs/recover_l3keybox.py#L50
|
||||
RunningCRC(funcAddr);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
hooked.push(funcAddr);
|
||||
required && hooked.push(funcAddr);
|
||||
print(Level.DEBUG, `Hooked (${funcAddr}): ${funcName}`);
|
||||
} catch (e) {
|
||||
print(Level.ERROR, `${e.message} for ${funcName}`);
|
||||
@ -311,10 +456,11 @@ const hookLibrary = (name) => {
|
||||
});
|
||||
|
||||
if (hooked.length < 3) {
|
||||
print(Level.CRITICAL, 'Insufficient functions hooked.');
|
||||
print(Level.CRITICAL, 'Insufficient functions hooked');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
@ -322,6 +468,7 @@ const hookLibrary = (name) => {
|
||||
|
||||
// RPC interfaces exposed to external calls.
|
||||
rpc.exports = {
|
||||
getlibrary: getLibrary,
|
||||
getversion: getVersion,
|
||||
getlibraries: getLibraries,
|
||||
hooklibrary: hookLibrary
|
||||
};
|
||||
|
@ -3,32 +3,32 @@ class Vendor:
|
||||
Represents a Vendor with SDK, OEM, version, and name attributes.
|
||||
"""
|
||||
|
||||
def __init__(self, sdk: int, oem: int, version: str, name: str):
|
||||
def __init__(self, sdk: int, oem: int, version: str, pattern: str):
|
||||
"""
|
||||
Initializes a Vendor instance.
|
||||
|
||||
Args:
|
||||
sdk (int): Minimum SDK version required.
|
||||
oem (int): OEM identifier.
|
||||
Parameters:
|
||||
sdk (int): Minimum SDK version required by the vendor.
|
||||
oem (int): OEM identifier for the vendor.
|
||||
version (str): Version of the vendor.
|
||||
name (str): Name of the vendor.
|
||||
pattern (str): Name pattern of the vendor.
|
||||
"""
|
||||
self.sdk = sdk
|
||||
self.oem = oem
|
||||
self.version = version
|
||||
self.name = name
|
||||
self.pattern = pattern
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Returns a string representation of the Vendor instance.
|
||||
|
||||
Returns:
|
||||
str: String representation of the Vendor instance.
|
||||
str: String representation of the Vendor instance with its attributes.
|
||||
"""
|
||||
return '{name}({items})'.format(
|
||||
return "{name}({items})".format(
|
||||
name=self.__class__.__name__,
|
||||
items=', '.join([f'{k}={repr(v)}' for k, v in self.__dict__.items()])
|
||||
items=", ".join([f"{k}={repr(v)}" for k, v in self.__dict__.items()])
|
||||
)
|
||||
|
||||
|
||||
__all__ = ('Vendor',)
|
||||
__all__ = ("Vendor",)
|
||||
|
@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry]
|
||||
name = "keydive"
|
||||
version = "2.1.0"
|
||||
version = "2.2.1"
|
||||
description = "Extract Widevine L3 keys from Android devices effortlessly, spanning multiple Android versions for DRM research and education."
|
||||
license = "MIT"
|
||||
authors = ["hyugogirubato <65763543+hyugogirubato@users.noreply.github.com>"]
|
||||
@ -36,14 +36,14 @@ include = [
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.8"
|
||||
coloredlogs = "^15.0.1"
|
||||
frida = "^16.5.6"
|
||||
frida = "^16.6.0"
|
||||
pathlib = "^1.0.1"
|
||||
pycryptodomex = "^3.21.0"
|
||||
pywidevine = "^1.8.0"
|
||||
pathvalidate = "^3.2.1"
|
||||
PyYAML = { version = "^6.0.2", optional = true }
|
||||
requests = "^2.32.3"
|
||||
xmltodict = "^0.14.2"
|
||||
Flask = { version = "^3.0.3", optional = true }
|
||||
xmltodict = { version = "^0.14.2", optional = true }
|
||||
|
||||
[tool.poetry.scripts]
|
||||
keydive = "keydive.__main__:main"
|
||||
|
Loading…
x
Reference in New Issue
Block a user