mirror of
https://github.com/danbulant/pkg-unpacker
synced 2026-05-19 03:58:35 +00:00
initial commit
This commit is contained in:
commit
5c818d3899
6 changed files with 448 additions and 0 deletions
3
.github/FUNDING.yml
vendored
Normal file
3
.github/FUNDING.yml
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
ko_fi: lockblock
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
package-lock.json
|
||||
unpacked/
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 LockBlock-dev
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
89
README.md
Normal file
89
README.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# pkg unpacker
|
||||
|
||||
Unpack any [pkg](https://github.com/vercel/pkg) application.
|
||||
|
||||
Keep in mind that <span style="color:red">this doesn't give you the full source code if the application was compiled into V8 bytecode</span>. See [How it works](#how-it-works).
|
||||
|
||||
This should work with any pkg application, but errors may occur.
|
||||
|
||||
This app may broke at any pkg major update.
|
||||
|
||||
## Installation
|
||||
|
||||
- Install [NodeJS](https://nodejs.org).
|
||||
- Download or clone the project.
|
||||
- Go to the `pkg-unpacker` folder and run `npm install`.
|
||||
|
||||
## Usage
|
||||
|
||||
```console
|
||||
node unpack.js [options]
|
||||
|
||||
Options:
|
||||
|
||||
-i input file name / path to the input file
|
||||
-o output file name / path to the output file
|
||||
--run try to run the entrypoint of the app
|
||||
|
||||
Examples:
|
||||
|
||||
– Unpack an UNIX app
|
||||
$ node unpack.js -i ./pkg_app -o ./unpacked
|
||||
– Unpack a Windows app
|
||||
$ node unpack.js -i ./pkg_app.exe -o ./unpacked
|
||||
– Unpack an UNIX app and run it
|
||||
$ node unpack.js -i ./pkg_app -o ./unpacked --run
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- Compression detection (Gzip, Brotli)
|
||||
- Code evaluation
|
||||
- Symlink handling
|
||||
- Unpack binaries of all operating systems
|
||||
|
||||
## How it works
|
||||
|
||||
This application **DOES NOT** decompile any code. By default [pkg](https://github.com/vercel/pkg) compiles code to V8 bytecode. Extracted files will remain in this format except for assets.
|
||||
|
||||
Code evaluation works best with small applications. Requirements can be broken.
|
||||
|
||||
[pkg](https://github.com/vercel/pkg) writes the file name, path, offset, length and compression at the bottom of each binary. This application analyzes these fields, then extracts and decompresses (if compressed) all the files of the binary.
|
||||
|
||||
Examples:
|
||||
|
||||
```js
|
||||
//UNIX app
|
||||
|
||||
{"/snapshot/pkg/index.js":{"0":[0,568],"3":[568,118]},"/snapshot/pkg":{"2":[686,12],"3":[698,117]},"/snapshot":{"2":[815,7],"3":[822,117]}} //virtual file system
|
||||
,
|
||||
"/snapshot/pkg/index.js" //entrypoint
|
||||
,
|
||||
{} //symlinks
|
||||
,
|
||||
{} //files dictionnary
|
||||
,
|
||||
0 //0: no compression, 1: Gzip, 2: Brotli
|
||||
```
|
||||
|
||||
```js
|
||||
//Windows app
|
||||
|
||||
{"C:\\snapshot\\pkg\\index.js":{"0":[0,568],"3":[568,118]},"C:\\snapshot\\pkg":{"2":[686,12],"3":[698,117]},"C:\\snapshot":{"2":[815,7],"3":[822,117]}} //virtual file system
|
||||
,
|
||||
"C:\\snapshot\\pkg\\index.js" //entrypoint
|
||||
,
|
||||
{} //symlinks
|
||||
,
|
||||
{} //files dictionnary
|
||||
,
|
||||
0 //0: no compression, 1: Gzip, 2: Brotli
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
[pkg](https://github.com/vercel/pkg)
|
||||
|
||||
## Copyright
|
||||
|
||||
See the [license](/LICENSE).
|
||||
19
package.json
Normal file
19
package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "pkg-unpacker",
|
||||
"version": "1.0.0",
|
||||
"description": "Unpack any pkg application",
|
||||
"main": "unpack.js",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/LockBlock-dev/pkg-unpacker.git"
|
||||
},
|
||||
"author": "LockBlock-dev",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/LockBlock-dev/pkg-unpacker/issues"
|
||||
},
|
||||
"homepage": "https://github.com/LockBlock-dev/pkg-unpacker#readme"
|
||||
}
|
||||
313
unpack.js
Normal file
313
unpack.js
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { Script } = require("vm");
|
||||
const { gunzipSync, brotliDecompressSync } = require("zlib");
|
||||
|
||||
const READ_ERR = "This might happen if your binary is not readable or if pkg changed their code.";
|
||||
|
||||
const initProps = (props) => {
|
||||
let propObj = {
|
||||
vfs: {},
|
||||
entryPoint: "",
|
||||
symlinks: {},
|
||||
filesDict: {},
|
||||
doCompress: 0,
|
||||
};
|
||||
|
||||
for (let idx = 0; idx < props.length; idx++) {
|
||||
let prop = "";
|
||||
|
||||
try {
|
||||
switch (idx) {
|
||||
case 0:
|
||||
prop = "vfs";
|
||||
propObj.vfs = JSON.parse(props[idx]);
|
||||
break;
|
||||
case 1:
|
||||
prop = "entryPoint";
|
||||
propObj.entryPoint = props[idx].replaceAll(`"`, "");
|
||||
break;
|
||||
case 2:
|
||||
prop = "symlinks";
|
||||
propObj.symlinks = JSON.parse(props[idx]);
|
||||
break;
|
||||
case 3:
|
||||
prop = "filesDict";
|
||||
propObj.filesDict = JSON.parse(props[idx]);
|
||||
break;
|
||||
case 4:
|
||||
prop = "doCompress";
|
||||
propObj.doCompress = Number(props[idx]);
|
||||
break;
|
||||
}
|
||||
} catch {
|
||||
throw new Error(
|
||||
`Error while parsing the binary props! ${READ_ERR}\nParsing ${prop} at index ${idx}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return propObj;
|
||||
};
|
||||
|
||||
const argv = require("minimist")(process.argv.slice(2));
|
||||
|
||||
if (!argv.i || !argv.o)
|
||||
throw new Error("You need to provide an input file and an output directory!");
|
||||
|
||||
const binary = fs.readFileSync(argv.i, { encoding: "utf-8" });
|
||||
let rawProps = binary.match(/\{.*\}\n,\n".*"\n,\n\{.*\}\n,\n\{.*\}\n,\n(0|1|2)/g);
|
||||
|
||||
try {
|
||||
rawProps = rawProps[0].split("\n,\n");
|
||||
} catch {
|
||||
throw new Error(`Error while reading the binary props! ${READ_ERR}`);
|
||||
}
|
||||
|
||||
const props = initProps(rawProps);
|
||||
|
||||
const GZIP = 1;
|
||||
const BROTLI = 2;
|
||||
const DOCOMPRESS = props.doCompress;
|
||||
const DICT = props.filesDict;
|
||||
const VIRTUAL_FILESYSTEM = props.vfs;
|
||||
const SYMLINKS = props.symlinks;
|
||||
|
||||
// /////////////////////////////////////////////////////////////////
|
||||
// PKG CODE https://github.com/vercel/pkg //////////////////////////
|
||||
// /////////////////////////////////////////////////////////////////
|
||||
|
||||
const win32 = process.platform === "win32";
|
||||
const hasURL = typeof URL !== "undefined";
|
||||
const symlinksEntries = Object.entries(SYMLINKS);
|
||||
const separator = "/";
|
||||
// separator for substitution depends on platform;
|
||||
const sepsep = DOCOMPRESS ? separator : path.sep;
|
||||
const dictRev = {};
|
||||
let maxKey = Object.values(DICT).length;
|
||||
|
||||
Object.entries(DICT).forEach(([k, v]) => {
|
||||
dictRev[v] = k;
|
||||
});
|
||||
|
||||
const uppercaseDriveLetter = (f) => {
|
||||
if (f.slice(1, 3) !== ":\\") return f;
|
||||
return f[0].toUpperCase() + f.slice(1);
|
||||
};
|
||||
|
||||
const removeTrailingSlashes = (f) => {
|
||||
if (f === "/") {
|
||||
return f; // dont remove from "/"
|
||||
}
|
||||
|
||||
if (f.slice(1) === ":\\") {
|
||||
return f; // dont remove from "D:\"
|
||||
}
|
||||
|
||||
let last = f.length - 1;
|
||||
|
||||
while (true) {
|
||||
const char = f.charAt(last);
|
||||
|
||||
if (char === "\\") {
|
||||
f = f.slice(0, -1);
|
||||
last -= 1;
|
||||
} else if (char === "/") {
|
||||
f = f.slice(0, -1);
|
||||
last -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return f;
|
||||
};
|
||||
|
||||
const isUrl = (p) => hasURL && p instanceof URL;
|
||||
|
||||
const pathToString = (p, win) => {
|
||||
let result;
|
||||
if (Buffer.isBuffer(p)) {
|
||||
result = p.toString();
|
||||
} else if (isUrl(p)) {
|
||||
result = win ? p.pathname.replace(/^\//, "") : p.pathname;
|
||||
} else {
|
||||
result = p;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const normalizePath = (f) => {
|
||||
let file = pathToString(f, win32);
|
||||
|
||||
if (!/^.:$/.test(file)) {
|
||||
file = path.normalize(file);
|
||||
} // 'c:' -> 'c:.'
|
||||
|
||||
if (win32) {
|
||||
file = uppercaseDriveLetter(file);
|
||||
}
|
||||
|
||||
return removeTrailingSlashes(file);
|
||||
};
|
||||
|
||||
const replace = (k) => {
|
||||
let v = DICT[k];
|
||||
// we have found a part of a missing file => let record for latter use
|
||||
if (v === undefined) {
|
||||
maxKey += 1;
|
||||
v = maxKey.toString(36);
|
||||
DICT[k] = v;
|
||||
dictRev[v] = k;
|
||||
}
|
||||
return v;
|
||||
};
|
||||
|
||||
const findVirtualFileSystemKey = (path_, slash) => {
|
||||
const normalizedPath = normalizePath(path_);
|
||||
if (!DOCOMPRESS) {
|
||||
return normalizedPath;
|
||||
}
|
||||
const a = normalizedPath.split(slash).map(replace).join(separator);
|
||||
return a || normalizedPath;
|
||||
};
|
||||
|
||||
const toOriginal = (fShort) => {
|
||||
if (!DOCOMPRESS) {
|
||||
return fShort;
|
||||
}
|
||||
return fShort
|
||||
.split(separator)
|
||||
.map((x) => dictRev[x])
|
||||
.join(path.sep);
|
||||
};
|
||||
|
||||
const findVirtualFileSystemKeyAndFollowLinks = (path_) => {
|
||||
let vfsKey = findVirtualFileSystemKey(path_, path.sep);
|
||||
let needToSubstitute = true;
|
||||
while (needToSubstitute) {
|
||||
needToSubstitute = false;
|
||||
for (const [k, v] of symlinksEntries) {
|
||||
if (vfsKey.startsWith(`${k}${sepsep}`) || vfsKey === k) {
|
||||
vfsKey = vfsKey.replace(k, v);
|
||||
needToSubstitute = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return vfsKey;
|
||||
};
|
||||
|
||||
const findVirtualFileSystemEntry = (path_) => {
|
||||
const vfsKey = findVirtualFileSystemKeyAndFollowLinks(path_);
|
||||
return VIRTUAL_FILESYSTEM[vfsKey];
|
||||
};
|
||||
|
||||
// /////////////////////////////////////////////////////////////////
|
||||
// END OF PKG CODE https://github.com/vercel/pkg ///////////////////
|
||||
// /////////////////////////////////////////////////////////////////
|
||||
|
||||
const reverseLinks = (path_) => {
|
||||
let needToSubstitute = true;
|
||||
while (needToSubstitute) {
|
||||
needToSubstitute = false;
|
||||
for (const [k, v] of symlinksEntries) {
|
||||
if (path_.startsWith(`${v}${sepsep}`) || path_ === v) {
|
||||
path_ = path_.replace(v, k);
|
||||
needToSubstitute = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return path_;
|
||||
};
|
||||
|
||||
const getFile = (binPath, [startPos, size]) => {
|
||||
const fd = fs.openSync(binPath, "r");
|
||||
const file = fs.readFileSync(binPath);
|
||||
let code = Buffer.alloc(size);
|
||||
const placeholder = "var PAYLOAD_POSITION = ";
|
||||
const idx = file.indexOf(Buffer.from(placeholder));
|
||||
|
||||
if (idx === -1) throw new Error(`Cannot find the pkg payload! ${READ_ERR}`);
|
||||
|
||||
let payload = file.slice(idx);
|
||||
payload = payload.slice(0, payload.indexOf(Buffer.from("\n")));
|
||||
|
||||
const PAYLOAD_POSITION = Number(payload.toString().match(/\d+/)[0]);
|
||||
|
||||
fs.readSync(fd, code, 0, size, PAYLOAD_POSITION + startPos);
|
||||
|
||||
try {
|
||||
if (DOCOMPRESS === GZIP) code = gunzipSync(code);
|
||||
else if (DOCOMPRESS === BROTLI) code = brotliDecompressSync(code);
|
||||
} catch {}
|
||||
|
||||
return code;
|
||||
};
|
||||
|
||||
const writeFile = (vfsPath, outputPath, blob) => {
|
||||
if (vfsPath.startsWith("C:")) vfsPath = vfsPath.replace("C:", "");
|
||||
|
||||
outputPath = path.join(path.resolve(outputPath), vfsPath);
|
||||
|
||||
if (!fs.existsSync(outputPath)) fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
fs.writeFileSync(outputPath, blob);
|
||||
};
|
||||
|
||||
const executeFile = (blob) => {
|
||||
const options = {
|
||||
lineOffset: 0,
|
||||
displayErrors: true,
|
||||
cachedData: blob,
|
||||
sourceless: true,
|
||||
};
|
||||
const script = new Script(undefined, options);
|
||||
const wrapper = script.runInThisContext();
|
||||
|
||||
if (!wrapper)
|
||||
throw new Error(
|
||||
`Internal JavaScript Evaluation Failure (for example VERSION_MISMATCH). Cannot execute the code!`
|
||||
);
|
||||
|
||||
try {
|
||||
wrapper();
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Error while executing the code! Got the following error:\n${e.toString()}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let exec = false;
|
||||
|
||||
console.log(`Unpacking, ${Object.keys(VIRTUAL_FILESYSTEM).length} elements to go...`);
|
||||
|
||||
for (let path in VIRTUAL_FILESYSTEM) {
|
||||
if (DOCOMPRESS) path = toOriginal(reverseLinks(path));
|
||||
|
||||
const vfs = findVirtualFileSystemEntry(path);
|
||||
let blob;
|
||||
|
||||
if (vfs["0"]) {
|
||||
blob = getFile(argv.i, vfs["0"]);
|
||||
|
||||
if (argv.run && path === props.entryPoint) {
|
||||
exec = executeFile(blob);
|
||||
}
|
||||
|
||||
writeFile(path, argv.o, blob);
|
||||
} else if (vfs["1"]) {
|
||||
blob = getFile(argv.i, vfs["1"]);
|
||||
|
||||
writeFile(path, argv.o, blob);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Binary unpacked to ${argv.o}`);
|
||||
|
||||
if (argv.run && !exec)
|
||||
console.log(
|
||||
"The code has not been executed! It may be because the entry point could not be found."
|
||||
);
|
||||
Loading…
Reference in a new issue