initial commit

This commit is contained in:
LockBlock-dev 2022-08-04 17:48:35 +02:00
commit 5c818d3899
6 changed files with 448 additions and 0 deletions

3
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1,3 @@
# These are supported funding model platforms
ko_fi: lockblock

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
package-lock.json
unpacked/

21
LICENSE Normal file
View 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
View 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
View 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
View 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."
);