j4k0xb / webcrack

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

Other bundlers support #117

Open ivan-collab-git opened 1 month ago

ivan-collab-git commented 1 month ago

I want to contribute with support for other bundlers. Right now i am working at a website which uses metro bundler. Which files in the project should i take into consideration for doing this? and would you consider this as something feasible to implement?

j4k0xb commented 1 month ago

Hey, take a look at these files for reference:

and would you consider this as something feasible to implement?

Yeah the bundle format looks pretty straight-forward.

I suggest you look through https://github.com/j4k0xb/webcrack/blob/master/CONTRIBUTING.md first and try debugging to get a better idea on how the existing unpacker works (only reading the code may be a bit confusing).

Then start by creating the boilerplate files and use a { VariableDeclaration(path) { visitor in src/unpack/metro/index.ts to check if var __BUNDLE_START_TIME__ is present. Highly recommend using @codemod/matchers to help with finding AST nodes.

The next easiest thing would be extracting the entry module id from __r(0); (could use path.getAllNextSiblings()).

Example bundle:

var __BUNDLE_START_TIME__ = this.nativePerformanceNow ? nativePerformanceNow() : Date.now();
var __DEV__ = false;
var process = this.process || {};
var __METRO_GLOBAL_PREFIX__ = "";
process.env = process.env || {};
process.env.NODE_ENV = process.env.NODE_ENV || "production";
(function (global) {
  // ...
})(typeof globalThis !== "undefined" ? globalThis : typeof global !== "undefined" ? global : typeof window !== "undefined" ? window : this);
__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  const lib = _$$_REQUIRE(_dependencyMap[0]);
  console.log(lib.foo);
}, 0, [1]);
__d(function (global, _$$_REQUIRE, _$$_IMPORT_DEFAULT, _$$_IMPORT_ALL, module, exports, _dependencyMap) {
  "use strict";

  exports.foo = "bar";
}, 1, []);
__r(0);
ivan-collab-git commented 1 month ago

great. ill give it a check!

ivan-collab-git commented 1 month ago

Hey what's up. I did this notes the other day, and i was expecting to work on the implementation, but i have had little to no time, so in the mean time ill share this with you, so you can give me a sanity check. I tried to map the concepts from a post i red about the webpack require function, and i think it is pretty similar. Basically the same.

MetroRequire.<hash>.js

The runtime is usually called MetroRequire.<hash>.js and it defines these variables:

var __BUNDLE_START_TIME__ = this.nativePerformanceNow
    ? nativePerformanceNow()
    : Date.now(),
  __DEV__ = false,
  process = this.process || {};
process.env = process.env || {};
process.env.NODE_ENV = process.env.NODE_ENV || "production";

asyncRequire.\<hash\>.js

So how all of this holds up together?

Translations

ivan-collab-git commented 1 month ago

this is the metroRequire file. The one that contains the runtime. I tallked about other files at the end of the notes, but this is the main one.

var __BUNDLE_START_TIME__ = this.nativePerformanceNow
    ? nativePerformanceNow()
    : Date.now(),
  __DEV__ = false,
  process = this.process || {};
