free-piAll articles

Reverse engineering

Inside a .NET tool: metadata, native imports and one method's CIL

A .NET program ships as an assembly: a file with metadata (names, types, references) and method bodies in CIL, the Common Intermediate Language. REA can read all of that statically, without loading or running the assembl

Published

A .NET program ships as an assembly: a file with metadata (names, types, references) and method bodies in CIL, the Common Intermediate Language. REA can read all of that statically, without loading or running the assembly. We pointed it at ICSharpCode.Decompiler.dll, the library inside ILSpy, and asked four questions: what is this assembly, which assemblies does it reference, which native (P/Invoke) functions does it declare, and what is the CIL of one small public method?

The target

ILSpy is MIT-licensed (https://github.com/icsharpcode/ILSpy). We used release v11.1:

  • Archive: ILSpy_windows_11.1.0.9782-x64.zip
  • Archive SHA-256: 37a9a19220f7621d9bc7ad9a00ed6f98e6a7a90f582165e52170e90b1b22138a (matches the digest GitHub lists for the release asset)
  • Analysed file: ICSharpCode.Decompiler.dll, 3,120,128 bytes
  • DLL SHA-256: d457bac6fcd0a6ee8f1bae086da2de2a2f830767506ddf1bb14abaa3a5ca7123 (also reported by REA)

We downloaded the Windows zip on a Mac. That works because REA only reads the file.

Requirements

  • curl and unzip.
  • Node.js that REA accepts: 22.19 or a later 22.x, 24.11 or a later 24.x, or 26 and later. We used 24.15.0. free-pi itself also needs Node.
  • A GitHub account and free-pi 0.4.5 or newer. No API key, no card. free-pi allows one active CLI session per account.
  • No .NET SDK is needed. REA does not run the assembly.

Commands

mkdir -p r05 && cd r05
curl -L -o ILSpy_windows_11.1.0.9782-x64.zip \
  https://github.com/icsharpcode/ILSpy/releases/download/v11.1/ILSpy_windows_11.1.0.9782-x64.zip
shasum -a 256 ILSpy_windows_11.1.0.9782-x64.zip
unzip -q ILSpy_windows_11.1.0.9782-x64.zip -d ilspy
shasum -a 256 ilspy/ICSharpCode.Decompiler.dll
npx free-pi-cli

Inside free-pi:

/reverse-engineer-anything
/mcp

When /mcp shows rea connected, send one prompt. Use the absolute path to the DLL in your own folder:

Use REA to inspect the .NET assembly /tmp/rea-demo/r05/ilspy/ICSharpCode.Decompiler.dll
with its managed-code tools (inspect_managed_artifact, inspect_managed_native_boundaries
and inspect_managed_members). Report (1) the assembly metadata (name, version, target
framework), (2) the referenced assemblies, (3) the native (P/Invoke) imports it declares,
if any, and (4) the CIL of one small public method of your choice. The assembly is large,
so pass type_names to inspect_managed_members to select only the one small type you pick.
Do not run the assembly and do not write scripts to parse it yourself; keep to these four
questions. Cite the tool output for each fact, and keep the final answer under 40 lines.

The prompt names the tools and tells the model to select one type. We added both after two earlier attempts went badly (see "What went wrong").

A recorded free-pi session inspecting ICSharpCode.Decompiler.dll with REA. Sped up 12x.

Sped up 12x with long pauses shortened. The recording starts at the /reverse-engineer-anything step; the session took about three minutes in real time.

The evidence

1. Assembly metadata (REA inspect_managed_artifact, summary detail):

  • Name ICSharpCode.Decompiler, version 11.1.0.9782, public key token d4bfe873e7598c49, flagged il-only and strong-name-signed.
  • Target framework .NETStandard,Version=v2.0, taken from the assembly's TargetFrameworkAttribute. REA classifies the runtime family as "unknown", because .NET Standard describes an API set and not a runtime.
  • Module MVID e9aaeffa-6973-4f54-9da9-565bb6a7b75b, metadata version v4.0.30319, 1,662 types, 16,259 methods and 6,971 fields.

2. Referenced assemblies (REA inspect_analysis_view, references collection, 5 of 5 rows):

  • netstandard 2.0.0.0, token cc7b13ffcd2ddd51
  • System.Memory 4.0.2.0, token cc7b13ffcd2ddd51
  • System.Reflection.Metadata 9.0.0.0, token b03f5f7f11d50a3a
  • System.Collections.Immutable 9.0.0.0, token b03f5f7f11d50a3a
  • System.Runtime.CompilerServices.Unsafe 6.0.0.0, token b03f5f7f11d50a3a

3. Native imports (REA inspect_managed_native_boundaries): one native module reference, libc, and two declared P/Invoke imports, both with no name mangling, no SetLastError, and the default winapi calling convention:

  • GetRealPath, imported as realpath (method token 0x06003662)
  • Free, imported as free (method token 0x06003663)

REA also listed 10 methods with no IL body: these 2 plus 8 delegate methods the runtime supplies. REA says these rows prove declarations only. It did not check that libc exports these functions, and neither did we.

4. CIL of one small public method (REA inspect_managed_members with type_names set to ICSharpCode.Decompiler.Util.NewLine). The model chose the public static method NewLine.GetDelimiterLength(char c, Func<char> func), token 0x060003de, a 65-byte body with 35 instructions. Its logic, as the model described it: if the character is a carriage return, ask the callback for the next character and return 2 for a CRLF pair, otherwise 1; if it is a line feed, NEL, vertical tab, form feed, line separator or paragraph separator, return 1; otherwise return 0. The model's listing (labels are hexadecimal byte offsets; branch targets are decimal byte offsets, so 22 means IL_0016):

Model reconstruction from REA's opcode anchors, not verbatim REA output. Lines marked * are reconstructed by the model (every ldarg.*, ldc.i4.0, ldc.i4.1, ldc.i4.2 and ret). Unmarked lines (constants, branches, the callvirt) are instructions REA reported as anchors with offsets.

IL_0000: ldarg.0*              IL_0016: ldarg.0*           IL_002d: ldarg.0*
IL_0001: ldc.i4.s 13          IL_0017: ldc.i4.s 10       IL_002e: ldc.i4 8232
IL_0003: bne.un.s 22          IL_0019: beq.s 61          IL_0033: beq.s 61
IL_0005: ldarg.1*              IL_001b: ldarg.0*           IL_0035: ldarg.0*
IL_0006: brfalse.s 20         IL_001c: ldc.i4 133        IL_0036: ldc.i4 8233
IL_0008: ldarg.1*              IL_0021: beq.s 61          IL_003b: bne.un.s 63
IL_0009: callvirt 0x0a000368  IL_0023: ldarg.0*           IL_003d: ldc.i4.1*
IL_000e: ldc.i4.s 10          IL_0024: ldc.i4.s 11       IL_003e: ret*
IL_0010: bne.un.s 20          IL_0026: beq.s 61          IL_003f: ldc.i4.0*
IL_0012: ldc.i4.2*             IL_0028: ldarg.0*           IL_0040: ret*
IL_0013: ret*                  IL_0029: ldc.i4.s 12
IL_0014: ldc.i4.1*             IL_002b: beq.s 61
IL_0015: ret*

The REA result gave the opcode counts for the whole method (35 instructions) and "anchors" with exact offsets for the instructions that carry an operand: the branches, the constants and the callvirt (token 0x0a000368; the listing shows that raw token; the model read it as a call to Invoke, which REA did not print). The model filled in the one-byte instructions between the anchors from those counts and checked that the sizes add up to 65 bytes. We did not compare the listing with a second disassembler such as ILSpy itself, so treat the listing as likely but not independently verified.

What went wrong

We made three attempts. Only the third is in the GIF.

  • Attempt 1 (226 credits): the final answer was cut off. The model used the right REA tools and found the facts above. Its final message ended after the first section with the text Response was truncated before completion. The model's earlier notes held the rest, but the final answer was not complete, so we did not use this run.
  • Attempt 2 (400 credits): the model started parsing the file by hand. Without a hint about which tool to use for the P/Invoke names, it wrote Python scripts to decode the metadata tables itself. They failed repeatedly with errors such as IndexError: list index out of range and TypeError: unhashable type: 'list'. We stopped this run during the recording when our own wait script exited early, so the run is incomplete. That is our mistake, not the model's, and the model kept working past the balance reading we took. REA has a tool for this question (inspect_managed_native_boundaries) and the model eventually found it in attempt 1, but not in attempt 2.
  • In the recorded attempt, inspect_managed_members still returned too much. Its call, even though the model had already selected ICSharpCode.Decompiler.Util.NewLine with type_names, produced a result of about 783,000 tokens that the CLI truncated (Warning: truncated output). The assembly-wide member references are included whatever you select. The model then paged the retained evidence with inspect_analysis_view instead, which worked.
  • It also ran a few ordinary shell commands (ls, file, and grep on ILSpy's XML documentation file to find candidate type names) while looking for a type. Those facts are not from REA, and we did not use them above.
  • The model's notes were long and contained arithmetic by hand. It worked out which flag bits mean "public" and recomputed instruction offsets in its visible reasoning. Check the evidence, not the narration.
  • The final listing has a mixed numbering style, which we explain above, and the model's footnote about it is hard to read.

What it cost

/usage before and after the recorded run (the first number is read inside the recording, in the part we trimmed from the GIF):

12,971 / 15,000 credits left today
12,686 / 15,000 credits left today

That is 285 credits for the recorded run. Across all three attempts the balance went from 13,617 to 12,686, which is 931 credits: 226 for attempt 1, 400 for attempt 2, 20 more that attempt 2 consumed after our balance snapshot (the model was still working when we stopped it, and /usage itself runs no model turn), and 285 for attempt 3.

Use REA responsibly

Use REA only for lawful, authorized analysis: your own apps, open-source software, CTFs, and security research.

Tool output goes to the model provider, and free-pi stores traces of sessions. Do not analyse files you are not allowed to share. ILSpy is open source, which is why we used it.

We build free-pi, and an AI model produced the analysis shown above.

free-pi is in early testing; features and limits may change.


Series: Use REA (Reverse Engineer Anything) free: no API key, no card, What's really inside this Go CLI?, What does your app store? Reading an app's SQLite database, Is your Electron app hardened? Reading its fuses and IPC surface.