fix: switch to rquickjs crate for deobfuscator

This commit is contained in:
ThetaDev 2025-01-13 03:30:07 +01:00
parent 74fe628f63
commit cdb56de3bd
3 changed files with 41 additions and 30 deletions

View file

@ -24,9 +24,7 @@ keywords = ["youtube", "video", "music"]
categories = ["api-bindings", "multimedia"] categories = ["api-bindings", "multimedia"]
[workspace.dependencies] [workspace.dependencies]
quick-js-dtp = { version = "0.4.1", default-features = false, features = [ rquickjs = "0.8.1"
"patch-dateparser",
] }
once_cell = "1.12.0" once_cell = "1.12.0"
regex = "1.6.0" regex = "1.6.0"
fancy-regex = "0.14.0" fancy-regex = "0.14.0"
@ -94,7 +92,7 @@ rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"]
rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"] rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"]
[dependencies] [dependencies]
quick-js-dtp.workspace = true rquickjs.workspace = true
once_cell.workspace = true once_cell.workspace = true
regex.workspace = true regex.workspace = true
fancy-regex.workspace = true fancy-regex.workspace = true

View file

@ -4,6 +4,7 @@ use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use reqwest::Client; use reqwest::Client;
use ress::tokens::Token; use ress::tokens::Token;
use rquickjs::{Context, Runtime};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
@ -13,7 +14,7 @@ use crate::{
}; };
pub struct Deobfuscator { pub struct Deobfuscator {
ctx: quick_js::Context, ctx: Context,
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)]
@ -76,29 +77,34 @@ impl DeobfData {
impl Deobfuscator { impl Deobfuscator {
/// Instantiate a new deobfuscator with the given data /// Instantiate a new deobfuscator with the given data
pub fn new(data: &DeobfData) -> Result<Self, DeobfError> { pub fn new(data: &DeobfData) -> Result<Self, DeobfError> {
let ctx = let rt = Runtime::new()?;
quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?; let ctx = Context::full(&rt)?;
ctx.eval(&data.sig_fn)?; ctx.with(|ctx| {
ctx.eval(&data.nsig_fn)?; let mut opts = rquickjs::context::EvalOptions::default();
opts.strict = false;
ctx.eval_with_options::<(), _>(data.sig_fn.as_bytes(), opts)?;
let mut opts = rquickjs::context::EvalOptions::default();
opts.strict = false;
ctx.eval_with_options::<(), _>(data.nsig_fn.as_bytes(), opts)
})?;
Ok(Self { ctx }) Ok(Self { ctx })
} }
/// Deobfuscate the `s` parameter from the `signature_cipher` field /// Deobfuscate the `s` parameter from the `signature_cipher` field
pub fn deobfuscate_sig(&self, sig: &str) -> Result<String, DeobfError> { pub fn deobfuscate_sig(&self, sig: &str) -> Result<String, DeobfError> {
let res = self.ctx.call_function(DEOBF_SIG_FUNC_NAME, [sig])?; let res = self
.ctx
res.into_string() .with(|ctx| call_fn(&ctx, DEOBF_SIG_FUNC_NAME, sig))?;
.ok_or(DeobfError::Other("sig deobfuscation fn returned no string")) tracing::trace!("deobf sig: {sig} -> {res}");
Ok(res)
} }
/// Deobfuscate the `n` stream URL parameter to circumvent throttling /// Deobfuscate the `n` stream URL parameter to circumvent throttling
pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String, DeobfError> { pub fn deobfuscate_nsig(&self, nsig: &str) -> Result<String, DeobfError> {
let res = self.ctx.call_function(DEOBF_NSIG_FUNC_NAME, [nsig])?; let res = self
let res = res.into_string().ok_or(DeobfError::Other( .ctx
"nsig deobfuscation fn returned no string", .with(|ctx| call_fn(&ctx, DEOBF_NSIG_FUNC_NAME, nsig))?;
))?; tracing::trace!("deobf nsig: {nsig} -> {res}");
tracing::debug!("deobf nsig: {nsig} -> {res}");
if res.starts_with("enhanced_except_") || res.ends_with(nsig) { if res.starts_with("enhanced_except_") || res.ends_with(nsig) {
return Err(DeobfError::Other("nsig fn returned an exception")); return Err(DeobfError::Other("nsig fn returned an exception"));
} }
@ -279,6 +285,7 @@ fn extract_js_fn(js: &str, offset: usize, name: &str) -> Result<String, DeobfErr
let fn_range = (offset + start)..(offset + end); let fn_range = (offset + start)..(offset + end);
let mut code = format!("var {};", &js[fn_range.clone()]); let mut code = format!("var {};", &js[fn_range.clone()]);
let rt = rquickjs::Runtime::new()?;
for (ident, _) in idents.into_iter().filter(|(_, v)| *v == 1) { for (ident, _) in idents.into_iter().filter(|(_, v)| *v == 1) {
let var_pattern_str = format!(r#"\b{}\b\s*=[^=]"#, regex::escape(&ident)); let var_pattern_str = format!(r#"\b{}\b\s*=[^=]"#, regex::escape(&ident));
@ -288,13 +295,13 @@ fn extract_js_fn(js: &str, offset: usize, name: &str) -> Result<String, DeobfErr
.filter(|m| !fn_range.contains(&m.start()) && !fn_range.contains(&m.end())) .filter(|m| !fn_range.contains(&m.start()) && !fn_range.contains(&m.end()))
.find_map(|m| extract_js_var(&js[m.start()..])); .find_map(|m| extract_js_var(&js[m.start()..]));
if let Some(var_code) = found_variable { if let Some(var_code) = found_variable {
let ctx = quick_js::Context::new() let ctx = Context::full(&rt)?;
.or(Err(DeobfError::Other("could not create QuickJS rt")))?; let var_code = format!("var {var_code};");
if let Err(e) = ctx.eval(var_code) { if let Err(e) = ctx.with(|ctx| ctx.eval::<(), _>(var_code.as_bytes())) {
tracing::warn!("invalid var ({e}): {var_code}"); tracing::warn!("invalid var ({e}): {var_code}");
code = format!("var {ident}={{}}; {code}"); code = format!("var {ident}={{}}; {code}");
} else { } else {
code = format!("var {var_code}; {code}"); code = format!("{var_code} {code}");
} }
} }
} }
@ -362,15 +369,21 @@ fn extract_js_var(js: &str) -> Option<&str> {
} }
} }
fn call_fn(ctx: &rquickjs::Ctx, fn_name: &str, arg: &str) -> Result<String, rquickjs::Error> {
let f: rquickjs::Function = ctx.globals().get(fn_name)?;
f.call((arg,))
}
/// Verify if the deobfuscation function successfully processes a random input string /// Verify if the deobfuscation function successfully processes a random input string
fn verify_fn(js_fn: &str, fn_name: &str) -> Result<(), DeobfError> { fn verify_fn(js_fn: &str, fn_name: &str) -> Result<(), DeobfError> {
let ctx = quick_js::Context::new().or(Err(DeobfError::Other("could not create QuickJS rt")))?; let rt = Runtime::new()?;
ctx.eval(js_fn)?; let ctx = Context::full(&rt)?;
let testinp = util::generate_content_playback_nonce(); let testinp = util::generate_content_playback_nonce();
let res = ctx let res = ctx.with(|ctx| {
.call_function(fn_name, [testinp.to_owned()])? ctx.eval::<(), _>(js_fn)?;
.into_string() call_fn(&ctx, fn_name, &testinp)
.ok_or(DeobfError::Other("deobfuscation fn returned no string"))?; })?;
if res.is_empty() { if res.is_empty() {
return Err(DeobfError::Other("deobfuscation fn returned empty string")); return Err(DeobfError::Other("deobfuscation fn returned empty string"));
} }

View file

@ -156,7 +156,7 @@ pub(crate) mod internal {
pub enum DeobfError { pub enum DeobfError {
/// Error during JavaScript execution /// Error during JavaScript execution
#[error("js execution error: {0}")] #[error("js execution error: {0}")]
JavaScript(#[from] quick_js::ExecutionError), JavaScript(#[from] rquickjs::Error),
/// Error during JavaScript parsing /// Error during JavaScript parsing
#[error("js parsing: {0}")] #[error("js parsing: {0}")]
JsParser(#[from] ress::error::Error), JsParser(#[from] ress::error::Error),