rea-reverse

Reverse Engineer a Windows EXE or DLL with AI, Ghidra and REA

Written for REA 6.4.0Updated 8 min read
On this page

Short version: REA, the open-source project by morluto, analyzes native Windows PE files through a headless Ghidra provider, plus a PE resource parser that needs no engine at all. You don’t need a Windows machine to do this: Ghidra on Linux or macOS accepts x86 and x86-64 PE files. On Windows itself, REA 6.4.0’s Ghidra support is an experimental, read-only P0 with a narrow admission boundary. This guide builds a small DLL and EXE from source, then walks through PE triage (identity, exports, imports, resources, strings, decompilation) from the CLI and from an AI agent.

It is for developers who want to understand a PE file they are allowed to analyze: their own build, a dependency, or an open-source release. It assumes REA and Ghidra are already set up as in REA + Ghidra for native binaries. Skim how to verify a genuine REA install first. REA is distributed through npm as rea-agents and is never a standalone .exe download.

Where to run the analysis

Pick the host first, because it decides which REA features you get. These are the 6.4.0 facts, taken from the provider code in the published package:

Linux / macOS host Windows x64 host (P0)
Status Supported Experimental
PE targets Executables whose architecture is x86, x86-64, ARM or ARM64 (x86-64 PE is the verified cross-format case) Native x86 and x86-64 PE applications and DLLs only
Managed (.NET) PE in Ghidra Use the managed tools instead Refused
Renaming / annotations Yes (set_addresses_names, annotate_native_function) No: database mutation is disabled
No-return repair after analysis Yes Disabled
Target location Any local path Fixed local NTFS volume. UNC paths, reparse points, alternate streams and other filesystems are rejected
Extra requirements None Windows 10+, the matching Windows x64 package with its bundled native addon
capture-process (runtime) Linux, macOS Unavailable on native Windows. REA points you to Linux/WSL for Linux commands

The practical recommendation: analyze PE files on Linux or macOS when you can, and use the Windows P0 only when the file can’t leave a Windows machine. The static results are the same kind of Evidence either way.

Prerequisites

  • REA 6.4.0 (npm install --global rea-agents) with Ghidra 12.1.x and a 64-bit JDK 21+. rea doctor --provider ghidra --json must pass.
  • A Windows-targeting C toolchain: MinGW-w64 (x86_64-w64-mingw32-gcc, windres) on Linux/macOS, or MSVC (cl, rc) on Windows.
  • An MCP client with REA registered (rea setup), if you want the agent half.

On Windows, set the paths in PowerShell before running doctor and setup:

$env:GHIDRA_INSTALL_DIR = "C:\tools\ghidra_12.1.4_PUBLIC"
$env:JAVA_HOME = "C:\tools\jdk-21"
rea doctor --provider ghidra --json
rea setup

Keep these paths, and your target paths, free of " % ^ ! & | < >. The 6.4.0 Windows launcher refuses command-interpreter metacharacters in any path it passes to Ghidra’s batch script.

Step 1: Build a practice DLL and EXE

A target you wrote yourself gives you an answer key for every claim the agent makes. This DLL has two exports, imports a few Win32 file APIs, and carries a version resource and a string table.

greet.c:

#include <windows.h>
#include <string.h>

__declspec(dllexport) unsigned greet_checksum(const char *name) {
    unsigned h = 2166136261u;                 /* FNV-1a */
    for (; *name; name++) { h ^= (unsigned char)*name; h *= 16777619u; }
    return h;
}

__declspec(dllexport) int greet_write_log(const char *path, const char *name) {
    HANDLE f = CreateFileA(path, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS,
                           FILE_ATTRIBUTE_NORMAL, NULL);
    if (f == INVALID_HANDLE_VALUE) return 0;
    DWORD written = 0;
    const char *prefix = "greet-log: hello, ";
    WriteFile(f, prefix, (DWORD)strlen(prefix), &written, NULL);
    WriteFile(f, name, (DWORD)strlen(name), &written, NULL);
    CloseHandle(f);
    return 1;
}

greet.rc:

#include <winver.h>
1 VERSIONINFO
FILEVERSION 1,2,0,0
PRODUCTVERSION 1,2,0,0
BEGIN
  BLOCK "StringFileInfo"
  BEGIN
    BLOCK "040904B0"
    BEGIN
      VALUE "FileDescription", "Greet demo library"
      VALUE "FileVersion", "1.2.0.0"
      VALUE "ProductName", "REA practice target"
    END
  END
  BLOCK "VarFileInfo"
  BEGIN
    VALUE "Translation", 0x409, 1200
  END
