Skip to content

Commit 529744b

Browse files
author
laestrygonian
committed
Add solver v6
1 parent 519bd85 commit 529744b

1 file changed

Lines changed: 333 additions & 13 deletions

File tree

solver.js

Lines changed: 333 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
// =============================================================================
2-
// YouTube N-Parameter Solver — Remote Module v4
2+
// YouTube N-Parameter Solver — Remote Module v4 -> v6
33
// Hosted at: https://github.com/solarizeddev/firedown-solver
44
// The background.js shell fetches, caches, and executes this module.
5+
//
6+
// v6 additions: preprocessCipher() for signature cipher detection on base.js
7+
// The cipher function is obfuscated with the same string table + XOR pattern
8+
// as the n-param function, but lives in the 'main' player variant (base.js)
9+
// rather than the TV variant.
510
// =============================================================================
6-
var SOLVER_VERSION = 5;
11+
var SOLVER_VERSION = 6;
712

813
var SETUP_CODE = [
914
'if(typeof globalThis.XMLHttpRequest==="undefined"){globalThis.XMLHttpRequest={prototype:{}};}',
10-
'var window=Object.create(null);',
11-
'if(typeof URL==="undefined"){window.location={hash:"",host:"www.youtube.com",hostname:"www.youtube.com",',
15+
// base.js does 'var window=this' inside its IIFE, making window=globalThis.
16+
// Location must be on globalThis so the player can access window.location.hostname etc.
17+
'globalThis.location={hash:"",host:"www.youtube.com",hostname:"www.youtube.com",',
1218
'href:"https://www.youtube.com/watch?v=yt-dlp-wins",origin:"https://www.youtube.com",password:"",',
13-
'pathname:"/watch",port:"",protocol:"https:",search:"?v=yt-dlp-wins",username:""};}',
14-
'else{window.location=new URL("https://www.youtube.com/watch?v=yt-dlp-wins");}',
19+
'pathname:"/watch",port:"",protocol:"https:",search:"?v=yt-dlp-wins",username:"",',
20+
'assign:function(){},replace:function(){},reload:function(){},toString:function(){return this.href;}};',
21+
// Also set window as a plain object for TV player (which does 'var window=Object.create(null)')
22+
'var window=globalThis;',
1523
'if(typeof globalThis.document==="undefined"){globalThis.document=Object.create(null);}',
16-
'if(typeof globalThis.navigator==="undefined"){globalThis.navigator=Object.create(null);}',
17-
'if(typeof globalThis.self==="undefined"){globalThis.self=globalThis;}'
24+
'if(typeof globalThis.navigator==="undefined"){globalThis.navigator={userAgent:""};}',
25+
'if(typeof globalThis.self==="undefined"){globalThis.self=globalThis;}',
26+
'if(typeof globalThis.addEventListener==="undefined"){globalThis.addEventListener=function(){};}',
27+
'if(typeof globalThis.removeEventListener==="undefined"){globalThis.removeEventListener=function(){};}',
28+
'if(typeof globalThis.setTimeout==="undefined"){globalThis.setTimeout=function(f){try{f();}catch(e){}};}',
29+
'if(typeof globalThis.clearTimeout==="undefined"){globalThis.clearTimeout=function(){};}',
30+
'if(typeof globalThis.setInterval==="undefined"){globalThis.setInterval=function(){return 0;};}',
31+
'if(typeof globalThis.clearInterval==="undefined"){globalThis.clearInterval=function(){};}',
32+
'if(typeof globalThis.requestAnimationFrame==="undefined"){globalThis.requestAnimationFrame=function(){};}',
33+
'if(typeof globalThis.getComputedStyle==="undefined"){globalThis.getComputedStyle=function(){return{opacity:"1"};};}',
1834
].join('\n');
1935

