Skip to content

unweave

Techniques

Techniques
Stages

Strings & literals

Turn an encoded or calculated value back into the real thing.

3

String decoding

stringDecoding

Puts the real text back into your code.

Obfuscators move every piece of text into one big lookup table, so "getElementById" arrives as something like _0x3f2a(0x1b). This finds that table and the function that reads from it, works out what each call returns, and writes the text back where it belongs. Once nothing refers to the table any more, the table is deleted too.

Text hidden in a lookup table

strings.inline ×3 strings.prune-decoders ×2

Inputstrings.js
const _0x4f1a = ['https://api.example.com/v2/session', 'Session expired', 'POST'];
function _0x2b8d(_0x1c3e) {
  return _0x4f1a[_0x1c3e - 0x0];
}
export function refresh(token) {
  if (!token) {
    throw new Error(_0x2b8d(0x1));
  }
  return fetch(_0x2b8d(0x0), {
    method: _0x2b8d(0x2),
    body: token
  });
}
Output
export function refresh(token) {
  if (!token) {
    throw new Error('Session expired');
  }
  return fetch('https://api.example.com/v2/session', {
    method: 'POST',
    body: token
  });
}

Detectedstring-array-wrapperstring-array

How it worksshow
Runs in
strings
On by preset
conservative: enabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
3 options
TuningStringDecodingOptions
  • tiersEvaluatorTier[]

    Which methods may be used to replay a decoder, in the order they are tried. No preset includes sandbox: it runs code from the file you pasted, so you have to ask for it.

    CONS
    ['native', 'interpreter']
    BAL
    ['native', 'interpreter']
    AGGR
    ['native', 'interpreter']
  • maxDecodeCallsnumber

    How many times the decoder may be called before the engine stops. Bounds a file that is broken or hostile rather than merely large.

    CONS
    200000
    BAL
    1000000
    AGGR
    no limit (Number.MAX_SAFE_INTEGER)
  • decodeDespiteStringCodeboolean

    Put the text back, and delete the table, even when the file builds code out of a string while it runs - eval, Function, a string handed to setTimeout. Code made that way can read the table, and can also change it, in which case the text written back is what the table held rather than what the program would have used. Off, those references are left encoded and the report names what stopped it.

    CONS
    false
    BAL
    false
    AGGR
    true
Passes gated
  • strings.discoverLocate string arrays and decoder functionsstrings
  • strings.inlineReplace decoder references with literalsstrings
  • strings.prune-decodersDelete decoder machinery once inlinedstrings
  • strings.merge-splitReassemble split string concatenationsstrings

Literal simplification

literalSimplification

Works out values that were never going to change.

Obfuscators write constants as work: 0x2 * 0x3 + 0x1 instead of 7, 'un' + 'weave' instead of 'jsrift', !![] instead of true. Anything whose result is fixed is calculated once, here, and replaced by the result. It happens in three places: before anything else, again on code that came out of a packed file after that first sweep had already finished, and again later, because decoding text keeps producing more of these.

Constants written as work

prepare.fold-numbers ×3 prepare.normalize-literals ×5 simplify.fold-constants ×4

Inputliterals.js
const retries = 0x2 * 0x3 + 0x1;
const label = 'un' + 'weave';
const enabled = !![];
const missing = void 0;
const mask = 0x1 << 0x4;
const name = 'hello';
Output
const retries = 7;
const label = 'jsrift';
const enabled = true;
const missing = undefined;
const mask = 16;
const name = 'hello';
How it worksshow
Runs in
preparestringssimplify
On by preset
conservative: enabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
Boolean only
Tuningnone

No options. This one is either on or off.

Passes gated
  • prepare.fold-numbersFold numeric constant arithmeticprepare
  • prepare.normalize-literalsNormalise escaped and numeric literalsprepare
  • strings.normalize-literalsRe-normalise literals revealed by unpackingstrings
  • simplify.fold-constantsFold constant expressionssimplify

Property normalization

propertyNormalization

Writes property access the way you would write it.

