Pin Ghidra 12.2 and keep clients on the server's release
The server's Ghidra project is now built with Ghidra 12.2 (JDK 25); a project saved by a newer Ghidra cannot be opened by an older one. /api/ghidra-project announces the release (url, sha256, dir, jdk); td ghidra download upgrades a td-installed Ghidra to it and refuses with instructions for a user-configured older one; td decompile/disasm fall back to the server's export while the local Ghidra is older than the pin. The unpacked project is handed to the local user (Ghidra opens a project only for its recorded owner). The rodecomp server image ships Ghidra 12.2 and Temurin JDK 25.
This commit is contained in:
parent
7952a0a650
commit
0481ebbd6b
10 changed files with 110 additions and 26 deletions
|
|
@ -5,9 +5,9 @@ set -eu
|
||||||
|
|
||||||
HOST="${1:?usage: install.sh <public-hostname>}"
|
HOST="${1:?usage: install.sh <public-hostname>}"
|
||||||
NODE_VERSION=v22.21.0
|
NODE_VERSION=v22.21.0
|
||||||
GHIDRA_VERSION=11.2.1
|
GHIDRA_VERSION=12.2
|
||||||
GHIDRA_URL=https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.2.1_build/ghidra_11.2.1_PUBLIC_20241105.zip
|
GHIDRA_URL=https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.2_build/ghidra_12.2_PUBLIC_20261016.zip
|
||||||
GHIDRA_SHA256=ce4db5117da0fbaf8f33863fec4f40902f754f06b68945a59fb1c0f9b1bc461c
|
GHIDRA_SHA256=2c2cd776db3c1f6a097ba418922378ac3649cadd5e844a6f5f088ae628a7be67
|
||||||
SRC=$(cd "$(dirname "$0")/.." && pwd)
|
SRC=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
APP=/opt/togetherdecomp
|
APP=/opt/togetherdecomp
|
||||||
DATA=/var/lib/togetherdecomp
|
DATA=/var/lib/togetherdecomp
|
||||||
|
|
@ -31,7 +31,7 @@ fi
|
||||||
|
|
||||||
echo "== ghidra $GHIDRA_VERSION (server-side project and pseudocode)"
|
echo "== ghidra $GHIDRA_VERSION (server-side project and pseudocode)"
|
||||||
# The JDK, not just the runtime: Ghidra compiles its .java scripts.
|
# The JDK, not just the runtime: Ghidra compiles its .java scripts.
|
||||||
apt-get install -y -q openjdk-21-jdk-headless unzip >/dev/null
|
apt-get install -y -q openjdk-25-jdk-headless unzip >/dev/null
|
||||||
GHIDRA_HOME="/opt/ghidra/ghidra_${GHIDRA_VERSION}_PUBLIC"
|
GHIDRA_HOME="/opt/ghidra/ghidra_${GHIDRA_VERSION}_PUBLIC"
|
||||||
if [ ! -x "$GHIDRA_HOME/support/analyzeHeadless" ]; then
|
if [ ! -x "$GHIDRA_HOME/support/analyzeHeadless" ]; then
|
||||||
tmp=$(mktemp -d)
|
tmp=$(mktemp -d)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
FROM docker:29-cli AS docker-cli
|
FROM docker:29-cli AS docker-cli
|
||||||
|
FROM eclipse-temurin:25-jdk AS jdk
|
||||||
|
FROM node:24-bookworm-slim AS ghidra
|
||||||
|
ARG GHIDRA_URL=https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.2_build/ghidra_12.2_PUBLIC_20261016.zip
|
||||||
|
ARG GHIDRA_SHA256=2c2cd776db3c1f6a097ba418922378ac3649cadd5e844a6f5f088ae628a7be67
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates unzip && rm -rf /var/lib/apt/lists/*
|
||||||
|
RUN curl -fsSL "$GHIDRA_URL" -o /tmp/ghidra.zip && echo "$GHIDRA_SHA256 /tmp/ghidra.zip" | sha256sum -c - && mkdir -p /opt/ghidra && unzip -q /tmp/ghidra.zip -d /opt/ghidra && rm /tmp/ghidra.zip
|
||||||
|
|
||||||
FROM node:24-bookworm-slim
|
FROM node:24-bookworm-slim
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends git binutils ca-certificates && rm -rf /var/lib/apt/lists/*
|
RUN apt-get update && apt-get install -y --no-install-recommends git binutils ca-certificates fontconfig && rm -rf /var/lib/apt/lists/*
|
||||||
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
|
COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker
|
||||||
|
COPY --from=jdk /opt/java/openjdk /opt/java/openjdk
|
||||||
|
COPY --from=ghidra /opt/ghidra /opt/ghidra
|
||||||
|
ENV JAVA_HOME=/opt/java/openjdk
|
||||||
|
ENV PATH=/opt/java/openjdk/bin:$PATH
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
RUN npm ci --omit=dev --ignore-scripts
|
RUN npm ci --omit=dev --ignore-scripts
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,7 @@ connect your AI with: td connect --workspace "${ws.dir}"`);
|
||||||
}
|
}
|
||||||
case 'config': {
|
case 'config': {
|
||||||
// td config show settings (token hidden)
|
// td config show settings (token hidden)
|
||||||
// td config <key> <value> set one, e.g. `td config ghidraHome D:\ghidra_11.2.1_PUBLIC`
|
// td config <key> <value> set one, e.g. `td config ghidraHome D:\ghidra_12.2_PUBLIC`
|
||||||
const [key, value] = args;
|
const [key, value] = args;
|
||||||
const SETTABLE = ['ghidraHome', 'server'];
|
const SETTABLE = ['ghidraHome', 'server'];
|
||||||
if (!key) return print({ ...ws.config, token: ws.config.token ? '(set)' : null });
|
if (!key) return print({ ...ws.config, token: ws.config.token ? '(set)' : null });
|
||||||
|
|
@ -311,7 +311,7 @@ connect your AI with: td connect --workspace "${ws.dir}"`);
|
||||||
if (args[0] === 'sync') return void (await ghidra.syncGhidraProject(ws), print('symbols applied'));
|
if (args[0] === 'sync') return void (await ghidra.syncGhidraProject(ws), print('symbols applied'));
|
||||||
if (args[0] === 'download') {
|
if (args[0] === 'download') {
|
||||||
const r = await ghidra.downloadGhidraProject(ws, { force: values.force });
|
const r = await ghidra.downloadGhidraProject(ws, { force: values.force });
|
||||||
return print(r.updated ? `Ghidra project of ${r.info.version} downloaded${ws.config.ghidraHome ? '' : ' (open it with Ghidra 11.2.1, or run td ghidra install)'}` : 'the Ghidra project is up to date');
|
return print(r.updated ? `Ghidra project of ${r.info.version} downloaded${ws.config.ghidraHome ? '' : ` (open it with Ghidra ${r.info.ghidra}, or run td ghidra install)`}` : 'the Ghidra project is up to date');
|
||||||
}
|
}
|
||||||
throw new Error('usage: td ghidra install|download|build|sync');
|
throw new Error('usage: td ghidra install|download|build|sync');
|
||||||
case 'decompile':
|
case 'decompile':
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { fileURLToPath } from 'node:url';
|
||||||
import { run } from '../shared/build.js';
|
import { run } from '../shared/build.js';
|
||||||
import { hex } from '../shared/annotations.js';
|
import { hex } from '../shared/annotations.js';
|
||||||
import { loadToolchainLock } from '../shared/project.js';
|
import { loadToolchainLock } from '../shared/project.js';
|
||||||
|
import { GHIDRA_RELEASE, ghidraAtLeast, ghidraVersionAt } from '../shared/ghidra.js';
|
||||||
import { Api } from './api.js';
|
import { Api } from './api.js';
|
||||||
|
|
||||||
const SCRIPT_NAMES = ['ApplySymbols.java', 'DecompileAt.java', 'DisassembleAt.java', 'ExportFunctions.java', 'ExportOriginals.java'];
|
const SCRIPT_NAMES = ['ApplySymbols.java', 'DecompileAt.java', 'DisassembleAt.java', 'ExportFunctions.java', 'ExportOriginals.java'];
|
||||||
|
|
@ -55,26 +56,46 @@ async function runHeadless(ws, args, timeoutMs) {
|
||||||
return r.output;
|
return r.output;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Downloads and unpacks the Ghidra release pinned in data/toolchain.lock.json. */
|
/**
|
||||||
export async function installGhidra(ws) {
|
* Downloads and unpacks a Ghidra release: the one the server builds its project with (so the
|
||||||
const lock = loadToolchainLock(ws.dir).ghidra;
|
* downloaded project opens), else the one pinned in data/toolchain.lock.json, else td's own pin.
|
||||||
if (!lock?.url || !lock?.sha256) throw new Error('data/toolchain.lock.json has no ghidra { url, sha256, dir } entry');
|
*/
|
||||||
|
export async function installGhidra(ws, release = null) {
|
||||||
|
const lock = release?.url ? release : loadToolchainLock(ws.dir).ghidra?.url ? loadToolchainLock(ws.dir).ghidra : GHIDRA_RELEASE;
|
||||||
|
if (!lock?.url || !lock?.sha256) throw new Error('no Ghidra release to install (data/toolchain.lock.json has no ghidra { url, sha256, dir } entry)');
|
||||||
const toolsDir = path.join(ws.tdDir, 'tools');
|
const toolsDir = path.join(ws.tdDir, 'tools');
|
||||||
fs.mkdirSync(toolsDir, { recursive: true });
|
fs.mkdirSync(toolsDir, { recursive: true });
|
||||||
const zip = path.join(toolsDir, path.basename(new URL(lock.url).pathname));
|
const zip = path.join(toolsDir, path.basename(new URL(lock.url).pathname));
|
||||||
if (!fs.existsSync(zip)) {
|
if (!fs.existsSync(zip)) {
|
||||||
console.log(`downloading ${lock.url}`);
|
console.error(`downloading ${lock.url}`);
|
||||||
await new Api('').download(lock.url, zip, lock.sha256);
|
await new Api('').download(lock.url, zip, lock.sha256);
|
||||||
}
|
}
|
||||||
// Windows' own tar.exe (bsdtar) unpacks zip files; a GNU tar earlier on PATH (Git Bash) would not.
|
// Windows' own tar.exe (bsdtar) unpacks zip files; a GNU tar earlier on PATH (Git Bash) would not.
|
||||||
const winTar = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'tar.exe');
|
const winTar = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'tar.exe');
|
||||||
const r = os.platform() === 'win32' ? await run(winTar, ['-xf', zip, '-C', toolsDir], { timeoutMs: 600_000 }) : await run('unzip', ['-q', '-o', zip, '-d', toolsDir], { timeoutMs: 600_000 });
|
const r = os.platform() === 'win32' ? await run(winTar, ['-xf', zip, '-C', toolsDir], { timeoutMs: 600_000 }) : await run('unzip', ['-q', '-o', zip, '-d', toolsDir], { timeoutMs: 600_000 });
|
||||||
if (r.code !== 0) throw new Error(`could not unpack Ghidra: ${r.output}`);
|
if (r.code !== 0) throw new Error(`could not unpack Ghidra: ${r.output}`);
|
||||||
|
fs.rmSync(zip, { force: true });
|
||||||
ws.config.ghidraHome = path.join(toolsDir, lock.dir);
|
ws.config.ghidraHome = path.join(toolsDir, lock.dir);
|
||||||
ws.save();
|
ws.save();
|
||||||
return ws.config.ghidraHome;
|
return ws.config.ghidraHome;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A local Ghidra older than the one the server's project was saved with cannot open it. Upgrades
|
||||||
|
* the Ghidra td installed itself; a Ghidra the user configured is reported, not replaced.
|
||||||
|
*/
|
||||||
|
export async function ensureGhidraFor(ws, release) {
|
||||||
|
if (!release?.version || !ws.config.ghidraHome) return null;
|
||||||
|
const local = ghidraVersionAt(ws.config.ghidraHome);
|
||||||
|
if (local && ghidraAtLeast(local, release.version)) return null;
|
||||||
|
const managed = path.resolve(ws.config.ghidraHome).startsWith(path.resolve(path.join(ws.tdDir, 'tools')));
|
||||||
|
if (!managed || !release.url) {
|
||||||
|
throw new Error(`the server's Ghidra project needs Ghidra ${release.version}${release.jdk ? ` (JDK ${release.jdk})` : ''}, but ${ws.config.ghidraHome} is ${local ?? 'unknown'}: install it and run \`td config ghidraHome <dir>\`, or run \`td ghidra install\``);
|
||||||
|
}
|
||||||
|
console.error(`upgrading Ghidra ${local ?? '?'} -> ${release.version} (the server's project needs it)`);
|
||||||
|
return installGhidra(ws, release);
|
||||||
|
}
|
||||||
|
|
||||||
function writeSymbolFile(ws) {
|
function writeSymbolFile(ws) {
|
||||||
const file = path.join(ws.tdDir, 'ghidra-symbols.tsv');
|
const file = path.join(ws.tdDir, 'ghidra-symbols.tsv');
|
||||||
const lines = [];
|
const lines = [];
|
||||||
|
|
@ -127,6 +148,7 @@ export function disassemble(ws, addr) {
|
||||||
export async function downloadGhidraProject(ws, { force = false } = {}) {
|
export async function downloadGhidraProject(ws, { force = false } = {}) {
|
||||||
const client = new Api(ws.config.server, ws.config.token);
|
const client = new Api(ws.config.server, ws.config.token);
|
||||||
const info = await client.get('/api/ghidra-project');
|
const info = await client.get('/api/ghidra-project');
|
||||||
|
await ensureGhidraFor(ws, info.release ?? (info.ghidra ? { version: info.ghidra } : null));
|
||||||
if (!force && ws.config.ghidraPackage === info.sha256 && fs.existsSync(path.join(ws.tdDir, 'ghidra', `${PROJECT_NAME}.gpr`))) {
|
if (!force && ws.config.ghidraPackage === info.sha256 && fs.existsSync(path.join(ws.tdDir, 'ghidra', `${PROJECT_NAME}.gpr`))) {
|
||||||
return { updated: false, info };
|
return { updated: false, info };
|
||||||
}
|
}
|
||||||
|
|
@ -140,7 +162,22 @@ export async function downloadGhidraProject(ws, { force = false } = {}) {
|
||||||
const r = await run(tar, ['-xzf', tgz, '-C', projDir], { timeoutMs: 1800_000 });
|
const r = await run(tar, ['-xzf', tgz, '-C', projDir], { timeoutMs: 1800_000 });
|
||||||
if (r.code !== 0) throw new Error(`could not unpack the Ghidra project: ${r.output}`);
|
if (r.code !== 0) throw new Error(`could not unpack the Ghidra project: ${r.output}`);
|
||||||
fs.rmSync(tgz, { force: true });
|
fs.rmSync(tgz, { force: true });
|
||||||
|
takeProjectOwnership(projDir);
|
||||||
ws.config.ghidraPackage = info.sha256;
|
ws.config.ghidraPackage = info.sha256;
|
||||||
ws.save();
|
ws.save();
|
||||||
return { updated: true, info };
|
return { updated: true, info };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ghidra opens a project only for the user recorded as its owner (its user.name, spaces removed).
|
||||||
|
* The server packs its project under its own account, so the unpacked copy is handed to whoever
|
||||||
|
* runs td.
|
||||||
|
*/
|
||||||
|
function takeProjectOwnership(projDir) {
|
||||||
|
const prp = path.join(projDir, `${PROJECT_NAME}.rep`, 'project.prp');
|
||||||
|
if (!fs.existsSync(prp)) return;
|
||||||
|
const user = os.userInfo().username.replace(/\s/g, '').replace(/[<>&"]/g, '');
|
||||||
|
const text = fs.readFileSync(prp, 'utf8');
|
||||||
|
const owned = text.replace(/(<STATE NAME="OWNER" TYPE="string" VALUE=")[^"]*(" \/>)/, (_, a, b) => a + user + b);
|
||||||
|
if (owned !== text) fs.writeFileSync(prp, owned);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -240,10 +240,13 @@ async function remoteCheck(ws, targets, { wait = true, explicit = false } = {})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The original function as text. A local Ghidra (td ghidra install/build) answers locally;
|
* The original function as text. A local Ghidra (td ghidra install/build) answers locally;
|
||||||
* otherwise the server's Ghidra export does.
|
* otherwise, or when it is older than the release td pins (it cannot open the server's project),
|
||||||
|
* the server's Ghidra export does.
|
||||||
*/
|
*/
|
||||||
export async function original(ws, query, kind) {
|
export async function original(ws, query, kind) {
|
||||||
if (ws.config.ghidraHome) {
|
const { ghidraVersionAt, ghidraAtLeast, GHIDRA_RELEASE } = await import('../shared/ghidra.js');
|
||||||
|
const local = ws.config.ghidraHome ? ghidraVersionAt(ws.config.ghidraHome) : null;
|
||||||
|
if (local && ghidraAtLeast(local, GHIDRA_RELEASE.version)) {
|
||||||
const { decompile, disassemble } = await import('./ghidra.js');
|
const { decompile, disassemble } = await import('./ghidra.js');
|
||||||
const s = ws.findSymbol(query);
|
const s = ws.findSymbol(query);
|
||||||
const addr = s?.addr ?? (/^0x[0-9a-f]+$/i.test(query) ? Number.parseInt(query, 16) : null);
|
const addr = s?.addr ?? (/^0x[0-9a-f]+$/i.test(query) ? Number.parseInt(query, 16) : null);
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import { PEImage } from '../shared/pe.js';
|
||||||
import { loadProject, loadToolchainLock } from '../shared/project.js';
|
import { loadProject, loadToolchainLock } from '../shared/project.js';
|
||||||
import { DEFAULT_LIMITS, lintSubmission } from '../shared/lint.js';
|
import { DEFAULT_LIMITS, lintSubmission } from '../shared/lint.js';
|
||||||
import { hex } from '../shared/annotations.js';
|
import { hex } from '../shared/annotations.js';
|
||||||
|
import { GHIDRA_RELEASE } from '../shared/ghidra.js';
|
||||||
|
|
||||||
// TD_WEB_DIR points through the release symlink in production, so new pages are live without a restart.
|
// TD_WEB_DIR points through the release symlink in production, so new pages are live without a restart.
|
||||||
const WEB_DIR = process.env.TD_WEB_DIR ?? path.join(path.dirname(fileURLToPath(import.meta.url)), 'web');
|
const WEB_DIR = process.env.TD_WEB_DIR ?? path.join(path.dirname(fileURLToPath(import.meta.url)), 'web');
|
||||||
|
|
@ -1011,7 +1012,8 @@ export function createApp(ctx) {
|
||||||
auth(req);
|
auth(req);
|
||||||
const info = ctx.ghidra.info();
|
const info = ctx.ghidra.info();
|
||||||
if (!info) throw new HttpError(404, 'no Ghidra project has been built on this server yet');
|
if (!info) throw new HttpError(404, 'no Ghidra project has been built on this server yet');
|
||||||
return { ...info, download: `${config.publicUrl}/api/ghidra-project/download` };
|
const ghidra = GHIDRA_RELEASE.version === info.ghidra ? GHIDRA_RELEASE : { version: info.ghidra };
|
||||||
|
return { ...info, release: ghidra, download: `${config.publicUrl}/api/ghidra-project/download` };
|
||||||
});
|
});
|
||||||
|
|
||||||
router.on('GET', '/api/ghidra-project/download', ({ req, res }) => {
|
router.on('GET', '/api/ghidra-project/download', ({ req, res }) => {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import { Accounts } from './accounts.js';
|
||||||
import { agentGuidance, setAgentGuidance, resetAgentGuidance } from './guidance.js';
|
import { agentGuidance, setAgentGuidance, resetAgentGuidance } from './guidance.js';
|
||||||
import { productivity, formatProductivity } from './stats.js';
|
import { productivity, formatProductivity } from './stats.js';
|
||||||
import { PEImage } from '../shared/pe.js';
|
import { PEImage } from '../shared/pe.js';
|
||||||
|
import { GHIDRA_RELEASE } from '../shared/ghidra.js';
|
||||||
|
|
||||||
const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'template');
|
const TEMPLATE_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'template');
|
||||||
|
|
||||||
|
|
@ -323,9 +324,9 @@ async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cmd === 'ghidra' && (sub === 'build' || sub === 'refresh')) {
|
if (cmd === 'ghidra' && (sub === 'build' || sub === 'refresh')) {
|
||||||
// The server's Ghidra project (needs ghidraHome in server.json and a JDK 21).
|
// The server's Ghidra project (needs ghidraHome in server.json and the JDK that Ghidra needs).
|
||||||
const ctx = openServer(dataDir);
|
const ctx = openServer(dataDir);
|
||||||
if (!ctx.ghidra.enabled) throw new Error('set ghidraHome in server.json to a Ghidra 11.2.1 installation');
|
if (!ctx.ghidra.enabled) throw new Error(`set ghidraHome in server.json to a Ghidra ${GHIDRA_RELEASE.version} installation`);
|
||||||
ctx.store.setMeta('ghidra.commit', ctx.repo.head());
|
ctx.store.setMeta('ghidra.commit', ctx.repo.head());
|
||||||
const info = sub === 'build' ? await ctx.ghidra.build() : await ctx.ghidra.refresh();
|
const info = sub === 'build' ? await ctx.ghidra.build() : await ctx.ghidra.refresh();
|
||||||
console.log(JSON.stringify(info, null, 2));
|
console.log(JSON.stringify(info, null, 2));
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import crypto from 'node:crypto';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { run } from '../shared/build.js';
|
import { run } from '../shared/build.js';
|
||||||
import { hex } from '../shared/annotations.js';
|
import { hex } from '../shared/annotations.js';
|
||||||
|
import { GHIDRA_RELEASE, ghidraVersionAt } from '../shared/ghidra.js';
|
||||||
|
|
||||||
const SCRIPTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'ghidra_scripts');
|
const SCRIPTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'ghidra_scripts');
|
||||||
const PROJECT = 'td';
|
const PROJECT = 'td';
|
||||||
|
|
@ -47,9 +48,8 @@ export class ServerGhidra {
|
||||||
async headless(args, timeoutMs) {
|
async headless(args, timeoutMs) {
|
||||||
fs.mkdirSync(this.projectDir, { recursive: true });
|
fs.mkdirSync(this.projectDir, { recursive: true });
|
||||||
const exe = path.join(this.config.ghidraHome, 'support', 'analyzeHeadless');
|
const exe = path.join(this.config.ghidraHome, 'support', 'analyzeHeadless');
|
||||||
// Ghidra 11.2 is made for JDK 21; the system default may be newer.
|
// Ghidra 12.2 needs JDK 25; `ghidraJavaHome` overrides a different default JDK.
|
||||||
const jdk21 = '/usr/lib/jvm/java-21-openjdk-amd64';
|
const javaHome = this.config.ghidraJavaHome ?? process.env.JAVA_HOME;
|
||||||
const javaHome = this.config.ghidraJavaHome ?? (fs.existsSync(jdk21) ? jdk21 : process.env.JAVA_HOME);
|
|
||||||
const env = { ...process.env, MAXMEM: this.config.ghidraMaxMem ?? '6G', ...(javaHome ? { JAVA_HOME: javaHome, PATH: `${javaHome}/bin:${process.env.PATH}` } : {}) };
|
const env = { ...process.env, MAXMEM: this.config.ghidraMaxMem ?? '6G', ...(javaHome ? { JAVA_HOME: javaHome, PATH: `${javaHome}/bin:${process.env.PATH}` } : {}) };
|
||||||
const r = await run(exe, [this.projectDir, PROJECT, ...args, '-scriptPath', SCRIPTS_DIR], { timeoutMs, env });
|
const r = await run(exe, [this.projectDir, PROJECT, ...args, '-scriptPath', SCRIPTS_DIR], { timeoutMs, env });
|
||||||
if (r.code !== 0) throw new Error(`Ghidra failed (exit ${r.code}): ${r.output.slice(-2000)}`);
|
if (r.code !== 0) throw new Error(`Ghidra failed (exit ${r.code}): ${r.output.slice(-2000)}`);
|
||||||
|
|
@ -101,7 +101,8 @@ export class ServerGhidra {
|
||||||
const old = this.info();
|
const old = this.info();
|
||||||
fs.renameSync(tmp, path.join(this.dir, name));
|
fs.renameSync(tmp, path.join(this.dir, name));
|
||||||
const sha256 = await sha256File(path.join(this.dir, name));
|
const sha256 = await sha256File(path.join(this.dir, name));
|
||||||
const info = { version: v.id, file: name, sha256, size: fs.statSync(path.join(this.dir, name)).size, builtAt: Date.now(), commit: head, functions: n, ghidra: this.config.ghidraVersion ?? '11.2.1' };
|
const ghidra = ghidraVersionAt(this.config.ghidraHome) ?? this.config.ghidraVersion ?? GHIDRA_RELEASE.version;
|
||||||
|
const info = { version: v.id, file: name, sha256, size: fs.statSync(path.join(this.dir, name)).size, builtAt: Date.now(), commit: head, functions: n, ghidra };
|
||||||
fs.writeFileSync(path.join(this.dir, 'package.json'), JSON.stringify(info, null, 2));
|
fs.writeFileSync(path.join(this.dir, 'package.json'), JSON.stringify(info, null, 2));
|
||||||
if (old?.file && old.file !== name) fs.rmSync(path.join(this.dir, old.file), { force: true });
|
if (old?.file && old.file !== name) fs.rmSync(path.join(this.dir, old.file), { force: true });
|
||||||
this.store.setMeta('ghidra.refreshedAt', Date.now());
|
this.store.setMeta('ghidra.refreshedAt', Date.now());
|
||||||
|
|
|
||||||
29
src/shared/ghidra.js
Normal file
29
src/shared/ghidra.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// The Ghidra release the server builds its project with. A project saved by a newer Ghidra cannot
|
||||||
|
// be opened by an older one, so clients that download it need the same release.
|
||||||
|
export const GHIDRA_RELEASE = {
|
||||||
|
version: '12.2',
|
||||||
|
url: 'https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.2_build/ghidra_12.2_PUBLIC_20261016.zip',
|
||||||
|
sha256: '2c2cd776db3c1f6a097ba418922378ac3649cadd5e844a6f5f088ae628a7be67',
|
||||||
|
dir: 'ghidra_12.2_PUBLIC',
|
||||||
|
jdk: '25',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** The application.version of the Ghidra installed at `home`, or null. */
|
||||||
|
export function ghidraVersionAt(home) {
|
||||||
|
if (!home) return null;
|
||||||
|
try {
|
||||||
|
const props = fs.readFileSync(path.join(home, 'Ghidra', 'application.properties'), 'utf8');
|
||||||
|
return props.match(/^application\.version=(.+)$/m)?.[1].trim() ?? null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when Ghidra version `a` is at least `b` (dotted numeric versions). */
|
||||||
|
export function ghidraAtLeast(a, b) {
|
||||||
|
const key = (v) => String(v ?? '0').split('.').map((x) => Number.parseInt(x, 10) || 0).concat([0, 0, 0]).slice(0, 3).reduce((n, x) => n * 1000 + x, 0);
|
||||||
|
return key(a) >= key(b);
|
||||||
|
}
|
||||||
|
|
@ -5,10 +5,10 @@
|
||||||
"clSha256": null
|
"clSha256": null
|
||||||
},
|
},
|
||||||
"ghidra": {
|
"ghidra": {
|
||||||
"version": "11.2.1",
|
"version": "12.2",
|
||||||
"url": "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_11.2.1_build/ghidra_11.2.1_PUBLIC_20241105.zip",
|
"url": "https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.2_build/ghidra_12.2_PUBLIC_20261016.zip",
|
||||||
"sha256": "ce4db5117da0fbaf8f33863fec4f40902f754f06b68945a59fb1c0f9b1bc461c",
|
"sha256": "2c2cd776db3c1f6a097ba418922378ac3649cadd5e844a6f5f088ae628a7be67",
|
||||||
"dir": "ghidra_11.2.1_PUBLIC",
|
"dir": "ghidra_12.2_PUBLIC",
|
||||||
"jdk": "21"
|
"jdk": "25"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue