Files
reverse-engineering-anything/src/dotnet/ManagedMethodBodyReader.ts
T

194 lines
5.8 KiB
TypeScript

import type { ManagedPeLayout } from "./ManagedPeReader.js";
import type {
ManagedMemberInspectionLimits,
ManagedMethodBody,
} from "./ManagedMemberInspectorCore.js";
import {
decodeInstructions,
parseExceptionRegions,
} from "./ManagedMemberInstructionDecoder.js";
import { sha256Bytes } from "./ManagedMetadataHeaps.js";
interface MethodBodyHeader {
readonly format: "tiny" | "fat";
readonly size: number;
readonly flags: number;
readonly maxStack: number;
readonly ilSize: number;
readonly localSig: number;
}
const readMethodBodyHeader = (
bytes: Buffer,
offset: number,
): MethodBodyHeader => {
const first = bytes.readUInt8(offset);
if ((first & 3) === 2)
return {
format: "tiny",
size: 1,
flags: 0,
maxStack: 8,
ilSize: first >> 2,
localSig: 0,
};
if ((first & 3) !== 3) throw new RangeError("Unsupported method body header");
if (offset > bytes.length - 12)
throw new RangeError("Fat method body header leaves artifact");
const flagsAndSize = bytes.readUInt16LE(offset);
const headerDwords = flagsAndSize >>> 12;
if (headerDwords < 3)
throw new RangeError("Fat method body header is smaller than 12 bytes");
const size = headerDwords * 4;
if (offset > bytes.length - size)
throw new RangeError("Method body header leaves artifact");
return {
format: "fat",
size,
flags: flagsAndSize & 0x0fff,
maxStack: bytes.readUInt16LE(offset + 2),
ilSize: bytes.readUInt32LE(offset + 4),
localSig: bytes.readUInt32LE(offset + 8),
};
};
export const methodBody = (
bytes: Buffer,
pe: ManagedPeLayout,
rva: number,
limits: ManagedMemberInspectionLimits,
): ManagedMethodBody => {
if (rva === 0) return emptyMethodBody(rva, "absent", null);
try {
const offset = pe.rvaToOffset(rva, 1, "method.body");
const header = readMethodBodyHeader(bytes, offset);
if (header.ilSize > limits.maxMethodBodyBytes)
return oversizedMethodBody(header, rva, offset, limits);
const ilOffset = offset + header.size;
if (ilOffset > bytes.length - header.ilSize)
throw new RangeError("Method IL bytes leave artifact");
const il = bytes.subarray(ilOffset, ilOffset + header.ilSize);
const decoded = decodeInstructions(il, limits.maxMethodInstructions);
const opcodeCounts: Record<string, number> = {};
for (const instruction of decoded.parsed)
opcodeCounts[instruction.opcode] =
(opcodeCounts[instruction.opcode] ?? 0) + 1;
const anchors = decoded.parsed
.map((instruction, index) => ({ instruction, index }))
.filter(({ instruction }) => instruction.operandKind !== "none")
.slice(0, limits.instructionAnchorLimit)
.map(({ instruction }) => ({
il_offset: instruction.offset,
opcode: instruction.opcode,
operand_kind: instruction.operandKind,
operand: instruction.operand,
}));
const normalized = Buffer.from(
JSON.stringify(
decoded.parsed.map(({ opcode, operandKind, operand }) => [
opcode,
operandKind,
operand,
]),
),
"utf8",
);
const methodEnd = ilOffset + header.ilSize;
const sectionOffset = (methodEnd + 3) & ~3;
const status =
decoded.issue !== null
? "malformed"
: decoded.truncated > 0
? "partial"
: "present";
return {
status,
header_format: header.format,
rva,
file_offset: offset,
max_stack: header.maxStack,
init_locals: (header.flags & 0x10) !== 0,
local_var_sig_token:
header.localSig === 0
? null
: `0x${header.localSig.toString(16).padStart(8, "0")}`,
il_size: header.ilSize,
il_sha256: sha256Bytes(il),
normalized_il_sha256:
status === "present" ? sha256Bytes(normalized) : null,
instruction_count: decoded.count,
decoded_instruction_count: decoded.parsed.length,
truncated_instructions: decoded.truncated,
opcode_counts: opcodeCounts,
anchors,
exception_regions:
header.format === "fat" && (header.flags & 8) !== 0
? parseExceptionRegions(bytes, sectionOffset, bytes.length)
: [],
issue:
decoded.issue ??
(decoded.truncated > 0
? `Instruction decode reached max_method_instructions ${String(limits.maxMethodInstructions)} before the end of the method body`
: null),
};
} catch (cause: unknown) {
return emptyMethodBody(
rva,
"malformed",
cause instanceof Error ? cause.message : "Method body parse failed",
);
}
};
const emptyMethodBody = (
rva: number,
status: "absent" | "malformed",
issue: string | null,
): ManagedMethodBody => ({
status,
header_format: status === "absent" ? "none" : "unknown",
rva,
file_offset: null,
max_stack: null,
init_locals: null,
local_var_sig_token: null,
il_size: 0,
il_sha256: null,
normalized_il_sha256: null,
instruction_count: 0,
decoded_instruction_count: 0,
truncated_instructions: 0,
opcode_counts: {},
anchors: [],
exception_regions: [],
issue,
});
const oversizedMethodBody = (
header: MethodBodyHeader,
rva: number,
offset: number,
limits: ManagedMemberInspectionLimits,
): ManagedMethodBody => ({
status: "too-large",
header_format: header.format,
rva,
file_offset: offset,
max_stack: header.maxStack,
init_locals: (header.flags & 0x10) !== 0,
local_var_sig_token:
header.localSig === 0
? null
: `0x${header.localSig.toString(16).padStart(8, "0")}`,
il_size: header.ilSize,
il_sha256: null,
normalized_il_sha256: null,
instruction_count: 0,
decoded_instruction_count: 0,
truncated_instructions: 0,
opcode_counts: {},
anchors: [],
exception_regions: [],
issue: `Method body exceeds max_method_body_bytes ${String(limits.maxMethodBodyBytes)}`,
});