Obfuscated code reaches properties through strings: config['retry']['limit'] rather than config.retry.limit. Where the key is a plain name, it is rewritten with a dot. Keys that cannot be written that way - 'on-error', '2fa' - are left alone, and so are __proto__, constructor and prototype, where the bracket form can be doing real work.

Properties reached through strings

simplify.properties ×5

Inputprops.js
export function apply(config, target) {
  target['retryLimit'] = config['retry']['limit'];
  target['on-error'] = config['fallback'];
  return target['toString']();
}
Output
export function apply(config, target) {
  target.retryLimit = config.retry.limit;
  target['on-error'] = config.fallback;
  return target.toString();
}
How it worksshow
Runs in
simplify
On by preset
conservative: enabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
Boolean only
Tuningnone

No options. This one is either on or off.

Passes gated
  • simplify.propertiesRewrite computed string keys as dot accesssimplify

Code structure

Put statements back where the obfuscator moved them.

3

Control flow analysis

controlFlowAnalysis

Puts scrambled code back in the order it runs.

Some obfuscators chop a function into numbered chunks and wrap them in a loop with a switch, so the code jumps around instead of reading top to bottom. The real order is kept in a list nearby - '3|0|4|1|2' means chunk 3 runs first. This reads that list, puts the chunks back in order, and removes the loop.

A function chopped into numbered chunks

structure.control-flow ×1

Inputflow.js
function greet(user) {
  const order = '2|0|3|1'.split('|');
  let index = 0;
  while (true) {
    switch (order[index++]) {
      case '0':
        message += user.name;
        continue;
      case '1':
        return message;
      case '2':
        var message = 'Hello, ';
        continue;
      case '3':
        message += '!';
        continue;
    }
    break;
  }
}
Output
function greet(user) {
  var message = 'Hello, ';
  message += user.name;
  message += '!';
  return message;
}

Detectedcontrol-flow-flattening

How it worksshow
Runs in
structure
On by preset
conservative: disabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
2 options
TuningControlFlowOptions
  • maxStatesnumber

    Leave a scrambled function alone if it has more chunks than this.

    CONS
    256
    BAL
    2048
    AGGR
    16384
  • aggressiveDispatchersboolean

    Also rebuild functions whose running order is calculated at runtime rather than written down as text.

    CONS
    false
    BAL
    false
    AGGR
    true
Passes gated
  • structure.register-vmLinearise register-VM dispatchersstructure
  • structure.control-flowUnflatten dispatcher state machinesstructure

Statement recovery

statementRecovery

Turns packed one-liners back into ordinary statements.

Obfuscators squeeze several statements into one expression: a && b() in place of an if, commas in place of separate lines, a ? b() : c() in place of if/else. Where the value of that expression is thrown away anyway, the ordinary form means exactly the same thing, so it is written back that way. Loop bodies that were left without braces get them.

Several statements squeezed into one

simplify.sequences ×1 structure.conditionals ×3

Inputstatements.js
function submit(form, session) {
  return session.valid && form.dirty && send(form), form.reset(), true;
}
function sync(state, queue) {
  if (state.online) {
    if (queue.length) {
      flush(queue);
    }
  }
  state.dirty ? persist(state) : discard(state);
}
Output
function submit(form, session) {
  if (session.valid && form.dirty) {
    send(form);
  }
  form.reset();
  return true;
}
function sync(state, queue) {
  if (state.online && queue.length) {
    flush(queue);
  }
  if (state.dirty) {
    persist(state);
  } else {
    discard(state);
  }
}
How it worksshow
Runs in
simplifystructurefinalize
On by preset
conservative: disabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
Boolean only
Tuningnone

No options. This one is either on or off.

Passes gated
  • simplify.sequencesExpand comma sequences into statementssimplify
  • structure.conditionalsRestore if/else from expression formsstructure
  • structure.loopsRestore idiomatic loop formsstructure
  • finalize.tidyFinal readability tidy-upfinalize

Function unwrapping

functionUnwrapping

