
Static malware analysis with REA, Ghidra, and Codex
From a MalwareBazaar sample to useful defensive indicators: installing REA and statically analyzing a Windows loader, with evidence and limitations.
Contents
In this article, I want to show how to use REA and Codex to analyze malware statically. The goal is to understand how it works and extract information useful for defense: where it might write files, which processes it interacts with, which servers it contacts, and what traces to look for on a suspicious system.
We start with a sample from MalwareBazaar, the abuse.ch project, initially described as an infostealer. That label is the starting point of the investigation: as we will see, the available code supports some capabilities, while others depend on remote components we did not acquire.
The case was analyzed on October 17, 2026, without executing the malware or contacting its endpoints. The findings below come from the case’s technical report; this article presents the parts most useful for understanding the method and its operational implications.
For specific instructions on installing REA and Ghidra on macOS Apple Silicon and preparing the lab, see the appendix at the end of this article.
The prompt that starts the investigation
The sample is identified by this SHA-256:
5ecd3cbaa22a3298feebd051e9230103009dcd55eeaeeedc8f6473387eae0d55
The initial request to Codex was simple: use REA to inspect the executable and produce a report covering installation directories, processes involved, hardcoded destinations, DGA, and update mechanisms.
To reuse the method, it helps to make the constraints explicit too. This prompt is adapted from the original request, with a path to replace:
Use REA to statically analyze /path/to/case/sample.exe.
The sample is suspected to be an infostealer: check what evidence
in the available code supports that classification.
Work in the prepared Docker lab, with no network access and the sample
mounted read-only. Verify isolation before opening the sample.
Do not execute the malware or extracted code, do not use Wine, and do
not contact any endpoints found in the sample.
Produce a Markdown report covering:
- sample hashes and identification;
- directories and files created or merely referenced;
- processes started, enumerated, or modified;
- persistence, distinguishing configuration from demonstrated creation;
- domains, URLs, any DGA, and server-switching mechanisms;
- payload downloads and any updates;
- defensive indicators, with roles and code references;
- analysis limitations and unavailable components.
Distinguish operations inferred from code from observed behavior.
Preserve the evidence and stop the container when finished.
During this work, the agent produced a chat summary and a more detailed report with artifacts, code references, and verification results. The summary provides an overview; the report lets us assess why a conclusion is supported.
What REA, Ghidra, and Codex do
Reverse engineering an executable means reconstructing a program’s behavior from the available artifacts. A disassembler translates machine instructions into assembly; a decompiler attempts to produce a more readable representation resembling source code. That representation still requires interpretation and verification.
Ghidra, developed by the NSA’s Research Directorate, is a reverse engineering framework that includes disassembly, decompilation, graphing, and scripting, and can also run in automated mode.
REA gives the agent tools to inspect binaries and other artifacts through MCP, the protocol Codex uses to communicate with external tools. It can use Ghidra as its native analysis engine and also provides tools for static JavaScript analysis. Agent integration and operating instructions are covered in REA’s official installation guide.
Codex organizes queries, follows clues, and produces an explanation. The value of the result depends on the evidence supporting that explanation: a string found in a file does not, by itself, prove that an operation is performed.
In this case, for example, the Windows executable embeds a Bun runtime and a JavaScript bundle. After the initial PE inspection, the work focused on the extracted application code. We did not fully decompile the roughly 98 MB native executable.
What the sample actually contains
The file is a 98,092,544-byte Windows x64 PE, with an embedded Bun runtime and JavaScript. A 345,177-byte bundle spanning 4,059 lines was extracted from it.
The classification supported by the code is a loader: a component that prepares to load other programs. The bundle contains system information collection, screen capture, HTTPS communication, PE payload download and decryption, in-memory loading, and a branch dedicated to a component called a “miner.”
The name “miner” appears in the configuration and program messages. Without that payload, we cannot confirm actual mining or identify a pool or wallet. Likewise, the initial suspicion of an infostealer does not allow us to attribute password, cookie, or cryptocurrency wallet theft to the missing payload.
The reconstructed flow starts with environment checks, contacts the server to obtain a value needed for decryption, prepares telemetry and a screenshot, and then provides for component downloads. The PEs are decrypted with AES-256-GCM using a key derived from a remote seed and a local constant. The constant in the sample alone is not enough to recover the missing content.
This chain depends on valid responses and completion of earlier stages. Reading the code does not demonstrate that the chain completed on a victim’s system.
Directories, files, and persistence
The deployment branch provides for creating a directory and writing a component to:
%LOCALAPPDATA%\Microsoft\EdgeWebView\msedge_proxy.exe
The loader then attempts to start that file and searches for its process. This behavior is explicit in the code, conditional on payload availability and continued execution of the flow. It is not evidence of a file observed on a compromised machine, nor does it indicate that the sample copies itself to that location.
Two other values are passed to a remote stage:
%LOCALAPPDATA%\Packages\Microsoft.MicrosoftEdge.Stable_8wekyb3d8bbwe\AC\Temp
EdgeUpdateService
The first is a directory designated for a watchdog, a component responsible for monitoring or restarting other processes. EdgeUpdateService is the configured task name. The available JavaScript does not demonstrate creation of that task, and the name does not prove installation of a Windows service.
The code also references a sentinel file, 98765.txt, and a log, loader_debug.log, in the loader’s directory. It checks for the sentinel and, if present, enables logging and skips an environment check. It does not create the sentinel itself.
These details are useful in an investigation, but they mean different things: a write destination, a parameter passed to another component, and a file searched for by the program are not interchangeable evidence.
Which processes are involved
The loader contains a manual PE loader: it allocates memory, copies sections, resolves imports, and starts code through a thread inside its own process. The examined bundle provides no evidence of injection into an external browser process.
Process enumeration is also used to look for msedge_proxy.exe after the launch attempt. It does not demonstrate that the malware selects browsers or antivirus processes for termination.
One significant detail concerns ExitProcess and TerminateProcess: the loader patches their behavior locally within its own process. In particular, the TerminateProcess patch returns success without performing termination. Finding the name of this API therefore does not justify concluding that the sample kills other processes.
Hardcoded servers, Telegram, and DGA
The bundle contains a primary server and four fallback destinations. The addresses below are defanged:
| Role | Indicator |
|---|---|
| Primary server | nightly-sun[.]cc |
| Fallback | cold-signal[.]pw |
| Fallback | iron-pulse[.]cc |
| Fallback | amm-frosty[.]lol |
| Fallback | bears-speed[.]lol |
| Alternative destination retrieval | hxxps://t[.]me/s/spllcrash |
These values were extracted from the sample; they do not establish current reachability or ownership. We did not contact them during the analysis.
If the initial attempts do not return a valid seed, the code provides for reading the Telegram channel’s public page, extracting HTML metadata, and decrypting its content to obtain a new address. If retrieval fails, it tries the fallback domains.
This mechanism allows the server to be changed through remote content. No DGA was identified in the analyzed bundle: a Domain Generation Algorithm would generate domain names algorithmically, for example from a date or seed. Here, we see fixed destinations and retrieval of a remote address. This conclusion does not extend to the unavailable payloads.
The HTTP paths include /sys/pull/health, /cdn/sync/blob, /app/task/submit, /v2/info/pack, and /hub/init/gate, serving roles related to the seed, payload, report submission, and additional components. The build identifier is 1d940c97b823.
The code also uses public services to determine the external IP address. Their use by the loader does not make them malicious infrastructure. Similarly, the Telegram channel can guide a targeted search, whereas blocking the entire platform would be a much broader decision.
What it collects and what it can update
The first report prepared by the loader includes the public IP address, CPU, RAM, and operating system information, hostname, username, environment check data, a timestamp, and a screenshot. The image is reduced to a maximum width of 480 pixels, encoded, and included in the report: the code does not show the screenshot being saved to disk.
The code provides for encrypted transmission of this data to the server. A second report contains the program’s declared operation results and file and process checks. These fields describe what the program intends to communicate; they are not measurements collected by running it in the lab.
There are also checks on Chrome, Firefox, Edge, and Brave profiles, as well as the Desktop, recent files, and temporary directory. In the examined code, these assess how populated and plausible the environment is. They do not demonstrate access to browser password or cookie databases.
As for updates, the server can supply different payloads and the Telegram channel can provide a new destination. However, there is no demonstrated mechanism for the loader executable to update itself. The URL parameter v contains a fixed build identifier: on its own, it is not a version comparison.
Verifying the agent’s conclusions
The JavaScript obfuscation includes string tables, rotations, RC4, Base64, and XOR. Reconstruction used analyst-written decoders, without executing the malicious source through eval, imports, or a JavaScript VM.
Checks covered hashes, the bundle extraction range, checksums for the four rotations, and 81 values from an RC4 table, also compared against an independent Python implementation. The decoded view preserves line and offset references so findings can be traced back to the original artifact. These checks support specific parts of the analysis; they do not prove complete equivalence of the entire deobfuscation.
Verification itself revealed an anomaly: some decrypted names used in the anti-VM module’s FFI bindings are inconsistent with their points of use. This suggests that part of the initialization may not work in the examined build. The report preserves the anomaly and leaves dynamic verification of the overall effects open, without “correcting” values to match expected names.
Static analysis establishes which operations are described by the code and under what conditions. Actual execution, established persistence, and remote stage capabilities remain separate questions. At the end of the work, the container was stopped and the report and evidence were preserved.
From analysis to threat intelligence
One possible use for this approach is a collection and analysis pipeline. A spamtrap, for example, can collect suspicious messages and attachments; a mail parser such as mailparser can extract their artifacts. Arrival in a spamtrap is a selection criterion, not automatic confirmation of maliciousness.
Selected files could be identified by hash and passed to isolated labs, where an agent produces reports and structured indicators. Before feeding an internal blocklist, each indicator should retain its provenance, role, analysis date, and confidence level.
In our case, the hash identifies the exact file; domains and endpoints can guide network log checks; the full path to msedge_proxy.exe can guide endpoint searches. The process name alone, however, is not enough to declare an infection: it needs to be correlated with its path, hash, process origin, and communications.
An operational workflow could therefore be:
- Collect and select suspicious artifacts.
- Record their hashes, provenance, and dates.
- Perform static analysis in an isolated lab.
- Validate the evidence and assess the indicators.
- Distribute approved indicators to detection or blocking systems.
This is a proposed use of the method, not a pipeline implemented and measured in this case. The agent’s contribution is to make exploration faster and more organized while keeping conclusions verifiable.
Value in a forensic investigation
During an incident, an initial analysis of this kind can provide concrete questions: does the file exist at the specified path? Which process created it? Do logs show contacts with the extracted domains or endpoints? Are there tasks matching the name passed to the remote stage?
The report guides searches across disk, memory, endpoint telemetry, and network logs. The absence of an indicator does not rule out compromise, particularly when the sample can download different components; the presence of a common name requires correlation before drawing a conclusion.
In this case, REA and Codex helped turn a large executable into a documented reconstruction of the loader, its destinations, and its limitations. Being able to trace a finding back to the code is what keeps the report useful beyond the first reading.
Further details about the technical report, artifacts, and code references can be provided privately.
Appendix: installation and lab setup
Installing the tools on macOS Apple Silicon
The following procedure reconstructs the configuration used on the Mac, with generic paths under the user’s home directory. It applies to macOS Apple Silicon: we are not presenting it as a procedure already tested on Windows or Linux.
The reference versions are REA 6.5.0, Ghidra 12.1.4, Temurin JDK 25.0.4.1, and Node.js 24.21.0. These versions are pinned to reproduce the environment described. The variant including the skill was checked through a setup plan preview; the entire sequence was not repeated on a clean Mac.
1. Prepare the Mac
You need Codex already installed and authenticated, Internet access to download the tools, and Apple’s build tools. Check the architecture and toolchain:
uname -m
xcode-select -p
On Apple Silicon, the first command should return arm64. If Apple’s tools are not installed:
xcode-select --install
Wait for installation to complete before continuing. Run the following steps in the same terminal session to preserve the environment variables:
mkdir -p "$HOME/.local/bin"
mkdir -p "$HOME/.local/opt/jdks"
mkdir -p "$HOME/Downloads/rea-setup"
cd "$HOME/Downloads/rea-setup"
2. Install Java
Download the ARM64 JDK from the official Eclipse Temurin release:
curl -fL \
'https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.4.1%2B1/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.4.1_1.tar.gz' \
-o temurin-jdk.tar.gz
echo '61979887f7506a24a57439ff99adb8b3a7fc89977d9cfe3b8984f58a981b7b9d temurin-jdk.tar.gz' \
| shasum -a 256 -c -
The digest is the one recorded for the archive used in our installation. If verification does not return OK, stop and check the download. Then:
tar -xzf temurin-jdk.tar.gz
ditto "jdk-25.0.4.1+1/Contents/Home" \
"$HOME/.local/opt/jdks/temurin-25.0.4.1-arm64"
export JAVA_HOME="$HOME/.local/opt/jdks/temurin-25.0.4.1-arm64"
export PATH="$JAVA_HOME/bin:$PATH"
java -version
javac -version
3. Install Ghidra and build the native components
Download the public distribution from the Ghidra 12.1.4 release. Archives named “Source code” are different from the ready-to-use distribution.
curl -fL \
'https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.1.4_build/ghidra_12.1.4_PUBLIC_20260921.zip' \
-o ghidra.zip
echo 'ddac49f903da9d5bac833e5cc79395098b9c33cfd3279be5f31bd00387d2d4db ghidra.zip' \
| shasum -a 256 -c -
Again, wait for OK. The destination must be a new directory: do not overlay different Ghidra versions.
unzip -q ghidra.zip
ditto ghidra_12.1.4_PUBLIC "$HOME/.local/opt/ghidra_12.1.4_PUBLIC"
ln -s ghidra_12.1.4_PUBLIC "$HOME/.local/opt/ghidra_latest"
export GHIDRA_INSTALL_DIR="$HOME/.local/opt/ghidra_latest"
(
cd "$GHIDRA_INSTALL_DIR/support/gradle"
./gradlew --no-daemon buildNatives
)
The wrapper may download Gradle. Wait for BUILD SUCCESSFUL, then check the decompiler:
file "$GHIDRA_INSTALL_DIR/Ghidra/Features/Decompiler/build/os/mac_arm_64/decompile"
The output should identify a Mach-O ARM64 executable. To try the graphical interface:
"$GHIDRA_INSTALL_DIR/ghidraRun"
4. Install Node.js and REA
We use NVM to manage Node. If it is already installed, skip downloading and running its installer:
curl -fL \
https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh \
-o "$HOME/Downloads/rea-setup/install-nvm.sh"
bash "$HOME/Downloads/rea-setup/install-nvm.sh"
Load NVM and select the version used on the Mac:
export NVM_DIR="$HOME/.nvm"
. "$NVM_DIR/nvm.sh"
nvm install 24.21.0
nvm use 24.21.0
node --version
npm --version
npm install --global \
--prefix "$HOME/.local/opt/rea" \
--ignore-scripts \
rea-agents@6.5.0
The prefix belongs to the user, so sudo is not needed. Create a command that explicitly selects Node, Java, and Ghidra:
cat > "$HOME/.local/bin/rea" <<'EOF'
#!/bin/sh
set -eu
export JAVA_HOME="$HOME/.local/opt/jdks/temurin-25.0.4.1-arm64"
export GHIDRA_INSTALL_DIR="$HOME/.local/opt/ghidra_latest"
export REA_ANALYSIS_PROVIDER=ghidra
export PATH="$HOME/.nvm/versions/node/v24.21.0/bin:$JAVA_HOME/bin:$PATH"
exec "$HOME/.nvm/versions/node/v24.21.0/bin/node" \
"$HOME/.local/opt/rea/lib/node_modules/rea-agents/scripts/rea.mjs" \
"$@"
EOF
chmod +x "$HOME/.local/bin/rea"
export PATH="$HOME/.local/bin:$PATH"
rea --version
The expected version is 6.5.0. If ~/.local/bin is not already in PATH, add this line to ~/.zshrc for future terminal sessions:
export PATH="$HOME/.local/bin:$PATH"
5. Connect REA to Codex
Check Ghidra and preview the plan before applying it:
rea doctor --provider ghidra --json
rea setup --client codex --skill=true --dry-run --json
The plan should include MCP registration and skill installation. To apply it:
rea setup --client codex --skill=true --yes --json
The MCP server exposes the tools; the skill provides operating instructions to the agent. We use the explicit form --skill=true, verified in this version’s setup plan.
When Node is managed by NVM, Codex launched from the graphical interface may have a different PATH from the terminal. If the generated registration does not select the intended runtime, adjust the REA sections in ~/.codex/config.toml:
[mcp_servers.rea]
command = "/Users/USERNAME/.nvm/versions/node/v24.21.0/bin/node"
args = [
"/Users/USERNAME/.local/opt/rea/lib/node_modules/rea-agents/scripts/rea.mjs",
"mcp"
]
startup_timeout_sec = 30
tool_timeout_sec = 450
[mcp_servers.rea.env]
GHIDRA_INSTALL_DIR = "/Users/USERNAME/.local/opt/ghidra_latest"
JAVA_HOME = "/Users/USERNAME/.local/opt/jdks/temurin-25.0.4.1-arm64"
REA_ANALYSIS_PROVIDER = "ghidra"
Replace USERNAME with your username and edit the existing sections without duplicating them or deleting other servers. Paths are absolute: do not assume $HOME will be expanded in TOML.
The official Codex MCP documentation describes the command, arguments, environment, and timeouts. startup_timeout_sec applies to MCP server startup; tool_timeout_sec applies to individual calls. The 450 seconds allow time for analysis operations and for Ghidra to start when required by a call: this is a choice for our environment, not a universal requirement.
Complete the checks:
rea doctor --client codex --provider ghidra --json
rea doctor --skill --json
rea mcp doctor --json
Look for healthy: true in the selected checks, then close and reopen Codex. An initial prompt could be:
Use REA to check the server version and Ghidra availability,
without opening any files.
These checks verify tool availability. Analyzing a program is a separate verification step.
Preparing the analysis lab
Installation on the Mac makes the tools available to the agent. The malware analysis was performed in a separate Linux/amd64 lab in Docker Desktop, with REA 6.5.0 and Node 24.22.0. The host and container configurations are two distinct environments.
A Windows executable can be studied statically from Linux: reading its structure, strings, and code does not require launching it on Windows. In our case, Wine was not used, and neither the sample, the extracted JavaScript, nor any PEs were executed.
The image and dependencies were prepared before analysis. During inspection, the container used network mode none, a read-only root filesystem, an unprivileged user, dropped capabilities, and no-new-privileges. The sample was mounted read-only; writes were limited to the case directory and the container’s temporary areas.
Starting Docker does not automatically apply these conditions. The lab must be explicitly configured and checked. The container limits the tools’ access to the system and network; for this work, the central constraint remains static analysis only.
The lab’s network and the connection used by Codex are also separate: the container can be offline while the agent communicates with the model service. “Offline” therefore does not mean the entire AI workflow has no external communications; this matters when handling confidential samples or evidence.