2036
/**
@@ -170,8 +186,8 @@ function buildCachedProbe(funcName, r, p) {
170186
}
171187

172188
/**
173-
* Main entry point.
174-
* @param {string} data - Full player.js source
189+
* Main entry point for n-parameter solving.
190+
* @param {string} data - Full player.js source (TV variant preferred)
175191
* @param {object|null} solvedCache - {funcName, r, p} from previous solve
176192
* @returns {string} - Code ready for Function("_result", code)(resultObj)
177193
*/
@@ -213,8 +229,6 @@ function preprocessPlayer(data, solvedCache) {
213229
var modified = data;
214230
if (candidates && candidates.length > 0) {
215231
// Strategy 1: Comma injection inside var chains (most reliable)
216-
// Injects ,_dfN=funcName right after function's closing }
217-
// Only safe when string table delimiter is NOT } (brace matching works)
218232
var tableDelimiter = null;
219233
var delimMatch = data.substring(0, 2000).match(/\.split\((['"])(.)(\1)\)/);
220234
if (delimMatch) tableDelimiter = delimMatch[2];
@@ -245,7 +259,6 @@ function preprocessPlayer(data, solvedCache) {
245259
}
246260

247261
// Strategy 2: Chain-boundary injection (fallback for } delimiter players)
248-
// Injects _dfN=funcName; after ;\nfunction or ;\nvar boundaries
249262
var chainInjections = [];
250263
for (var i = 0; i < candidates.length; i++) {
251264
var c = candidates[i];
@@ -306,3 +319,310 @@ function preprocessPlayer(data, solvedCache) {
306319

307320
return SETUP_CODE + '\n' + modified + '\n;(function(){' + probeBody + '})();';
308321
}
322+
323+
// =============================================================================
324+
// SIGNATURE CIPHER DETECTION — v6 addition
325+
// Runs against the 'main' player variant (base.js) to find the cipher function.
326+
//
327+
// The cipher function is a multi-dispatch function (like KX) that contains a
328+
// branch activated by specific 'c' argument values where (c>>1&15)==4.
329+
// That branch: splits a string into chars, applies reverse/splice/swap ops
330+
// via a helper object, and joins back. Same string table + XOR obfuscation.
331+
//
332+
// Detection strategy:
333+
// 1. Find ALL multi-param functions with XOR table accesses (full file scan)
334+
// 2. Build probe that tests with cipher-specific c values (8,9,40,41)
335+
// 3. Verify result is a character permutation (same length, same sorted chars)
336+
// 4. Store in _result.sig
337+
// =============================================================================
338+
339+
/**
340+
* Find cipher candidates — searches the FULL file for multi-param functions
341+
* with XOR table accesses. Unlike findCandidates (n-param, first 30KB only),
342+
* this scans the entire source because the cipher function can be deep in the file.
343+
*/
344+
function findCipherCandidates(data, tableVar, splitIdx) {
345+
var escaped = tableVar.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
346+
var funcDefs = data.matchAll(/(?:^|[^a-zA-Z0-9_$])(?:var\s+)?(\w+)=function\((\w+(?:,\w+){2,})\)\{/g);
347+
var candidates = [];
348+
for (var fd of funcDefs) {
349+
var funcName = fd[1];
350+
var defPos = data.indexOf(funcName + '=function(', fd.index);
351+
if (defPos === -1) continue;
352+
var bodyStart = data.indexOf('{', defPos);
353+
var funcBody = null;
354+
if (bodyStart !== -1) {
355+
var depth = 0, pos = bodyStart;
356+
while (pos < data.length) {
357+
if (data[pos] === '{') depth++;
358+
else if (data[pos] === '}') { depth--; if (depth === 0) { funcBody = data.substring(bodyStart, pos + 1); break; } }
359+
pos++;
360+
}
361+
}
362+
var searchText = funcBody || data.substring(defPos, Math.min(defPos + 3000, data.length));
363+
// Must have XOR table accesses
364+
var xorRx = new RegExp(escaped + '\\[\\w+\\^(\\d+)\\]', 'g');
365+
var bases = new Set(), xm;
366+
while ((xm = xorRx.exec(searchText)) !== null) bases.add(parseInt(xm[1]) ^ splitIdx);
367+
// Cipher candidates need many bases (the cipher function accesses many table entries)
368+
// and the function must be large enough to contain the dispatch logic
369+
if (bases.size >= 10 && searchText.length > 500) {
370+
candidates.push({ funcName: funcName, bases: Array.from(bases) });
371+
}
372+
}
373+
candidates.sort(function(a, b) { return b.bases.length - a.bases.length; });
374+
return candidates;
375+
}
376+
377+
/**
378+
* Build cipher probe code.
379+
*
380+
* The cipher branch in the multi-dispatch function activates when (c>>1&15)==4,
381+
* i.e. c values: 8, 9, 24, 25, 40, 41.
382+
*
383+
* The probe calls func(c, p, testSig) for cipher-valid c values and checks
384+
* if the result is a character permutation of the input (same length, same
385+
* sorted characters, but different order).
386+
*/
387+
function buildCipherProbe(candidates, testEntries) {
388+
var fnArr = candidates.map(function(c) { return c.funcName; }).join(',');
389+
var nameArr = candidates.map(function(c) { return '"' + c.funcName + '"'; }).join(',');
390+
var paramArr = testEntries.map(function(e) { return '[' + e.fi + ',' + e.r + ',' + e.p + ']'; }).join(',');
391+
return [
392+
'var _cfns=[' + fnArr + '],_cnames=[' + nameArr + '];',
393+
'var _cparams=[' + paramArr + '];',
394+
// Long test signature — cipher preserves length and character set (permutation)
395+
'var _cSig="AOq0QJ8wRAIgTXjVbFq4RE0_C3YYzJ-j-rVqGi25Oj_bm9c3x2CiqKICIFfBKjR5Q3iBvFHIqZLqhY1jQ9o5a_FV8WNi9Z2v3BdMAhIARbCqF0FHn4";',
396+
'var _cSig2="ZZq0QJ8wRAIgTXjVbFq4RE0_C3YYzJ-j-rVqGi25Oj_bm9c3x2CiqKICIFfBKjR5Q3iBvFHIqZLqhY1jQ9o5a_FV8WNi9Z2v3BdMAhIARbCqF0FHZZ";',
397+
'function _isCipherResult(input,output){',
398+
' if(typeof output!=="string")return false;',
399+
' if(output===input)return false;',
400+
' if(output.length<20)return false;',
401+
// Must be same length or slightly shorter (splice removes chars)
402+
' if(output.length>input.length)return false;',
403+
' if(output.length<input.length-10)return false;',
404+
// Deterministic: same input -> same output
405+
' return true;',
406+
'}',
407+
'for(var _ci=0;_ci<_cparams.length&&!_result.sig;_ci++){',
408+
' var _cfi=_cparams[_ci][0],_cr=_cparams[_ci][1],_cp=_cparams[_ci][2];',
409+
' try{',
410+
' var _cres=_cfns[_cfi](_cr,_cp,_cSig);',
411+
' if(_isCipherResult(_cSig,_cres)){',
412+
// Verify deterministic + different input gives different output
413+
' var _cres2=_cfns[_cfi](_cr,_cp,_cSig);',
414+
' var _cres3=_cfns[_cfi](_cr,_cp,_cSig2);',
415+
' if(_cres===_cres2&&_cres3!==_cres&&_isCipherResult(_cSig2,_cres3)){',
416+
' (function(fi,r,p){_result.sig=function(s){return _cfns[fi](r,p,s);};})',
417+
' (_cfi,_cr,_cp);',
418+
' _result._sigName=_cnames[_cfi]+"("+_cr+","+_cp+",s)";',
419+
' break;',
420+
' }',
421+
' }',
422+
' }catch(e){}',
423+
'}'
424+
].join('\n');
425+
}
426+
427+
/**
428+
* Build _df-based cipher probe (fallback when direct names are overwritten).
429+
*/
430+
function buildDfCipherProbe(candidates, testEntries) {
431+
var fnArr = candidates.map(function(_, i) { return '_cdf' + i; }).join(',');
432+
var nameArr = candidates.map(function(c) { return '"' + c.funcName + '"'; }).join(',');
433+
var paramArr = testEntries.map(function(e) { return '[' + e.fi + ',' + e.r + ',' + e.p + ']'; }).join(',');
434+
return [
435+
'var _cfns2=[' + fnArr + '],_cnames2=[' + nameArr + '];',
436+
'var _cparams2=[' + paramArr + '];',
437+
'var _cSig3="AOq0QJ8wRAIgTXjVbFq4RE0_C3YYzJ-j-rVqGi25Oj_bm9c3x2CiqKICIFfBKjR5Q3iBvFHIqZLqhY1jQ9o5a_FV8WNi9Z2v3BdMAhIARbCqF0FHn4";',
438+
'var _cSig4="ZZq0QJ8wRAIgTXjVbFq4RE0_C3YYzJ-j-rVqGi25Oj_bm9c3x2CiqKICIFfBKjR5Q3iBvFHIqZLqhY1jQ9o5a_FV8WNi9Z2v3BdMAhIARbCqF0FHZZ";',
439+
'function _isCipherResult2(input,output){',
440+
' if(typeof output!=="string")return false;',
441+
' if(output===input)return false;',
442+
' if(output.length<20)return false;',
443+
' if(output.length>input.length)return false;',
444+
' if(output.length<input.length-10)return false;',
445+
' return true;',
446+
'}',
447+
'for(var _ci2=0;_ci2<_cparams2.length&&!_result.sig;_ci2++){',
448+
' var _cfi2=_cparams2[_ci2][0],_cr2=_cparams2[_ci2][1],_cp2=_cparams2[_ci2][2];',
449+
' if(typeof _cfns2[_cfi2]!=="function")continue;',
450+
' try{',
451+
' var _cres4=_cfns2[_cfi2](_cr2,_cp2,_cSig3);',
452+
' if(_isCipherResult2(_cSig3,_cres4)){',
453+
' var _cres5=_cfns2[_cfi2](_cr2,_cp2,_cSig3);',
454+
' var _cres6=_cfns2[_cfi2](_cr2,_cp2,_cSig4);',
455+
' if(_cres4===_cres5&&_cres6!==_cres4&&_isCipherResult2(_cSig4,_cres6)){',
456+
' (function(fi,r,p){_result.sig=function(s){return _cfns2[fi](r,p,s);};})',
457+
' (_cfi2,_cr2,_cp2);',
458+
' _result._sigName=_cnames2[_cfi2]+"("+_cr2+","+_cp2+",s)";',
459+
' break;',
460+
' }',
461+
' }',
462+
' }catch(e){}',
463+
'}'
464+
].join('\n');
465+
}
466+
467+
/**
468+
* Cipher entry point — extract signature cipher function from base.js.
469+
* Called separately from preprocessPlayer (which handles n-param on TV player).
470+
*
471+
* @param {string} data - Full player.js source (main/base.js variant)
472+
* @param {object|null} cipherCache - {funcName, r, p} from previous solve
473+
* @returns {string} - Code ready for Function("_result", code)(resultObj)
474+
* Populates _result.sig (cipher function) and _result._sigName
475+
*/
476+
function preprocessCipher(data, cipherCache) {
477+
var probeBody;
478+
var candidates = null;
479+
480+
if (cipherCache && cipherCache.funcName) {
481+
// Fast path: use cached params
482+
probeBody = [
483+
'var _cSig="AOq0QJ8wRAIgTXjVbFq4RE0_C3YYzJ-j-rVqGi25Oj_bm9c3x2CiqKICIFfBKjR5Q3iBvFHIqZLqhY1jQ9o5a_FV8WNi9Z2v3BdMAhIARbCqF0FHn4";',
484+
'var _cSig2="ZZq0QJ8wRAIgTXjVbFq4RE0_C3YYzJ-j-rVqGi25Oj_bm9c3x2CiqKICIFfBKjR5Q3iBvFHIqZLqhY1jQ9o5a_FV8WNi9Z2v3BdMAhIARbCqF0FHZZ";',
485+
'try{',
486+
' var _cres=' + cipherCache.funcName + '(' + cipherCache.r + ',' + cipherCache.p + ',_cSig);',
487+
' if(typeof _cres==="string"&&_cres!==_cSig&&_cres.length>=20&&_cres.length<=_cSig.length){',
488+
' var _cres2=' + cipherCache.funcName + '(' + cipherCache.r + ',' + cipherCache.p + ',_cSig);',
489+
' var _cres3=' + cipherCache.funcName + '(' + cipherCache.r + ',' + cipherCache.p + ',_cSig2);',
490+
' if(_cres===_cres2&&_cres3!==_cres){',
491+
' _result.sig=function(s){return ' + cipherCache.funcName + '(' + cipherCache.r + ',' + cipherCache.p + ',s);};',
492+
' _result._sigName="' + cipherCache.funcName + '(' + cipherCache.r + ',' + cipherCache.p + ',s)";',
493+
' }',
494+
' }',
495+
'}catch(e){}'
496+
].join('\n');
497+
} else {
498+
// base.js has its string table after copyright comments (~3KB in),
499+
// so try findStringTable on the source starting from 'use strict'
500+
var table = findStringTable(data);
501+
if (!table) {
502+
var strictIdx = data.indexOf("'use strict';");
503+
if (strictIdx > 0 && strictIdx < 10000) {
504+
table = findStringTable(data.substring(strictIdx));
505+
}
506+
}
507+
if (table) {
508+
candidates = findCipherCandidates(data, table.tableVar, table.splitIdx);
509+
if (candidates.length > 0) {
510+
// Build test entries: for cipher, use c values where (c>>1&15)==4
511+
// Valid c: 8, 9, 40, 41 (within r=0..50 range)
512+
var cipherCValues = [8, 9, 40, 41];
513+
var testEntries = [];
514+
var seen = {};
515+
for (var fi = 0; fi < candidates.length; fi++) {
516+
for (var bi = 0; bi < candidates[fi].bases.length; bi++) {
517+
var base = candidates[fi].bases[bi];
518+
for (var ci = 0; ci < cipherCValues.length; ci++) {
519+
var r = cipherCValues[ci]; // r_solver = c in KX
520+
var p = base ^ r; // p_solver = r in KX, so P = base
521+
var key = fi + ':' + r + ',' + p;
522+
if (!seen[key]) { seen[key] = true; testEntries.push({ fi: fi, r: r, p: p }); }
523+
}
524+
}
525+
}
526+
probeBody = buildCipherProbe(candidates, testEntries);
527+
528+
// Also build _df fallback probe (same pattern as n-param solver)
529+
var dfCipherProbe = buildDfCipherProbe(candidates, testEntries);
530+
probeBody = probeBody + '\nif(!_result.sig){\n' + dfCipherProbe + '\n}';
531+
}
532+
}
533+
}
534+
535+
if (!probeBody) probeBody = '/* no cipher candidates found */';
536+
537+
// --- Inject _df captures and wrap player (mirrors preprocessPlayer) ---
538+
var modified = data;
539+
if (candidates && candidates.length > 0) {
540+
var tableDelimiter = null;
541+
var delimMatch = data.substring(0, 10000).match(/\.split\((['"])(.)(\1)\)/);
542+
if (delimMatch) tableDelimiter = delimMatch[2];
543+
544+
if (tableDelimiter !== '}') {
545+
var commaInjections = [];
546+
for (var i = 0; i < candidates.length; i++) {
547+
var c = candidates[i];
548+
var defIdx = modified.indexOf(c.funcName + '=function(');
549+
if (defIdx === -1) continue;
550+
var bodyStart = modified.indexOf('{', defIdx);
551+
if (bodyStart === -1) continue;
552+
var depth = 0, pos = bodyStart;
553+
while (pos < modified.length) {
554+
if (modified[pos] === '{') depth++;
555+
else if (modified[pos] === '}') { depth--; if (depth === 0) break; }
556+
pos++;
557+
}
558+
if (depth === 0) {
559+
commaInjections.push({ pos: pos + 1, code: ',_cdf' + i + '=' + c.funcName });
560+
}
561+
}
562+
commaInjections.sort(function(a, b) { return b.pos - a.pos; });
563+
for (var j = 0; j < commaInjections.length; j++) {
564+
var inj = commaInjections[j];
565+
modified = modified.substring(0, inj.pos) + inj.code + modified.substring(inj.pos);
566+
}
567+
}
568+
569+
// Strategy 2: Chain-boundary injection
570+
var chainInjections = [];
571+
for (var i = 0; i < candidates.length; i++) {
572+
var c = candidates[i];
573+
var defIdx = modified.indexOf(c.funcName + '=function(');
574+
if (defIdx === -1) continue;
575+
var searchFrom = defIdx;
576+
var chainEnd = -1;
577+
while (searchFrom < modified.length) {
578+
var nextSemiNl = modified.indexOf(';\n', searchFrom);
579+
if (nextSemiNl === -1) break;
580+
var afterSemi = modified.substring(nextSemiNl + 2, nextSemiNl + 12);
581+
if (afterSemi.indexOf('function ') === 0 || afterSemi.indexOf('var ') === 0) {
582+
chainEnd = nextSemiNl + 1;
583+
break;
584+
}
585+
searchFrom = nextSemiNl + 2;
586+
}
587+
if (chainEnd !== -1) {
588+
chainInjections.push({ pos: chainEnd, code: '\ntry{_cdf' + i + '=' + c.funcName + ';}catch(e){}' });
589+
}
590+
}
591+
var mergedByPos = {};
592+
for (var j = 0; j < chainInjections.length; j++) {
593+
var pp = chainInjections[j].pos;
594+
if (!mergedByPos[pp]) mergedByPos[pp] = '';
595+
mergedByPos[pp] += chainInjections[j].code;
596+
}
597+
var sorted = Object.keys(mergedByPos).map(Number).sort(function(a, b) { return b - a; });
598+
for (var j = 0; j < sorted.length; j++) {
599+
var ppos = sorted[j];
600+
modified = modified.substring(0, ppos) + mergedByPos[ppos] + modified.substring(ppos);
601+
}
602+
}
603+
604+
// Wrap player in try-catch, declare _cdf vars, append probe
605+
var iifeCloseIdx = modified.lastIndexOf('}).call(this)');
606+
if (iifeCloseIdx === -1) iifeCloseIdx = modified.lastIndexOf('})(_yt_player)');
607+
if (iifeCloseIdx === -1) iifeCloseIdx = modified.lastIndexOf('})(');
608+
609+
if (iifeCloseIdx !== -1) {
610+
var strictIdx = modified.indexOf("'use strict';");
611+
var afterStrict = strictIdx !== -1 ? strictIdx + "'use strict';".length : modified.indexOf('{') + 1;
612+
613+
var dfDecl = '';
614+
if (candidates && candidates.length > 0) {
615+
var dfNames = [];
616+
for (var i = 0; i < candidates.length; i++) dfNames.push('_cdf' + i);
617+
dfDecl = '\nvar ' + dfNames.join(',') + ';\n';
618+
}
619+
620+
return SETUP_CODE + '\n' +
621+
modified.substring(0, afterStrict) + dfDecl + '\ntry{\n' +
622+
modified.substring(afterStrict, iifeCloseIdx) + '\n}catch(_e){}\n' +
623+
probeBody + '\n' +
624+
modified.substring(iifeCloseIdx);
625+
}
626+
627+
return SETUP_CODE + '\n' + modified + '\n;(function(){' + probeBody + '})();';
628+
}

0 commit comments

Comments
 (0)