Replaces go-between functions with what they do.

Obfuscators route ordinary work through extra functions and lookup objects, so base * tax arrives as _0x3a2f['nPzL'](base, tax). Where a wrapper does nothing but hand its arguments to a single expression, that expression is written at the place it was called and the wrapper is deleted. A wrapper is only expanded when the arguments line up and moving them cannot change the order things happen in.

Arithmetic routed through a lookup object

simplify.object-maps ×3 simplify.proxy-functions ×2

Inputproxy.js
const _0x3a2f = {
  'qWxT': function (a, b) {
    return a + b;
  },
  'nPzL': function (a, b) {
    return a * b;
  }
};
function _0x1d9c(a, b) {
  return a - b;
}
export function price(base, tax, discount) {
  return _0x3a2f['qWxT'](_0x3a2f['nPzL'](base, tax), _0x1d9c(0, discount));
}
Output
export function price(base, tax, discount) {
  return base * tax + (0 - discount);
}

Detectedobject-key-mapproxy-functions

How it worksshow
Runs in
simplify
On by preset
conservative: disabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
2 options
TuningFunctionUnwrappingOptions
  • maxInlineSizenumber

    How large a function body may be and still be worth writing out at each call. Measured in pieces of parsed code, not characters.

    CONS
    12
    BAL
    48
    AGGR
    160
  • inlineSingleUseboolean

    Write out a function that is called exactly once, however large it is.

    CONS
    false
    BAL
    true
    AGGR
    true
Passes gated
  • simplify.object-mapsInline object property-alias mapssimplify
  • simplify.proxy-functionsInline proxy and wrapper functionssimplify
  • simplify.discard-wrappersUnwrap argument-discarding wrapperssimplify

Cleanup

Delete code the program does not need in order to run.

2

Dead code removal

deadCodeRemoval

Removes code that never runs.

Obfuscators pad files with branches that can never be true - if ('LWmvB' === 'pxKtR') - and with variables nothing ever reads. This works out which branches can actually happen, keeps those, and deletes the rest along with anything left over that nothing points at.

A branch that can never run, and a variable nothing reads

clean.dead-branches ×1 clean.unused ×1

Inputdead.js
export function total(items) {
  const scratch = 'never read';
  if ('LWmvB' === 'pxKtR') {
    console.log('injected filler');
  }
  let sum = 0;
  for (const item of items) {
    sum += item.price;
  }
  return sum;
}
Output
export function total(items) {
  let sum = 0;
  for (const item of items) {
    sum += item.price;
  }
  return sum;
}

Detecteddead-code-injection

How it worksshow
Runs in
clean
On by preset
conservative: disabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
4 options
TuningDeadCodeOptions
  • removeConstantBranchesboolean

    Drop the side of an `if` that can never be reached.

    CONS
    false
    BAL
    true
    AGGR
    true
  • removeUnusedBindingsboolean

    Remove variables that are declared and never read.

    CONS
    false
    BAL
    true
    AGGR
    true
  • removeUnusedFunctionsboolean

    Remove functions that nothing ever calls.

    CONS
    false
    BAL
    true
    AGGR
    true
  • keepCommentedboolean

    Never delete a statement that has a comment above it. Aggressive turns this guard off, so commented-out code can go.

    CONS
    true
    BAL
    true
    AGGR
    false
Passes gated
  • clean.dead-branchesRemove constant and unreachable branchesclean
  • clean.injected-codeRemove obfuscator dead-code injectionclean
  • clean.unusedRemove unused bindings and functionsclean

Anti-tamper removal

antiTamperRemoval

Removes the traps that fight being inspected.

Protected files carry code whose only job is to stop you reading them: loops that trip the debugger over and over, checks that hang the program once the source has been reformatted, and code that quietly switches console.log off. This recognises those three shapes and removes each one whole - the trap, the guard around it and the line that starts it. If anything else in the file still uses a piece of that machinery, it is left in place instead.

A debugger trap that restarts itself

clean.anti-tamper ×2

