391 lines
11 KiB
JavaScript
391 lines
11 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { parseBinaryTarget } from "../dist/domain/binaryTarget.js";
|
|
import { inspectManagedArtifactBytes } from "../dist/dotnet/ManagedArtifactInspector.js";
|
|
import { inspectManagedMembersBytes } from "../dist/dotnet/ManagedMemberInspector.js";
|
|
import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativeBoundaryInspector.js";
|
|
import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js";
|
|
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
|
|
import { planManagedRuntimeCorrelationEvidence } from "../dist/application/ManagedRuntimeCorrelationService.js";
|
|
import { createPermissionAuthority } from "../dist/application/PermissionAuthority.js";
|
|
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
|
|
import { createEvidence } from "../dist/domain/evidence.js";
|
|
import {
|
|
alternateMvid,
|
|
buildManagedPeFixture,
|
|
buildNativePeFixture,
|
|
} from "./lib/managed-pe-fixture.mjs";
|
|
|
|
const inspectionLimits = {
|
|
referenceOffset: 0,
|
|
referenceLimit: 100,
|
|
resourceOffset: 0,
|
|
resourceLimit: 100,
|
|
attributeOffset: 0,
|
|
attributeLimit: 100,
|
|
maxMetadataBytes: 1024 * 1024,
|
|
maxTableRows: 1_000,
|
|
maxHeapItemBytes: 1024 * 1024,
|
|
};
|
|
|
|
const memberLimits = {
|
|
typeOffset: 0,
|
|
typeLimit: 100,
|
|
methodOffset: 0,
|
|
methodLimit: 100,
|
|
fieldOffset: 0,
|
|
fieldLimit: 100,
|
|
memberRefOffset: 0,
|
|
memberRefLimit: 100,
|
|
edgeOffset: 0,
|
|
edgeLimit: 100,
|
|
instructionAnchorLimit: 100,
|
|
maxMetadataBytes: 1024 * 1024,
|
|
maxTableRows: 1_000,
|
|
maxHeapItemBytes: 1024 * 1024,
|
|
maxMethodBodyBytes: 1024 * 1024,
|
|
maxMethodInstructions: 1_000,
|
|
};
|
|
|
|
const nativeBoundaryLimits = {
|
|
moduleRefOffset: 0,
|
|
moduleRefLimit: 100,
|
|
importOffset: 0,
|
|
importLimit: 100,
|
|
implementationOffset: 0,
|
|
implementationLimit: 100,
|
|
maxMetadataBytes: 1024 * 1024,
|
|
maxTableRows: 1_000,
|
|
maxHeapItemBytes: 1024 * 1024,
|
|
};
|
|
|
|
const comparisonLimits = {
|
|
max_method_matches: 100,
|
|
max_field_matches: 100,
|
|
max_candidates: 20,
|
|
};
|
|
|
|
const defaultIlBody = Buffer.from([
|
|
0x32, 0x02, 0x7b, 0x01, 0x00, 0x00, 0x04, 0x28, 0x01, 0x00, 0x00, 0x0a, 0x2a,
|
|
]);
|
|
|
|
const workspace = await mkdtemp(join(tmpdir(), "rea-managed-conformance-"));
|
|
|
|
try {
|
|
const modern = await fixture("modern-anycpu.exe", {
|
|
references: ["System.Runtime"],
|
|
resourceData: Buffer.from("source-owned managed resource"),
|
|
});
|
|
const modernArtifact = inspectManagedArtifactBytes(
|
|
modern.bytes,
|
|
modern.target,
|
|
inspectionLimits,
|
|
);
|
|
assert.equal(modernArtifact.classification.status, "managed");
|
|
assert.equal(modernArtifact.classification.runtime_family, "modern-dotnet");
|
|
assert.equal(modernArtifact.classification.managed_architecture, "anycpu");
|
|
assert.deepEqual(modernArtifact.target_frameworks, [
|
|
".NETCoreApp,Version=v8.0",
|
|
]);
|
|
assert.equal(
|
|
modernArtifact.resources.items[0]?.data_sha256,
|
|
sha256(Buffer.from("source-owned managed resource")),
|
|
);
|
|
|
|
const framework = await fixture("framework-x86-pinvoke.exe", {
|
|
cliFlags: 0x0000_0003,
|
|
targetFramework: ".NETFramework,Version=v4.8",
|
|
pinvoke: {
|
|
moduleName: "user32.dll",
|
|
importName: "MessageBoxW",
|
|
mappingFlags: 0x0345,
|
|
},
|
|
});
|
|
const frameworkArtifact = inspectManagedArtifactBytes(
|
|
framework.bytes,
|
|
framework.target,
|
|
inspectionLimits,
|
|
);
|
|
assert.equal(
|
|
frameworkArtifact.classification.runtime_family,
|
|
"dotnet-framework",
|
|
);
|
|
assert.equal(frameworkArtifact.classification.managed_architecture, "x86");
|
|
const frameworkBoundaries = inspectManagedNativeBoundariesBytes(
|
|
framework.bytes,
|
|
framework.target,
|
|
nativeBoundaryLimits,
|
|
);
|
|
assert.equal(frameworkBoundaries.pinvoke_imports.total, 1);
|
|
assert.equal(
|
|
frameworkBoundaries.pinvoke_imports.items[0]?.verification,
|
|
"managed-declaration-only",
|
|
);
|
|
assert.equal(
|
|
frameworkBoundaries.pinvoke_imports.items[0]?.char_set,
|
|
"unicode",
|
|
);
|
|
assert.equal(
|
|
frameworkBoundaries.pinvoke_imports.items[0]?.call_convention,
|
|
"stdcall",
|
|
);
|
|
|
|
const readyToRun = await fixture("r2r-x64.exe", {
|
|
machine: 0x8664,
|
|
readyToRun: true,
|
|
methods: [
|
|
{
|
|
name: "NativeBody",
|
|
implFlags: 0x0001,
|
|
body: Buffer.from([0x2a]),
|
|
},
|
|
],
|
|
});
|
|
const r2rArtifact = inspectManagedArtifactBytes(
|
|
readyToRun.bytes,
|
|
readyToRun.target,
|
|
inspectionLimits,
|
|
);
|
|
assert.equal(r2rArtifact.pe.architecture, "x86_64");
|
|
assert.equal(
|
|
r2rArtifact.classification.implementation,
|
|
"cil-and-ready-to-run",
|
|
);
|
|
const r2rBoundaries = inspectManagedNativeBoundariesBytes(
|
|
readyToRun.bytes,
|
|
readyToRun.target,
|
|
nativeBoundaryLimits,
|
|
);
|
|
assert.equal(r2rBoundaries.summary.ready_to_run, true);
|
|
assert.equal(
|
|
r2rBoundaries.native_implementations.items[0]?.code_type,
|
|
"native",
|
|
);
|
|
assert.equal(
|
|
r2rBoundaries.native_implementations.items[0]?.boundary_kind,
|
|
"native-body",
|
|
);
|
|
|
|
const obfuscated = await fixture("obfuscated.exe", {
|
|
typeName: "ꙮType",
|
|
methodName: "λ⛧",
|
|
fieldName: "字段",
|
|
});
|
|
const obfuscatedMembers = inspectManagedMembersBytes(
|
|
obfuscated.bytes,
|
|
obfuscated.target,
|
|
memberLimits,
|
|
);
|
|
assert.equal(obfuscatedMembers.types.items[0]?.full_name, "Fixture.ꙮType");
|
|
assert.equal(obfuscatedMembers.methods.items[0]?.name, "λ⛧");
|
|
assert.equal(obfuscatedMembers.fields.items[0]?.name, "字段");
|
|
const runtimeExecutable = join(workspace, "dotnet");
|
|
await writeFile(runtimeExecutable, "#!/bin/sh\n");
|
|
const runtimeCeiling = {
|
|
capability: "managed_runtime",
|
|
roots: [workspace],
|
|
executables: [runtimeExecutable],
|
|
environment_names: [],
|
|
network: "none",
|
|
mount: false,
|
|
};
|
|
const runtimeAuthority = await createPermissionAuthority(
|
|
[runtimeCeiling],
|
|
[
|
|
{
|
|
...runtimeCeiling,
|
|
grant_id: "administrator:managed_runtime",
|
|
lifetime: "administrator",
|
|
operation_identity: null,
|
|
expires_at: null,
|
|
},
|
|
],
|
|
);
|
|
assert.equal(runtimeAuthority.ok, true);
|
|
const runtimeMethod = obfuscatedMembers.methods.items[0];
|
|
assert.ok(runtimeMethod);
|
|
const obfuscatedMembersEvidence = createEvidence(
|
|
undefined,
|
|
MANAGED_STATIC_PROVIDER,
|
|
{
|
|
operation: "inspect_managed_members",
|
|
parameters: { path: obfuscated.path },
|
|
result: obfuscatedMembers,
|
|
rawResult: null,
|
|
limitations: obfuscatedMembers.limitations,
|
|
},
|
|
);
|
|
const reconstructionImport = importManagedReconstructionEvidence({
|
|
static_members: obfuscatedMembersEvidence,
|
|
decompiler: {
|
|
name: "ilspycmd",
|
|
version: "9.1.0.7988",
|
|
family: "ilspy",
|
|
executable_sha256: null,
|
|
options: ["--type", "Fixture.ꙮType"],
|
|
},
|
|
methods: [
|
|
{
|
|
token: runtimeMethod.token,
|
|
signature_sha256: runtimeMethod.signature.raw_sha256,
|
|
normalized_il_sha256: runtimeMethod.body.normalized_il_sha256,
|
|
reconstruction: {
|
|
kind: "decompiled-csharp",
|
|
language: "csharp",
|
|
text: "internal static void λ⛧() { /* synthetic fixture */ }",
|
|
},
|
|
},
|
|
],
|
|
notes: ["source-owned synthetic decompiler reconstruction"],
|
|
});
|
|
assert.equal(reconstructionImport.ok, true);
|
|
assert.equal(
|
|
reconstructionImport.value.normalized_result.methods[0].validation
|
|
.canonical_observation,
|
|
false,
|
|
);
|
|
assert.equal(reconstructionImport.value.confidence, "inferred");
|
|
const runtimePlan = await planManagedRuntimeCorrelationEvidence(
|
|
{
|
|
policy: {
|
|
enabled: true,
|
|
roots: [workspace],
|
|
executablePath: runtimeExecutable,
|
|
},
|
|
authority: runtimeAuthority.value,
|
|
},
|
|
{
|
|
static_members: obfuscatedMembersEvidence,
|
|
method: {
|
|
token: runtimeMethod.token,
|
|
signature_sha256: runtimeMethod.signature.raw_sha256,
|
|
normalized_il_sha256: runtimeMethod.body.normalized_il_sha256,
|
|
},
|
|
requested_effect: "debugger",
|
|
host: {
|
|
os: "linux",
|
|
clr_family: "dotnet",
|
|
architecture: "x86_64",
|
|
},
|
|
bounds: {
|
|
timeout_ms: 5_000,
|
|
max_threads: 32,
|
|
max_output_bytes: 65_536,
|
|
allow_network: false,
|
|
allow_ui: false,
|
|
},
|
|
},
|
|
);
|
|
assert.equal(runtimePlan.ok, true);
|
|
assert.equal(runtimePlan.value.normalized_result.executed, false);
|
|
assert.equal(
|
|
runtimePlan.value.normalized_result.authority_model.capability,
|
|
"managed_runtime",
|
|
);
|
|
assert.equal(
|
|
runtimePlan.value.normalized_result.effect_taxonomy.uses_debugger,
|
|
true,
|
|
);
|
|
|
|
const left = await fixture("token-drift-left.exe", {
|
|
methodName: "StableSemanticSlice",
|
|
});
|
|
const right = await fixture("token-drift-right.exe", {
|
|
mvid: alternateMvid,
|
|
methods: [
|
|
{ name: "InsertedHelper", body: Buffer.from([0x2a]) },
|
|
{ name: "RenamedSemanticSlice", body: defaultIlBody },
|
|
],
|
|
});
|
|
const comparison = await compareManagedMemberPaths({
|
|
leftPath: left.path,
|
|
rightPath: right.path,
|
|
memberLimits: {
|
|
maxFileBytes: 1024 * 1024,
|
|
...memberLimits,
|
|
},
|
|
comparisonLimits,
|
|
});
|
|
assert.equal(comparison.ok, true);
|
|
const comparisonResult = comparison.value.normalized_result;
|
|
assert.equal(comparisonResult.algorithm.name_matching, "not-used");
|
|
assert.equal(comparisonResult.matching.exact_il_signature, 1);
|
|
assert.equal(
|
|
comparisonResult.methods.some(
|
|
({ left, right, match }) =>
|
|
left.token === "0x06000001" &&
|
|
right?.token === "0x06000002" &&
|
|
match.basis === "exact-il-signature",
|
|
),
|
|
true,
|
|
);
|
|
|
|
const nativeOnly = await fixtureBytes(
|
|
"native-only.exe",
|
|
buildNativePeFixture(),
|
|
);
|
|
const nativeResult = inspectManagedArtifactBytes(
|
|
nativeOnly.bytes,
|
|
nativeOnly.target,
|
|
inspectionLimits,
|
|
);
|
|
assert.equal(nativeResult.classification.status, "not-managed");
|
|
|
|
const malformed = await fixture("malformed-metadata.exe", {
|
|
corruptMetadataSignature: true,
|
|
});
|
|
const malformedResult = inspectManagedArtifactBytes(
|
|
malformed.bytes,
|
|
malformed.target,
|
|
inspectionLimits,
|
|
);
|
|
assert.equal(malformedResult.classification.status, "malformed");
|
|
|
|
process.stdout.write(
|
|
`${JSON.stringify({
|
|
verified: 9,
|
|
managedSurfaces: [
|
|
"inspect_managed_artifact",
|
|
"inspect_managed_members",
|
|
"inspect_managed_native_boundaries",
|
|
"compare_managed_members",
|
|
"import_managed_reconstruction",
|
|
"plan_managed_runtime_correlation",
|
|
],
|
|
coverage: [
|
|
"modern-dotnet-anycpu",
|
|
"dotnet-framework-x86-pinvoke",
|
|
"x64-ready-to-run-native-body",
|
|
"unicode-obfuscated-identifiers",
|
|
"decompiler-reconstruction-import",
|
|
"runtime-correlation-admission-plan",
|
|
"mvid-and-token-drift",
|
|
"not-managed",
|
|
"malformed-metadata",
|
|
],
|
|
})}\n`,
|
|
);
|
|
} finally {
|
|
await rm(workspace, { recursive: true, force: true });
|
|
}
|
|
|
|
async function fixture(name, options) {
|
|
return fixtureBytes(name, buildManagedPeFixture(options));
|
|
}
|
|
|
|
async function fixtureBytes(name, bytes) {
|
|
const path = join(workspace, name);
|
|
await writeFile(path, bytes);
|
|
const parsed = await parseBinaryTarget(path);
|
|
if (!parsed.ok) throw parsed.error;
|
|
return { bytes, path, target: parsed.value };
|
|
}
|
|
|
|
function sha256(bytes) {
|
|
return createHash("sha256").update(bytes).digest("hex");
|
|
}
|