mirror of
https://github.com/DevLARLEY/AndroidWidevineProxy.git
synced 2026-07-15 19:10:02 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
import Java from "frida-java-bridge";
|
||||||
|
|
||||||
|
if (Java.available) {
|
||||||
|
let MediaDrm = Java.use("android.media.MediaDrm");
|
||||||
|
const certInstances = new Map();
|
||||||
|
|
||||||
|
function bytesToArray(bytes: any) {
|
||||||
|
if (bytes === null || bytes === undefined) return null;
|
||||||
|
let arr = [];
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
arr.push(bytes[i] & 0xFF);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
function bytesToHex(bytes: any): string {
|
||||||
|
if (bytes === null) return "";
|
||||||
|
let hex = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
hex += ("0" + (bytes[i] & 0xFF).toString(16)).slice(-2);
|
||||||
|
}
|
||||||
|
return hex;
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaDrm.setPropertyByteArray.implementation = function(propertyName: string, value: any): void {
|
||||||
|
//console.log("CALL setPropertyByteArray");
|
||||||
|
|
||||||
|
if (propertyName === "serviceCertificate") {
|
||||||
|
//console.log("service cert was set:", bytesToHex(value));
|
||||||
|
certInstances.set(this, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setPropertyByteArray(propertyName, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaDrm.getKeyRequest.implementation = function(scope: any, init: any, mimeType: any, keyType: any, optionalParams: any) {
|
||||||
|
//console.log("CALL getKeyRequest", bytesToHex(scope));
|
||||||
|
|
||||||
|
let result = this.getKeyRequest(scope, init, mimeType, keyType, optionalParams);
|
||||||
|
|
||||||
|
send({
|
||||||
|
type: "challenge",
|
||||||
|
challenge: bytesToArray(result.getData()),
|
||||||
|
service_certificate: bytesToArray(certInstances.get(this))
|
||||||
|
});
|
||||||
|
|
||||||
|
const op: any = recv('response', function(value: any) {
|
||||||
|
if (value.newChallenge) {
|
||||||
|
result.mData.value = Java.array('byte', value.newChallenge);
|
||||||
|
//console.log("replaced challenge:", result.mData.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
op.wait();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
MediaDrm.provideKeyResponse.implementation = function(scope: any, response: any) {
|
||||||
|
//console.log("CALL provideKeyResponse", bytesToHex(scope));
|
||||||
|
|
||||||
|
send({
|
||||||
|
type: "license",
|
||||||
|
license: bytesToArray(response)
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.provideKeyResponse(scope, response);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No Java VM in this process");
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
let s = fs.readFileSync(process.argv[2], 'utf8');
|
||||||
|
const parts = s.split("✄");
|
||||||
|
fs.writeFileSync(process.argv[3], parts[1].trim(), 'utf8');
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import frida
|
||||||
|
from Crypto.Cipher import PKCS1_OAEP
|
||||||
|
from Crypto.Hash import SHA1, HMAC, SHA256
|
||||||
|
from Crypto.Signature import pss
|
||||||
|
from frida.core import Session
|
||||||
|
from pywidevine import Device, Cdm, PSSH, Key
|
||||||
|
from pywidevine.exceptions import SignatureMismatch
|
||||||
|
from pywidevine.license_protocol_pb2 import SignedMessage, LicenseRequest, LicenseType, License
|
||||||
|
|
||||||
|
|
||||||
|
class AWP:
|
||||||
|
def __init__(self, session: Session, script: str, token_only: bool, widevine_device: Device, keys_callback):
|
||||||
|
self.session = session
|
||||||
|
self.script = self.session.create_script(script)
|
||||||
|
self.token_only = token_only
|
||||||
|
|
||||||
|
self.widevine_device = widevine_device
|
||||||
|
self.cdm = Cdm.from_device(widevine_device)
|
||||||
|
|
||||||
|
self.keys_callback = keys_callback
|
||||||
|
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
self._sessions = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def extract_optional_parameters(license_request: LicenseRequest) -> dict:
|
||||||
|
if not license_request.client_id.ListFields():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
optional_params = {}
|
||||||
|
|
||||||
|
for nv in license_request.client_id.client_info:
|
||||||
|
if nv.name == "company_name":
|
||||||
|
break
|
||||||
|
|
||||||
|
optional_params[nv.name] = nv.value
|
||||||
|
|
||||||
|
return optional_params
|
||||||
|
|
||||||
|
def __hook(self, message: dict, data: bytes) -> None:
|
||||||
|
if message["type"] == "error":
|
||||||
|
self._logger.error(message["stack"])
|
||||||
|
|
||||||
|
elif message["type"] == "send":
|
||||||
|
payload = message["payload"]
|
||||||
|
|
||||||
|
if payload["type"] == "challenge":
|
||||||
|
challenge = bytes(payload["challenge"])
|
||||||
|
|
||||||
|
self._logger.info("Received challenge")
|
||||||
|
|
||||||
|
if self.token_only:
|
||||||
|
signed_message = SignedMessage()
|
||||||
|
signed_message.ParseFromString(challenge)
|
||||||
|
|
||||||
|
license_request = LicenseRequest()
|
||||||
|
license_request.ParseFromString(signed_message.msg)
|
||||||
|
|
||||||
|
if not license_request.client_id.ListFields():
|
||||||
|
raise Exception("Token only is not supported when a service certificate is being used")
|
||||||
|
|
||||||
|
# we can do this because the client info is in no way linked to the SignedDrmCertificate
|
||||||
|
license_request.client_id.token = self.widevine_device.client_id.token
|
||||||
|
request_id = license_request.content_id.widevine_pssh_data.request_id
|
||||||
|
|
||||||
|
new_license_request_msg = license_request.SerializeToString()
|
||||||
|
|
||||||
|
signed_message.msg = new_license_request_msg
|
||||||
|
signed_message.signature = pss.new(self.widevine_device.private_key).sign(SHA1.new(new_license_request_msg))
|
||||||
|
|
||||||
|
new_challenge = signed_message.SerializeToString()
|
||||||
|
|
||||||
|
self._sessions[request_id] = Cdm.derive_context(new_license_request_msg)
|
||||||
|
else:
|
||||||
|
initial_signed_message = SignedMessage()
|
||||||
|
initial_signed_message.ParseFromString(challenge)
|
||||||
|
|
||||||
|
# print(initial_signed_message)
|
||||||
|
|
||||||
|
initial_license_request = LicenseRequest()
|
||||||
|
initial_license_request.ParseFromString(initial_signed_message.msg)
|
||||||
|
|
||||||
|
wv_pssh_data = initial_license_request.content_id.widevine_pssh_data
|
||||||
|
pssh = PSSH(wv_pssh_data.pssh_data[0])
|
||||||
|
optional_params = self.extract_optional_parameters(initial_license_request)
|
||||||
|
|
||||||
|
session_id = self.cdm.open()
|
||||||
|
|
||||||
|
if payload["service_certificate"]:
|
||||||
|
provider_id = self.cdm.set_service_certificate(session_id, bytes(payload["service_certificate"]))
|
||||||
|
self._logger.info(f"Service certificate has been set: {provider_id}")
|
||||||
|
|
||||||
|
generated_challenge = self.cdm.get_license_challenge(session_id, pssh, LicenseType.Name(wv_pssh_data.license_type), optional_parameters=optional_params)
|
||||||
|
|
||||||
|
signed_message = SignedMessage()
|
||||||
|
signed_message.ParseFromString(generated_challenge)
|
||||||
|
signed_message.oemcrypto_core_message = initial_signed_message.oemcrypto_core_message
|
||||||
|
|
||||||
|
# print(signed_message)
|
||||||
|
|
||||||
|
license_request = LicenseRequest()
|
||||||
|
license_request.ParseFromString(signed_message.msg)
|
||||||
|
|
||||||
|
request_id = license_request.content_id.widevine_pssh_data.request_id
|
||||||
|
self._sessions[request_id] = session_id
|
||||||
|
|
||||||
|
new_challenge = signed_message.SerializeToString()
|
||||||
|
|
||||||
|
self.script.post({
|
||||||
|
"type": "response",
|
||||||
|
"newChallenge": list(new_challenge)
|
||||||
|
})
|
||||||
|
|
||||||
|
elif payload["type"] == "license":
|
||||||
|
self._logger.info("Received license")
|
||||||
|
|
||||||
|
licence = bytes(payload["license"])
|
||||||
|
|
||||||
|
signed_message = SignedMessage()
|
||||||
|
signed_message.ParseFromString(licence)
|
||||||
|
|
||||||
|
license_message = License()
|
||||||
|
license_message.ParseFromString(signed_message.msg)
|
||||||
|
|
||||||
|
request_id = license_message.id.request_id
|
||||||
|
|
||||||
|
if self.token_only:
|
||||||
|
context = self._sessions[request_id]
|
||||||
|
|
||||||
|
enc_key, mac_key_server, _ = Cdm.derive_keys(
|
||||||
|
*context,
|
||||||
|
key=PKCS1_OAEP.new(self.widevine_device.private_key).decrypt(signed_message.session_key)
|
||||||
|
)
|
||||||
|
|
||||||
|
computed_signature = HMAC. \
|
||||||
|
new(mac_key_server, digestmod=SHA256). \
|
||||||
|
update(signed_message.oemcrypto_core_message or b""). \
|
||||||
|
update(signed_message.msg). \
|
||||||
|
digest()
|
||||||
|
|
||||||
|
if signed_message.signature != computed_signature:
|
||||||
|
raise SignatureMismatch("Signature Mismatch on License Message, rejecting license")
|
||||||
|
|
||||||
|
keys = [
|
||||||
|
Key.from_key_container(key, enc_key)
|
||||||
|
for key in license_message.key
|
||||||
|
if key.type == License.KeyContainer.CONTENT
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
session_id = self._sessions[request_id]
|
||||||
|
|
||||||
|
self.cdm.parse_license(session_id, licence)
|
||||||
|
keys = self.cdm.get_keys(session_id, type_="CONTENT")
|
||||||
|
self.cdm.close(session_id)
|
||||||
|
|
||||||
|
self.keys_callback(keys)
|
||||||
|
|
||||||
|
del self._sessions[request_id]
|
||||||
|
|
||||||
|
def attach(self) -> None:
|
||||||
|
self.script.on('message', lambda message, data: self.__hook(message, data))
|
||||||
|
self.script.load()
|
||||||
|
|
||||||
|
self._logger.info("Ready. Waiting for events...")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
parser = argparse.ArgumentParser(description="AWP - Android Widevine Proxy")
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"app_name",
|
||||||
|
metavar="<APP_NAME>",
|
||||||
|
type=str,
|
||||||
|
help="The name of the app to intercept"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"widevine_device",
|
||||||
|
metavar="<WVD>",
|
||||||
|
type=Path,
|
||||||
|
help="Path of the widevine device"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--key-format",
|
||||||
|
type=str,
|
||||||
|
choices=["default", "mp4decrypt", "shaka-packager"],
|
||||||
|
default="default",
|
||||||
|
help="Format of printed keys"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--token-only",
|
||||||
|
action="store_true",
|
||||||
|
help="Only replace the token in the challenge (only if privacy mode is off)"
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
logger.info("Finding USB device...")
|
||||||
|
device = frida.get_usb_device()
|
||||||
|
|
||||||
|
logger.info("Hooking app...")
|
||||||
|
session = device.attach(args.app_name)
|
||||||
|
|
||||||
|
script_code = open("_agent.js", "r").read()
|
||||||
|
|
||||||
|
def log_keys(keys: List[Key]):
|
||||||
|
if args.key_format == "default":
|
||||||
|
for key in keys:
|
||||||
|
logger.info(f"[{key.type}] {key.kid.hex}:{key.key.hex()}")
|
||||||
|
elif args.key_format == "mp4decrypt":
|
||||||
|
logger.info(" ".join(f"--key {k.kid.hex}:{k.key.hex()}" for k in keys))
|
||||||
|
elif args.key_format == "shaka-packager":
|
||||||
|
logger.info(" ".join(f"--keys key_id={k.kid.hex}:key={k.key.hex()}" for k in keys))
|
||||||
|
|
||||||
|
awp = AWP(
|
||||||
|
session=session,
|
||||||
|
script=script_code,
|
||||||
|
widevine_device=Device.load(args.widevine_device),
|
||||||
|
token_only=args.token_only,
|
||||||
|
keys_callback=log_keys
|
||||||
|
)
|
||||||
|
|
||||||
|
awp.attach()
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "widevineproxyandroid",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "myhook.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"compile": "frida-compile agent/hook.ts -o _agent.js && node clean.js _agent.js _agent.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/frida-gum": "^19.0.0",
|
||||||
|
"@types/node": "^18.19.111",
|
||||||
|
"frida-compile": "^19.0.4",
|
||||||
|
"frida-java-bridge": "^7.0.8",
|
||||||
|
"protobufjs": "^7.5.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
frida-tools
|
||||||
|
pywidevinex
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2019",
|
||||||
|
"module": "ESNext",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["agent/**/*.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user