Inputtamper.js
function _0x5c31(_0x1a2b) {
  function _0x2a7f(_0x3c4d) {
    if (typeof _0x3c4d === 'string') {
      return function (_0x4e) {}.constructor('while (true) {}').apply('counter');
    } else {
      if (('' + _0x3c4d / _0x3c4d).length !== 1 || _0x3c4d % 20 === 0) {
        (function () {
          return true;
        }).constructor('debugger').call('action');
      } else {
        (function () {
          return false;
        }).constructor('debugger').apply('stateObject');
      }
    }
    _0x2a7f(++_0x3c4d);
  }
  try {
    if (_0x1a2b) {
      return _0x2a7f;
    } else {
      _0x2a7f(0);
    }
  } catch (_0x5f) {}
}
setInterval(function () {
  _0x5c31();
}, 4000);
export function checkout(cart) {
  return cart.total;
}
Output
setInterval(function () {}, 4000);
export function checkout(cart) {
  return cart.total;
}

Detecteddebug-protection

console methods replaced with empty ones

clean.anti-tamper ×7

Inputconsole.js
const _0x2c19 = function () {
  console.log = function () {};
  console.warn = function () {};
  console.info = function () {};
  console.error = function () {};
  console.exception = function () {};
  console.table = function () {};
  console.trace = function () {};
};
_0x2c19();
export function checkout(cart) {
  return cart.total;
}
Output
const _0x2c19 = function () {};
_0x2c19();
export function checkout(cart) {
  return cart.total;
}

Detectedconsole-disable

How it worksshow
Runs in
clean
On by preset
conservative: disabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
Boolean only
Tuningnone

No options. This one is either on or off.

Passes gated
  • clean.anti-tamperRemove debugger traps and self-defendingclean

Naming & modules

Work on names and file shape, once nothing else is changing.

3

Variable renaming

variableRenaming

Names variables after what they are used for.

Obfuscated code calls everything _0x4d28ce. This looks at what each variable is actually used for - what it is passed to and in which position, what is read off it, what is assigned to it - and takes a name from that: the value handed to fetch first becomes url. A guess that is not strong enough is left alone, and a new name is only used where it cannot collide with another variable of the same name.

Hex names, with enough use to guess from

rename.identifiers ×5

Inputrename.js
function _0x4d28ce(_0x1f3a, _0x2c9b) {
  fetch(_0x1f3a, {
    method: 'POST',
    body: _0x2c9b
  }).then(function (_0x5b02) {
    return _0x5b02.json();
  }).catch(function (_0x4a7d) {
    console.error(_0x4a7d.message, _0x4a7d.stack);
  });
  const _0x6c88 = setTimeout(function () {}, 5000);
  return _0x6c88;
}
Output
function _0x4d28ce(url, body) {
  fetch(url, {
    method: 'POST',
    body: body
  }).then(function (result) {
    return result.json();
  }).catch(function (error) {
    console.error(error.message, error.stack);
  });
  const timeoutId = setTimeout(function () {}, 5000);
  return timeoutId;
}

Detectedhex-identifiers

How it worksshow
Runs in
rename
On by preset
conservative: disabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
5 options
TuningVariableRenamingOptions
  • minConfidencenumber (0..1)

    How sure the engine has to be about a guessed name before it uses it.

    CONS
    0.9
    BAL
    0.55
    AGGR
    0.3
  • renameParametersboolean

    Also rename the arguments a function takes, not only the variables inside it.

    CONS
    false
    BAL
    true
    AGGR
    true
  • renameReadableboolean

    Rename variables that already have a readable name.

    CONS
    false
    BAL
    false
    AGGR
    true
  • renamePropertiesboolean

    Rename the keys on an object. Off in every preset: it breaks the moment a key is reached through a string the engine cannot see.

    CONS
    false
    BAL
    false
    AGGR
    false
  • hintsRecord<string, string>

    Your own pattern-to-name mappings, applied over the guessed ones.

    CONS
    {}
    BAL
    {}
    AGGR
    {}
