Skip to content

Commit

Permalink
Merge pull request #9 from vitalygashkov/next
Browse files Browse the repository at this point in the history
Added switch for request interception, fixed active client state with single imported client, added method to convert from base64 to hex
  • Loading branch information
vitalygashkov authored Nov 1, 2024
2 parents 7307f14 + f9efe8d commit 896d1a2
Show file tree
Hide file tree
Showing 16 changed files with 295 additions and 163 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "azot",
"version": "0.6.0",
"version": "0.6.1",
"description": "Research & pentesting toolkit for Google's Widevine DRM",
"type": "module",
"files": [
Expand Down
29 changes: 25 additions & 4 deletions src/extension/entrypoints/background.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { appStorage } from '@/utils/storage';
import { Client, fromBuffer } from '@azot/lib';
import { Client, fromBase64, fromBuffer } from '@azot/lib';
import { getMessageType } from '@azot/lib/message';

export default defineBackground({
Expand Down Expand Up @@ -37,8 +37,29 @@ export default defineBackground({
(async () => {
console.log('[azot] Received message', message);

const spoofingEnabled = await appStorage.spoofingEnabled.getValue();
if (!spoofingEnabled) {
const settings = await appStorage.settings.getValue();

if (
settings?.emeInterception &&
message.action === 'keystatuseschange'
) {
const keys = Object.entries(message.keyStatuses).map(
([id, status]: any) => ({
id: fromBase64(id).toHex(),
value: status,
url: message.url,
mpd: message.mpd,
pssh: message.initData,
createdAt: new Date().getTime(),
}),
);
appStorage.recentKeys.setValue(keys);
appStorage.allKeys.add(...keys);
sendResponse();
return;
}

if (!settings?.spoofing) {
console.log('[azot] Spoofing disabled, skipping message...');
sendResponse();
return;
Expand Down Expand Up @@ -129,7 +150,7 @@ export default defineBackground({
};
const results = keys.map((key) => toKey(key));
console.log('[azot] Received keys', results);
appStorage.keys.setValue(results);
appStorage.recentKeys.setValue(results);
appStorage.allKeys.add(...results);
sendResponse({ keys: results });
},
Expand Down
15 changes: 10 additions & 5 deletions src/extension/entrypoints/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,18 @@ export default defineContentScript({
allFrames: true,
async main() {
// Checking if interception enabled
const enabled = await appStorage.interceptionEnabled.getValue();
if (enabled) console.log(`[azot] Injecting...`);
else return console.log(`[azot] Interception disabled`);
const settings = await appStorage.settings.getValue();

// Injecting scripts into current page
inject('eme.js');
inject('network.js');
inject('manifest.js');
if (settings?.requestInterception) {
console.log(`[azot] Injecting request interception...`);
inject('network.js');
}
if (settings?.emeInterception) {
console.log(`[azot] Injecting EME interception...`);
inject('eme.js');
}

// Listen for event from injected script
window.addEventListener(
Expand Down
26 changes: 26 additions & 0 deletions src/extension/entrypoints/eme.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ export default defineUnlistedScript(() => {
console.log(`Initialization data (PSSH): ${session.initData}`);
console.log(`Keys count: ${session.keyStatuses.size}`);
console.groupEnd();

const keyStatuses: Record<string, string> = {};
for (const [id, status] of session.keyStatuses.entries()) {
keyStatuses[base64.stringify(id)] = status;
}

await send({
sessionId: session.sessionId,
action: 'keystatuseschange',
initData: session.initData,
initDataType: session.initDataType,
mpd: window.MPD_LIST.get(session.initData!),
keyStatuses,
});
return;
};

Expand Down Expand Up @@ -150,6 +164,11 @@ export default defineUnlistedScript(() => {
);
for (const [id, value] of keys) console.log(`${id}:${value}`);
console.groupEnd();
} else {
setTimeout(() => {
if (session.keyStatuses.size === 0) return;
onKeyStatusesChange(session);
}, 1000);
}
return;
};
Expand Down Expand Up @@ -264,6 +283,13 @@ export default defineUnlistedScript(() => {
},
});

interceptProperty(MediaKeySession.prototype, 'onkeystatuseschange', {
call: async (_target, _this, [event]) => {
onKeyStatusesChange(event.target as MediaKeySession);
return _target?.apply(_this, [event]);
},
});

interceptMethod(
MediaKeySession.prototype,
'update',
Expand Down
34 changes: 34 additions & 0 deletions src/extension/entrypoints/manifest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
declare global {
interface Window {
MPD_LIST: Map<string, string>;
}
}

export default defineUnlistedScript(() => {
window.MPD_LIST = new Map();

const parsePssh = (text: string, url: string) => {
const parser = new DOMParser();
const mpd = parser.parseFromString(text, 'text/xml');
const contentProtectionList = mpd.querySelectorAll(
'ContentProtection[schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"]',
);
for (const contentProtection of contentProtectionList) {
const children = Array.from(contentProtection.children);
for (const child of children) {
if (child.nodeName === 'cenc:pssh') {
const pssh = child.textContent;
if (!pssh) continue;
window.MPD_LIST.set(pssh, url);
}
}
}
};

window.addEventListener('message', (event) => {
const isResponse = event.data.method === 'response';
if (!isResponse) return;
const { url, headers, text } = event.data.params;
parsePssh(text, url);
});
});
157 changes: 80 additions & 77 deletions src/extension/entrypoints/network.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
declare global {
interface Window {
MPD_LIST: Map<string, string>;
}
}

export default defineUnlistedScript(() => {
window.MPD_LIST = new Map();

const parsePssh = (text: string, url: string) => {
const isXml = text.startsWith('<?xml') || text.startsWith('<MPD');
if (!isXml) return;
const parser = new DOMParser();
const mpd = parser.parseFromString(text, 'text/xml');
const contentProtectionList = mpd.querySelectorAll(
'ContentProtection[schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"]',
const filterHead = (url: string, headers: Record<string, string>) => {
const MAX_SIZE = 1024 * 1024 * 1; // 1 MB
const size = headers['content-length'];
const isSizeOk = Number(size) < MAX_SIZE;
if (size && !isSizeOk) return false;

const type = headers['content-type'];
const isTypeOk =
type?.includes('xml') ||
type?.includes('dash') ||
type?.includes('octet-stream');
if (!isTypeOk) return false;

return true;
};

const filterData = (url: string, text: string) => {
const isManifest = text.startsWith('<?xml') || text.startsWith('<MPD');
return isManifest;
};

const postMessage = (
url: string,
headers: Record<string, string>,
text: string,
) => {
const message = {
jsonrpc: '2.0',
method: 'response',
params: { url, text, headers },
id: Date.now(),
};
window.postMessage(message, '*');
};

const isResponseMessage = (event: MessageEvent) => {
return (
event.data &&
event.data.jsonrpc === '2.0' &&
event.data.method === 'response'
);
for (const contentProtection of contentProtectionList) {
const children = Array.from(contentProtection.children);
for (const child of children) {
if (child.nodeName === 'cenc:pssh') {
const pssh = child.textContent;
if (!pssh) continue;
window.MPD_LIST.set(pssh, url);
}
};

const originalWorker = window.Worker;
// @ts-ignore
window.Worker = function (scriptUrl: string | URL, options: WorkerOptions) {
const worker = new originalWorker(scriptUrl, options);
worker.addEventListener('message', (event) => {
if (isResponseMessage(event)) {
const { url, headers, text } = event.data.params;
postMessage(url, headers, text);
}
}
});
return worker;
};

const patchFetch = () => {
Expand All @@ -34,22 +62,16 @@ export default defineUnlistedScript(() => {
resource: URL | RequestInfo,
options?: RequestInit,
) {
let url: URL;
if (typeof resource === 'string' && !options) {
url = new URL(resource);
} else {
const request = new Request(resource, options);
url = new URL(request.url);
}
const response = await originalFetch(resource, options);

// Detect manifest URL and parse PSSH from response
const isManifest = url.pathname.endsWith('.mpd');
if (isManifest) {
response
.clone()
.text()
.then((text) => parsePssh(text, response.url));
const clone = response.clone();
const url = clone.url;
const headers = Object.fromEntries(clone.headers.entries());
const hasHeadMatch = filterHead(url, headers);
if (hasHeadMatch) {
const text = await clone.text();
const hasDataMatch = filterData(url, text);
if (hasDataMatch) postMessage(url, headers, text);
}

return response;
Expand Down Expand Up @@ -78,48 +100,29 @@ export default defineUnlistedScript(() => {
}

#handleResponse() {
const url = new URL(this.responseURL);
const isManifest = url.pathname.endsWith('.mpd');
if (isManifest) {
parsePssh(this.responseText, this.responseURL);
const url = this.responseURL;
const headersString = this.getAllResponseHeaders();
const headersArray = headersString.trim().split(/[\r\n]+/);
const headers: Record<string, string> = {};
for (const line of headersArray) {
const parts = line.split(': ');
const header = parts.shift();
if (!header) continue;
const value = parts.join(': ');
headers[header] = value;
}
const hasHeadMatch = filterHead(url, headers);
if (hasHeadMatch && this.responseType === 'text') {
const text = this.response;
const hasDataMatch = filterData(url, text);
if (hasDataMatch) postMessage(url, headers, text);
}
}
}
window.XMLHttpRequest = PatchedXHR;
};

const patchBlobFetch = () => {
window.addEventListener('message', (event) => {
if (
event.data &&
event.data.jsonrpc === '2.0' &&
event.data.method === 'intercepted_response'
) {
const { url, text, headers } = event.data.params;
parsePssh(text, url);
}
});

// Monitor for worker creation
const originalWorker = window.Worker;
window.Worker = function (scriptUrl, options) {
const worker = new originalWorker(scriptUrl, options);

worker.addEventListener('message', (event) => {
if (
event.data &&
event.data.jsonrpc === '2.0' &&
event.data.method === 'intercepted_response'
) {
const { url, text, headers } = event.data.params;
parsePssh(text, url);
}
});

return worker;
};

// Store original blob URL creation
const originalCreateObjectURL = URL.createObjectURL;
URL.createObjectURL = function (blob) {
if (
Expand All @@ -135,7 +138,7 @@ export default defineUnlistedScript(() => {
xhr.overrideMimeType('text/plain; charset=x-user-defined');
xhr.open('GET', url, false);
xhr.send();
const blobDataBuffer = Uint8Array.from(xhr.response, (c) =>
const blobDataBuffer = Uint8Array.from(xhr.response as string, (c) =>
c.charCodeAt(0),
);
const sourceCode = new TextDecoder().decode(blobDataBuffer);
Expand All @@ -150,14 +153,14 @@ export default defineUnlistedScript(() => {
if (size && !isSizeOk) return response;
const type = response.headers.get('content-type');
const isTypeOk = type?.includes('xml') || type?.includes('dash');
const isTypeOk = type?.includes('xml') || type?.includes('dash') || type?.includes('octet-stream');
if (!isTypeOk) return response;
const clone = response.clone();
clone.text().then(text => {
const message = {
jsonrpc: '2.0',
method: 'intercepted_response',
method: 'response',
params: {
url: response.url,
text,
Expand Down Expand Up @@ -186,5 +189,5 @@ export default defineUnlistedScript(() => {
patchXmlHttpRequest();
patchBlobFetch();

console.log('[azot] Request interception added');
console.log('[azot] Response interception added');
});
Loading

0 comments on commit 896d1a2

Please sign in to comment.