j4k0xb / webcrack

Deobfuscate obfuscator.io, unminify and unpack bundled javascript
https://webcrack.netlify.app
MIT License
663 stars 72 forks source link

Array unpooler #20

Closed Memexurer closed 9 months ago

Memexurer commented 9 months ago

I need a transformer which would be able to simplify functions like this: (varargs are empty btw)

function W8xps6(...ZQUk8w) {
  !((ZQUk8w.length = 0),
  (ZQUk8w[223] = 25),
  (ZQUk8w[ZQUk8w[ZQUk8w[ZQUk8w[223] + 198] - (ZQUk8w[223] - 223)] - 25] =
    xgCiq2(
      ZQUk8w[ZQUk8w[ZQUk8w[ZQUk8w[223] + 198] + 198] - (ZQUk8w[223] - 223)] -
        (ZQUk8w[
          ZQUk8w[223] - (ZQUk8w[223] - (ZQUk8w[223] - (ZQUk8w[223] - 223)))
        ] -
          (ZQUk8w[ZQUk8w[ZQUk8w[223] - (ZQUk8w[223] - 223)] + 198] + 7))
    )),
  (ZQUk8w[ZQUk8w[223] + 222] = ZQUk8w[ZQUk8w[223] - 23]),
  (ZQUk8w[ZQUk8w[223] - 24] = jZoT758(
    ZQUk8w[223] + (ZQUk8w[ZQUk8w[223] - (ZQUk8w[223] - 223)] - 19)
  )),
  (ZQUk8w[ZQUk8w[223] + 197] = ZQUk8w[223] + 75),
  (ZQUk8w[ZQUk8w[ZQUk8w[222] + 122] + 147] = jZoT758(30)),
  (ZQUk8w[ZQUk8w[223] + 198] = ZQUk8w[223] - (ZQUk8w[223] + 116)),
  (ZQUk8w[ZQUk8w[222] - (ZQUk8w[222] - 3)] = yvO5GCA(ZQUk8w[223] + 145)),
  (ZQUk8w[ZQUk8w[222] - 96] = jZoT758(27)),
  (ZQUk8w[ZQUk8w[ZQUk8w[223] + 339] + 121] = jZoT758(ZQUk8w[222] - 76)),
  (ZQUk8w[ZQUk8w[222] + 72] = ZQUk8w[5]),
  (ZQUk8w[ZQUk8w[ZQUk8w[223] + 339] - (ZQUk8w[222] - 222)] = jZoT758(23)),
  (ZQUk8w[ZQUk8w[222] - (ZQUk8w[223] + 209)] = yvO5GCA(22)),
  (ZQUk8w[ZQUk8w[222] - 92] = nPVOJ4v(
    VR7dvi +
      ZQUk8w[
        ZQUk8w[ZQUk8w[223] - (ZQUk8w[223] - 222)] -
          (ZQUk8w[223] - (ZQUk8w[ZQUk8w[223] + 339] - 93))
      ] +
      ZQUk8w[
        ZQUk8w[ZQUk8w[222] + 122] -
          (ZQUk8w[222] -
            (ZQUk8w[222] - (ZQUk8w[222] - (ZQUk8w[222] - (ZQUk8w[222] - 6)))))
      ] +
      ZQUk8w[ZQUk8w[223] - (ZQUk8w[ZQUk8w[222] - (ZQUk8w[223] - 6)] - 388)] +
      DIdw2v5 +
      '5X',
    UhTH66F +
      ZQUk8w[ZQUk8w[ZQUk8w[222] + 122] - 96] +
      pcaDZd +
      ZQUk8w[
        ZQUk8w[ZQUk8w[222] + 123] +
          (ZQUk8w[ZQUk8w[222] - (ZQUk8w[223] - 7)] - (ZQUk8w[223] - 119))
      ] +
      ZQUk8w[ZQUk8w[222] + (ZQUk8w[222] + 47)],
    ZQUk8w[1],
    ZQUk8w[ZQUk8w[ZQUk8w[ZQUk8w[222] + 122] + 123] + 116] + fWAd1b + Ke3skc
  )),
  j7ibOsb(ZQUk8w[ZQUk8w[223] + 124]))
}
j4k0xb commented 9 months ago

Seems to be specific to another obfuscator and there isn't yet a way to customize transforms when importing webcrack, but you're free to clone the repo and add it somewhere

Theoretically you could do some static analysis to keep track of the array contents at every line and inline accesses (ZQUk8w[223] + 198 -> 25 + 198 -> 223), but that's cumbersome and gets even more complicated because of the global variables And its basically the same as executing the js, so why not just cheat and execute it in a sandbox :) very similar to how https://github.com/j4k0xb/webcrack/blob/7bae73b10f6046032c8e1d9081375fe6ad8e1d1c/src/deobfuscator/inlineDecodedStrings.ts, https://github.com/j4k0xb/webcrack/blob/7bae73b10f6046032c8e1d9081375fe6ad8e1d1c/src/deobfuscator/vm.ts works

detecting this type of function:

const param = m.capture(m.identifier());
const clearArray = m.expressionStatement(
  m.assignmentExpression(
    '=',
    m.memberExpression(m.fromCapture(param), m.identifier('length')),
    m.numericLiteral(0)
  )
);
const assignment = m.expressionStatement(
  m.assignmentExpression(
    '=',
    m.memberExpression(m.fromCapture(param), m.anything(), true)
  )
);
const lastExpression = m.capture(m.anyExpression());
const lastCallName = m.capture(m.identifier());
const matcher = m.functionDeclaration(
  m.anything(),
  [m.restElement(param)],
  m.blockStatement(
    m.anyList(
      clearArray, // param.length = 0;
      m.oneOrMore(assignment), // param[223] = 25; or param[param[223] + 222] = fn(anything);
      m.expressionStatement(m.callExpression(lastCallName, [lastExpression])) // otherFn(param[param[223] + 124]);
    )
  )
);
traverse(ast, {
  FunctionDeclaration(path) {
    if (!matcher.match(path.node)) return;
    // ...
  }
});

First you have to get the code of global variables/functions like xgCiq2 or pcaDZd so they can be executed later:

let code = '';

for (const statement of path.get('body').get('body')) {
  if (assignment.match(statement.node)) {
    statement.traverse({
      ReferencedIdentifier(path) {
        if (path.node.name === param.current!.name) return;
        const binding = path.scope.getBinding(path.node.name)!;
        code += binding.path.getStatementParent()!.toString();
      },
    });
  }
}

I assume this call is the last statement which has some side effect, to get the argument it is replaced with:

- j7ibOsb(ZQUk8w[ZQUk8w[223] + 124]);
+ return ZQUk8w[ZQUk8w[223] + 124];
}
const fnClone = t.cloneNode(path.node, true);
fnClone.body.body.pop();
fnClone.body.body.push(t.returnStatement(lastExpression.current));
code += `(${generate(fnClone).code})()`;

So now the code looks like this and evaluates to the last returned expression:

function xgCiq2(x) { ... }
const pcaDZd = 3;
(function W8xps6(...ZQUk8w) {
  ZQUk8w.length = 0;
  ...
  return ZQUk8w[ZQUk8w[223] + 124];
})()

Finally execute the code and replace the whole original function body with:

j7ibOsb(1337 /* result */);
const result = t.valueToNode(sandbox(code));

path
  .get('body')
  .replaceWith(
    t.blockStatement([
      t.expressionStatement(
        t.callExpression(lastCallName.current, [result])
      ),
    ])
  );
Memexurer commented 9 months ago

nvm, my brain is too flat for coding in typescript