Passes gated
  • rename.identifiersAssign inferred, scope-safe namesrename

Module unwrapping

moduleUnwrapping

Opens packed files and bundles into readable code.

Some files are not really source. They are one eval call with the whole program compressed inside, one Function("...") call holding the program as text, or a bundler's map of numbered modules. The first two are opened without being run, again and again while there are layers left; the third is split so each module becomes a normal named function you can read.

A whole file packed into one eval call

unpack.eval-packer ×1

Inputpacked.js
eval(function (p, a, c, k, e, d) {
  e = function (c) {
    return c;
  };
  if (!''.replace(/^/, String)) {
    while (c--) {
      d[c] = k[c] || c;
    }
    k = [function (e) {
      return d[e];
    }];
    e = function () {
      return '\w+';
    };
    c = 1;
  }
  while (c--) {
    if (k[c]) {
      p = p.replace(new RegExp('\b' + e(c) + '\b', 'g'), k[c]);
    }
  }
  return p;
}('4 3(1,2){0 1+2}', 5, 5, 'return|left|right|add|function'.split('|'), 0, {}));
Output
function add(left, right) {
  return left + right;
}

Detectedeval-packer

A webpack map of numbered modules

unpack.webpack-modules ×2

Inputchunk.js
(self.webpackChunk = self.webpackChunk || []).push([[471], {
  8210: function (module, exports, __webpack_require__) {
    exports.formatPrice = function (cents) {
      return '$' + (cents / 100).toFixed(2);
    };
  },
  4477: function (module, exports, __webpack_require__) {
    const price = __webpack_require__(8210);
    exports.render = function (cart) {
      return price.formatPrice(cart.total);
    };
  }
}]);
Output
// webpack module "8210"
// ==========================================================
function module_8210(module, exports, __webpack_require__) {
  exports.formatPrice = function (cents) {
    return '$' + (cents / 100).toFixed(2);
  };
}
// webpack module "4477"
// ==========================================================
function module_4477(module, exports, __webpack_require__) {
  const price = __webpack_require__(8210);
  exports.render = function (cart) {
    return price.formatPrice(cart.total);
  };
}
(self.webpackChunk = self.webpackChunk || []).push([[471], {
  8210: module_8210,
  4477: module_4477
}]);

Detectedwebpack-bundle

How it worksshow
Runs in
unpack
On by preset
conservative: disabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
Boolean only
Tuningnone

No options. This one is either on or off.

Passes gated
  • unpack.function-constructorRewrite Function-constructor wrappersunpack
  • unpack.eval-packerUnwrap eval-based packersunpack
  • unpack.webpack-modulesSplit webpack chunk maps into named modulesunpack

JSX restoration

jsxRestoration

Turns compiled React calls back into JSX.

A build tool turns <td>{label}</td> into _jsx('td', { children: label }). This turns those calls back into tags - but only where the two are exactly the same thing. _jsx(foo, {}) is not <foo />, because the tag would ask React for an HTML element called foo instead of using your variable, so a call it cannot prove is left as a call.

React tags compiled down to function calls

finalize.jsx ×3

Inputview.jsx
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
export function StatusRow(props) {
  return _jsxs('tr', {
    className: 'row',
    children: [_jsx('td', {
      children: props.label
    }), _jsx('td', {
      className: 'num',
      children: props.value
    })]
  });
}
Output
import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime';
export function StatusRow(props) {
  return <tr className="row"><td>{props.label}</td><td className="num">{props.value}</td></tr>;
}

Detectedjsx-runtime

How it worksshow
Runs in
finalize
On by preset
conservative: disabledCONSbalanced: enabledBALaggressive: enabledAGGR
Passes gated
Tuning
Boolean only
Tuningnone

No options. This one is either on or off.

Passes gated
  • finalize.jsxRestore JSX from jsx-runtime callsfinalize

Every pair above was produced by running the engine over the code on the left with the balanced preset and every other technique switched off, so what changed is this technique's doing and nothing else's.