diff --git a/src/bin/report.rs b/src/bin/report.rs index d33718a..58ace08 100644 --- a/src/bin/report.rs +++ b/src/bin/report.rs @@ -108,7 +108,16 @@ fn report_generated_corpus() { println!("\nobfuscator.io corpus (samples/generated/) — aggregated by profile"); println!( "{:<13} {:>5} {:>9} {:>9} {:>6} {:>8} {:>8} {:>8} {:>6} {:>5}", - "profile", "files", "in_bytes", "out_bytes", "kept%", "brackets", "opaque%", "hexrefs", "rounds", "conv" + "profile", + "files", + "in_bytes", + "out_bytes", + "kept%", + "brackets", + "opaque%", + "hexrefs", + "rounds", + "conv" ); let mut profiles: Vec = by_profile.keys().cloned().collect(); @@ -163,7 +172,16 @@ fn print_agg_row(label: &str, a: &ProfileAgg) { }; println!( "{:<13} {:>5} {:>9} {:>9} {:>5}% {:>8} {:>7}% {:>8} {:>6} {:>5}", - label, a.files, a.in_bytes, a.out_bytes, kept, a.brackets, opaque, a.hexrefs, a.rounds_max, conv + label, + a.files, + a.in_bytes, + a.out_bytes, + kept, + a.brackets, + opaque, + a.hexrefs, + a.rounds_max, + conv ); } @@ -176,8 +194,20 @@ fn profile_of(name: &str) -> Option { } fn count_bracket_access(s: &str) -> usize { - // crude: count `["` occurrences (string-keyed member access) - s.matches("[\"").count() + // Count `["` only where it is a string-keyed *member access* (`obj["x"]`), + // not an array/object literal (`= ["x"]`, `, ["x"]`). The discriminator is + // the byte immediately before `[`: a member access follows an expression + // (identifier char, `)`, `]`, or a closing quote) with no space, whereas a + // literal follows an operator/punctuator/space. Excludes the array-literal + // false positives that dominate the raw `["` count. + let b = s.as_bytes(); + (1..b.len().saturating_sub(1)) + .filter(|&i| b[i] == b'[' && b[i + 1] == b'"' && is_member_lhs(b[i - 1])) + .count() +} + +fn is_member_lhs(p: u8) -> bool { + p.is_ascii_alphanumeric() || matches!(p, b'_' | b'$' | b')' | b']' | b'"' | b'\'') } /// Raw (non-distinct) occurrence count of `_0x…`-style hex-named identifiers diff --git a/src/passes/dce.rs b/src/passes/dce.rs index e6ffe5a..bcb49f2 100644 --- a/src/passes/dce.rs +++ b/src/passes/dce.rs @@ -76,10 +76,10 @@ fn keep_statement(s: &Statement<'_>, scoping: &Scoping) -> bool { match s { Statement::EmptyStatement(_) => false, Statement::VariableDeclaration(vd) => !vd.declarations.is_empty(), - Statement::FunctionDeclaration(f) => f - .id - .as_ref() - .is_none_or(|id| !fn_decl_is_dead(id.symbol_id(), f, scoping)), + Statement::FunctionDeclaration(f) => { + f.id.as_ref() + .is_none_or(|id| !fn_decl_is_dead(id.symbol_id(), f, scoping)) + } // A bare literal statement (`80214130;`, `true;`, `null;`) computes a // value that is immediately discarded with no side effect — pure // obfuscation noise (often a spent opaque-predicate constant). Drop it. diff --git a/src/passes/mod.rs b/src/passes/mod.rs index f630427..894cda6 100644 --- a/src/passes/mod.rs +++ b/src/passes/mod.rs @@ -14,6 +14,7 @@ mod math_fingerprint; mod member_normalize; mod proxy_inline; mod pure_eval; +mod reconstruct_object; mod rename; mod sequence_split; mod unflatten; @@ -35,6 +36,7 @@ pub use math_fingerprint::MathFingerprint; pub use member_normalize::MemberNormalize; pub use proxy_inline::ProxyInline; pub use pure_eval::PureEval; +pub use reconstruct_object::ReconstructObject; pub use rename::RenameByRole; pub use sequence_split::SequenceSplit; pub use unflatten::Unflatten; @@ -102,6 +104,11 @@ pub fn default_pipeline() -> Vec> { // lift, and the const folds below reach further. Runs before them so the // exposed work is picked up in the same round. See `src/ir/inline.rs`. Box::new(crate::ir::InlineSingleUse), + // Fold `X = {}; X.a = …; X.b = …;` runs back into the object literal + // they were lowered from (transformObjectKeys). Runs before ProxyInline + // so operator-proxy *tables* built in this split form are reconstituted + // into the `{ m: function… }` literal ProxyInline can recognize. + Box::new(ReconstructObject::default()), // Collapse operator-proxy wrappers (a OP b) before lifting, so decoder // calls hidden inside proxy expansions become visible. Box::new(ProxyInline::default()), diff --git a/src/passes/reconstruct_object.rs b/src/passes/reconstruct_object.rs new file mode 100644 index 0000000..79c9eda --- /dev/null +++ b/src/passes/reconstruct_object.rs @@ -0,0 +1,250 @@ +//! Object-literal reconstruction. +//! +//! `transformObjectKeys` (and hand-written packers) lower an object literal into +//! an empty object plus a run of property writes: +//! +//! ```js +//! const O = {}; +//! O.name = "P1"; +//! O.price = 600; +//! ``` +//! +//! This pass folds that contiguous run back into the literal it came from — +//! `const O = { name: "P1", price: 600 }`. That alone improves readability, but +//! the bigger payoff is downstream: obfuscators build their *operator-proxy +//! tables* the same way (`const t = {}; t.m = function(a,b){…}; …`), so +//! reconstruction is what lets `proxy_inline` recognize the table at all, which +//! in turn folds the opaque predicates guarding dead branches so DCE can remove +//! them. +//! +//! Soundness — we fold only when the rewrite cannot change observable behavior: +//! * The seed is a single declarator `X = {}` with an *empty* object literal. +//! * We consume only the *immediately following, contiguous* statements of the +//! form `X. = ` (plain `=`, non-computed key). Contiguity +//! guarantees nothing reads the half-built object between the writes. +//! * The value expression must not reference `X` at all (even inside a nested +//! function): in the literal, `X` is not yet bound, so a value that read `X` +//! would change meaning. This is conservative (a closure-only reference would +//! be safe) but always sound. +//! * `__proto__` is excluded (its literal form has special prototype-setting +//! semantics) and a repeated key stops the run (last-write-wins is preserved +//! by not merging across a duplicate). +//! +//! Property values keep their original order, so any side effects in them run in +//! the same sequence as the original writes. + +use std::collections::HashSet; + +use oxc_allocator::{Allocator, Vec as ArenaVec}; +use oxc_ast::ast::{ + AssignmentOperator, AssignmentTarget, Expression, ObjectPropertyKind, Program, PropertyKind, + Statement, +}; +use oxc_ast::AstBuilder; +use oxc_ast_visit::Visit; +use oxc_semantic::Scoping; +use oxc_syntax::symbol::SymbolId; +use oxc_traverse::{traverse_mut, Traverse, TraverseCtx}; + +use crate::pass::Pass; +use crate::util::build_scoping; + +#[derive(Default)] +pub struct ReconstructObject { + changed: bool, +} + +impl Pass for ReconstructObject { + fn name(&self) -> &'static str { + "reconstruct-object" + } + + fn run<'a>(&mut self, program: &mut Program<'a>, allocator: &'a Allocator) -> bool { + let scoping = build_scoping(program); + self.changed = false; + traverse_mut(self, allocator, program, scoping, ()); + self.changed + } +} + +/// Symbol bound by a `X = {}` seed statement, plus a mutable handle to its +/// (currently empty) object literal is obtained later; here we just classify. +fn seed_symbol(stmt: &Statement<'_>) -> Option { + let Statement::VariableDeclaration(vd) = stmt else { + return None; + }; + if vd.declarations.len() != 1 { + return None; + } + let decl = &vd.declarations[0]; + let id = decl.id.get_binding_identifier()?; + match &decl.init { + Some(Expression::ObjectExpression(obj)) if obj.properties.is_empty() => id.symbol_id.get(), + _ => None, + } +} + +/// If `stmt` is `seed. = ` (plain assign, static key) for `seed`, +/// return the property name. Read-only — used to validate a run before folding. +fn member_assign_key<'a>( + stmt: &Statement<'a>, + seed: SymbolId, + scoping: &Scoping, +) -> Option { + let Statement::ExpressionStatement(es) = stmt else { + return None; + }; + let Expression::AssignmentExpression(a) = &es.expression else { + return None; + }; + if a.operator != AssignmentOperator::Assign { + return None; + } + let AssignmentTarget::StaticMemberExpression(m) = &a.left else { + return None; + }; + let Expression::Identifier(obj) = &m.object else { + return None; + }; + let rid = obj.reference_id.get()?; + if scoping.get_reference(rid).symbol_id() != Some(seed) { + return None; + } + Some(m.property.name.to_string()) +} + +/// Does `expr` contain any reference resolving to `seed`? +fn references_symbol(expr: &Expression<'_>, seed: SymbolId, scoping: &Scoping) -> bool { + struct Finder<'s> { + seed: SymbolId, + scoping: &'s Scoping, + found: bool, + } + impl<'a, 's> Visit<'a> for Finder<'s> { + fn visit_identifier_reference(&mut self, id: &oxc_ast::ast::IdentifierReference<'a>) { + if let Some(rid) = id.reference_id.get() { + if self.scoping.get_reference(rid).symbol_id() == Some(self.seed) { + self.found = true; + } + } + } + } + let mut f = Finder { + seed, + scoping, + found: false, + }; + f.visit_expression(expr); + f.found +} + +impl<'a> Traverse<'a, ()> for ReconstructObject { + fn enter_statements( + &mut self, + stmts: &mut ArenaVec<'a, Statement<'a>>, + ctx: &mut TraverseCtx<'a, ()>, + ) { + // Cheap precheck: any empty-object seed at all? + if !stmts.iter().any(|s| seed_symbol(s).is_some()) { + return; + } + + let n = stmts.len(); + let mut old: Vec>> = std::mem::replace(stmts, ctx.ast.vec()) + .into_iter() + .map(Some) + .collect(); + + let mut i = 0; + while i < n { + // Plan a fold using read-only borrows of `old` + scoping. + let plan: Option> = seed_symbol(old[i].as_ref().unwrap()).and_then(|seed| { + let scoping = ctx.scoping(); + let mut consumed = Vec::new(); + let mut keys = HashSet::new(); + let mut j = i + 1; + while j < n { + let Some(key) = member_assign_key(old[j].as_ref().unwrap(), seed, scoping) + else { + break; + }; + if key == "__proto__" || !keys.insert(key) { + break; + } + // value must not observe the not-yet-bound seed object. + let rhs = assign_rhs(old[j].as_ref().unwrap()); + if references_symbol(rhs, seed, scoping) { + break; + } + consumed.push(j); + j += 1; + } + (!consumed.is_empty()).then_some(consumed) + }); + + match plan { + Some(consumed) => { + let next = consumed.last().unwrap() + 1; + // Build the property list, then attach it to the seed's literal. + let mut props = ctx.ast.vec_with_capacity(consumed.len()); + for cj in &consumed { + let stmt = old[*cj].take().unwrap(); + props.push(take_member_property(stmt, ctx.ast)); + } + let mut seed_stmt = old[i].take().unwrap(); + if let Statement::VariableDeclaration(vd) = &mut seed_stmt { + if let Some(Expression::ObjectExpression(obj)) = + &mut vd.declarations[0].init + { + obj.properties = props; + } + } + stmts.push(seed_stmt); + self.changed = true; + i = next; + } + None => { + stmts.push(old[i].take().unwrap()); + i += 1; + } + } + } + } +} + +fn assign_rhs<'b, 'a>(stmt: &'b Statement<'a>) -> &'b Expression<'a> { + let Statement::ExpressionStatement(es) = stmt else { + unreachable!() + }; + let Expression::AssignmentExpression(a) = &es.expression else { + unreachable!() + }; + &a.right +} + +/// Consume a validated `seed.name = value` statement into the object property +/// `name: value`. Reuses the member key's arena-allocated identifier directly. +fn take_member_property<'a>(stmt: Statement<'a>, ast: AstBuilder<'a>) -> ObjectPropertyKind<'a> { + let Statement::ExpressionStatement(es) = stmt else { + unreachable!() + }; + let Expression::AssignmentExpression(a) = es.unbox().expression else { + unreachable!() + }; + let a = a.unbox(); + let AssignmentTarget::StaticMemberExpression(m) = a.left else { + unreachable!() + }; + let m = m.unbox(); + let span = m.span; + let key = ast.property_key_static_identifier(span, m.property.name); + ast.object_property_kind_object_property( + span, + PropertyKind::Init, + key, + a.right, + false, + false, + false, + ) +} diff --git a/src/passes/rename.rs b/src/passes/rename.rs index 16abda3..66469fb 100644 --- a/src/passes/rename.rs +++ b/src/passes/rename.rs @@ -194,8 +194,8 @@ fn callback_param_roles(method: &str) -> Option<&'static [&'static str]> { // (accumulator, currentValue, index, array) "reduce" | "reduceRight" => &["acc", "value", "index", "arr"], // (element, index, array) - "map" | "forEach" | "filter" | "find" | "findIndex" | "findLast" - | "findLastIndex" | "some" | "every" | "flatMap" => &["item", "index", "arr"], + "map" | "forEach" | "filter" | "find" | "findIndex" | "findLast" | "findLastIndex" + | "some" | "every" | "flatMap" => &["item", "index", "arr"], // (a, b) comparator "sort" => &["left", "right"], _ => return None, diff --git a/tests/golden.rs b/tests/golden.rs index bef1462..19594e6 100644 --- a/tests/golden.rs +++ b/tests/golden.rs @@ -226,7 +226,8 @@ fn render_generated_corpus(s: &mut String) { continue; } let name = p.file_name().unwrap().to_string_lossy().into_owned(); - let Some((_, profile)) = name.strip_suffix(".js").and_then(|st| st.rsplit_once("__")) else { + let Some((_, profile)) = name.strip_suffix(".js").and_then(|st| st.rsplit_once("__")) + else { continue; }; let src = fs::read_to_string(&p).unwrap(); @@ -326,7 +327,20 @@ fn push_agg_row(s: &mut String, label: &str, a: &ProfileAgg) { } fn count_bracket_access(s: &str) -> usize { - s.matches("[\"").count() + // Count `["` only where it is a string-keyed *member access* (`obj["x"]`), + // not an array/object literal — the byte before `[` is part of the object + // expression (identifier char, `)`, `]`, or a closing quote) with no space. + // Mirrors `src/bin/report.rs`. Excludes the array-literal false positives + // that dominate the raw `["` count. + let b = s.as_bytes(); + (1..b.len().saturating_sub(1)) + .filter(|&i| { + b[i] == b'[' + && b[i + 1] == b'"' + && (b[i - 1].is_ascii_alphanumeric() + || matches!(b[i - 1], b'_' | b'$' | b')' | b']' | b'"' | b'\'')) + }) + .count() } /// Fraction (%) of distinct identifier-ish tokens that look obfuscated: length diff --git a/tests/phase1.rs b/tests/phase1.rs index 8a1a757..ee79d56 100644 --- a/tests/phase1.rs +++ b/tests/phase1.rs @@ -28,7 +28,10 @@ fn member_normalization_optional_chain() { // `a?.["foo"]` parses as a ChainElement, not an Expression, so it needs a // dedicated hook. The object is an opaque param so the access survives. let out = deob(r#"function f(a){ return a?.["foo"]?.["1bad"]; } sink(f);"#); - assert!(out.contains("?.foo"), "optional chain not normalized: {out}"); + assert!( + out.contains("?.foo"), + "optional chain not normalized: {out}" + ); // non-identifier key stays bracketed assert!( out.contains(r#"?.["1bad"]"#), @@ -227,3 +230,24 @@ fn idempotent_on_nested_obfuscation() { let twice = deobfuscate(&once, "t.js").code; assert_eq!(once, twice, "pipeline is not idempotent"); } + +#[test] +fn object_reconstruction_folds_split_writes() { + // `transformObjectKeys` lowers `{a,b}` into `o = {}; o.a = …; o.b = …`. + // Reconstruction folds the contiguous writes back into the literal. + let out = deob(r#"function f(){ var o = {}; o.a = "x"; o.b = "y"; return o; } sink(f);"#); + assert!(out.contains(r#"a: "x""#), "not reconstructed: {out}"); + assert!(out.contains(r#"b: "y""#), "not reconstructed: {out}"); + assert!(!out.contains(".a ="), "split write survived: {out}"); +} + +#[test] +fn object_reconstruction_exposes_proxy_table() { + // An operator-proxy table built in split-write form is invisible to + // proxy-inline until reconstructed; once folded, the call inlines and folds. + let out = deob( + r#"function f(){ var t = {}; t.sum = function(a,b){ return a + b; }; return t.sum(2,3); } sink(f);"#, + ); + assert!(out.contains('5'), "proxy table not inlined+folded: {out}"); + assert!(!out.contains("t.sum"), "proxy survived: {out}"); +} diff --git a/tests/snapshots/SCOREBOARD.md b/tests/snapshots/SCOREBOARD.md index 388938f..c365829 100644 --- a/tests/snapshots/SCOREBOARD.md +++ b/tests/snapshots/SCOREBOARD.md @@ -4,20 +4,20 @@ Regenerated by `tests/golden.rs` (re-bless with `UPDATE_SNAPSHOTS=1 cargo test - | sample | in_bytes | out_bytes | kept% | brackets | opaque% | hexrefs | rounds | converged | |--------|---------:|----------:|------:|---------:|--------:|--------:|-------:|:---------:| -| sample_1.js | 1263501 | 887839 | 70% | 9 | 14% | 0 | 7 | yes | -| sample_10.js | 2118557 | 1673976 | 79% | 96 | 8% | 0 | 7 | yes | -| sample_11.js | 341931 | 195307 | 57% | 9 | 3% | 0 | 5 | yes | +| sample_1.js | 1263501 | 887839 | 70% | 4 | 14% | 0 | 7 | yes | +| sample_10.js | 2118557 | 1673976 | 79% | 6 | 8% | 0 | 7 | yes | +| sample_11.js | 341931 | 195289 | 57% | 2 | 3% | 0 | 5 | yes | | sample_12.js | 389 | 30 | 7% | 0 | 0% | 0 | 4 | yes | | sample_13.js | 439 | 17 | 3% | 0 | 0% | 0 | 2 | yes | -| sample_14_recaptcha.js | 894957 | 1700418 | 189% | 93 | 8% | 0 | 6 | yes | -| sample_15_hcaptcha.js | 317963 | 666653 | 209% | 58 | 5% | 0 | 5 | yes | +| sample_14_recaptcha.js | 894957 | 1700418 | 189% | 4 | 8% | 0 | 6 | yes | +| sample_15_hcaptcha.js | 317963 | 666653 | 209% | 17 | 5% | 0 | 5 | yes | | sample_2.js | 1175 | 1168 | 99% | 4 | 5% | 0 | 3 | yes | -| sample_3.js | 116976 | 102021 | 87% | 65 | 5% | 0 | 3 | yes | -| sample_4.js | 168723 | 130103 | 77% | 4 | 25% | 0 | 6 | yes | +| sample_3.js | 116976 | 102021 | 87% | 62 | 5% | 0 | 3 | yes | +| sample_4.js | 168723 | 130103 | 77% | 0 | 25% | 0 | 6 | yes | | sample_5.js | 47801 | 22945 | 48% | 0 | 5% | 0 | 4 | yes | | sample_6.js | 49513 | 20968 | 42% | 0 | 4% | 0 | 6 | yes | -| sample_7.js | 745944 | 609723 | 81% | 576 | 6% | 0 | 6 | yes | -| sample_8.js | 401876 | 232800 | 57% | 4 | 1% | 0 | 7 | yes | +| sample_7.js | 745944 | 609645 | 81% | 528 | 6% | 0 | 6 | yes | +| sample_8.js | 401876 | 232800 | 57% | 3 | 1% | 0 | 7 | yes | | sample_9.js | 156692 | 158004 | 100% | 0 | 5% | 0 | 5 | yes | ## obfuscator.io corpus (`samples/generated/`), by profile @@ -26,11 +26,11 @@ Synthetic javascript-obfuscator fixtures (one row per technique, aggregated over | profile | files | in_bytes | out_bytes | kept% | brackets | opaque% | hexrefs | rounds | converged | |---------|------:|---------:|----------:|------:|---------:|--------:|--------:|-------:|:---------:| -| minimal | 20 | 31067 | 19278 | 62% | 12 | 8% | 0 | 4 | yes | -| strarr_base64 | 20 | 68836 | 19730 | 28% | 12 | 8% | 3 | 4 | yes | -| strarr_rc4 | 20 | 107162 | 20954 | 19% | 12 | 8% | 3 | 5 | yes | -| controlflow | 20 | 62944 | 20229 | 32% | 12 | 8% | 3 | 7 | yes | -| deadcode | 20 | 56174 | 19641 | 34% | 12 | 8% | 5 | 5 | yes | -| numbers_keys | 20 | 90297 | 20766 | 22% | 12 | 9% | 71 | 4 | yes | -| strong | 20 | 164800 | 33842 | 20% | 12 | 11% | 132 | 5 | yes | -| **all** | 140 | 581280 | 154440 | 26% | 84 | 9% | 217 | 7 | yes | +| minimal | 20 | 31067 | 19278 | 62% | 0 | 8% | 0 | 4 | yes | +| strarr_base64 | 20 | 68836 | 19730 | 28% | 0 | 8% | 3 | 4 | yes | +| strarr_rc4 | 20 | 107162 | 20954 | 19% | 0 | 8% | 3 | 5 | yes | +| controlflow | 20 | 62944 | 20229 | 32% | 0 | 8% | 3 | 7 | yes | +| deadcode | 20 | 56174 | 19641 | 34% | 0 | 8% | 5 | 5 | yes | +| numbers_keys | 20 | 90297 | 20364 | 22% | 0 | 9% | 37 | 5 | yes | +| strong | 20 | 164800 | 32768 | 19% | 0 | 11% | 98 | 6 | yes | +| **all** | 140 | 581280 | 152964 | 26% | 0 | 9% | 149 | 7 | yes | diff --git a/tests/snapshots/sample_11.js.out.js b/tests/snapshots/sample_11.js.out.js index d73b2b2..33527d5 100644 --- a/tests/snapshots/sample_11.js.out.js +++ b/tests/snapshots/sample_11.js.out.js @@ -2434,8 +2434,7 @@ var DataDomeJsTag = (() => { }, function(var442, var443, var444) { function fn29(var445, var446) { - var obj19 = {}; - obj19.name = var445; + var obj19 = { name: var445 }; win.navigator.permissions.query(obj19).then(function(var447) { "denied" != var447.state || var358[15][161] == var358[99][105] ? var446() : var442("emd", "denied"); }).catch(function() { diff --git a/tests/snapshots/sample_7.js.out.js b/tests/snapshots/sample_7.js.out.js index cdd1c3d..960eeaa 100644 --- a/tests/snapshots/sample_7.js.out.js +++ b/tests/snapshots/sample_7.js.out.js @@ -12184,8 +12184,7 @@ function fn718(var2968) { return function(var2969, var2970, var2971) { var2970 = (function(var2972, var2973) { - var obj104 = {}; - obj104.f0x70a39114 = var2972; + var obj104 = { f0x70a39114: var2972 }; var2973 && (obj104.f0x24f7cb1 = var2973); return obj104; })(var2970, var2971); @@ -12986,8 +12985,7 @@ }; function fn756(var3226, var3227) { fn677("f0x6acb38"); - var obj130 = {}; - obj130.f0x1bfb0c97 = !(!var3226[1] || !var3226[1].withCredentials); + var obj130 = { f0x1bfb0c97: !(!var3226[1] || !var3226[1].withCredentials) }; var3227 = Object.assign({ g: var3226[0] }, var3227); var3218(var3217, obj130, var3227); fn678("f0x6acb38"); @@ -15829,14 +15827,15 @@ } function fn845(var3961) { try { - var obj184 = {}; - obj184.vid = (var3961.match(new RegExp("window\\._pxVid\\s*=\\s*([\"'])([\\w-]{36})\\1\\s*;", "")) || [])[2] || fn37(); - obj184.uuid = (var3961.match(new RegExp("window\\._pxUuid\\s*=\\s*([\"'])([\\w-]{36}(:true)?)\\1\\s*;", "")) || [])[2] || or4(); - obj184.appId = (var3961.match(new RegExp("window\\._pxAppId\\s*=\\s*(['\"])(PX\\w{4,8})\\1\\s*;", "")) || [])[2] || fn35(); - obj184.blockScript = (var3961.match(new RegExp("(?:\\.src|pxCaptchaSrc)\\s*=\\s*([\"'])((?:(?!\\1).)*captcha\\.js(?:(?!\\1).)*)\\1\\s*;", "")) || [])[2] || call97(); - obj184.hostUrl = (var3961.match(new RegExp("window\\._pxHostUrl\\s*=\\s*([\"'])((?:(?!\\1).)*)\\1\\s*;", "")) || [])[2]; - obj184.jsClientSrc = (var3961.match(new RegExp("window\\._pxJsClientSrc\\s*=\\s*([\"'])((?:(?!\\1).)*)\\1\\s*;", "")) || [])[2]; - obj184.firstPartyEnabled = (var3961.match(new RegExp("window\\._pxFirstPartyEnabled\\s*=\\s*(true|false)\\s*;", "")) || [])[1]; + var obj184 = { + vid: (var3961.match(new RegExp("window\\._pxVid\\s*=\\s*([\"'])([\\w-]{36})\\1\\s*;", "")) || [])[2] || fn37(), + uuid: (var3961.match(new RegExp("window\\._pxUuid\\s*=\\s*([\"'])([\\w-]{36}(:true)?)\\1\\s*;", "")) || [])[2] || or4(), + appId: (var3961.match(new RegExp("window\\._pxAppId\\s*=\\s*(['\"])(PX\\w{4,8})\\1\\s*;", "")) || [])[2] || fn35(), + blockScript: (var3961.match(new RegExp("(?:\\.src|pxCaptchaSrc)\\s*=\\s*([\"'])((?:(?!\\1).)*captcha\\.js(?:(?!\\1).)*)\\1\\s*;", "")) || [])[2] || call97(), + hostUrl: (var3961.match(new RegExp("window\\._pxHostUrl\\s*=\\s*([\"'])((?:(?!\\1).)*)\\1\\s*;", "")) || [])[2], + jsClientSrc: (var3961.match(new RegExp("window\\._pxJsClientSrc\\s*=\\s*([\"'])((?:(?!\\1).)*)\\1\\s*;", "")) || [])[2], + firstPartyEnabled: (var3961.match(new RegExp("window\\._pxFirstPartyEnabled\\s*=\\s*(true|false)\\s*;", "")) || [])[1] + }; return obj184; } catch (error440) {} }