process.env = process.env || {};
process.env.NODE_ENV = process.env.NODE_ENV || "production";
!(function (e) {
  "use strict";
  (e.__r = i),
    (e.__d = function (e, n, o) {
      if (null != t[n]) return;
      const i = {
        dependencyMap: o,
        factory: e,
        hasError: !1,
        importedAll: r,
        importedDefault: r,
        isInitialized: !1,
        publicModule: { exports: {} },
      };
      t[n] = i;
    }),
    (e.__c = o),
    (e.__registerSegment = function (e, r, n) {
      (p[e] = r),
        n &&
          n.forEach((r) => {
            t[r] || h.has(r) || h.set(r, e);
          });
    });
  var t = o();
  const r = {},
    { hasOwnProperty: n } = {};
  function o() {
    return (t = Object.create(null));
  }
  function i(e) {
    const r = e,
      n = t[r];
    return n && n.isInitialized ? n.publicModule.exports : d(r, n);
  }
  function l(e) {
    const n = e;
    if (t[n] && t[n].importedDefault !== r) return t[n].importedDefault;
    const o = i(n),
      l = o && o.__esModule ? o.default : o;
    return (t[n].importedDefault = l);
  }
  function u(e) {
    const o = e;
    if (t[o] && t[o].importedAll !== r) return t[o].importedAll;
    const l = i(o);
    let u;
    if (l && l.__esModule) u = l;
    else {
      if (((u = {}), l)) for (const e in l) n.call(l, e) && (u[e] = l[e]);
      u.default = l;
    }
    return (t[o].importedAll = u);
  }
  (i.importDefault = l), (i.importAll = u);
  let c = !1;
  function d(t, r) {
    if (!c && e.ErrorUtils) {
      let n;
      c = !0;
      try {
        n = m(t, r);
      } catch (t) {
        e.ErrorUtils.reportFatalError(t);
      }
      return (c = !1), n;
    }
    return m(t, r);
  }
  const s = 16,
    a = 65535;
  function f(e) {
    return { segmentId: e >>> s, localId: e & a };
  }
  (i.unpackModuleId = f),
    (i.packModuleId = function (e) {
      return (e.segmentId << s) + e.localId;
    });
  const p = [],
    h = new Map();
  function m(r, n) {
    if (!n && p.length > 0) {
      const e = h.get(r) ?? 0,
        o = p[e];
      null != o && (o(r), (n = t[r]), h.delete(r));
    }
    const o = e.nativeRequire;
    if (!n && o) {
      const { segmentId: e, localId: i } = f(r);
      o(i, e), (n = t[r]);
    }
    if (!n) throw g(r);
    if (n.hasError) throw w(r, n.error);
    n.isInitialized = !0;
    const { factory: c, dependencyMap: d } = n;
    try {
      const t = n.publicModule;
      return (
        (t.id = r),
        c(e, i, l, u, t, t.exports, d),
        (n.factory = void 0),
        (n.dependencyMap = void 0),
        t.exports
      );
    } catch (e) {
      throw (
        ((n.hasError = !0),
        (n.error = e),
        (n.isInitialized = !1),
        (n.publicModule.exports = void 0),
        e)
      );
    }
  }
  function g(e) {
    return Error('Requiring unknown module "' + e + '".');
  }
  function w(e, t) {
    return Error(
      'Requiring module "' + e + '", which threw an exception: ' + t,
    );
  }
})(
  "undefined" != typeof globalThis
    ? globalThis
    : "undefined" != typeof global
      ? global
      : "undefined" != typeof window
        ? window
        : this,
);
j4k0xb commented 1 month ago

seems right so far for analyzing how the runtime works, its easier to create your own test bundles without minifying or to search in the source code of metro. also helpful to compare it with other bundles:

supporting multi-file bundles is not gonna be easy but can be added later, so for now it would be enough to focus on the __d(...) calls of a single script.

example:

__d(
    function (g, r, i, a, m, e, d) {
        "use strict";
        const t = r(d[0]).default || r(d[0]);
        let c;
        m.exports = () => c || ((c = t("locale")), c || "en");
    },
    "44cd5c",
    ["b2dff4"],
);

to

"use strict";
const t = require("./b2dff4.js").default || require("./b2dff4.js");
let c;
module.exports = () => c || ((c = t("locale")), c || "en");

the third parameter is a function that receives a module id, and returns its property importedDefault, which for what ive seen is a global variable, initialized an empty object {}, and for what iv've seen, it is not modified in this file. The fourth parameter, is similar to the third, a function that receives a module id and returns a property importedAll, which for every module, it is initially the same object as importedDefault.

they are used for import v from 'foo' and import * as w from 'bar'; in ESM

Then there are some bundles that call the __r function directly, this i think are the entrypoints

yes. apparently there can even be multiple top-level __r calls: https://github.com/getsentry/sentry-cli/blob/844cee0d263204b0b2fb75688b58fd83f13b15b9/tests/integration/_fixtures/file-ram-bundle/index.android.bundle