Files
craft-anything/crates/testkit/src/managed.rs
Brandon Thomas f64ae3bf3c Craft Anything
A native, pure-Rust app that shows anyone how a piece of software works, with
evidence-backed answers, then helps them rebuild it. It ships a desktop app, a
CLI and an MCP server, all driven by one engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:52:14 -04:00

471 lines
17 KiB
Rust

//! A synthetic .NET assembly writer: a PE32 DLL with a CLI header, IL method bodies (tiny and
//! fat, with locals and a finally clause), an embedded resource and ECMA-335 metadata
//! (`#~`, `#Strings`, `#US`, `#GUID`, `#Blob`).
//!
//! The sample (`Sample.dll`, assembly `Sample` 1.2.3.4, TargetFramework `.NETCoreApp,Version=v8.0`):
//!
//! ```text
//! namespace Sample {
//! public class Calculator {
//! private int _total;
//! public int Add(int a, int b) => a + b; // v2: a - b
//! public void Accumulate(int x) { _total += x; Console.WriteLine("hi"); try { Add(1, 2); } finally {} }
//! public static T Identity<T>(T value) => value;
//! public Calculator() {}
//! public int Multiply(int a, int b) => a * b; // v2 only
//! public class Options { public bool Verbose; }
//! }
//! public static class NativeMethods {
//! [DllImport("craft", SetLastError = true)] public static extern int helper(int n);
//! [DllImport("craft", EntryPoint = "missing_fn", CharSet = Ansi)] public static extern void MissingFn(); // v1 only
//! [MethodImpl(InternalCall)] public static extern long GetTicks();
//! }
//! }
//! ```
use std::collections::BTreeMap;
pub const ASSEMBLY_NAME: &str = "Sample";
pub const TFM: &str = ".NETCoreApp,Version=v8.0";
pub const NATIVE_MODULE: &str = "craft";
#[derive(Default)]
struct Heaps {
strings: Vec<u8>,
blob: Vec<u8>,
us: Vec<u8>,
guid: Vec<u8>,
}
fn compressed(n: usize) -> Vec<u8> {
if n < 0x80 {
vec![n as u8]
} else if n < 0x4000 {
vec![0x80 | (n >> 8) as u8, n as u8]
} else {
vec![0xc0 | (n >> 24) as u8, (n >> 16) as u8, (n >> 8) as u8, n as u8]
}
}
fn ser_string(s: &str) -> Vec<u8> {
let mut v = compressed(s.len());
v.extend_from_slice(s.as_bytes());
v
}
impl Heaps {
fn new() -> Self {
Heaps { strings: vec![0], blob: vec![0], us: vec![0], guid: Vec::new() }
}
fn s(&mut self, x: &str) -> u32 {
if x.is_empty() {
return 0;
}
let at = self.strings.len() as u32;
self.strings.extend_from_slice(x.as_bytes());
self.strings.push(0);
at
}
fn b(&mut self, data: &[u8]) -> u32 {
if data.is_empty() {
return 0;
}
let at = self.blob.len() as u32;
self.blob.extend(compressed(data.len()));
self.blob.extend_from_slice(data);
at
}
fn u(&mut self, s: &str) -> u32 {
let at = self.us.len() as u32;
let units: Vec<u16> = s.encode_utf16().collect();
self.us.extend(compressed(units.len() * 2 + 1));
for u in units {
self.us.extend_from_slice(&u.to_le_bytes());
}
self.us.push(0);
at
}
}
/// Column widths for the tables the sample uses (small tables: 2-byte indexes). `3` marks a
/// heap index, written with 2 or 4 bytes depending on the heap-size flags.
fn widths(table: u8) -> &'static [usize] {
match table {
0x00 => &[2, 3, 3, 3, 3],
0x01 => &[2, 3, 3],
0x02 => &[4, 3, 3, 2, 2, 2],
0x04 => &[2, 3, 3],
0x05 => &[2],
0x06 => &[4, 2, 2, 3, 3, 2],
0x08 => &[2, 2, 3],
0x0a => &[2, 3, 3],
0x0c => &[2, 2, 3],
0x11 => &[3],
0x1a => &[3],
0x1c => &[2, 2, 3, 2],
0x20 => &[4, 2, 2, 2, 2, 4, 3, 3, 3],
0x23 => &[2, 2, 2, 2, 4, 3, 3, 3, 3],
0x28 => &[4, 4, 3, 2],
0x29 => &[2, 2],
0x2a => &[2, 2, 2, 3],
_ => panic!("table {table:#x} not supported by the writer"),
}
}
fn pad4(v: &mut Vec<u8>) {
while !v.len().is_multiple_of(4) {
v.push(0);
}
}
fn tiny(code: &[u8]) -> Vec<u8> {
let mut v = vec![((code.len() as u8) << 2) | 2];
v.extend_from_slice(code);
v
}
/// Build the sample assembly. `version` 1 or 2 (2 changes `Add`, adds `Multiply`, drops `MissingFn`).
pub fn managed_assembly(version: u8) -> Vec<u8> {
build(version, false, false)
}
/// The sample with an uncompressed `#-` tables stream whose method lists go through a MethodPtr
/// table (identity order).
pub fn managed_assembly_with_ptr_tables(version: u8) -> Vec<u8> {
build(version, false, true)
}
/// The same sample with 4-byte heap indexes and the extra-data flag (HeapSizes `0x47`).
pub fn managed_assembly_wide_heaps(version: u8) -> Vec<u8> {
build(version, true, false)
}
fn build(version: u8, wide: bool, ptr: bool) -> Vec<u8> {
let v2 = version >= 2;
let mut h = Heaps::new();
let mut tables: BTreeMap<u8, Vec<Vec<u32>>> = BTreeMap::new();
let mut add = |t: u8, row: Vec<u32>| tables.entry(t).or_default().push(row);
// Method bodies, laid out after the 72-byte CLI header at .text+0x48.
const TEXT_RVA: u32 = 0x2000;
let mut bodies: Vec<u8> = Vec::new();
let body_base = TEXT_RVA + 0x48;
let put_body = |b: Vec<u8>, bodies: &mut Vec<u8>| -> u32 {
pad4(bodies);
let rva = body_base + bodies.len() as u32;
bodies.extend(b);
rva
};
let add_rva = put_body(tiny(&[0x03, 0x04, if v2 { 0x59 } else { 0x58 }, 0x2a]), &mut bodies);
// Accumulate: fat header, one local, a finally clause.
let mut acc_code = vec![0x02, 0x02, 0x7b, 1, 0, 0, 4, 0x03, 0x58, 0x7d, 1, 0, 0, 4, 0x72, 1, 0, 0, 0x70, 0x28, 1, 0, 0, 0x0a];
acc_code.extend([0x02, 0x17, 0x18, 0x28, 1, 0, 0, 6, 0x0a, 0xde, 0x01, 0xdc, 0x2a]);
assert_eq!(acc_code.len(), 37);
let mut acc = Vec::new();
acc.extend_from_slice(&0x301bu16.to_le_bytes());
acc.extend_from_slice(&3u16.to_le_bytes());
acc.extend_from_slice(&(acc_code.len() as u32).to_le_bytes());
acc.extend_from_slice(&0x1100_0001u32.to_le_bytes());
acc.extend_from_slice(&acc_code);
pad4(&mut acc);
acc.extend([0x01, 16, 0, 0]);
acc.extend_from_slice(&2u16.to_le_bytes());
acc.extend_from_slice(&24u16.to_le_bytes());
acc.push(11);
acc.extend_from_slice(&35u16.to_le_bytes());
acc.push(1);
acc.extend_from_slice(&0u32.to_le_bytes());
let acc_rva = put_body(acc, &mut bodies);
let identity_rva = put_body(tiny(&[0x02, 0x2a]), &mut bodies);
let ctor_rva = put_body(tiny(&[0x02, 0x28, 2, 0, 0, 0x0a, 0x2a]), &mut bodies);
let mul_rva = put_body(tiny(&[0x03, 0x04, 0x5a, 0x2a]), &mut bodies);
pad4(&mut bodies);
let res_rva = body_base + bodies.len() as u32;
let mut resources = 5u32.to_le_bytes().to_vec();
resources.extend_from_slice(b"hello");
pad4(&mut resources);
// Module and references.
let n = h.s("Sample.dll");
h.guid.extend_from_slice(&[0x11; 16]);
add(0x00, vec![0, n, 1, 0, 0]);
let object = h.s("Object");
let system = h.s("System");
add(0x01, vec![(1 << 2) | 2, object, system]);
let console = h.s("Console");
add(0x01, vec![(2 << 2) | 2, console, system]);
let tfa = h.s("TargetFrameworkAttribute");
let versioning = h.s("System.Runtime.Versioning");
add(0x01, vec![(1 << 2) | 2, tfa, versioning]);
// Methods (rows in order) and their params.
struct M {
name: &'static str,
flags: u32,
impl_flags: u32,
rva: u32,
sig: Vec<u8>,
params: Vec<&'static str>,
}
let mut calc_methods = vec![
M { name: "Add", flags: 0x86, impl_flags: 0, rva: add_rva, sig: vec![0x20, 2, 0x08, 0x08, 0x08], params: vec!["a", "b"] },
M { name: "Accumulate", flags: 0x86, impl_flags: 0, rva: acc_rva, sig: vec![0x20, 1, 0x01, 0x08], params: vec!["x"] },
M { name: "Identity", flags: 0x96, impl_flags: 0, rva: identity_rva, sig: vec![0x10, 1, 1, 0x1e, 0, 0x1e, 0], params: vec!["value"] },
M { name: ".ctor", flags: 0x1886, impl_flags: 0, rva: ctor_rva, sig: vec![0x20, 0, 0x01], params: vec![] },
];
if v2 {
calc_methods.push(M { name: "Multiply", flags: 0x86, impl_flags: 0, rva: mul_rva, sig: vec![0x20, 2, 0x08, 0x08, 0x08], params: vec!["a", "b"] });
}
let mut native_methods = vec![M { name: "helper", flags: 0x2096, impl_flags: 0x80, rva: 0, sig: vec![0, 1, 0x08, 0x08], params: vec!["n"] }];
if !v2 {
native_methods.push(M { name: "MissingFn", flags: 0x2096, impl_flags: 0x80, rva: 0, sig: vec![0, 0, 0x01], params: vec![] });
}
native_methods.push(M { name: "GetTicks", flags: 0x96, impl_flags: 0x1000, rva: 0, sig: vec![0, 0, 0x0a], params: vec![] });
let calc_first = 1u32;
let native_first = calc_first + calc_methods.len() as u32;
let total_methods = native_first + native_methods.len() as u32 - 1;
let mut param_row = 1u32;
let mut method_rows = Vec::new();
for m in calc_methods.iter().chain(native_methods.iter()) {
let name = h.s(m.name);
let sig = h.b(&m.sig);
method_rows.push(vec![m.rva, m.impl_flags, m.flags, name, sig, param_row]);
for (i, p) in m.params.iter().enumerate() {
let pn = h.s(p);
add(0x08, vec![0, i as u32 + 1, pn]);
param_row += 1;
}
}
for (i, r) in method_rows.into_iter().enumerate() {
add(0x06, r);
if ptr {
add(0x05, vec![i as u32 + 1]);
}
}
// Types.
let module_name = h.s("<Module>");
add(0x02, vec![0, module_name, 0, 0, 1, 1]);
let calc = h.s("Calculator");
let sample = h.s("Sample");
add(0x02, vec![0x0010_0001, calc, sample, (1 << 2) | 1, 1, calc_first]);
let nm = h.s("NativeMethods");
add(0x02, vec![0x181, nm, sample, (1 << 2) | 1, 2, native_first]);
let opts = h.s("Options");
add(0x02, vec![0x2, opts, 0, (1 << 2) | 1, 2, total_methods + 1]);
// Fields.
let total = h.s("_total");
let fsig = h.b(&[0x06, 0x08]);
add(0x04, vec![0x1, total, fsig]);
let verbose = h.s("Verbose");
let bsig = h.b(&[0x06, 0x02]);
add(0x04, vec![0x6, verbose, bsig]);
// Member references.
let wl = h.s("WriteLine");
let wls = h.b(&[0, 1, 0x01, 0x0e]);
add(0x0a, vec![(2 << 3) | 1, wl, wls]);
let ctor = h.s(".ctor");
let ctor_sig = h.b(&[0x20, 0, 0x01]);
add(0x0a, vec![(1 << 3) | 1, ctor, ctor_sig]);
let tfa_sig = h.b(&[0x20, 1, 0x01, 0x0e]);
add(0x0a, vec![(3 << 3) | 1, ctor, tfa_sig]);
// [assembly: TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]
let mut ca = vec![1, 0];
ca.extend(ser_string(TFM));
ca.extend([1, 0, 0x54, 0x0e]);
ca.extend(ser_string("FrameworkDisplayName"));
ca.extend(ser_string(".NET 8.0"));
let cab = h.b(&ca);
add(0x0c, vec![(1 << 5) | 14, (3 << 3) | 3, cab]);
let locals = h.b(&[0x07, 1, 0x08]);
add(0x11, vec![locals]);
let craft = h.s(NATIVE_MODULE);
add(0x1a, vec![craft]);
let helper = h.s("helper");
add(0x1c, vec![0x0140, (native_first << 1) | 1, helper, 1]);
if !v2 {
let missing = h.s("missing_fn");
add(0x1c, vec![0x0102, ((native_first + 1) << 1) | 1, missing, 1]);
}
let asm = h.s(ASSEMBLY_NAME);
add(0x20, vec![0x8004, 1, 2, 3, 4, 0, 0, asm, 0]);
let token_blob = h.b(&[0xb0, 0x3f, 0x5f, 0x7f, 0x11, 0xd5, 0x0a, 0x3a]);
let sr = h.s("System.Runtime");
add(0x23, vec![8, 0, 0, 0, 0, token_blob, sr, 0, 0]);
let sc = h.s("System.Console");
add(0x23, vec![8, 0, 0, 0, 0, token_blob, sc, 0, 0]);
let resn = h.s("Sample.Strings.resources");
add(0x28, vec![0, 1, resn, 0]);
add(0x29, vec![4, 2]);
let tname = h.s("T");
add(0x2a, vec![0, 0, (3 << 1) | 1, tname]);
h.u("hi");
// #~ stream.
let mut ts = vec![0, 0, 0, 0, 2, 0, if wide { 0x47 } else { 0 }, 1];
let valid: u64 = tables.keys().fold(0, |m, &t| m | (1u64 << t));
ts.extend_from_slice(&valid.to_le_bytes());
ts.extend_from_slice(&0u64.to_le_bytes());
for rows in tables.values() {
ts.extend_from_slice(&(rows.len() as u32).to_le_bytes());
}
if wide {
ts.extend_from_slice(&0xdead_beefu32.to_le_bytes());
}
for (&t, rows) in &tables {
for row in rows {
assert_eq!(row.len(), widths(t).len(), "table {t:#x}");
for (v, w) in row.iter().zip(widths(t)) {
let w = if *w == 3 { if wide { 4 } else { 2 } } else { *w };
if w == 2 {
ts.extend_from_slice(&(*v as u16).to_le_bytes());
} else {
ts.extend_from_slice(&v.to_le_bytes());
}
}
}
}
pad4(&mut ts);
for heap in [&mut h.strings, &mut h.blob, &mut h.us, &mut h.guid] {
pad4(heap);
}
// Metadata root.
let streams: [(&str, &Vec<u8>); 5] =
[(if ptr { "#-" } else { "#~" }, &ts), ("#Strings", &h.strings), ("#US", &h.us), ("#GUID", &h.guid), ("#Blob", &h.blob)];
let mut header_len = 16 + 12 + 4;
for (name, _) in &streams {
header_len += 8 + (name.len() + 1).div_ceil(4) * 4;
}
let mut md = Vec::new();
md.extend_from_slice(b"BSJB");
md.extend_from_slice(&1u16.to_le_bytes());
md.extend_from_slice(&1u16.to_le_bytes());
md.extend_from_slice(&0u32.to_le_bytes());
md.extend_from_slice(&12u32.to_le_bytes());
md.extend_from_slice(b"v4.0.30319\0\0");
md.extend_from_slice(&0u16.to_le_bytes());
md.extend_from_slice(&(streams.len() as u16).to_le_bytes());
let mut off = header_len;
for (name, data) in &streams {
md.extend_from_slice(&(off as u32).to_le_bytes());
md.extend_from_slice(&(data.len() as u32).to_le_bytes());
let mut nb = name.as_bytes().to_vec();
nb.push(0);
pad4(&mut nb);
md.extend(nb);
off += data.len();
}
assert_eq!(md.len(), header_len);
for (_, data) in &streams {
md.extend_from_slice(data);
}
// .text: CLI header, bodies, resources, metadata.
let md_rva = res_rva + resources.len() as u32;
let mut text = Vec::new();
text.extend_from_slice(&72u32.to_le_bytes());
text.extend_from_slice(&2u16.to_le_bytes());
text.extend_from_slice(&5u16.to_le_bytes());
text.extend_from_slice(&md_rva.to_le_bytes());
text.extend_from_slice(&(md.len() as u32).to_le_bytes());
text.extend_from_slice(&1u32.to_le_bytes()); // ILONLY
text.extend_from_slice(&0u32.to_le_bytes()); // entry point (a library)
text.extend_from_slice(&res_rva.to_le_bytes());
text.extend_from_slice(&(resources.len() as u32).to_le_bytes());
text.resize(72, 0);
text.extend(bodies);
text.extend(resources);
text.extend(md);
let raw_size = text.len().div_ceil(0x200) * 0x200;
let mut f = vec![0u8; 0x200];
f[0] = b'M';
f[1] = b'Z';
f[0x3c..0x40].copy_from_slice(&0x80u32.to_le_bytes());
f[0x80..0x84].copy_from_slice(b"PE\0\0");
let coff = 0x84;
f[coff..coff + 2].copy_from_slice(&0x14cu16.to_le_bytes());
f[coff + 2..coff + 4].copy_from_slice(&1u16.to_le_bytes());
f[coff + 16..coff + 18].copy_from_slice(&224u16.to_le_bytes());
f[coff + 18..coff + 20].copy_from_slice(&0x2102u16.to_le_bytes());
let opt = coff + 20;
f[opt..opt + 2].copy_from_slice(&0x10bu16.to_le_bytes());
f[opt + 28..opt + 32].copy_from_slice(&0x40_0000u32.to_le_bytes());
f[opt + 32..opt + 36].copy_from_slice(&0x2000u32.to_le_bytes());
f[opt + 36..opt + 40].copy_from_slice(&0x200u32.to_le_bytes());
f[opt + 56..opt + 60].copy_from_slice(&(TEXT_RVA + 0x2000 * (raw_size as u32).div_ceil(0x2000)).to_le_bytes());
f[opt + 60..opt + 64].copy_from_slice(&0x200u32.to_le_bytes());
f[opt + 68..opt + 70].copy_from_slice(&3u16.to_le_bytes());
f[opt + 92..opt + 96].copy_from_slice(&16u32.to_le_bytes());
let dd14 = opt + 96 + 14 * 8;
f[dd14..dd14 + 4].copy_from_slice(&TEXT_RVA.to_le_bytes());
f[dd14 + 4..dd14 + 8].copy_from_slice(&72u32.to_le_bytes());
let sec = opt + 224;
f[sec..sec + 5].copy_from_slice(b".text");
f[sec + 8..sec + 12].copy_from_slice(&(text.len() as u32).to_le_bytes());
f[sec + 12..sec + 16].copy_from_slice(&TEXT_RVA.to_le_bytes());
f[sec + 16..sec + 20].copy_from_slice(&(raw_size as u32).to_le_bytes());
f[sec + 20..sec + 24].copy_from_slice(&0x200u32.to_le_bytes());
f[sec + 36..sec + 40].copy_from_slice(&0x6000_0020u32.to_le_bytes());
text.resize(raw_size, 0);
f.extend(text);
f
}
/// Decompiled-looking C# for the v1 sample, with one deliberate mismatch (`Add` takes `long`) and
/// one extra method (`Reset`) so reconstruction checks have something to find.
pub const RECONSTRUCTION_CS: &str = r#"
// Reconstructed by hand for tests.
using System;
using System.Runtime.InteropServices;
namespace Sample
{
public class Calculator
{
private int _total;
public int Add(long a, int b) { return (int)a + b; }
public void Accumulate(int x)
{
_total += x;
Console.WriteLine("hi { not a brace");
try { Add(1, 2); } finally { }
}
public static T Identity<T>(T value) => value;
public Calculator() { }
public void Reset() { _total = 0; }
public class Options
{
public bool Verbose;
}
}
public static class NativeMethods
{
[DllImport("craft", SetLastError = true)]
public static extern int helper(int n);
[DllImport("craft", EntryPoint = "missing_fn", CharSet = CharSet.Ansi)]
public static extern void MissingFn();
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.InternalCall)]
public static extern long GetTicks();
}
}
"#;