END

STRINGTABLE
BEGIN
  101 "Greeting log written"
END

main.c:

#include <stdio.h>
__declspec(dllimport) unsigned greet_checksum(const char *name);
__declspec(dllimport) int greet_write_log(const char *path, const char *name);

int main(int argc, char **argv) {
    const char *who = argc > 1 ? argv[1] : "world";
    printf("checksum=%08x\n", greet_checksum(who));
    return greet_write_log("greet.log", who) ? 0 : 1;
}

Build with MinGW-w64 (Linux/macOS):

x86_64-w64-mingw32-windres greet.rc -O coff -o greet_res.o
x86_64-w64-mingw32-gcc -O1 -shared -o greet.dll greet.c greet_res.o -Wl,--out-implib,libgreet.a
x86_64-w64-mingw32-gcc -O1 -o hello.exe main.c -L. -lgreet
DLL="$PWD/greet.dll"; EXE="$PWD/hello.exe"

Or with MSVC in a Developer PowerShell:

rc greet.rc
cl /O1 /LD greet.c greet.res
cl /O1 main.c greet.lib

For a larger open-source target afterwards, the official 7-Zip Windows binaries are a good choice: 7-Zip is LGPL, and you can read its source to check the agent’s conclusions.

Step 2: Identify the file before choosing a route

A .dll extension says nothing about what’s inside. The file could be native code, a .NET assembly, or a NativeAOT image. Ask REA’s managed parser first. It reads headers only and never executes anything:

rea inspect-managed-artifact "$DLL" --json

For our native DLL the classification should come back as not managed. If it reports a managed assembly, stop here and follow .NET assembly static analysis. The Windows P0 refuses managed PE in Ghidra anyway. A NativeAOT image is native code, so it stays on this route, with the extra metadata steps described in that .NET guide.

Then get the native overview:

rea analyze "$DLL" --provider ghidra --json > greet-overview.json

The overview lists segments (the PE sections as Ghidra mapped them, with start, end and length), the procedure count and the string count. Note the image base: Ghidra normally maps the PE headers as the lowest segment, starting at the image base. You will need it to turn an RVA into an address in step 4.

Step 3: Exports and imports

REA 6.4.0 has no dedicated “list exports” CLI command. The information comes from Ghidra’s inventories, which REA returns with exact flags:

  • Imports appear in the procedure inventory as functions with external: true, usually with local thunks (thunk: true and a thunk_target). They also appear in the name inventory as external symbols.
  • Exports are the external entry points. inspect-native-load-image reports them as entry_points, alongside the PE entry routine.
rea inspect-native-load-image "$DLL" --provider ghidra --json > greet-load.json
rea search "$DLL" greet_ --kind procedures --provider ghidra --json
rea search "$DLL" CreateFile --kind procedures --provider ghidra --json

Two notes. First, for PE files inspect-native-load-image returns its measurements with an unsupported attestation status. That means REA did not independently re-verify the PE load image. It does not mean the file failed to load. Second, Ghidra qualifies imported names with their library namespace. Copy names exactly as the inventory returns them before passing them to xrefs.

Now ask the question that matters most for a DLL: which exports reach which imports?

rea xrefs "$DLL" 0xCREATEFILEA_THUNK_ADDRESS --provider ghidra --json
rea function "$DLL" greet_write_log --provider ghidra --json

The first command should lead back to greet_write_log. The function dossier lists its callees (CreateFileA, WriteFile, CloseHandle), the strings it references and its pseudocode, each with Evidence. inspect-native-api goes one step further. It analyzes the API boundary of one function and returns a confidence level, Evidence, any jump-table dispatch mappings, unsupported branches and residual unknowns:

rea inspect-native-api "$DLL" greet_write_log --provider ghidra --json

Step 4: Resources

PE resources hold version information, icons, manifests, dialogs and string tables. They are often the quickest way to learn what a binary claims to be. REA 6.4.0’s inspect-pe-resources parses the resource tree directly. It needs no Ghidra, no Windows SDK and no execution, and it works on any host:

rea inspect-pe-resources "$DLL" --json > greet-res.json

For every resource it returns the type, name and language identities (numeric IDs, or original UTF-16 names), the file locations of the directory and data entries, and the payload’s RVA, file range and SHA-256. Icon groups are linked to their candidate image resources. Defaults are a 64 MiB read budget (--max-file-bytes) and 4,096 directory entries (--max-entries). If either is exceeded, the command fails without a partial result.

Know what it does not do: it doesn’t decode payloads. You’ll see a type-16 (RT_VERSION) resource with a file range, but not “Greet demo library”. To read the contents, use the payload range it reported:

xxd -s PAYLOAD_OFFSET -l PAYLOAD_BYTES greet.dll     # version strings are UTF-16LE
rea read-bytes "$DLL" 0xIMAGE_BASE_PLUS_RVA --length 256 --provider ghidra --json

The second form reads Ghidra’s loaded memory, so it attaches Evidence to the bytes. Other common type IDs: 6 is a string table block, 14 an icon group, 24 a manifest.

Step 5: Strings and decompilation

The rest is the familiar loop from the Ghidra walkthrough, applied to a PE:

rea search "$DLL" "greet-log" --provider ghidra --json
rea decompile "$DLL" greet_checksum --provider ghidra --json
rea trace "$EXE" "checksum=" --provider ghidra --json

Expect the pseudocode for greet_checksum to show the FNV-1a constants (0x811c9dc5, 0x1000193). Constants like these are easy to verify against your source, and an agent that recognizes them should say so. On the EXE, trace follows the format string back to main and to the imported call into greet.dll. Analyze the DLL separately to see the other side. Each REA Ghidra session imports exactly one program.

Two Ghidra-on-PE caveats:

  • Ghidra’s string inventory only covers what Ghidra defined as string data. Wide strings or strings built at runtime may be missing, so an empty search doesn’t prove the text is absent.
  • REA 6.4.0 has no setting for a PDB, and Ghidra imports a private copy of your file, so don’t expect symbols from a .pdb next to the binary. Exported names come from the export table regardless. Internal functions will have address-based names unless you name them. See tuning headless Ghidra for seed files that persist names.

Step 6: Ask the agent

The CLI is good for scripting. The agent is good for deciding what to look at next. Open the file once with an explicit provider, so the expensive import happens one time per session:

Use REA with provider ghidra to open /absolute/path/to/greet.dll.
1. Tell me what kind of PE this is (architecture, DLL vs EXE, managed or native).
2. List its exports and, for each export, the imported Win32 APIs it can reach.
3. Inspect its PE resources and decode the version information from the
   resource payload bytes.
Cite the Evidence for each claim and list anything you could not resolve.

Follow-ups that work well on a DLL:

Which exported functions write to the filesystem? Show the call path from the
export to the import and the pseudocode lines involved.
Decompile greet_checksum, name the algorithm if you recognize it, and show the
instructions or constants that support that identification.
Compare what the version resource claims with what the exports actually do.
Flag anything the metadata does not explain.

Habits that keep the answers honest:

  • Ask for Evidence and unknowns every time. REA keeps unresolved computed calls as unknown instead of guessing, and a good answer passes that through.
  • Make the agent read bytes, not guess. For resource contents or data tables, ask it to use read_bytes on the reported range.
  • Check against the source. That’s why the practice target is your own build.
  • On Windows, don’t ask for renames. The P0 is read-only, so set_addresses_names and annotate_native_function are unavailable there. Ask the agent to keep a name map in its reply instead.

Troubleshooting

Doctor reports the Windows native controls as unavailable. The Windows P0 needs the matching Windows x64 package with its bundled addon. Reinstall rea-agents with npm on that machine. Changing the MCP registration cannot supply the addon.

target_format_unsupported, architecture_unsupported or managed_target_unsupported on Windows. The P0 takes native x86/x86-64 PE applications and DLLs only. ARM64 PE, .NET assemblies and non-PE files are refused by design. Move the analysis to Linux or macOS, or use the managed tools.

Path rejected on Windows. Copy the target to a plain folder on a fixed local NTFS drive. UNC/network paths, reparse points (junctions, symlinks), alternate data streams and paths with metacharacters are rejected. subst drives are outside the verified boundary too.

JAVA_HOME or Ghidra path problems on Windows. Use absolute paths. Prefer a path made of plain ASCII characters with no metacharacters, then rerun rea doctor --provider ghidra --json. It names the failing check.

The agent times out on the first question. The cold Ghidra import can outlast a client’s default tool timeout. REA’s own deadline is 330,000 ms (REA_GHIDRA_STARTUP_TIMEOUT_MS). Raise the client’s timeout, and see the headless tuning guide for the close/reopen recovery.

You want runtime behaviour. REA’s process capture does not run on native Windows in 6.4.0. Static analysis is the supported Windows path.

Responsible use

Practise on programs you compiled, open-source releases such as 7-Zip, CTF challenges or REA’s official showcase targets. This workflow is for understanding software you are authorized to examine. It is not a way around licensing or activation mechanisms. Read REA’s DISCLAIMER.md before analyzing anything you didn’t build.

Next steps