Text Generation
GGUF
English
llama.cpp
qwen
qwen3
qwen3.5
cybersecurity
malware-analysis
reverse-engineering
pe
elf
ghidra
agent
research
conversational
Instructions to use AgentreBench/xref-9b with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- llama-cpp-python
How to use AgentreBench/xref-9b with llama-cpp-python:
# !pip install llama-cpp-python from llama_cpp import Llama llm = Llama.from_pretrained( repo_id="AgentreBench/xref-9b", filename="xref-9b-f16.gguf", )
llm.create_chat_completion( messages = [ { "role": "user", "content": "What is the capital of France?" } ] ) - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- llama.cpp
How to use AgentreBench/xref-9b with llama.cpp:
Install (macOS, Linux)
curl -LsSf https://llama.app/install.sh | sh # Start a local OpenAI-compatible server with a web UI: llama serve -hf AgentreBench/xref-9b:F16 # Run inference directly in the terminal: llama cli -hf AgentreBench/xref-9b:F16
Install from WinGet (Windows)
winget install llama.cpp # Start a local OpenAI-compatible server with a web UI: llama serve -hf AgentreBench/xref-9b:F16 # Run inference directly in the terminal: llama cli -hf AgentreBench/xref-9b:F16
Use pre-built binary
# Download pre-built binary from: # https://github.com/ggerganov/llama.cpp/releases # Start a local OpenAI-compatible server with a web UI: ./llama-server -hf AgentreBench/xref-9b:F16 # Run inference directly in the terminal: ./llama-cli -hf AgentreBench/xref-9b:F16
Build from source code
git clone https://github.com/ggerganov/llama.cpp.git cd llama.cpp cmake -B build cmake --build build -j --target llama-server llama-cli # Start a local OpenAI-compatible server with a web UI: ./build/bin/llama-server -hf AgentreBench/xref-9b:F16 # Run inference directly in the terminal: ./build/bin/llama-cli -hf AgentreBench/xref-9b:F16
Use Docker
docker model run hf.co/AgentreBench/xref-9b:F16
- LM Studio
- Jan
- vLLM
How to use AgentreBench/xref-9b with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "AgentreBench/xref-9b" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/chat/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "AgentreBench/xref-9b", "messages": [ { "role": "user", "content": "What is the capital of France?" } ] }'Use Docker
docker model run hf.co/AgentreBench/xref-9b:F16
- Ollama
How to use AgentreBench/xref-9b with Ollama:
ollama run hf.co/AgentreBench/xref-9b:F16
- Unsloth Studio
How to use AgentreBench/xref-9b with Unsloth Studio:
Install Unsloth Studio (macOS, Linux, WSL)
curl -fsSL https://unsloth.ai/install.sh | sh # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for AgentreBench/xref-9b to start chatting
Install Unsloth Studio (Windows)
irm https://unsloth.ai/install.ps1 | iex # Run unsloth studio unsloth studio -H 0.0.0.0 -p 8888 # Then open http://localhost:8888 in your browser # Search for AgentreBench/xref-9b to start chatting
Using HuggingFace Spaces for Unsloth
# No setup required # Open https://huggingface.co/spaces/unsloth/studio in your browser # Search for AgentreBench/xref-9b to start chatting
- Pi
How to use AgentreBench/xref-9b with Pi:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf AgentreBench/xref-9b:F16
Configure the model in Pi
# Install Pi: npm install -g @mariozechner/pi-coding-agent # Add to ~/.pi/agent/models.json: { "providers": { "llama-cpp": { "baseUrl": "http://localhost:8080/v1", "api": "openai-completions", "apiKey": "none", "models": [ { "id": "AgentreBench/xref-9b:F16" } ] } } }Run Pi
# Start Pi in your project directory: pi
- Hermes Agent new
How to use AgentreBench/xref-9b with Hermes Agent:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf AgentreBench/xref-9b:F16
Configure Hermes
# Install Hermes: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash hermes setup # Point Hermes at the local server: hermes config set model.provider custom hermes config set model.base_url http://127.0.0.1:8080/v1 hermes config set model.default AgentreBench/xref-9b:F16
Run Hermes
hermes
- Atomic Chat new
- OpenClaw new
How to use AgentreBench/xref-9b with OpenClaw:
Start the llama.cpp server
# Install llama.cpp: brew install llama.cpp # Start a local OpenAI-compatible server: llama serve -hf AgentreBench/xref-9b:F16
Configure OpenClaw
# Install OpenClaw: npm install -g openclaw@latest # Register the local server and set it as the default model: openclaw onboard --non-interactive --mode local \ --auth-choice custom-api-key \ --custom-base-url http://127.0.0.1:8080/v1 \ --custom-model-id "AgentreBench/xref-9b:F16" \ --custom-provider-id llama-cpp \ --custom-compatibility openai \ --custom-text-input \ --accept-risk \ --skip-health
Run OpenClaw
openclaw agent --local --agent main --message "Hello from Hugging Face"
- Docker Model Runner
How to use AgentreBench/xref-9b with Docker Model Runner:
docker model run hf.co/AgentreBench/xref-9b:F16
- Lemonade
How to use AgentreBench/xref-9b with Lemonade:
Pull the model
# Download Lemonade from https://lemonade-server.ai/ lemonade pull AgentreBench/xref-9b:F16
Run and chat with the model
lemonade run user.xref-9b-F16
List all available models
lemonade list
Release xref-9b research preview
Browse files- .gitattributes +1 -34
- README.md +230 -0
- SHA256SUMS +2 -0
- agentre.py +983 -0
- agentre_triage.py +1265 -0
- assets/xref-9b-banner.svg +40 -0
- assets/xref-9b-eval-chart.svg +40 -0
- release_manifest.json +36 -0
- requirements.txt +3 -0
- reverse_engineering_spec_unified.md +115 -0
- scripts/check_tools.sh +26 -0
- scripts/run_xref9b_chat.sh +15 -0
- scripts/run_xref9b_triage.sh +15 -0
- scripts/xref9b.env.example +6 -0
- tools/ghidra_scripts/AgentRESummary.java +152 -0
- xref-9b-f16.gguf +3 -0
- xref-9b-q4_k_m.gguf +3 -0
.gitattributes
CHANGED
|
@@ -1,35 +1,2 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
| 1 |
+
*.gguf filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
language:
|
| 4 |
+
- en
|
| 5 |
+
pipeline_tag: text-generation
|
| 6 |
+
library_name: llama.cpp
|
| 7 |
+
tags:
|
| 8 |
+
- gguf
|
| 9 |
+
- qwen
|
| 10 |
+
- qwen3
|
| 11 |
+
- qwen3.5
|
| 12 |
+
- cybersecurity
|
| 13 |
+
- malware-analysis
|
| 14 |
+
- reverse-engineering
|
| 15 |
+
- pe
|
| 16 |
+
- elf
|
| 17 |
+
- ghidra
|
| 18 |
+
- agent
|
| 19 |
+
- research
|
| 20 |
+
inference: false
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
<p align="center">
|
| 24 |
+
<img src="assets/xref-9b-banner.svg" alt="xref-9b" width="860">
|
| 25 |
+
</p>
|
| 26 |
+
|
| 27 |
+
# xref-9b
|
| 28 |
+
|
| 29 |
+
`xref-9b` is a research-preview reverse-engineering assistant for static PE and ELF malware triage. It is intended to be used with a local tool wrapper that exposes static analysis tools such as `file`, `strings`, `readelf`, `objdump`, PE header/import inspection, entropy checks, and optional Ghidra headless summaries.
|
| 30 |
+
|
| 31 |
+
This release is distributed as GGUF for local inference with llama.cpp.
|
| 32 |
+
|
| 33 |
+
## What Is Included
|
| 34 |
+
|
| 35 |
+
| File | Purpose |
|
| 36 |
+
|---|---|
|
| 37 |
+
| `xref-9b-q4_k_m.gguf` | Recommended GGUF for local use. |
|
| 38 |
+
| `xref-9b-f16.gguf` | Full precision GGUF artifact for conversion/experimentation. |
|
| 39 |
+
| `agentre.py` | Interactive chat wrapper for PE/ELF reverse engineering. |
|
| 40 |
+
| `agentre_triage.py` | Static tool runtime and batch triage backend. |
|
| 41 |
+
| `reverse_engineering_spec_unified.md` | Tool-use and verdict spec given to the model. |
|
| 42 |
+
| `tools/ghidra_scripts/AgentRESummary.java` | Ghidra headless script used by `/ghidra` and `ghidra_summary`. |
|
| 43 |
+
| `scripts/` | Convenience scripts and environment template. |
|
| 44 |
+
| `assets/` | Release graphics and evaluation chart. |
|
| 45 |
+
|
| 46 |
+
## Intended Use
|
| 47 |
+
|
| 48 |
+
This model is for defensive research, malware triage, reverse-engineering education, and local analyst-assist workflows. It is not a production malware detector and should not be used as the only basis for blocking, attribution, incident response, or legal decisions.
|
| 49 |
+
|
| 50 |
+
The model should be treated as an assistant that proposes evidence-backed hypotheses from static tool output. It can be wrong, especially on stripped, packed, obfuscated, or sparse-string binaries.
|
| 51 |
+
|
| 52 |
+
## Training Summary
|
| 53 |
+
|
| 54 |
+
`xref-9b` was trained from a Qwen-family 9B base model using:
|
| 55 |
+
|
| 56 |
+
- supervised fine-tuning (SFT) on reverse-engineering instruction traces;
|
| 57 |
+
- additional PE and ELF malware/benign triage data;
|
| 58 |
+
- reinforcement learning / preference optimization with IPO over curated preference pairs;
|
| 59 |
+
- tool-use formatting for local static analysis workflows.
|
| 60 |
+
|
| 61 |
+
Training data sources are intentionally not enumerated in this public model card.
|
| 62 |
+
|
| 63 |
+
## Evaluation Snapshot
|
| 64 |
+
|
| 65 |
+
Held-out mixed PE/ELF evaluation, 40 binaries total, balanced by format and benign/malicious label. Results are research-preview numbers and should be read as directional rather than definitive.
|
| 66 |
+
|
| 67 |
+
<p align="center">
|
| 68 |
+
<img src="assets/xref-9b-eval-chart.svg" alt="xref-9b evaluation chart" width="760">
|
| 69 |
+
</p>
|
| 70 |
+
|
| 71 |
+
| Checkpoint / workflow | Strict accuracy | Binary accuracy | Notes |
|
| 72 |
+
|---|---:|---:|---|
|
| 73 |
+
| SFT baseline | 55.0% | 62.5% | Initial supervised model. |
|
| 74 |
+
| SFT + IPO | 57.5% | 57.5% | Preference-tuned checkpoint; improved answer rate but still weak on ELF malware. |
|
| 75 |
+
| SFT + IPO + release static workflow | 67.5% | 67.5% | Same IPO family with deeper static-analysis workflow and final-answer forcing. |
|
| 76 |
+
|
| 77 |
+
Observed gains:
|
| 78 |
+
|
| 79 |
+
- strict accuracy improved from 55.0% to 67.5% in the release workflow (+12.5 percentage points);
|
| 80 |
+
- labeled ELF malware performance improved substantially under the deeper static workflow;
|
| 81 |
+
- benign ELF calibration regressed in that run, so unknown/benign decisions still need analyst review.
|
| 82 |
+
|
| 83 |
+
## Quick Start
|
| 84 |
+
|
| 85 |
+
### 1. Install Runtime Dependencies
|
| 86 |
+
|
| 87 |
+
Required:
|
| 88 |
+
|
| 89 |
+
- Python 3.10+
|
| 90 |
+
- llama.cpp with `llama-completion`
|
| 91 |
+
- common Unix tools: `file`, `strings`, `readelf`, `objdump`, `nm`, `hexdump` or `xxd`
|
| 92 |
+
|
| 93 |
+
Optional but recommended:
|
| 94 |
+
|
| 95 |
+
- Ghidra headless (`analyzeHeadless`)
|
| 96 |
+
- MinGW binutils for richer PE inspection on Linux (`x86_64-w64-mingw32-objdump`, `x86_64-w64-mingw32-nm`)
|
| 97 |
+
|
| 98 |
+
### 2. Download
|
| 99 |
+
|
| 100 |
+
Example with Hugging Face CLI:
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
hf download AgentreBench/xref-9b xref-9b-q4_k_m.gguf agentre.py agentre_triage.py reverse_engineering_spec_unified.md --local-dir xref-9b
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
For full local package including scripts and Ghidra helper:
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
hf download AgentreBench/xref-9b --local-dir xref-9b
|
| 110 |
+
```
|
| 111 |
+
|
| 112 |
+
### 3. Configure Environment
|
| 113 |
+
|
| 114 |
+
```bash
|
| 115 |
+
cd xref-9b
|
| 116 |
+
cp scripts/xref9b.env.example scripts/xref9b.env
|
| 117 |
+
$EDITOR scripts/xref9b.env
|
| 118 |
+
source scripts/xref9b.env
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
At minimum set:
|
| 122 |
+
|
| 123 |
+
```bash
|
| 124 |
+
export XREF9B_GGUF="$PWD/xref-9b-q4_k_m.gguf"
|
| 125 |
+
export XREF9B_LLAMA_CLI="/path/to/llama.cpp/build/bin/llama-completion"
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
Optional Ghidra:
|
| 129 |
+
|
| 130 |
+
```bash
|
| 131 |
+
export GHIDRA_HEADLESS="/path/to/ghidra/support/analyzeHeadless"
|
| 132 |
+
```
|
| 133 |
+
|
| 134 |
+
### 4. Chat With One Binary
|
| 135 |
+
|
| 136 |
+
```bash
|
| 137 |
+
python3 agentre.py chat /path/to/sample --model "$XREF9B_GGUF" --llama-cli "$XREF9B_LLAMA_CLI" --ctx-size 65536 --gpu-layers 99 --max-turns 8 --max-tool-calls 16 --max-tokens 1600 --obs-limit 8000
|
| 138 |
+
```
|
| 139 |
+
|
| 140 |
+
Inside chat:
|
| 141 |
+
|
| 142 |
+
```text
|
| 143 |
+
/deep run the deeper static workflow
|
| 144 |
+
/ghidra run Ghidra headless summary
|
| 145 |
+
/verdict force a concise final verdict
|
| 146 |
+
/compact compact long chat history
|
| 147 |
+
/tools list tools used
|
| 148 |
+
/save save transcript
|
| 149 |
+
/quit exit
|
| 150 |
+
```
|
| 151 |
+
|
| 152 |
+
You can also ask naturally:
|
| 153 |
+
|
| 154 |
+
```text
|
| 155 |
+
is this malicious or benign?
|
| 156 |
+
use ghidra and explain the key functions
|
| 157 |
+
is this packed?
|
| 158 |
+
what evidence supports malicious behavior?
|
| 159 |
+
```
|
| 160 |
+
|
| 161 |
+
### 5. Directory Mode
|
| 162 |
+
|
| 163 |
+
Directory mode is explicit:
|
| 164 |
+
|
| 165 |
+
```bash
|
| 166 |
+
python3 agentre.py chat -d /path/to/binaries --model "$XREF9B_GGUF" --llama-cli "$XREF9B_LLAMA_CLI" --ctx-size 65536 --gpu-layers 99
|
| 167 |
+
```
|
| 168 |
+
|
| 169 |
+
Use `/samples` and `/open N` inside the session.
|
| 170 |
+
|
| 171 |
+
### 6. Batch Triage
|
| 172 |
+
|
| 173 |
+
```bash
|
| 174 |
+
python3 agentre.py triage /path/to/binaries --model "$XREF9B_GGUF" --llama-cli "$XREF9B_LLAMA_CLI" --ctx-size 65536 --gpu-layers 99 --max-tool-calls 16 --max-tokens 1600 --obs-limit 8000
|
| 175 |
+
```
|
| 176 |
+
|
| 177 |
+
## Ghidra Support
|
| 178 |
+
|
| 179 |
+
`xref-9b` does not run arbitrary Ghidra commands. The wrapper exposes one Ghidra tool, `ghidra_summary`, implemented with Ghidra headless and `tools/ghidra_scripts/AgentRESummary.java`.
|
| 180 |
+
|
| 181 |
+
The wrapper runs a command equivalent to:
|
| 182 |
+
|
| 183 |
+
```bash
|
| 184 |
+
analyzeHeadless /tmp/agentre_ghidra_xxx agentre_project -import <staged_sample> -overwrite -analysisTimeoutPerFile 180 -scriptPath tools/ghidra_scripts -postScript AgentRESummary.java /tmp/agentre_ghidra_summary.txt
|
| 185 |
+
```
|
| 186 |
+
|
| 187 |
+
Ghidra is used for static summaries: program metadata, memory blocks, imports/external symbols, strings, functions, and short instruction excerpts. It does not unpack malware, execute binaries, emulate payloads, or dynamically dump memory.
|
| 188 |
+
|
| 189 |
+
## Limitations
|
| 190 |
+
|
| 191 |
+
- Research preview; not production detection.
|
| 192 |
+
- Static-only; no execution, sandboxing, unpacking, or memory dumping.
|
| 193 |
+
- Stripped/static/packed binaries can require manual analyst follow-up.
|
| 194 |
+
- The model can overfit to tool artifacts or overclaim from weak signals.
|
| 195 |
+
- PE and ELF performance is uneven across families and compiler/linker settings.
|
| 196 |
+
- The model may classify offensive dual-use tools as `hackware` or `malicious` depending on evidence and workflow.
|
| 197 |
+
- Long tool histories can degrade answers; use `/compact`.
|
| 198 |
+
|
| 199 |
+
## Improving The Model
|
| 200 |
+
|
| 201 |
+
Useful next steps for contributors:
|
| 202 |
+
|
| 203 |
+
- Add more stripped ELF and statically linked ELF training traces.
|
| 204 |
+
- Add calibrated benign PE examples that contain network/admin APIs but no malicious behavior.
|
| 205 |
+
- Add preference pairs that penalize overclaiming from entropy or strings alone.
|
| 206 |
+
- Add tool traces that distinguish virtual addresses from file offsets.
|
| 207 |
+
- Improve Ghidra summaries with function call graphs, xrefs, and decompiler snippets.
|
| 208 |
+
- Add safer structured verdict calibration: `malicious`, `benign`, `hackware`, `unknown`.
|
| 209 |
+
- Evaluate with family-disjoint and compiler-disjoint splits.
|
| 210 |
+
|
| 211 |
+
## Recommended Settings
|
| 212 |
+
|
| 213 |
+
For most local analysis:
|
| 214 |
+
|
| 215 |
+
```bash
|
| 216 |
+
--ctx-size 65536 --max-turns 8 --max-tool-calls 16 --max-tokens 1600 --obs-limit 8000
|
| 217 |
+
```
|
| 218 |
+
|
| 219 |
+
Use larger `--max-tokens` for deeper explanations, and `/compact` if the session becomes repetitive or context-heavy.
|
| 220 |
+
|
| 221 |
+
## File Hashes
|
| 222 |
+
|
| 223 |
+
```text
|
| 224 |
+
87f320a4edda407a00ddb005774640acac4c45882a9d03674e5f2c37e9dbafd6 xref-9b-q4_k_m.gguf
|
| 225 |
+
2e9438786b7aed9a9314629e811900616c1b4666a2f7cffa2f537fd566f238d8 xref-9b-f16.gguf
|
| 226 |
+
```
|
| 227 |
+
|
| 228 |
+
## Citation
|
| 229 |
+
|
| 230 |
+
If you use this model in research, cite AgentRE-Bench / xref-9b and include the exact GGUF hash used.
|
SHA256SUMS
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
87f320a4edda407a00ddb005774640acac4c45882a9d03674e5f2c37e9dbafd6 xref-9b-q4_k_m.gguf
|
| 2 |
+
2e9438786b7aed9a9314629e811900616c1b4666a2f7cffa2f537fd566f238d8 xref-9b-f16.gguf
|
agentre.py
ADDED
|
@@ -0,0 +1,983 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""AgentRE local launcher.
|
| 3 |
+
|
| 4 |
+
`chat` starts a persistent reverse-engineering session for one file, or a directory passed with `-d`.
|
| 5 |
+
`inspect` is kept as a compatibility alias for `chat`.
|
| 6 |
+
`triage` delegates to the existing bulk PE/ELF analyzer.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import argparse
|
| 12 |
+
import json
|
| 13 |
+
import os
|
| 14 |
+
import shlex
|
| 15 |
+
import shutil
|
| 16 |
+
import sys
|
| 17 |
+
import time
|
| 18 |
+
from datetime import datetime, timezone
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from typing import Any
|
| 21 |
+
|
| 22 |
+
import agentre_triage as triage
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
ROOT = Path(__file__).resolve().parent
|
| 26 |
+
LOCAL_AGENTRE_ENV = ROOT / "scripts" / "xref9b.env"
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def read_shell_exports(path: Path) -> dict[str, str]:
|
| 30 |
+
exports: dict[str, str] = {}
|
| 31 |
+
if not path.exists():
|
| 32 |
+
return exports
|
| 33 |
+
for raw in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
| 34 |
+
line = raw.strip()
|
| 35 |
+
if not line.startswith("export "):
|
| 36 |
+
continue
|
| 37 |
+
try:
|
| 38 |
+
parts = shlex.split(line)
|
| 39 |
+
except ValueError:
|
| 40 |
+
continue
|
| 41 |
+
for part in parts[1:]:
|
| 42 |
+
key, sep, value = part.partition("=")
|
| 43 |
+
if sep and key:
|
| 44 |
+
exports[key] = value
|
| 45 |
+
return exports
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
def load_local_agentre_env() -> dict[str, str]:
|
| 49 |
+
exports = read_shell_exports(LOCAL_AGENTRE_ENV)
|
| 50 |
+
for key, value in exports.items():
|
| 51 |
+
os.environ.setdefault(key, value)
|
| 52 |
+
return exports
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def now_stamp() -> str:
|
| 56 |
+
return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def print_top_help() -> None:
|
| 60 |
+
print(
|
| 61 |
+
"""AgentRE local PE/ELF reverse-engineering CLI.
|
| 62 |
+
|
| 63 |
+
Usage:
|
| 64 |
+
python3 agentre.py chat FILE [options]
|
| 65 |
+
python3 agentre.py chat -d DIR [options]
|
| 66 |
+
python3 agentre.py inspect FILE [options]
|
| 67 |
+
python3 agentre.py inspect -d DIR [options]
|
| 68 |
+
python3 agentre.py triage FILE_OR_DIR [options]
|
| 69 |
+
|
| 70 |
+
Commands:
|
| 71 |
+
chat Chat with the model while it uses static PE/ELF RE tools. Use -d for directories.
|
| 72 |
+
inspect Alias for chat.
|
| 73 |
+
triage Analyze one file or a directory of PE/ELF files and write reports.
|
| 74 |
+
|
| 75 |
+
Examples:
|
| 76 |
+
python3 agentre.py chat holdout/mixed40/staged/sample_0003.elf
|
| 77 |
+
python3 agentre.py chat -d holdout/mixed40/staged
|
| 78 |
+
python3 agentre.py triage holdout/mixed40/staged --model ./xref-9b-q4_k_m.gguf
|
| 79 |
+
"""
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def build_backend(args: argparse.Namespace) -> tuple[str, Any]:
|
| 84 |
+
backend_name = args.backend
|
| 85 |
+
if backend_name == "auto":
|
| 86 |
+
backend_name = "llama-cpp" if str(args.model).lower().endswith(".gguf") else "transformers"
|
| 87 |
+
print(f"[xref 9b] backend={backend_name} model={args.model}", flush=True)
|
| 88 |
+
if args.adapter and backend_name == "transformers":
|
| 89 |
+
print(f"[xref 9b] adapter={args.adapter}", flush=True)
|
| 90 |
+
elif args.adapter and backend_name == "llama-cpp":
|
| 91 |
+
print("[xref 9b] note: --adapter is ignored by llama-cpp; use a merged GGUF or --llama-lora", flush=True)
|
| 92 |
+
|
| 93 |
+
if backend_name == "transformers":
|
| 94 |
+
return backend_name, triage.LocalQwenBackend(args.model, args.adapter or None, args.dtype, args.device_map)
|
| 95 |
+
return backend_name, triage.LlamaCppBackend(
|
| 96 |
+
args.model,
|
| 97 |
+
args.llama_cli,
|
| 98 |
+
args.ctx_size,
|
| 99 |
+
args.threads,
|
| 100 |
+
args.gpu_layers,
|
| 101 |
+
args.llama_lora or None,
|
| 102 |
+
args.llama_extra_arg,
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def build_inspect_messages(spec: str, sample_id: str, fmt: str, request: str) -> list[dict[str, Any]]:
|
| 107 |
+
system = (
|
| 108 |
+
spec.strip()
|
| 109 |
+
+ "\n\nRuntime rules:\n"
|
| 110 |
+
+ f"- You are in an interactive reverse-engineering session for staged sample `{sample_id}` only.\n"
|
| 111 |
+
+ f"- Detected format: {fmt}.\n"
|
| 112 |
+
+ "- The original path is hidden from the model to avoid path/name bias.\n"
|
| 113 |
+
+ "- Any tool path argument is ignored; tools are routed to the staged sample.\n"
|
| 114 |
+
+ "- Use static analysis only. Never ask to execute the sample.\n"
|
| 115 |
+
+ "- Use tools when they would materially improve the answer.\n"
|
| 116 |
+
+ "- Answer the user's follow-up questions directly and cite concrete evidence from tool output.\n"
|
| 117 |
+
+ "- If evidence is insufficient, say so plainly; classify as unknown or suspicious/unknown rather than guessing.\n"
|
| 118 |
+
+ "- For stripped, static, packed, encrypted, or sparse-string samples where confidence is low, explicitly recommend closer disassembly with Ghidra or another disassembler.\n"
|
| 119 |
+
+ "- Think internally, but do not expose chain-of-thought.\n"
|
| 120 |
+
+ "- Only call final_answer when the user asks for a final verdict or when your answer is a classification decision.\n"
|
| 121 |
+
)
|
| 122 |
+
user = (
|
| 123 |
+
f"{request.strip()}\n\n"
|
| 124 |
+
"Start by inspecting basic metadata and strings, then give a concise initial assessment. "
|
| 125 |
+
"Do not require the user to know tool names."
|
| 126 |
+
)
|
| 127 |
+
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
def stage_one(source_path: Path, run_dir: Path, sample_id: str = "inspect_0001") -> tuple[Path, str]:
|
| 131 |
+
fmt = triage.detect_format(source_path)
|
| 132 |
+
suffix = ".elf" if fmt == "ELF" else ".exe" if fmt == "PE" else source_path.suffix
|
| 133 |
+
stage_dir = run_dir / "staged"
|
| 134 |
+
stage_dir.mkdir(parents=True, exist_ok=True)
|
| 135 |
+
staged_path = (stage_dir / f"{sample_id}{suffix}").resolve()
|
| 136 |
+
shutil.copy2(source_path, staged_path)
|
| 137 |
+
return staged_path, fmt
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def progress(state: dict[str, Any], message: str) -> None:
|
| 141 |
+
if state.get("progress", True):
|
| 142 |
+
print(f"[xref 9b] {message}", file=sys.stderr, flush=True)
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def looks_truncated_reply(text: str) -> bool:
|
| 146 |
+
stripped = text.rstrip()
|
| 147 |
+
if len(stripped) < 600:
|
| 148 |
+
return False
|
| 149 |
+
if stripped.endswith((".", "!", "?", ")", "]", "}", "`")):
|
| 150 |
+
return False
|
| 151 |
+
tail = stripped.rsplit(maxsplit=1)[-1].lower().strip(",:;")
|
| 152 |
+
return tail in {
|
| 153 |
+
"a",
|
| 154 |
+
"an",
|
| 155 |
+
"and",
|
| 156 |
+
"are",
|
| 157 |
+
"as",
|
| 158 |
+
"because",
|
| 159 |
+
"but",
|
| 160 |
+
"for",
|
| 161 |
+
"from",
|
| 162 |
+
"given",
|
| 163 |
+
"in",
|
| 164 |
+
"is",
|
| 165 |
+
"of",
|
| 166 |
+
"or",
|
| 167 |
+
"that",
|
| 168 |
+
"the",
|
| 169 |
+
"this",
|
| 170 |
+
"to",
|
| 171 |
+
"with",
|
| 172 |
+
} or not stripped.endswith((".", "!", "?", ":", ";"))
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def requested_direct_tool(text: str) -> tuple[str, dict[str, Any]] | None:
|
| 176 |
+
lowered = text.lower()
|
| 177 |
+
if "ghidra" in lowered and any(
|
| 178 |
+
word in lowered
|
| 179 |
+
for word in ("analyze", "disassembl", "dive", "open", "run", "try", "use", "with")
|
| 180 |
+
):
|
| 181 |
+
return "ghidra_summary", {"timeout": 180}
|
| 182 |
+
return None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def format_final(answer: dict[str, Any] | None) -> str:
|
| 186 |
+
if not answer:
|
| 187 |
+
return "Final verdict was submitted, but it was not valid JSON."
|
| 188 |
+
label = triage.normalize_prediction(answer)
|
| 189 |
+
summary = answer.get("summary") or answer.get("rationale") or answer.get("reason") or ""
|
| 190 |
+
evidence = answer.get("evidence") or answer.get("key_evidence") or []
|
| 191 |
+
lines = [f"Final verdict: {label}"]
|
| 192 |
+
if summary:
|
| 193 |
+
lines.append(str(summary))
|
| 194 |
+
if isinstance(evidence, list) and evidence:
|
| 195 |
+
lines.append("Evidence:")
|
| 196 |
+
for item in evidence[:6]:
|
| 197 |
+
lines.append(f"- {item}")
|
| 198 |
+
return "\n".join(lines)
|
| 199 |
+
|
| 200 |
+
|
| 201 |
+
def shorten_text(text: str, limit: int) -> str:
|
| 202 |
+
stripped = "\n".join(line.rstrip() for line in text.strip().splitlines())
|
| 203 |
+
if len(stripped) <= limit:
|
| 204 |
+
return stripped
|
| 205 |
+
return stripped[:limit].rstrip() + f"\n[truncated to {limit} chars]"
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
def deep_tool_sequence(fmt: str) -> list[tuple[str, dict[str, Any]]]:
|
| 209 |
+
common: list[tuple[str, dict[str, Any]]] = [
|
| 210 |
+
("file", {}),
|
| 211 |
+
("entropy", {}),
|
| 212 |
+
("strings", {"min_length": 5}),
|
| 213 |
+
]
|
| 214 |
+
if fmt == "ELF":
|
| 215 |
+
return [
|
| 216 |
+
("file", {}),
|
| 217 |
+
("readelf", {}),
|
| 218 |
+
("nm", {}),
|
| 219 |
+
("entropy", {}),
|
| 220 |
+
("strings", {"min_length": 5}),
|
| 221 |
+
("objdump", {}),
|
| 222 |
+
("ghidra_summary", {"timeout": 240}),
|
| 223 |
+
]
|
| 224 |
+
if fmt == "PE":
|
| 225 |
+
return [
|
| 226 |
+
("file", {}),
|
| 227 |
+
("pe_headers", {}),
|
| 228 |
+
("pe_sections", {}),
|
| 229 |
+
("pe_imports", {}),
|
| 230 |
+
("pe_exports", {}),
|
| 231 |
+
("entropy", {}),
|
| 232 |
+
("strings", {"min_length": 5}),
|
| 233 |
+
("pe_disasm", {}),
|
| 234 |
+
("ghidra_summary", {"timeout": 240}),
|
| 235 |
+
]
|
| 236 |
+
return common + [("ghidra_summary", {"timeout": 240})]
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def compact_messages(
|
| 240 |
+
messages: list[dict[str, Any]],
|
| 241 |
+
tools: triage.StaticTools,
|
| 242 |
+
state: dict[str, Any],
|
| 243 |
+
max_tool_summaries: int = 12,
|
| 244 |
+
max_assistant_summaries: int = 5,
|
| 245 |
+
) -> list[dict[str, Any]]:
|
| 246 |
+
system_msg = next((msg for msg in messages if msg.get("role") == "system"), None)
|
| 247 |
+
first_user = next((msg for msg in messages if msg.get("role") == "user"), None)
|
| 248 |
+
|
| 249 |
+
tool_msgs = [msg for msg in messages if msg.get("role") == "tool"][-max_tool_summaries:]
|
| 250 |
+
assistant_msgs = [
|
| 251 |
+
msg for msg in messages
|
| 252 |
+
if msg.get("role") == "assistant" and str(msg.get("content") or "").strip()
|
| 253 |
+
][-max_assistant_summaries:]
|
| 254 |
+
|
| 255 |
+
lines = [
|
| 256 |
+
"Session context was compacted to reduce prompt length.",
|
| 257 |
+
f"Total tool calls before compaction: {state.get('tool_calls_seen', 0)}",
|
| 258 |
+
"Tools used: " + (", ".join(tools.used_tools[-40:]) if tools.used_tools else "none"),
|
| 259 |
+
]
|
| 260 |
+
if state.get("last_final"):
|
| 261 |
+
lines.append("Last submitted final verdict JSON: " + json.dumps(state["last_final"], ensure_ascii=True)[:1600])
|
| 262 |
+
|
| 263 |
+
if tool_msgs:
|
| 264 |
+
lines.append("\nRecent tool observations:")
|
| 265 |
+
for msg in tool_msgs:
|
| 266 |
+
name = str(msg.get("name") or "tool")
|
| 267 |
+
content = shorten_text(str(msg.get("content") or ""), 900)
|
| 268 |
+
lines.append(f"\n[{name}]\n{content}")
|
| 269 |
+
|
| 270 |
+
if assistant_msgs:
|
| 271 |
+
lines.append("\nRecent assistant conclusions:")
|
| 272 |
+
for msg in assistant_msgs:
|
| 273 |
+
content = shorten_text(str(msg.get("content") or ""), 700)
|
| 274 |
+
lines.append(f"- {content}")
|
| 275 |
+
|
| 276 |
+
lines.append("\nIf exact old bytes or full tool output are needed, rerun a targeted tool or /deep.")
|
| 277 |
+
|
| 278 |
+
compacted: list[dict[str, Any]] = []
|
| 279 |
+
if system_msg:
|
| 280 |
+
compacted.append(system_msg)
|
| 281 |
+
if first_user:
|
| 282 |
+
compacted.append(first_user)
|
| 283 |
+
compacted.append({"role": "assistant", "content": "\n".join(lines), "reasoning_content": ""})
|
| 284 |
+
return compacted
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
def save_compaction_snapshot(run_dir: Path, session: dict[str, Any]) -> Path:
|
| 288 |
+
snapshot_dir = run_dir / "compaction_snapshots"
|
| 289 |
+
snapshot_dir.mkdir(parents=True, exist_ok=True)
|
| 290 |
+
sample_id = str(session.get("sample_id") or "sample")
|
| 291 |
+
path = snapshot_dir / f"{sample_id}_{now_stamp()}_precompact.json"
|
| 292 |
+
payload = {
|
| 293 |
+
"sample_id": sample_id,
|
| 294 |
+
"source_path": str(session.get("source_path")),
|
| 295 |
+
"staged_path": str(session.get("staged_path")),
|
| 296 |
+
"format": session.get("fmt"),
|
| 297 |
+
"tools_used": session.get("tools").used_tools if session.get("tools") else [],
|
| 298 |
+
"state": session.get("state", {}),
|
| 299 |
+
"messages": session.get("messages", []),
|
| 300 |
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
| 301 |
+
}
|
| 302 |
+
path.write_text(json.dumps(payload, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
|
| 303 |
+
return path
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
def append_direct_tool_result(
|
| 307 |
+
canonical: str,
|
| 308 |
+
args: dict[str, Any],
|
| 309 |
+
tools: triage.StaticTools,
|
| 310 |
+
messages: list[dict[str, Any]],
|
| 311 |
+
cfg: triage.RuntimeConfig,
|
| 312 |
+
state: dict[str, Any],
|
| 313 |
+
) -> None:
|
| 314 |
+
call_id = f"manual_{int(time.time() * 1000)}"
|
| 315 |
+
started = time.monotonic()
|
| 316 |
+
state["tool_calls_seen"] = int(state.get("tool_calls_seen", 0)) + 1
|
| 317 |
+
progress(state, f"direct tool {state['tool_calls_seen']}: {canonical}")
|
| 318 |
+
try:
|
| 319 |
+
content = getattr(tools, canonical)(**args)
|
| 320 |
+
except Exception as exc:
|
| 321 |
+
content = f"tool error: {type(exc).__name__}: {exc}"
|
| 322 |
+
elapsed = time.monotonic() - started
|
| 323 |
+
progress(state, f"tool done: {canonical} in {elapsed:.1f}s, observation={len(str(content))} chars")
|
| 324 |
+
|
| 325 |
+
messages.append(
|
| 326 |
+
{
|
| 327 |
+
"role": "assistant",
|
| 328 |
+
"content": "",
|
| 329 |
+
"reasoning_content": "",
|
| 330 |
+
"tool_calls": [
|
| 331 |
+
{
|
| 332 |
+
"id": call_id,
|
| 333 |
+
"type": "function",
|
| 334 |
+
"function": {"name": canonical, "arguments": args},
|
| 335 |
+
}
|
| 336 |
+
],
|
| 337 |
+
}
|
| 338 |
+
)
|
| 339 |
+
messages.append(
|
| 340 |
+
{
|
| 341 |
+
"role": "tool",
|
| 342 |
+
"tool_call_id": call_id,
|
| 343 |
+
"name": canonical,
|
| 344 |
+
"content": str(content)[: cfg.obs_limit],
|
| 345 |
+
}
|
| 346 |
+
)
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def save_inspect_session(
|
| 350 |
+
run_dir: Path,
|
| 351 |
+
source_path: Path,
|
| 352 |
+
staged_path: Path,
|
| 353 |
+
fmt: str,
|
| 354 |
+
messages: list[dict[str, Any]],
|
| 355 |
+
tools: triage.StaticTools,
|
| 356 |
+
state: dict[str, Any],
|
| 357 |
+
) -> None:
|
| 358 |
+
sample_id = str(state.get("sample_id") or staged_path.stem)
|
| 359 |
+
payload = {
|
| 360 |
+
"sample_id": sample_id,
|
| 361 |
+
"source_path": str(source_path),
|
| 362 |
+
"staged_path": str(staged_path),
|
| 363 |
+
"format": fmt,
|
| 364 |
+
"tools_used": tools.used_tools,
|
| 365 |
+
"tool_calls": state.get("tool_calls_seen", 0),
|
| 366 |
+
"last_final": state.get("last_final"),
|
| 367 |
+
"messages": messages,
|
| 368 |
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
| 369 |
+
}
|
| 370 |
+
json_text = json.dumps(payload, indent=2, ensure_ascii=True) + "\n"
|
| 371 |
+
(run_dir / "inspect_transcript.json").write_text(json_text, encoding="utf-8")
|
| 372 |
+
(run_dir / f"{sample_id}_transcript.json").write_text(json_text, encoding="utf-8")
|
| 373 |
+
|
| 374 |
+
lines = [
|
| 375 |
+
"# AgentRE Chat Session",
|
| 376 |
+
"",
|
| 377 |
+
f"- Sample: `{sample_id}`",
|
| 378 |
+
f"- Source: `{source_path}`",
|
| 379 |
+
f"- Staged: `{staged_path}`",
|
| 380 |
+
f"- Format: `{fmt}`",
|
| 381 |
+
f"- Tool calls: `{state.get('tool_calls_seen', 0)}`",
|
| 382 |
+
"",
|
| 383 |
+
"## Conversation",
|
| 384 |
+
"",
|
| 385 |
+
]
|
| 386 |
+
for msg in messages:
|
| 387 |
+
role = msg.get("role")
|
| 388 |
+
if role not in {"user", "assistant"}:
|
| 389 |
+
continue
|
| 390 |
+
content = str(msg.get("content") or "").strip()
|
| 391 |
+
if not content:
|
| 392 |
+
continue
|
| 393 |
+
lines.append(f"### {role}")
|
| 394 |
+
lines.append("")
|
| 395 |
+
lines.append(content[:8000])
|
| 396 |
+
lines.append("")
|
| 397 |
+
md_text = "\n".join(lines)
|
| 398 |
+
(run_dir / "INSPECT.md").write_text(md_text, encoding="utf-8")
|
| 399 |
+
(run_dir / f"{sample_id}.md").write_text(md_text, encoding="utf-8")
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
def run_tool_calls(
|
| 403 |
+
tool_calls: list[dict[str, Any]],
|
| 404 |
+
tools: triage.StaticTools,
|
| 405 |
+
messages: list[dict[str, Any]],
|
| 406 |
+
cfg: triage.RuntimeConfig,
|
| 407 |
+
state: dict[str, Any],
|
| 408 |
+
turn: int,
|
| 409 |
+
) -> str | None:
|
| 410 |
+
final_text: str | None = None
|
| 411 |
+
for idx, tc in enumerate(tool_calls):
|
| 412 |
+
fn = tc.get("function", {})
|
| 413 |
+
name = str(fn.get("name") or "")
|
| 414 |
+
args = triage._parse_args(fn.get("arguments"))
|
| 415 |
+
tcid = tc.get("id") or f"inspect_{turn}_{idx}"
|
| 416 |
+
started = time.monotonic()
|
| 417 |
+
|
| 418 |
+
if name in triage.FINAL_TOOLS:
|
| 419 |
+
progress(state, "model submitted final_answer")
|
| 420 |
+
answer = triage.final_answer_from_args(args)
|
| 421 |
+
state["last_final"] = answer
|
| 422 |
+
content = "final verdict submitted"
|
| 423 |
+
final_text = format_final(answer)
|
| 424 |
+
else:
|
| 425 |
+
canonical = triage.TOOL_ALIASES.get(name)
|
| 426 |
+
if canonical is None:
|
| 427 |
+
progress(state, f"unknown tool requested: {name}")
|
| 428 |
+
content = f"unknown tool {name}. Use listed tools or answer naturally."
|
| 429 |
+
elif int(state.get("request_tool_calls_seen", 0)) >= cfg.max_tool_calls:
|
| 430 |
+
progress(state, "tool budget exhausted for this request")
|
| 431 |
+
content = "tool budget exhausted for this request; answer with the evidence already gathered, or ask the user to continue."
|
| 432 |
+
else:
|
| 433 |
+
state["request_tool_calls_seen"] = int(state.get("request_tool_calls_seen", 0)) + 1
|
| 434 |
+
state["tool_calls_seen"] += 1
|
| 435 |
+
progress(
|
| 436 |
+
state,
|
| 437 |
+
f"tool {state['request_tool_calls_seen']}/{cfg.max_tool_calls} this request "
|
| 438 |
+
f"(total {state['tool_calls_seen']}): {canonical}",
|
| 439 |
+
)
|
| 440 |
+
try:
|
| 441 |
+
content = getattr(tools, canonical)(**args)
|
| 442 |
+
except Exception as exc:
|
| 443 |
+
content = f"tool error: {type(exc).__name__}: {exc}"
|
| 444 |
+
elapsed = time.monotonic() - started
|
| 445 |
+
progress(state, f"tool done: {canonical} in {elapsed:.1f}s, observation={len(str(content))} chars")
|
| 446 |
+
|
| 447 |
+
messages.append(
|
| 448 |
+
{
|
| 449 |
+
"role": "tool",
|
| 450 |
+
"tool_call_id": tcid,
|
| 451 |
+
"name": name,
|
| 452 |
+
"content": str(content)[: cfg.obs_limit],
|
| 453 |
+
}
|
| 454 |
+
)
|
| 455 |
+
return final_text
|
| 456 |
+
|
| 457 |
+
|
| 458 |
+
def generate_visible_reply(
|
| 459 |
+
backend: Any,
|
| 460 |
+
messages: list[dict[str, Any]],
|
| 461 |
+
cfg: triage.RuntimeConfig,
|
| 462 |
+
tools: triage.StaticTools,
|
| 463 |
+
state: dict[str, Any],
|
| 464 |
+
) -> str:
|
| 465 |
+
final_text: str | None = None
|
| 466 |
+
last_visible_reply = ""
|
| 467 |
+
own_tool_budget = "request_tool_calls_seen" not in state
|
| 468 |
+
if own_tool_budget:
|
| 469 |
+
state["request_tool_calls_seen"] = 0
|
| 470 |
+
for turn in range(cfg.max_turns):
|
| 471 |
+
progress(state, f"model turn {turn + 1}/{cfg.max_turns}: generating")
|
| 472 |
+
started = time.monotonic()
|
| 473 |
+
generated, step_usage = backend.generate(
|
| 474 |
+
messages,
|
| 475 |
+
triage.tool_definitions(),
|
| 476 |
+
cfg.max_tokens,
|
| 477 |
+
cfg.temperature,
|
| 478 |
+
cfg.thinking,
|
| 479 |
+
)
|
| 480 |
+
elapsed = time.monotonic() - started
|
| 481 |
+
progress(state, f"model turn {turn + 1}/{cfg.max_turns}: generated in {elapsed:.1f}s")
|
| 482 |
+
usage = state.setdefault("usage", {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0})
|
| 483 |
+
for key, value in step_usage.items():
|
| 484 |
+
usage[key] = usage.get(key, 0) + int(value)
|
| 485 |
+
|
| 486 |
+
reasoning, visible = triage.split_thinking(generated)
|
| 487 |
+
tool_calls = triage.parse_tool_calls(generated)
|
| 488 |
+
if tool_calls:
|
| 489 |
+
progress(state, f"model requested {len(tool_calls)} tool call(s)")
|
| 490 |
+
assistant_content = triage.strip_tool_xml(visible)
|
| 491 |
+
if assistant_content:
|
| 492 |
+
last_visible_reply = assistant_content
|
| 493 |
+
assistant_msg: dict[str, Any] = {"role": "assistant", "content": assistant_content}
|
| 494 |
+
if reasoning and cfg.save_reasoning:
|
| 495 |
+
assistant_msg["reasoning_content"] = reasoning
|
| 496 |
+
elif reasoning:
|
| 497 |
+
assistant_msg["reasoning_content"] = ""
|
| 498 |
+
if tool_calls:
|
| 499 |
+
assistant_msg["tool_calls"] = [
|
| 500 |
+
triage.make_history_tool_call(tc, f"inspect_{turn}_{idx}")
|
| 501 |
+
for idx, tc in enumerate(tool_calls)
|
| 502 |
+
]
|
| 503 |
+
messages.append(assistant_msg)
|
| 504 |
+
|
| 505 |
+
if not tool_calls:
|
| 506 |
+
reply = assistant_content or generated.strip() or "(no response)"
|
| 507 |
+
if looks_truncated_reply(reply):
|
| 508 |
+
state["last_reply_truncated"] = True
|
| 509 |
+
progress(state, "reply may have hit --max-tokens; use /continue or raise --max-tokens")
|
| 510 |
+
reply += (
|
| 511 |
+
"\n\n[xref 9b] response may have hit --max-tokens before finishing; "
|
| 512 |
+
"use /continue or raise --max-tokens for longer answers."
|
| 513 |
+
)
|
| 514 |
+
if own_tool_budget:
|
| 515 |
+
state.pop("request_tool_calls_seen", None)
|
| 516 |
+
return reply
|
| 517 |
+
|
| 518 |
+
maybe_final = run_tool_calls(tool_calls, tools, messages, cfg, state, turn)
|
| 519 |
+
if maybe_final:
|
| 520 |
+
final_text = maybe_final
|
| 521 |
+
messages.append(
|
| 522 |
+
{
|
| 523 |
+
"role": "user",
|
| 524 |
+
"content": "Summarize that final verdict for the user in concise natural language. Do not call another tool unless needed.",
|
| 525 |
+
}
|
| 526 |
+
)
|
| 527 |
+
|
| 528 |
+
if final_text:
|
| 529 |
+
if own_tool_budget:
|
| 530 |
+
state.pop("request_tool_calls_seen", None)
|
| 531 |
+
return final_text
|
| 532 |
+
if last_visible_reply:
|
| 533 |
+
if own_tool_budget:
|
| 534 |
+
state.pop("request_tool_calls_seen", None)
|
| 535 |
+
return (
|
| 536 |
+
last_visible_reply
|
| 537 |
+
+ "\n\n[xref 9b] internal tool loop reached --max-turns before a final tool submission; "
|
| 538 |
+
+ "ask a follow-up or raise --max-turns if you want deeper analysis."
|
| 539 |
+
)
|
| 540 |
+
if own_tool_budget:
|
| 541 |
+
state.pop("request_tool_calls_seen", None)
|
| 542 |
+
return "[xref 9b] assistant turn stopped after the internal turn limit; ask a narrower follow-up or raise --max-turns."
|
| 543 |
+
|
| 544 |
+
|
| 545 |
+
def default_chat_model() -> str:
|
| 546 |
+
packaged = ROOT / "xref-9b-q4_k_m.gguf"
|
| 547 |
+
return (
|
| 548 |
+
os.environ.get("XREF9B_GGUF")
|
| 549 |
+
or os.environ.get("AGENTRE_GGUF")
|
| 550 |
+
or os.environ.get("AGENTRE_MODEL")
|
| 551 |
+
or (str(packaged) if packaged.exists() else triage.DEFAULT_BASE_MODEL)
|
| 552 |
+
)
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
def default_llama_cli() -> str:
|
| 556 |
+
return os.environ.get("XREF9B_LLAMA_CLI") or os.environ.get("AGENTRE_LLAMA_CLI") or triage.DEFAULT_LLAMA_CLI
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
def default_adapter_for_model(model: str) -> str:
|
| 560 |
+
return "" if str(model).lower().endswith(".gguf") else triage.DEFAULT_ADAPTER
|
| 561 |
+
|
| 562 |
+
|
| 563 |
+
def sample_id_for_index(index: int, total: int) -> str:
|
| 564 |
+
return "inspect_0001" if total == 1 else f"sample_{index + 1:04d}"
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
def display_path(path: Path, root: Path) -> str:
|
| 568 |
+
try:
|
| 569 |
+
base = root if root.is_dir() else root.parent
|
| 570 |
+
return str(path.relative_to(base))
|
| 571 |
+
except Exception:
|
| 572 |
+
return str(path)
|
| 573 |
+
|
| 574 |
+
|
| 575 |
+
def save_chat_index(
|
| 576 |
+
run_dir: Path,
|
| 577 |
+
target_root: Path,
|
| 578 |
+
targets: list[Path],
|
| 579 |
+
active_idx: int,
|
| 580 |
+
sessions: dict[int, dict[str, Any]],
|
| 581 |
+
) -> None:
|
| 582 |
+
samples: list[dict[str, Any]] = []
|
| 583 |
+
for idx, path in enumerate(targets):
|
| 584 |
+
session = sessions.get(idx)
|
| 585 |
+
state = session.get("state", {}) if session else {}
|
| 586 |
+
samples.append(
|
| 587 |
+
{
|
| 588 |
+
"index": idx + 1,
|
| 589 |
+
"sample_id": sample_id_for_index(idx, len(targets)),
|
| 590 |
+
"path": str(path),
|
| 591 |
+
"display_path": display_path(path, target_root),
|
| 592 |
+
"format": session.get("fmt") if session else triage.detect_format(path),
|
| 593 |
+
"opened": bool(session),
|
| 594 |
+
"tool_calls": state.get("tool_calls_seen", 0),
|
| 595 |
+
"last_final": state.get("last_final"),
|
| 596 |
+
}
|
| 597 |
+
)
|
| 598 |
+
payload = {
|
| 599 |
+
"target_root": str(target_root),
|
| 600 |
+
"active_index": active_idx + 1,
|
| 601 |
+
"samples": samples,
|
| 602 |
+
"saved_at": datetime.now(timezone.utc).isoformat(),
|
| 603 |
+
}
|
| 604 |
+
(run_dir / "chat_state.json").write_text(json.dumps(payload, indent=2, ensure_ascii=True) + "\n", encoding="utf-8")
|
| 605 |
+
|
| 606 |
+
lines = [
|
| 607 |
+
"# AgentRE Chat Index",
|
| 608 |
+
"",
|
| 609 |
+
f"- Target: `{target_root}`",
|
| 610 |
+
f"- Active sample: `{active_idx + 1}`",
|
| 611 |
+
f"- Samples: `{len(targets)}`",
|
| 612 |
+
"",
|
| 613 |
+
"## Samples",
|
| 614 |
+
"",
|
| 615 |
+
]
|
| 616 |
+
for sample in samples:
|
| 617 |
+
marker = "*" if sample["index"] == active_idx + 1 else "-"
|
| 618 |
+
opened = " opened" if sample["opened"] else ""
|
| 619 |
+
lines.append(
|
| 620 |
+
f"{marker} `{sample['index']:04d}` `{sample['format']}`{opened} "
|
| 621 |
+
f"tools={sample['tool_calls']} `{sample['display_path']}`"
|
| 622 |
+
)
|
| 623 |
+
(run_dir / "CHAT.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
| 624 |
+
|
| 625 |
+
|
| 626 |
+
def print_sample_list(target_root: Path, targets: list[Path], active_idx: int, sessions: dict[int, dict[str, Any]], limit: int = 80) -> None:
|
| 627 |
+
shown = targets[:limit]
|
| 628 |
+
for idx, path in enumerate(shown):
|
| 629 |
+
session = sessions.get(idx)
|
| 630 |
+
fmt = session.get("fmt") if session else triage.detect_format(path)
|
| 631 |
+
marker = "*" if idx == active_idx else " "
|
| 632 |
+
opened = " opened" if session else ""
|
| 633 |
+
print(f"{marker} {idx + 1:4d} {fmt:7s}{opened:8s} {display_path(path, target_root)}")
|
| 634 |
+
if len(targets) > limit:
|
| 635 |
+
print(f"... {len(targets) - limit} more samples not shown; use /open N by index.")
|
| 636 |
+
|
| 637 |
+
|
| 638 |
+
def resolve_sample_selector(selector: str, target_root: Path, targets: list[Path]) -> int:
|
| 639 |
+
text = selector.strip()
|
| 640 |
+
if not text:
|
| 641 |
+
raise ValueError("usage: /open N or /open path-substring")
|
| 642 |
+
if text.isdigit():
|
| 643 |
+
idx = int(text) - 1
|
| 644 |
+
if 0 <= idx < len(targets):
|
| 645 |
+
return idx
|
| 646 |
+
raise ValueError(f"sample index out of range: {text}")
|
| 647 |
+
|
| 648 |
+
maybe_path = Path(text).expanduser()
|
| 649 |
+
if maybe_path.exists():
|
| 650 |
+
resolved = maybe_path.resolve()
|
| 651 |
+
for idx, path in enumerate(targets):
|
| 652 |
+
if path == resolved:
|
| 653 |
+
return idx
|
| 654 |
+
|
| 655 |
+
lowered = text.lower()
|
| 656 |
+
matches = [
|
| 657 |
+
idx
|
| 658 |
+
for idx, path in enumerate(targets)
|
| 659 |
+
if lowered in path.name.lower() or lowered in display_path(path, target_root).lower() or lowered in str(path).lower()
|
| 660 |
+
]
|
| 661 |
+
if len(matches) == 1:
|
| 662 |
+
return matches[0]
|
| 663 |
+
if not matches:
|
| 664 |
+
raise ValueError(f"no sample matched: {text}")
|
| 665 |
+
preview = ", ".join(str(idx + 1) for idx in matches[:12])
|
| 666 |
+
raise ValueError(f"ambiguous sample selector; matched indexes: {preview}")
|
| 667 |
+
|
| 668 |
+
|
| 669 |
+
def run_tool_smoke_targets(targets: list[Path], run_dir: Path, cfg: triage.RuntimeConfig) -> int:
|
| 670 |
+
for idx, source_path in enumerate(targets):
|
| 671 |
+
sample_id = sample_id_for_index(idx, len(targets))
|
| 672 |
+
staged_path, fmt = stage_one(source_path, run_dir, sample_id)
|
| 673 |
+
tools = triage.StaticTools(staged_path, fmt, cfg.obs_limit, cfg.ghidra_script_dir)
|
| 674 |
+
print(f"== {idx + 1}: {source_path} ({fmt}) ==")
|
| 675 |
+
print("\n[file]")
|
| 676 |
+
print(tools.file())
|
| 677 |
+
print("\n[strings]")
|
| 678 |
+
print(tools.strings()[:2500])
|
| 679 |
+
print(f"\n[xref 9b] staged={staged_path}")
|
| 680 |
+
return 0
|
| 681 |
+
|
| 682 |
+
|
| 683 |
+
def run_chat(argv: list[str], command_name: str = "chat") -> int:
|
| 684 |
+
parser = argparse.ArgumentParser(
|
| 685 |
+
prog=f"agentre.py {command_name}",
|
| 686 |
+
description="Chat with AgentRE about one PE/ELF sample, or a directory passed with -d, while it uses static RE tools.",
|
| 687 |
+
)
|
| 688 |
+
loaded_env = load_local_agentre_env()
|
| 689 |
+
default_model = default_chat_model()
|
| 690 |
+
default_cli = default_llama_cli()
|
| 691 |
+
parser.add_argument("target", nargs="?", help="Binary file to inspect. Use -d/--directory for directory mode.")
|
| 692 |
+
parser.add_argument("-d", "--directory", default="", help="Directory of PE/ELF files to inspect interactively.")
|
| 693 |
+
parser.add_argument("--spec", default=str(triage.DEFAULT_SPEC), help="Unified reverse-engineering spec.")
|
| 694 |
+
parser.add_argument(
|
| 695 |
+
"--backend",
|
| 696 |
+
choices=["auto", "transformers", "llama-cpp"],
|
| 697 |
+
default="auto",
|
| 698 |
+
help="Inference backend. auto selects llama-cpp for .gguf models, otherwise transformers.",
|
| 699 |
+
)
|
| 700 |
+
parser.add_argument("--model", default=default_model, help="HF base model path/name or GGUF path for llama.cpp.")
|
| 701 |
+
parser.add_argument("--adapter", default=None, help="Optional HF LoRA adapter for transformers. Use '' to disable.")
|
| 702 |
+
parser.add_argument("--output-dir", default="", help="Output directory. Defaults to runs/agentre_chat_<timestamp>.")
|
| 703 |
+
parser.add_argument("--max-files", type=int, default=0, help="Directory mode limit; 0 means no limit.")
|
| 704 |
+
parser.add_argument("--include-unknown", action="store_true", help="Include files that are not PE/ELF by magic bytes.")
|
| 705 |
+
parser.add_argument("--max-turns", type=int, default=8, help="Internal assistant/tool rounds per user message.")
|
| 706 |
+
parser.add_argument("--max-tool-calls", type=int, default=40, help="Model-selected tool calls allowed per user request.")
|
| 707 |
+
parser.add_argument("--max-tokens", type=int, default=1200)
|
| 708 |
+
parser.add_argument("--obs-limit", type=int, default=7000)
|
| 709 |
+
parser.add_argument("--temperature", type=float, default=0.1)
|
| 710 |
+
parser.add_argument("--dtype", default="auto", help="auto, bfloat16, float16, float32.")
|
| 711 |
+
parser.add_argument("--device-map", default="auto")
|
| 712 |
+
parser.add_argument("--llama-cli", default=default_cli, help="Path to llama.cpp llama-completion.")
|
| 713 |
+
parser.add_argument("--ctx-size", type=int, default=65536, help="llama.cpp context size.")
|
| 714 |
+
parser.add_argument("--threads", type=int, default=0, help="llama.cpp CPU threads; 0 lets llama.cpp choose.")
|
| 715 |
+
parser.add_argument("--gpu-layers", type=int, default=-1, help="llama.cpp GPU layers; -1 leaves default.")
|
| 716 |
+
parser.add_argument("--llama-lora", default="", help="Optional llama.cpp-compatible LoRA adapter.")
|
| 717 |
+
parser.add_argument("--llama-extra-arg", action="append", default=[], help="Extra raw argument passed to llama-completion. Repeatable.")
|
| 718 |
+
parser.add_argument("--no-thinking", action="store_true", help="Disable Qwen thinking template flag.")
|
| 719 |
+
parser.add_argument("--quiet", action="store_true", help="Hide progress messages while the model/tools run.")
|
| 720 |
+
parser.add_argument("--save-reasoning", action="store_true", help=argparse.SUPPRESS)
|
| 721 |
+
parser.add_argument("--ghidra-script-dir", default=str(triage.DEFAULT_GHIDRA_SCRIPT_DIR))
|
| 722 |
+
parser.add_argument(
|
| 723 |
+
"--request",
|
| 724 |
+
default="Is this malicious or benign? Reverse engineer it and use the available tools.",
|
| 725 |
+
help="Initial user request.",
|
| 726 |
+
)
|
| 727 |
+
parser.add_argument("--one-shot", action="store_true", help="Run the initial inspection and exit instead of opening the prompt.")
|
| 728 |
+
parser.add_argument("--tool-smoke", action="store_true", help="Run static tools and exit without loading a model.")
|
| 729 |
+
args = parser.parse_args(argv)
|
| 730 |
+
|
| 731 |
+
if loaded_env and not args.quiet:
|
| 732 |
+
print(f"[xref 9b] loaded defaults from {LOCAL_AGENTRE_ENV}", flush=True)
|
| 733 |
+
|
| 734 |
+
args.adapter = default_adapter_for_model(args.model) if args.adapter is None else args.adapter
|
| 735 |
+
|
| 736 |
+
if args.directory and args.target:
|
| 737 |
+
print("[xref 9b] provide either one file target or -d/--directory, not both.", file=sys.stderr)
|
| 738 |
+
return 2
|
| 739 |
+
if not args.directory and not args.target:
|
| 740 |
+
print("[xref 9b] provide one binary file, or use -d/--directory DIR for directory mode.", file=sys.stderr)
|
| 741 |
+
return 2
|
| 742 |
+
|
| 743 |
+
directory_mode = bool(args.directory)
|
| 744 |
+
target_root = Path(args.directory or args.target).expanduser().resolve()
|
| 745 |
+
if directory_mode:
|
| 746 |
+
if not target_root.is_dir():
|
| 747 |
+
print(f"[xref 9b] -d/--directory expects a directory: {target_root}", file=sys.stderr)
|
| 748 |
+
return 2
|
| 749 |
+
elif target_root.is_dir():
|
| 750 |
+
print(f"[xref 9b] refusing implicit directory chat target: {target_root}", file=sys.stderr)
|
| 751 |
+
print("[xref 9b] use -d/--directory for directory mode, or pass the full file path.", file=sys.stderr)
|
| 752 |
+
return 2
|
| 753 |
+
|
| 754 |
+
try:
|
| 755 |
+
targets = triage.collect_targets(target_root, include_unknown=args.include_unknown, max_files=args.max_files)
|
| 756 |
+
except FileNotFoundError:
|
| 757 |
+
print(f"[xref 9b] target not found: {target_root}", file=sys.stderr)
|
| 758 |
+
return 2
|
| 759 |
+
if not targets:
|
| 760 |
+
print(f"[xref 9b] no PE/ELF targets found under {target_root}", file=sys.stderr)
|
| 761 |
+
return 2
|
| 762 |
+
|
| 763 |
+
run_dir = Path(args.output_dir).expanduser().resolve() if args.output_dir else (ROOT / "runs" / f"agentre_chat_{now_stamp()}").resolve()
|
| 764 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 765 |
+
cfg = triage.RuntimeConfig(
|
| 766 |
+
max_turns=args.max_turns,
|
| 767 |
+
max_tool_calls=args.max_tool_calls,
|
| 768 |
+
max_tokens=args.max_tokens,
|
| 769 |
+
obs_limit=args.obs_limit,
|
| 770 |
+
temperature=args.temperature,
|
| 771 |
+
thinking=not args.no_thinking,
|
| 772 |
+
save_transcripts=True,
|
| 773 |
+
save_reasoning=False,
|
| 774 |
+
ghidra_script_dir=Path(args.ghidra_script_dir).expanduser().resolve(),
|
| 775 |
+
)
|
| 776 |
+
|
| 777 |
+
if args.tool_smoke:
|
| 778 |
+
return run_tool_smoke_targets(targets, run_dir, cfg)
|
| 779 |
+
|
| 780 |
+
spec = Path(args.spec).read_text(encoding="utf-8")
|
| 781 |
+
sessions: dict[int, dict[str, Any]] = {}
|
| 782 |
+
active_idx = 0
|
| 783 |
+
|
| 784 |
+
print(f"[xref 9b] chat target={target_root}", flush=True)
|
| 785 |
+
print(f"[xref 9b] discovered={len(targets)} output={run_dir}", flush=True)
|
| 786 |
+
print(f"[xref 9b] thinking={'on' if cfg.thinking else 'off'}", flush=True)
|
| 787 |
+
if len(targets) > 1:
|
| 788 |
+
print("[xref 9b] use /samples to list files and /open N to switch samples", flush=True)
|
| 789 |
+
_, backend = build_backend(args)
|
| 790 |
+
|
| 791 |
+
def ensure_session(index: int) -> dict[str, Any]:
|
| 792 |
+
if index in sessions:
|
| 793 |
+
return sessions[index]
|
| 794 |
+
source_path = targets[index]
|
| 795 |
+
sample_id = sample_id_for_index(index, len(targets))
|
| 796 |
+
staged_path, fmt = stage_one(source_path, run_dir, sample_id)
|
| 797 |
+
session = {
|
| 798 |
+
"source_path": source_path,
|
| 799 |
+
"sample_id": sample_id,
|
| 800 |
+
"staged_path": staged_path,
|
| 801 |
+
"fmt": fmt,
|
| 802 |
+
"tools": triage.StaticTools(staged_path, fmt, cfg.obs_limit, cfg.ghidra_script_dir),
|
| 803 |
+
"messages": build_inspect_messages(spec, sample_id, fmt, args.request),
|
| 804 |
+
"state": {"sample_id": sample_id, "tool_calls_seen": 0, "last_final": None, "progress": not args.quiet},
|
| 805 |
+
"started": False,
|
| 806 |
+
}
|
| 807 |
+
sessions[index] = session
|
| 808 |
+
return session
|
| 809 |
+
|
| 810 |
+
def save_session(index: int) -> None:
|
| 811 |
+
session = ensure_session(index)
|
| 812 |
+
save_inspect_session(
|
| 813 |
+
run_dir,
|
| 814 |
+
session["source_path"],
|
| 815 |
+
session["staged_path"],
|
| 816 |
+
session["fmt"],
|
| 817 |
+
session["messages"],
|
| 818 |
+
session["tools"],
|
| 819 |
+
session["state"],
|
| 820 |
+
)
|
| 821 |
+
save_chat_index(run_dir, target_root, targets, index, sessions)
|
| 822 |
+
|
| 823 |
+
def activate_sample(index: int) -> dict[str, Any]:
|
| 824 |
+
session = ensure_session(index)
|
| 825 |
+
print(
|
| 826 |
+
f"[xref 9b] active {index + 1}/{len(targets)} {session['sample_id']} "
|
| 827 |
+
f"format={session['fmt']} path={display_path(session['source_path'], target_root)}",
|
| 828 |
+
flush=True,
|
| 829 |
+
)
|
| 830 |
+
if not session["started"]:
|
| 831 |
+
reply = generate_visible_reply(backend, session["messages"], cfg, session["tools"], session["state"])
|
| 832 |
+
session["started"] = True
|
| 833 |
+
print("\nxref 9b> " + reply.strip() + "\n")
|
| 834 |
+
save_session(index)
|
| 835 |
+
return session
|
| 836 |
+
|
| 837 |
+
current = activate_sample(active_idx)
|
| 838 |
+
if args.one_shot:
|
| 839 |
+
print(f"[xref 9b] transcript={run_dir / 'INSPECT.md'}")
|
| 840 |
+
return 0
|
| 841 |
+
|
| 842 |
+
print("Commands: /help, /samples, /open N, /current, /tools, /deep, /ghidra, /verdict, /compact, /continue, /save, /quit")
|
| 843 |
+
while True:
|
| 844 |
+
try:
|
| 845 |
+
user_text = input("you> ").strip()
|
| 846 |
+
except (EOFError, KeyboardInterrupt):
|
| 847 |
+
print()
|
| 848 |
+
break
|
| 849 |
+
if not user_text:
|
| 850 |
+
continue
|
| 851 |
+
if user_text in {"/q", "/quit", "quit", "exit"}:
|
| 852 |
+
break
|
| 853 |
+
if user_text == "/help":
|
| 854 |
+
print("Ask natural-language RE questions about the active sample.")
|
| 855 |
+
print("Examples: `is this packed?`, `run Ghidra`, `why malicious?`, `what strings matter?`")
|
| 856 |
+
print("Slash commands: /samples, /open N, /current, /tools, /deep, /ghidra, /verdict, /compact, /continue, /save, /quit.")
|
| 857 |
+
continue
|
| 858 |
+
if user_text == "/samples":
|
| 859 |
+
print_sample_list(target_root, targets, active_idx, sessions)
|
| 860 |
+
continue
|
| 861 |
+
if user_text.startswith("/open"):
|
| 862 |
+
selector = user_text[len("/open") :].strip()
|
| 863 |
+
try:
|
| 864 |
+
active_idx = resolve_sample_selector(selector, target_root, targets)
|
| 865 |
+
except ValueError as exc:
|
| 866 |
+
print(f"[xref 9b] {exc}")
|
| 867 |
+
continue
|
| 868 |
+
current = activate_sample(active_idx)
|
| 869 |
+
continue
|
| 870 |
+
if user_text == "/current":
|
| 871 |
+
state = current["state"]
|
| 872 |
+
print(
|
| 873 |
+
f"{active_idx + 1}/{len(targets)} {current['sample_id']} {current['fmt']} "
|
| 874 |
+
f"tools={state.get('tool_calls_seen', 0)} {display_path(current['source_path'], target_root)}"
|
| 875 |
+
)
|
| 876 |
+
continue
|
| 877 |
+
if user_text == "/tools":
|
| 878 |
+
used = current["tools"].used_tools
|
| 879 |
+
print(", ".join(used) if used else "(no tools used yet)")
|
| 880 |
+
continue
|
| 881 |
+
if user_text == "/save":
|
| 882 |
+
save_session(active_idx)
|
| 883 |
+
print(f"[xref 9b] transcript={run_dir / 'INSPECT.md'}")
|
| 884 |
+
continue
|
| 885 |
+
if user_text == "/compact":
|
| 886 |
+
before = len(current["messages"])
|
| 887 |
+
snapshot = save_compaction_snapshot(run_dir, current)
|
| 888 |
+
current["messages"] = compact_messages(current["messages"], current["tools"], current["state"])
|
| 889 |
+
after = len(current["messages"])
|
| 890 |
+
save_session(active_idx)
|
| 891 |
+
print(f"[xref 9b] compacted conversation {before}->{after} messages; precompact={snapshot}")
|
| 892 |
+
continue
|
| 893 |
+
if user_text == "/continue":
|
| 894 |
+
user_text = "Continue the previous answer from where it stopped. Do not repeat earlier content."
|
| 895 |
+
elif user_text == "/ghidra":
|
| 896 |
+
user_text = "Run Ghidra headless on this sample and explain what it shows."
|
| 897 |
+
|
| 898 |
+
if user_text == "/deep":
|
| 899 |
+
state = current["state"]
|
| 900 |
+
current["messages"].append({
|
| 901 |
+
"role": "user",
|
| 902 |
+
"content": "Run the deeper static reverse-engineering workflow for this sample, then summarize the evidence.",
|
| 903 |
+
})
|
| 904 |
+
sequence = deep_tool_sequence(current["fmt"])
|
| 905 |
+
progress(state, f"deep analysis: running {len(sequence)} static tools")
|
| 906 |
+
for canonical, tool_args in sequence:
|
| 907 |
+
append_direct_tool_result(canonical, tool_args, current["tools"], current["messages"], cfg, state)
|
| 908 |
+
current["messages"].append({
|
| 909 |
+
"role": "user",
|
| 910 |
+
"content": (
|
| 911 |
+
"Using the deep static tool results just provided, give a concise analyst summary. "
|
| 912 |
+
"Classify only if the evidence supports it; otherwise say unknown and name the missing evidence. "
|
| 913 |
+
"Do not repeat raw tool output and do not run the same broad tools again unless one targeted follow-up is essential."
|
| 914 |
+
),
|
| 915 |
+
})
|
| 916 |
+
state["request_tool_calls_seen"] = 0
|
| 917 |
+
try:
|
| 918 |
+
reply = generate_visible_reply(backend, current["messages"], cfg, current["tools"], state)
|
| 919 |
+
finally:
|
| 920 |
+
state.pop("request_tool_calls_seen", None)
|
| 921 |
+
print("\nxref 9b> " + reply.strip() + "\n")
|
| 922 |
+
save_session(active_idx)
|
| 923 |
+
continue
|
| 924 |
+
|
| 925 |
+
if user_text == "/verdict":
|
| 926 |
+
state = current["state"]
|
| 927 |
+
current["messages"].append({
|
| 928 |
+
"role": "user",
|
| 929 |
+
"content": (
|
| 930 |
+
"Give a concise final verdict now using the evidence already gathered. "
|
| 931 |
+
"Call final_answer with classification, confidence, summary, and key evidence. "
|
| 932 |
+
"Do not call additional tools; if evidence is insufficient, classify as unknown and explain why."
|
| 933 |
+
),
|
| 934 |
+
})
|
| 935 |
+
state["request_tool_calls_seen"] = cfg.max_tool_calls
|
| 936 |
+
try:
|
| 937 |
+
reply = generate_visible_reply(backend, current["messages"], cfg, current["tools"], state)
|
| 938 |
+
finally:
|
| 939 |
+
state.pop("request_tool_calls_seen", None)
|
| 940 |
+
print("\nxref 9b> " + reply.strip() + "\n")
|
| 941 |
+
save_session(active_idx)
|
| 942 |
+
continue
|
| 943 |
+
|
| 944 |
+
current["messages"].append({"role": "user", "content": user_text})
|
| 945 |
+
state = current["state"]
|
| 946 |
+
state["request_tool_calls_seen"] = 0
|
| 947 |
+
try:
|
| 948 |
+
direct_tool = requested_direct_tool(user_text)
|
| 949 |
+
if direct_tool:
|
| 950 |
+
canonical, tool_args = direct_tool
|
| 951 |
+
append_direct_tool_result(canonical, tool_args, current["tools"], current["messages"], cfg, state)
|
| 952 |
+
reply = generate_visible_reply(backend, current["messages"], cfg, current["tools"], state)
|
| 953 |
+
finally:
|
| 954 |
+
state.pop("request_tool_calls_seen", None)
|
| 955 |
+
print("\nxref 9b> " + reply.strip() + "\n")
|
| 956 |
+
save_session(active_idx)
|
| 957 |
+
|
| 958 |
+
save_session(active_idx)
|
| 959 |
+
print(f"[xref 9b] transcript={run_dir / 'INSPECT.md'}")
|
| 960 |
+
print(f"[xref 9b] chat index={run_dir / 'CHAT.md'}")
|
| 961 |
+
return 0
|
| 962 |
+
|
| 963 |
+
|
| 964 |
+
def main(argv: list[str] | None = None) -> int:
|
| 965 |
+
args = list(sys.argv[1:] if argv is None else argv)
|
| 966 |
+
if not args or args[0] in {"-h", "--help", "help"}:
|
| 967 |
+
print_top_help()
|
| 968 |
+
return 0
|
| 969 |
+
command, rest = args[0], args[1:]
|
| 970 |
+
if command == "triage":
|
| 971 |
+
script = ROOT / "agentre_triage.py"
|
| 972 |
+
os.execv(sys.executable, [sys.executable, str(script)] + rest)
|
| 973 |
+
if command == "chat":
|
| 974 |
+
return run_chat(rest, "chat")
|
| 975 |
+
if command == "inspect":
|
| 976 |
+
return run_chat(rest, "inspect")
|
| 977 |
+
print(f"Unknown command: {command}\n", file=sys.stderr)
|
| 978 |
+
print_top_help()
|
| 979 |
+
return 2
|
| 980 |
+
|
| 981 |
+
|
| 982 |
+
if __name__ == "__main__":
|
| 983 |
+
raise SystemExit(main())
|
agentre_triage.py
ADDED
|
@@ -0,0 +1,1265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""User-facing AgentRE PE/ELF triage CLI.
|
| 3 |
+
|
| 4 |
+
This is intentionally not an eval harness and does not require vLLM. It loads a
|
| 5 |
+
local Qwen/Qwen3.5-style model with an optional LoRA adapter through
|
| 6 |
+
Transformers, exposes static reverse-engineering tools, and lets the model
|
| 7 |
+
inspect one file or every PE/ELF file in a directory.
|
| 8 |
+
|
| 9 |
+
Static analysis only: samples are copied to neutral staging paths, never
|
| 10 |
+
executed, and tool path arguments are ignored.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import json
|
| 17 |
+
import math
|
| 18 |
+
import os
|
| 19 |
+
import re
|
| 20 |
+
import shutil
|
| 21 |
+
import subprocess
|
| 22 |
+
import sys
|
| 23 |
+
import tempfile
|
| 24 |
+
import time
|
| 25 |
+
from dataclasses import dataclass
|
| 26 |
+
from datetime import datetime, timezone
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Any
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
ROOT = Path(__file__).resolve().parent
|
| 32 |
+
STARTER = ROOT.parents[1] / "agentre_rl_starter"
|
| 33 |
+
|
| 34 |
+
DEFAULT_BASE_MODEL = os.environ.get("XREF9B_BASE_MODEL") or os.environ.get("AGENTRE_BASE_MODEL") or "Qwen/Qwen3.5-9B"
|
| 35 |
+
DEFAULT_ADAPTER = os.environ.get("XREF9B_ADAPTER") or os.environ.get("AGENTRE_ADAPTER", "")
|
| 36 |
+
DEFAULT_SPEC = ROOT / "reverse_engineering_spec_unified.md"
|
| 37 |
+
DEFAULT_GHIDRA_SCRIPT_DIR = ROOT / "tools" / "ghidra_scripts"
|
| 38 |
+
DEFAULT_LLAMA_CLI = os.environ.get("AGENTRE_LLAMA_CLI", "llama-completion")
|
| 39 |
+
|
| 40 |
+
FINAL_TOOLS = {"final_answer", "submit_answer", "submit"}
|
| 41 |
+
|
| 42 |
+
TOOL_LIMITS = {
|
| 43 |
+
"file": 3000,
|
| 44 |
+
"strings": 9000,
|
| 45 |
+
"readelf": 14000,
|
| 46 |
+
"objdump": 18000,
|
| 47 |
+
"nm": 8000,
|
| 48 |
+
"hexdump": 8000,
|
| 49 |
+
"xxd": 8000,
|
| 50 |
+
"entropy": 2000,
|
| 51 |
+
"disasm_func": 18000,
|
| 52 |
+
"pe_headers": 14000,
|
| 53 |
+
"pe_sections": 9000,
|
| 54 |
+
"pe_imports": 14000,
|
| 55 |
+
"pe_exports": 9000,
|
| 56 |
+
"pe_disasm": 18000,
|
| 57 |
+
"pe_symbols": 9000,
|
| 58 |
+
"ghidra_summary": 24000,
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
TOOL_ALIASES = {
|
| 62 |
+
"file": "file",
|
| 63 |
+
"run_file": "file",
|
| 64 |
+
"strings": "strings",
|
| 65 |
+
"run_strings": "strings",
|
| 66 |
+
"readelf": "readelf",
|
| 67 |
+
"run_readelf": "readelf",
|
| 68 |
+
"objdump": "objdump",
|
| 69 |
+
"run_objdump": "objdump",
|
| 70 |
+
"nm": "nm",
|
| 71 |
+
"run_nm": "nm",
|
| 72 |
+
"hexdump": "hexdump",
|
| 73 |
+
"run_hexdump": "hexdump",
|
| 74 |
+
"xxd": "xxd",
|
| 75 |
+
"run_xxd": "xxd",
|
| 76 |
+
"entropy": "entropy",
|
| 77 |
+
"run_entropy": "entropy",
|
| 78 |
+
"disasm_func": "disasm_func",
|
| 79 |
+
"run_disasm_func": "disasm_func",
|
| 80 |
+
"pe_headers": "pe_headers",
|
| 81 |
+
"run_pe_headers": "pe_headers",
|
| 82 |
+
"pe_sections": "pe_sections",
|
| 83 |
+
"run_pe_sections": "pe_sections",
|
| 84 |
+
"pe_imports": "pe_imports",
|
| 85 |
+
"run_pe_imports": "pe_imports",
|
| 86 |
+
"pe_exports": "pe_exports",
|
| 87 |
+
"run_pe_exports": "pe_exports",
|
| 88 |
+
"pe_disasm": "pe_disasm",
|
| 89 |
+
"run_pe_disasm": "pe_disasm",
|
| 90 |
+
"pe_symbols": "pe_symbols",
|
| 91 |
+
"run_pe_symbols": "pe_symbols",
|
| 92 |
+
"ghidra_summary": "ghidra_summary",
|
| 93 |
+
"run_ghidra": "ghidra_summary",
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
XML_TOOL_RE = re.compile(
|
| 97 |
+
r"<tool_call>\s*<function=([^>\n]+)>\s*(.*?)\s*</function>\s*</tool_call>",
|
| 98 |
+
re.DOTALL,
|
| 99 |
+
)
|
| 100 |
+
XML_PARAM_RE = re.compile(
|
| 101 |
+
r"<parameter=([^>\n]+)>\s*(.*?)\s*</parameter>",
|
| 102 |
+
re.DOTALL,
|
| 103 |
+
)
|
| 104 |
+
JSON_TOOL_RE = re.compile(r"<tool_call>\s*(\{.*?\})\s*</tool_call>", re.DOTALL)
|
| 105 |
+
THINK_RE = re.compile(r"<think>\s*(.*?)\s*</think>\s*", re.DOTALL)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _now_stamp() -> str:
|
| 109 |
+
return datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _json_from(text: str, pos: int = 0) -> Any | None:
|
| 113 |
+
decoder = json.JSONDecoder()
|
| 114 |
+
i = text.find("{", pos)
|
| 115 |
+
while i >= 0:
|
| 116 |
+
try:
|
| 117 |
+
obj, _ = decoder.raw_decode(text[i:])
|
| 118 |
+
return obj
|
| 119 |
+
except Exception:
|
| 120 |
+
i = text.find("{", i + 1)
|
| 121 |
+
return None
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _parse_json_maybe(value: Any) -> Any:
|
| 125 |
+
if not isinstance(value, str):
|
| 126 |
+
return value
|
| 127 |
+
text = value.strip()
|
| 128 |
+
if text.startswith("```"):
|
| 129 |
+
lines = text.splitlines()
|
| 130 |
+
if lines and lines[0].lstrip().startswith("```"):
|
| 131 |
+
lines = lines[1:]
|
| 132 |
+
if lines and lines[-1].strip().startswith("```"):
|
| 133 |
+
lines = lines[:-1]
|
| 134 |
+
text = "\n".join(lines).strip()
|
| 135 |
+
try:
|
| 136 |
+
return json.loads(text)
|
| 137 |
+
except Exception:
|
| 138 |
+
obj = _json_from(text)
|
| 139 |
+
return obj if obj is not None else value
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def _parse_args(raw: Any) -> dict[str, Any]:
|
| 143 |
+
if isinstance(raw, dict):
|
| 144 |
+
return raw
|
| 145 |
+
if raw in (None, ""):
|
| 146 |
+
return {}
|
| 147 |
+
parsed = _parse_json_maybe(raw)
|
| 148 |
+
return parsed if isinstance(parsed, dict) else {}
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _tool_call(name: str, args: Any, idx: int) -> dict[str, Any]:
|
| 152 |
+
if not isinstance(args, dict):
|
| 153 |
+
args = {}
|
| 154 |
+
return {
|
| 155 |
+
"id": f"call_{idx}",
|
| 156 |
+
"type": "function",
|
| 157 |
+
"function": {"name": name, "arguments": args},
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
def parse_tool_calls(text: str | None) -> list[dict[str, Any]]:
|
| 162 |
+
content = text or ""
|
| 163 |
+
calls: list[dict[str, Any]] = []
|
| 164 |
+
|
| 165 |
+
for idx, match in enumerate(XML_TOOL_RE.finditer(content)):
|
| 166 |
+
name = match.group(1).strip()
|
| 167 |
+
body = match.group(2)
|
| 168 |
+
args: dict[str, Any] = {}
|
| 169 |
+
for param in XML_PARAM_RE.finditer(body):
|
| 170 |
+
key = param.group(1).strip()
|
| 171 |
+
value = param.group(2).strip()
|
| 172 |
+
args[key] = _parse_json_maybe(value)
|
| 173 |
+
calls.append(_tool_call(name, args, idx))
|
| 174 |
+
if calls:
|
| 175 |
+
return calls
|
| 176 |
+
|
| 177 |
+
for idx, match in enumerate(JSON_TOOL_RE.finditer(content)):
|
| 178 |
+
try:
|
| 179 |
+
obj = json.loads(match.group(1))
|
| 180 |
+
except Exception:
|
| 181 |
+
continue
|
| 182 |
+
name = obj.get("name") or obj.get("function", {}).get("name")
|
| 183 |
+
args = obj.get("arguments") or obj.get("function", {}).get("arguments") or {}
|
| 184 |
+
if name:
|
| 185 |
+
calls.append(_tool_call(str(name), _parse_args(args), idx))
|
| 186 |
+
if calls:
|
| 187 |
+
return calls
|
| 188 |
+
|
| 189 |
+
lower = content.lower()
|
| 190 |
+
for marker in ("final_answer", "submit_answer", "submit"):
|
| 191 |
+
pos = lower.find(marker)
|
| 192 |
+
if pos >= 0:
|
| 193 |
+
obj = _json_from(content, pos)
|
| 194 |
+
if isinstance(obj, dict):
|
| 195 |
+
if obj.get("name") in FINAL_TOOLS:
|
| 196 |
+
return [_tool_call(str(obj["name"]), obj.get("arguments", {}), 0)]
|
| 197 |
+
return [_tool_call(marker, obj, 0)]
|
| 198 |
+
|
| 199 |
+
obj = _json_from(content)
|
| 200 |
+
if isinstance(obj, dict) and (
|
| 201 |
+
"classification" in obj
|
| 202 |
+
or "recommended_label" in obj
|
| 203 |
+
or "malware" in obj
|
| 204 |
+
or "hackware" in obj
|
| 205 |
+
):
|
| 206 |
+
return [_tool_call("final_answer", {"answer_json": obj}, 0)]
|
| 207 |
+
return []
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
def split_thinking(text: str) -> tuple[str | None, str]:
|
| 211 |
+
match = THINK_RE.search(text)
|
| 212 |
+
if match:
|
| 213 |
+
reasoning = match.group(1).strip()
|
| 214 |
+
visible = text[: match.start()] + text[match.end() :]
|
| 215 |
+
return reasoning, visible.strip()
|
| 216 |
+
|
| 217 |
+
# llama.cpp completion receives the opening <think> in the prompt, so the
|
| 218 |
+
# generated text may contain only the closing tag. Treat the prefix as
|
| 219 |
+
# hidden reasoning in that case.
|
| 220 |
+
end_tag = "</think>"
|
| 221 |
+
pos = text.find(end_tag)
|
| 222 |
+
if pos >= 0:
|
| 223 |
+
reasoning = text[:pos].strip()
|
| 224 |
+
visible = text[pos + len(end_tag) :].strip()
|
| 225 |
+
return reasoning or None, visible
|
| 226 |
+
return None, text
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def strip_tool_xml(text: str) -> str:
|
| 230 |
+
text = XML_TOOL_RE.sub("", text)
|
| 231 |
+
text = JSON_TOOL_RE.sub("", text)
|
| 232 |
+
return text.strip()
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def final_answer_from_args(args: dict[str, Any]) -> dict[str, Any] | None:
|
| 236 |
+
answer = args.get("answer_json", args)
|
| 237 |
+
answer = _parse_json_maybe(answer)
|
| 238 |
+
return answer if isinstance(answer, dict) else None
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def normalize_prediction(answer: Any) -> str:
|
| 242 |
+
if not isinstance(answer, dict):
|
| 243 |
+
return "unknown"
|
| 244 |
+
for key in ("classification", "recommended_label", "label", "verdict"):
|
| 245 |
+
value = answer.get(key)
|
| 246 |
+
if isinstance(value, str):
|
| 247 |
+
return normalize_prediction_string(value)
|
| 248 |
+
if bool(answer.get("hackware")):
|
| 249 |
+
return "hackware"
|
| 250 |
+
if isinstance(answer.get("malware"), bool):
|
| 251 |
+
return "malicious" if answer["malware"] else "benign"
|
| 252 |
+
return normalize_prediction_string(json.dumps(answer, sort_keys=True))
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
def normalize_prediction_string(value: str) -> str:
|
| 256 |
+
text = value.lower()
|
| 257 |
+
if "hackware" in text or "dual-use" in text or "offensive tool" in text:
|
| 258 |
+
return "hackware"
|
| 259 |
+
if "benign" in text or "not malware" in text or "non-malicious" in text:
|
| 260 |
+
return "benign"
|
| 261 |
+
if "malicious" in text or "malware" in text or "c2" in text or "reverse shell" in text:
|
| 262 |
+
return "malicious"
|
| 263 |
+
return "unknown"
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def openai_tool(name: str, description: str, props: dict[str, Any] | None = None) -> dict[str, Any]:
|
| 267 |
+
return {
|
| 268 |
+
"type": "function",
|
| 269 |
+
"function": {
|
| 270 |
+
"name": name,
|
| 271 |
+
"description": description,
|
| 272 |
+
"parameters": {
|
| 273 |
+
"type": "object",
|
| 274 |
+
"properties": props or {},
|
| 275 |
+
"required": [],
|
| 276 |
+
},
|
| 277 |
+
},
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
|
| 281 |
+
def tool_definitions() -> list[dict[str, Any]]:
|
| 282 |
+
path_prop = {"path": {"type": "string", "description": "Optional. Ignored; routed to current sample."}}
|
| 283 |
+
str_props = dict(path_prop)
|
| 284 |
+
str_props["min_length"] = {"type": "integer", "description": "Minimum printable string length."}
|
| 285 |
+
dump_props = dict(path_prop)
|
| 286 |
+
dump_props["length"] = {"type": "integer", "description": "Maximum bytes to dump."}
|
| 287 |
+
dump_props["offset"] = {"type": "integer", "description": "Start offset in bytes."}
|
| 288 |
+
disasm_props = {"function": {"type": "string", "description": "Function symbol name to disassemble."}}
|
| 289 |
+
ghidra_props = {
|
| 290 |
+
"timeout": {"type": "integer", "description": "Optional max seconds for Ghidra analysis."}
|
| 291 |
+
}
|
| 292 |
+
final_props = {"answer_json": {"type": "object", "description": "Final JSON verdict."}}
|
| 293 |
+
T = openai_tool
|
| 294 |
+
return [
|
| 295 |
+
T("run_file", "Identify file type, format, architecture, linkage, and stripped status.", path_prop),
|
| 296 |
+
T("file", "Alias for run_file.", path_prop),
|
| 297 |
+
T("run_strings", "Extract printable strings from the sample.", str_props),
|
| 298 |
+
T("strings", "Alias for run_strings.", str_props),
|
| 299 |
+
T("run_readelf", "Inspect ELF headers, sections, symbols, program headers, and dynamic imports.", path_prop),
|
| 300 |
+
T("readelf", "Alias for run_readelf.", path_prop),
|
| 301 |
+
T("run_objdump", "Disassemble or dump ELF sections with objdump.", path_prop),
|
| 302 |
+
T("objdump", "Alias for run_objdump.", path_prop),
|
| 303 |
+
T("nm", "List symbols when present.", path_prop),
|
| 304 |
+
T("run_pe_headers", "PE file and optional headers.", path_prop),
|
| 305 |
+
T("pe_headers", "Alias for run_pe_headers.", path_prop),
|
| 306 |
+
T("run_pe_sections", "PE section table and entropy.", path_prop),
|
| 307 |
+
T("pe_sections", "Alias for run_pe_sections.", path_prop),
|
| 308 |
+
T("run_pe_imports", "PE import tables and WinAPI symbols.", path_prop),
|
| 309 |
+
T("pe_imports", "Alias for run_pe_imports.", path_prop),
|
| 310 |
+
T("run_pe_exports", "PE export table and data directories.", path_prop),
|
| 311 |
+
T("pe_exports", "Alias for run_pe_exports.", path_prop),
|
| 312 |
+
T("run_pe_disasm", "Disassemble a PE sample.", path_prop),
|
| 313 |
+
T("pe_disasm", "Alias for run_pe_disasm.", path_prop),
|
| 314 |
+
T("run_pe_symbols", "List PE symbols when present.", path_prop),
|
| 315 |
+
T("pe_symbols", "Alias for run_pe_symbols.", path_prop),
|
| 316 |
+
T("run_disasm_func", "Disassemble one named function.", disasm_props),
|
| 317 |
+
T("disasm_func", "Alias for run_disasm_func.", disasm_props),
|
| 318 |
+
T("hexdump", "Hex dump bytes from the sample.", dump_props),
|
| 319 |
+
T("xxd", "Alias hex dump using xxd.", dump_props),
|
| 320 |
+
T("entropy", "Compute whole-file Shannon entropy.", path_prop),
|
| 321 |
+
T("ghidra_summary", "Run Ghidra headless static analysis and summarize output.", ghidra_props),
|
| 322 |
+
T("final_answer", "Submit exactly one final JSON verdict.", final_props),
|
| 323 |
+
T("submit_answer", "Alias for final_answer.", final_props),
|
| 324 |
+
]
|
| 325 |
+
|
| 326 |
+
|
| 327 |
+
def render_qwen_xml_tool_call(name: str, args: dict[str, Any]) -> str:
|
| 328 |
+
parts = ["<tool_call>\n", f"<function={name}>\n"]
|
| 329 |
+
for key, value in args.items():
|
| 330 |
+
if isinstance(value, (dict, list)):
|
| 331 |
+
value_text = json.dumps(value, ensure_ascii=True)
|
| 332 |
+
else:
|
| 333 |
+
value_text = str(value)
|
| 334 |
+
parts.extend([f"<parameter={key}>\n", value_text, "\n</parameter>\n"])
|
| 335 |
+
parts.append("</function>\n</tool_call>")
|
| 336 |
+
return "".join(parts)
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
def render_qwen_xml_chat(
|
| 340 |
+
messages: list[dict[str, Any]],
|
| 341 |
+
tools: list[dict[str, Any]],
|
| 342 |
+
thinking: bool,
|
| 343 |
+
add_generation_prompt: bool = True,
|
| 344 |
+
) -> str:
|
| 345 |
+
"""Render a Qwen3.5 XML-tool prompt for llama.cpp GGUF inference."""
|
| 346 |
+
chunks: list[str] = []
|
| 347 |
+
system_content = ""
|
| 348 |
+
start_index = 0
|
| 349 |
+
if messages and messages[0].get("role") == "system":
|
| 350 |
+
system_content = str(messages[0].get("content") or "").strip()
|
| 351 |
+
start_index = 1
|
| 352 |
+
|
| 353 |
+
if tools:
|
| 354 |
+
chunks.append("<|im_start|>system\n")
|
| 355 |
+
chunks.append("# Tools\n\nYou have access to the following functions:\n\n<tools>")
|
| 356 |
+
for tool in tools:
|
| 357 |
+
chunks.append("\n")
|
| 358 |
+
chunks.append(json.dumps(tool, ensure_ascii=True))
|
| 359 |
+
chunks.append("\n</tools>")
|
| 360 |
+
chunks.append(
|
| 361 |
+
"\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n"
|
| 362 |
+
"<tool_call>\n<function=example_function_name>\n"
|
| 363 |
+
"<parameter=example_parameter_1>\nvalue_1\n</parameter>\n"
|
| 364 |
+
"<parameter=example_parameter_2>\nThis is the value for the second parameter\n"
|
| 365 |
+
"that can span\nmultiple lines\n</parameter>\n"
|
| 366 |
+
"</function>\n</tool_call>\n\n"
|
| 367 |
+
"<IMPORTANT>\n"
|
| 368 |
+
"Reminder:\n"
|
| 369 |
+
"- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags\n"
|
| 370 |
+
"- Required parameters MUST be specified\n"
|
| 371 |
+
"- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n"
|
| 372 |
+
"- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n"
|
| 373 |
+
"</IMPORTANT>"
|
| 374 |
+
)
|
| 375 |
+
if system_content:
|
| 376 |
+
chunks.append("\n\n")
|
| 377 |
+
chunks.append(system_content)
|
| 378 |
+
chunks.append("<|im_end|>\n")
|
| 379 |
+
elif system_content:
|
| 380 |
+
chunks.append("<|im_start|>system\n")
|
| 381 |
+
chunks.append(system_content)
|
| 382 |
+
chunks.append("<|im_end|>\n")
|
| 383 |
+
|
| 384 |
+
i = start_index
|
| 385 |
+
while i < len(messages):
|
| 386 |
+
msg = messages[i]
|
| 387 |
+
role = msg.get("role")
|
| 388 |
+
content = str(msg.get("content") or "").strip()
|
| 389 |
+
if role == "user":
|
| 390 |
+
chunks.append("<|im_start|>user\n")
|
| 391 |
+
chunks.append(content)
|
| 392 |
+
chunks.append("<|im_end|>\n")
|
| 393 |
+
elif role == "assistant":
|
| 394 |
+
chunks.append("<|im_start|>assistant\n")
|
| 395 |
+
reasoning = str(msg.get("reasoning_content") or "").strip()
|
| 396 |
+
if reasoning:
|
| 397 |
+
chunks.append("<think>\n")
|
| 398 |
+
chunks.append(reasoning)
|
| 399 |
+
chunks.append("\n</think>\n\n")
|
| 400 |
+
elif msg.get("reasoning_content") is not None:
|
| 401 |
+
chunks.append("<think>\n\n</think>\n\n")
|
| 402 |
+
chunks.append(content)
|
| 403 |
+
tool_calls = msg.get("tool_calls") or []
|
| 404 |
+
if tool_calls:
|
| 405 |
+
if content:
|
| 406 |
+
chunks.append("\n\n")
|
| 407 |
+
for tc in tool_calls:
|
| 408 |
+
fn = tc.get("function", {})
|
| 409 |
+
name = str(fn.get("name") or "")
|
| 410 |
+
args = _parse_args(fn.get("arguments"))
|
| 411 |
+
chunks.append(render_qwen_xml_tool_call(name, args))
|
| 412 |
+
chunks.append("\n")
|
| 413 |
+
chunks.append("<|im_end|>\n")
|
| 414 |
+
elif role == "tool":
|
| 415 |
+
chunks.append("<|im_start|>user")
|
| 416 |
+
while i < len(messages) and messages[i].get("role") == "tool":
|
| 417 |
+
tool_msg = messages[i]
|
| 418 |
+
chunks.append("\n<tool_response>\n")
|
| 419 |
+
chunks.append(str(tool_msg.get("content") or ""))
|
| 420 |
+
chunks.append("\n</tool_response>")
|
| 421 |
+
i += 1
|
| 422 |
+
chunks.append("<|im_end|>\n")
|
| 423 |
+
continue
|
| 424 |
+
i += 1
|
| 425 |
+
|
| 426 |
+
if add_generation_prompt:
|
| 427 |
+
chunks.append("<|im_start|>assistant\n")
|
| 428 |
+
if thinking:
|
| 429 |
+
chunks.append("<think>\n")
|
| 430 |
+
else:
|
| 431 |
+
chunks.append("<think>\n\n</think>\n\n")
|
| 432 |
+
return "".join(chunks)
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def _parse_int(value: Any, default: int) -> int:
|
| 436 |
+
try:
|
| 437 |
+
if isinstance(value, str):
|
| 438 |
+
return int(value.strip(), 0)
|
| 439 |
+
return int(value)
|
| 440 |
+
except Exception:
|
| 441 |
+
return default
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def _dump_bounds(length: Any, offset: Any) -> tuple[int, int]:
|
| 445 |
+
length_i = _parse_int(length, 1024)
|
| 446 |
+
offset_i = _parse_int(offset, 0)
|
| 447 |
+
return max(64, min(length_i, 8192)), max(0, offset_i)
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
def detect_format(path: Path) -> str:
|
| 451 |
+
try:
|
| 452 |
+
magic = path.read_bytes()[:4]
|
| 453 |
+
except Exception:
|
| 454 |
+
return "unknown"
|
| 455 |
+
if magic == b"\x7fELF":
|
| 456 |
+
return "ELF"
|
| 457 |
+
if magic[:2] == b"MZ":
|
| 458 |
+
return "PE"
|
| 459 |
+
return "unknown"
|
| 460 |
+
|
| 461 |
+
|
| 462 |
+
def collect_targets(path: Path, include_unknown: bool, max_files: int) -> list[Path]:
|
| 463 |
+
if path.is_file():
|
| 464 |
+
return [path]
|
| 465 |
+
if not path.is_dir():
|
| 466 |
+
raise FileNotFoundError(path)
|
| 467 |
+
out: list[Path] = []
|
| 468 |
+
for p in sorted(path.rglob("*")):
|
| 469 |
+
if not p.is_file() or p.is_symlink():
|
| 470 |
+
continue
|
| 471 |
+
fmt = detect_format(p)
|
| 472 |
+
if fmt in {"PE", "ELF"} or include_unknown:
|
| 473 |
+
out.append(p)
|
| 474 |
+
if max_files and len(out) >= max_files:
|
| 475 |
+
break
|
| 476 |
+
return out
|
| 477 |
+
|
| 478 |
+
|
| 479 |
+
@dataclass
|
| 480 |
+
class RuntimeConfig:
|
| 481 |
+
max_turns: int
|
| 482 |
+
max_tool_calls: int
|
| 483 |
+
max_tokens: int
|
| 484 |
+
obs_limit: int
|
| 485 |
+
temperature: float
|
| 486 |
+
thinking: bool
|
| 487 |
+
save_transcripts: bool
|
| 488 |
+
save_reasoning: bool
|
| 489 |
+
ghidra_script_dir: Path
|
| 490 |
+
|
| 491 |
+
|
| 492 |
+
class StaticTools:
|
| 493 |
+
def __init__(self, staged_path: Path, fmt: str, obs_limit: int, ghidra_script_dir: Path):
|
| 494 |
+
self.staged_path = staged_path
|
| 495 |
+
self.format = fmt
|
| 496 |
+
self.obs_limit = obs_limit
|
| 497 |
+
self.ghidra_script_dir = ghidra_script_dir
|
| 498 |
+
self.used_tools: list[str] = []
|
| 499 |
+
self.file_metadata: str | None = None
|
| 500 |
+
self.mingw_objdump = shutil.which("x86_64-w64-mingw32-objdump") or "objdump"
|
| 501 |
+
self.mingw_nm = shutil.which("x86_64-w64-mingw32-nm") or "nm"
|
| 502 |
+
|
| 503 |
+
def _limit(self, text: str, limit: int) -> str:
|
| 504 |
+
limit = min(limit, self.obs_limit)
|
| 505 |
+
text = text.replace(str(self.staged_path), "<sample>")
|
| 506 |
+
if len(text) > limit:
|
| 507 |
+
return text[:limit] + f"\n[truncated to {limit} chars]"
|
| 508 |
+
return text
|
| 509 |
+
|
| 510 |
+
def _run(self, cmd: list[str], tool_name: str, limit: int, timeout: int = 45) -> str:
|
| 511 |
+
self.used_tools.append(tool_name)
|
| 512 |
+
try:
|
| 513 |
+
proc = subprocess.run(
|
| 514 |
+
cmd,
|
| 515 |
+
stdout=subprocess.PIPE,
|
| 516 |
+
stderr=subprocess.PIPE,
|
| 517 |
+
text=True,
|
| 518 |
+
errors="replace",
|
| 519 |
+
timeout=timeout,
|
| 520 |
+
cwd=str(self.staged_path.parent),
|
| 521 |
+
check=False,
|
| 522 |
+
)
|
| 523 |
+
except FileNotFoundError:
|
| 524 |
+
return f"tool unavailable: {cmd[0]} not found"
|
| 525 |
+
except subprocess.TimeoutExpired:
|
| 526 |
+
return f"tool timeout after {timeout}s: {' '.join(cmd[:3])}"
|
| 527 |
+
out = proc.stdout
|
| 528 |
+
if proc.stderr:
|
| 529 |
+
out += "\n[stderr]\n" + proc.stderr
|
| 530 |
+
if not out.strip():
|
| 531 |
+
out = f"{tool_name} completed with no output, exit={proc.returncode}"
|
| 532 |
+
return self._limit(out, limit)
|
| 533 |
+
|
| 534 |
+
def file(self, **_: Any) -> str:
|
| 535 |
+
out = self._run(["file", "-b", str(self.staged_path)], "file", TOOL_LIMITS["file"], timeout=20)
|
| 536 |
+
self.file_metadata = out
|
| 537 |
+
return out
|
| 538 |
+
|
| 539 |
+
def strings(self, min_length: Any = 5, **_: Any) -> str:
|
| 540 |
+
try:
|
| 541 |
+
n = int(min_length)
|
| 542 |
+
except Exception:
|
| 543 |
+
n = 5
|
| 544 |
+
n = max(3, min(n, 32))
|
| 545 |
+
return self._run(["strings", "-a", "-n", str(n), str(self.staged_path)], "strings", TOOL_LIMITS["strings"], 35)
|
| 546 |
+
|
| 547 |
+
def readelf(self, **_: Any) -> str:
|
| 548 |
+
if self.format != "ELF":
|
| 549 |
+
return "readelf is only useful for ELF samples; use PE tools for PE files."
|
| 550 |
+
return self._run(["readelf", "-aW", str(self.staged_path)], "readelf", TOOL_LIMITS["readelf"], 35)
|
| 551 |
+
|
| 552 |
+
def objdump(self, **_: Any) -> str:
|
| 553 |
+
if self.format == "PE":
|
| 554 |
+
return self.pe_disasm()
|
| 555 |
+
tool = shutil.which("x86_64-linux-gnu-objdump") or "objdump"
|
| 556 |
+
cmd = [tool, "-d", "-M", "intel", str(self.staged_path)] if self._is_x86() else [tool, "-d", str(self.staged_path)]
|
| 557 |
+
return self._run(cmd, "objdump", TOOL_LIMITS["objdump"], 60)
|
| 558 |
+
|
| 559 |
+
def nm(self, **_: Any) -> str:
|
| 560 |
+
nm_tool = self.mingw_nm if self.format == "PE" else "nm"
|
| 561 |
+
return self._run([nm_tool, "-an", str(self.staged_path)], "nm", TOOL_LIMITS["nm"], 30)
|
| 562 |
+
|
| 563 |
+
def disasm_func(self, function: Any = "", **_: Any) -> str:
|
| 564 |
+
name = str(function or "").strip()
|
| 565 |
+
if not name:
|
| 566 |
+
self.used_tools.append("disasm_func")
|
| 567 |
+
return "disasm_func requires a function symbol name."
|
| 568 |
+
if self.format == "PE":
|
| 569 |
+
cmd = [self.mingw_objdump, "-d", "-M", "intel", f"--disassemble={name}", str(self.staged_path)]
|
| 570 |
+
else:
|
| 571 |
+
tool = shutil.which("x86_64-linux-gnu-objdump") or "objdump"
|
| 572 |
+
cmd = [tool, "-d", "-M", "intel", f"--disassemble={name}", str(self.staged_path)] if self._is_x86() else [tool, "-d", f"--disassemble={name}", str(self.staged_path)]
|
| 573 |
+
return self._run(cmd, "disasm_func", TOOL_LIMITS["disasm_func"], 60)
|
| 574 |
+
|
| 575 |
+
def pe_headers(self, **_: Any) -> str:
|
| 576 |
+
if self.format != "PE":
|
| 577 |
+
return "pe_headers is only useful for PE samples; use readelf for ELF files."
|
| 578 |
+
a = self._run([self.mingw_objdump, "-f", str(self.staged_path)], "pe_headers", TOOL_LIMITS["pe_headers"], 25)
|
| 579 |
+
b = self._run([self.mingw_objdump, "-p", str(self.staged_path)], "pe_headers", TOOL_LIMITS["pe_headers"], 45)
|
| 580 |
+
return self._limit(a + "\n\n== private headers (-p) ==\n" + b, TOOL_LIMITS["pe_headers"])
|
| 581 |
+
|
| 582 |
+
def pe_sections(self, **_: Any) -> str:
|
| 583 |
+
if self.format != "PE":
|
| 584 |
+
return "pe_sections is only useful for PE samples."
|
| 585 |
+
hdr = self._run([self.mingw_objdump, "-h", str(self.staged_path)], "pe_sections", TOOL_LIMITS["pe_sections"], 25)
|
| 586 |
+
return self._limit(hdr + "\n\n== per-section entropy ==\n" + self._section_entropy(hdr), TOOL_LIMITS["pe_sections"])
|
| 587 |
+
|
| 588 |
+
def pe_imports(self, **_: Any) -> str:
|
| 589 |
+
if self.format != "PE":
|
| 590 |
+
return "pe_imports is only useful for PE samples."
|
| 591 |
+
return self._run([self.mingw_objdump, "-x", str(self.staged_path)], "pe_imports", TOOL_LIMITS["pe_imports"], 45)
|
| 592 |
+
|
| 593 |
+
def pe_exports(self, **_: Any) -> str:
|
| 594 |
+
if self.format != "PE":
|
| 595 |
+
return "pe_exports is only useful for PE samples."
|
| 596 |
+
return self._run([self.mingw_objdump, "-p", str(self.staged_path)], "pe_exports", TOOL_LIMITS["pe_exports"], 45)
|
| 597 |
+
|
| 598 |
+
def pe_disasm(self, **_: Any) -> str:
|
| 599 |
+
if self.format != "PE":
|
| 600 |
+
return "pe_disasm is only useful for PE samples; use objdump for ELF files."
|
| 601 |
+
return self._run([self.mingw_objdump, "-d", "-M", "intel", str(self.staged_path)], "pe_disasm", TOOL_LIMITS["pe_disasm"], 70)
|
| 602 |
+
|
| 603 |
+
def pe_symbols(self, **_: Any) -> str:
|
| 604 |
+
if self.format != "PE":
|
| 605 |
+
return "pe_symbols is only useful for PE samples; use nm for ELF files."
|
| 606 |
+
return self._run([self.mingw_nm, str(self.staged_path)], "pe_symbols", TOOL_LIMITS["pe_symbols"], 30)
|
| 607 |
+
|
| 608 |
+
def hexdump(self, length: Any = 1024, offset: Any = 0, **_: Any) -> str:
|
| 609 |
+
length_i, offset_i = _dump_bounds(length, offset)
|
| 610 |
+
if shutil.which("hexdump"):
|
| 611 |
+
return self._run(["hexdump", "-C", "-n", str(length_i), "-s", str(offset_i), str(self.staged_path)], "hexdump", TOOL_LIMITS["hexdump"], 20)
|
| 612 |
+
return self.xxd(length=length_i, offset=offset_i)
|
| 613 |
+
|
| 614 |
+
def xxd(self, length: Any = 1024, offset: Any = 0, **_: Any) -> str:
|
| 615 |
+
length_i, offset_i = _dump_bounds(length, offset)
|
| 616 |
+
return self._run(["xxd", "-g", "1", "-l", str(length_i), "-s", str(offset_i), str(self.staged_path)], "xxd", TOOL_LIMITS["xxd"], 20)
|
| 617 |
+
|
| 618 |
+
def entropy(self, **_: Any) -> str:
|
| 619 |
+
self.used_tools.append("entropy")
|
| 620 |
+
counts = [0] * 256
|
| 621 |
+
total = 0
|
| 622 |
+
with self.staged_path.open("rb") as fh:
|
| 623 |
+
while True:
|
| 624 |
+
chunk = fh.read(1 << 20)
|
| 625 |
+
if not chunk:
|
| 626 |
+
break
|
| 627 |
+
total += len(chunk)
|
| 628 |
+
for b in chunk:
|
| 629 |
+
counts[b] += 1
|
| 630 |
+
ent = 0.0 if total == 0 else -sum((c / total) * math.log2(c / total) for c in counts if c)
|
| 631 |
+
return json.dumps({"bytes": total, "shannon_entropy": round(ent, 4)})
|
| 632 |
+
|
| 633 |
+
def ghidra_summary(self, timeout: Any = 180, **_: Any) -> str:
|
| 634 |
+
self.used_tools.append("ghidra_summary")
|
| 635 |
+
try:
|
| 636 |
+
timeout_i = max(30, min(int(timeout), 900))
|
| 637 |
+
except Exception:
|
| 638 |
+
timeout_i = 180
|
| 639 |
+
headless = find_ghidra_headless()
|
| 640 |
+
if not headless:
|
| 641 |
+
return "ghidra unavailable: analyzeHeadless not found. Set GHIDRA_HEADLESS or add analyzeHeadless to PATH."
|
| 642 |
+
script_dir = self.ghidra_script_dir
|
| 643 |
+
script_file = script_dir / "AgentRESummary.java"
|
| 644 |
+
if not script_file.exists():
|
| 645 |
+
return f"ghidra script unavailable: {script_file}"
|
| 646 |
+
with tempfile.TemporaryDirectory(prefix="agentre_ghidra_") as tmp:
|
| 647 |
+
tmp_path = Path(tmp)
|
| 648 |
+
out_path = tmp_path / "agentre_ghidra_summary.txt"
|
| 649 |
+
cmd = [
|
| 650 |
+
headless,
|
| 651 |
+
str(tmp_path),
|
| 652 |
+
"agentre_project",
|
| 653 |
+
"-import",
|
| 654 |
+
str(self.staged_path),
|
| 655 |
+
"-overwrite",
|
| 656 |
+
"-analysisTimeoutPerFile",
|
| 657 |
+
str(timeout_i),
|
| 658 |
+
"-scriptPath",
|
| 659 |
+
str(script_dir),
|
| 660 |
+
"-postScript",
|
| 661 |
+
"AgentRESummary.java",
|
| 662 |
+
str(out_path),
|
| 663 |
+
]
|
| 664 |
+
try:
|
| 665 |
+
proc = subprocess.run(
|
| 666 |
+
cmd,
|
| 667 |
+
stdout=subprocess.PIPE,
|
| 668 |
+
stderr=subprocess.PIPE,
|
| 669 |
+
text=True,
|
| 670 |
+
errors="replace",
|
| 671 |
+
timeout=timeout_i + 90,
|
| 672 |
+
check=False,
|
| 673 |
+
)
|
| 674 |
+
except subprocess.TimeoutExpired:
|
| 675 |
+
return f"ghidra timeout after {timeout_i + 90}s"
|
| 676 |
+
out = out_path.read_text(encoding="utf-8", errors="replace") if out_path.exists() else ""
|
| 677 |
+
log = proc.stdout + (("\n[stderr]\n" + proc.stderr) if proc.stderr else "")
|
| 678 |
+
if out.strip():
|
| 679 |
+
return self._limit(out + "\n\n== Ghidra log tail ==\n" + log[-4000:], TOOL_LIMITS["ghidra_summary"])
|
| 680 |
+
return self._limit("Ghidra produced no summary file.\n\n" + log, TOOL_LIMITS["ghidra_summary"])
|
| 681 |
+
|
| 682 |
+
def _is_x86(self) -> bool:
|
| 683 |
+
meta = self.file_metadata or self.file()
|
| 684 |
+
low = meta.lower()
|
| 685 |
+
return "x86-64" in low or "80386" in low or "intel" in low or "amd64" in low
|
| 686 |
+
|
| 687 |
+
def _section_entropy(self, section_table: str) -> str:
|
| 688 |
+
try:
|
| 689 |
+
data = self.staged_path.read_bytes()
|
| 690 |
+
except Exception as exc:
|
| 691 |
+
return f"<cannot read: {exc}>"
|
| 692 |
+
rows: list[str] = []
|
| 693 |
+
for line in section_table.splitlines():
|
| 694 |
+
parts = line.split()
|
| 695 |
+
if len(parts) >= 6 and parts[0].isdigit():
|
| 696 |
+
try:
|
| 697 |
+
size, off = int(parts[2], 16), int(parts[5], 16)
|
| 698 |
+
except ValueError:
|
| 699 |
+
continue
|
| 700 |
+
blob = data[off : off + size]
|
| 701 |
+
if not blob:
|
| 702 |
+
rows.append(f"{parts[1]:14s} size={size:<8} entropy=n/a")
|
| 703 |
+
continue
|
| 704 |
+
counts = [0] * 256
|
| 705 |
+
for b in blob:
|
| 706 |
+
counts[b] += 1
|
| 707 |
+
n = len(blob)
|
| 708 |
+
ent = -sum((v / n) * math.log2(v / n) for v in counts if v)
|
| 709 |
+
flag = " <HIGH: packed/encrypted?>" if ent > 7.2 else ""
|
| 710 |
+
rows.append(f"{parts[1]:14s} size={size:<8} entropy={ent:4.2f}{flag}")
|
| 711 |
+
return "\n".join(rows) or "<no section table>"
|
| 712 |
+
|
| 713 |
+
|
| 714 |
+
def find_ghidra_headless() -> str | None:
|
| 715 |
+
env = os.environ.get("GHIDRA_HEADLESS")
|
| 716 |
+
if env and Path(env).exists():
|
| 717 |
+
return env
|
| 718 |
+
found = shutil.which("analyzeHeadless")
|
| 719 |
+
if found:
|
| 720 |
+
return found
|
| 721 |
+
|
| 722 |
+
# Keep this bounded. A recursive home-directory scan can hang on large
|
| 723 |
+
# workstations, so only check common Ghidra install layouts.
|
| 724 |
+
candidates = [
|
| 725 |
+
Path("/opt/ghidra/support/analyzeHeadless"),
|
| 726 |
+
Path("/usr/share/ghidra/support/analyzeHeadless"),
|
| 727 |
+
Path.home() / "ghidra" / "support" / "analyzeHeadless",
|
| 728 |
+
]
|
| 729 |
+
candidates.extend(Path("/opt").glob("ghidra*/support/analyzeHeadless") if Path("/opt").exists() else [])
|
| 730 |
+
for path in candidates:
|
| 731 |
+
if path.is_file() and os.access(path, os.X_OK):
|
| 732 |
+
return str(path)
|
| 733 |
+
return None
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
class LocalQwenBackend:
|
| 737 |
+
def __init__(self, base_model: str, adapter: str | None, dtype: str, device_map: str):
|
| 738 |
+
try:
|
| 739 |
+
import torch
|
| 740 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 741 |
+
except Exception as exc:
|
| 742 |
+
raise RuntimeError("Transformers local backend requires torch and transformers") from exc
|
| 743 |
+
|
| 744 |
+
self.torch = torch
|
| 745 |
+
tokenizer_path = adapter if adapter and Path(adapter).exists() else base_model
|
| 746 |
+
self.tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True)
|
| 747 |
+
torch_dtype = "auto" if dtype == "auto" else getattr(torch, dtype)
|
| 748 |
+
self.model = AutoModelForCausalLM.from_pretrained(
|
| 749 |
+
base_model,
|
| 750 |
+
trust_remote_code=True,
|
| 751 |
+
torch_dtype=torch_dtype,
|
| 752 |
+
device_map=device_map,
|
| 753 |
+
)
|
| 754 |
+
if adapter:
|
| 755 |
+
try:
|
| 756 |
+
from peft import PeftModel
|
| 757 |
+
except Exception as exc:
|
| 758 |
+
raise RuntimeError("Loading a LoRA adapter requires peft") from exc
|
| 759 |
+
self.model = PeftModel.from_pretrained(self.model, adapter)
|
| 760 |
+
self.model.eval()
|
| 761 |
+
|
| 762 |
+
def generate(
|
| 763 |
+
self,
|
| 764 |
+
messages: list[dict[str, Any]],
|
| 765 |
+
tools: list[dict[str, Any]],
|
| 766 |
+
max_new_tokens: int,
|
| 767 |
+
temperature: float,
|
| 768 |
+
thinking: bool,
|
| 769 |
+
) -> tuple[str, dict[str, int]]:
|
| 770 |
+
prompt = self.tokenizer.apply_chat_template(
|
| 771 |
+
messages,
|
| 772 |
+
tools=tools,
|
| 773 |
+
tokenize=False,
|
| 774 |
+
add_generation_prompt=True,
|
| 775 |
+
enable_thinking=thinking,
|
| 776 |
+
)
|
| 777 |
+
inputs = self.tokenizer([prompt], return_tensors="pt")
|
| 778 |
+
device = next(self.model.parameters()).device
|
| 779 |
+
inputs = {k: v.to(device) for k, v in inputs.items()}
|
| 780 |
+
do_sample = temperature > 0
|
| 781 |
+
with self.torch.inference_mode():
|
| 782 |
+
output = self.model.generate(
|
| 783 |
+
**inputs,
|
| 784 |
+
max_new_tokens=max_new_tokens,
|
| 785 |
+
do_sample=do_sample,
|
| 786 |
+
temperature=temperature if do_sample else None,
|
| 787 |
+
pad_token_id=self.tokenizer.eos_token_id,
|
| 788 |
+
)
|
| 789 |
+
input_len = int(inputs["input_ids"].shape[1])
|
| 790 |
+
generated_ids = output[0][input_len:]
|
| 791 |
+
text = self.tokenizer.decode(generated_ids, skip_special_tokens=False)
|
| 792 |
+
if "<|im_end|>" in text:
|
| 793 |
+
text = text.split("<|im_end|>", 1)[0]
|
| 794 |
+
usage = {
|
| 795 |
+
"prompt_tokens": input_len,
|
| 796 |
+
"completion_tokens": int(generated_ids.shape[0]),
|
| 797 |
+
"total_tokens": input_len + int(generated_ids.shape[0]),
|
| 798 |
+
}
|
| 799 |
+
return text, usage
|
| 800 |
+
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
def clean_llama_completion_output(text: str) -> str:
|
| 804 |
+
lines: list[str] = []
|
| 805 |
+
for line in text.splitlines():
|
| 806 |
+
stripped = line.strip()
|
| 807 |
+
if line.startswith("ggml_cuda_init:") or line.startswith(" Device "):
|
| 808 |
+
continue
|
| 809 |
+
if stripped in {"[end of text]", ""}:
|
| 810 |
+
if lines:
|
| 811 |
+
lines.append("")
|
| 812 |
+
continue
|
| 813 |
+
lines.append(line)
|
| 814 |
+
cleaned = "\n".join(lines).replace(" [end of text]", "").replace("[end of text]", "").strip()
|
| 815 |
+
return cleaned
|
| 816 |
+
|
| 817 |
+
|
| 818 |
+
class LlamaCppBackend:
|
| 819 |
+
def __init__(
|
| 820 |
+
self,
|
| 821 |
+
model_path: str,
|
| 822 |
+
llama_cli: str,
|
| 823 |
+
ctx_size: int,
|
| 824 |
+
threads: int,
|
| 825 |
+
gpu_layers: int,
|
| 826 |
+
llama_lora: str | None,
|
| 827 |
+
extra_args: list[str],
|
| 828 |
+
):
|
| 829 |
+
resolved_cli = shutil.which(llama_cli) or llama_cli
|
| 830 |
+
cli_path = Path(resolved_cli)
|
| 831 |
+
if cli_path.name == "llama-cli":
|
| 832 |
+
sibling_completion = cli_path.with_name("llama-completion")
|
| 833 |
+
if sibling_completion.exists():
|
| 834 |
+
resolved_cli = str(sibling_completion)
|
| 835 |
+
if not Path(resolved_cli).exists() and shutil.which(resolved_cli) is None:
|
| 836 |
+
raise RuntimeError(f"llama.cpp completion binary not found: {llama_cli}. Set --llama-cli or AGENTRE_LLAMA_CLI.")
|
| 837 |
+
if not Path(model_path).exists():
|
| 838 |
+
raise RuntimeError(f"GGUF model not found: {model_path}")
|
| 839 |
+
self.llama_cli = resolved_cli
|
| 840 |
+
self.model_path = model_path
|
| 841 |
+
self.ctx_size = ctx_size
|
| 842 |
+
self.threads = threads
|
| 843 |
+
self.gpu_layers = gpu_layers
|
| 844 |
+
self.llama_lora = llama_lora
|
| 845 |
+
self.extra_args = extra_args
|
| 846 |
+
|
| 847 |
+
def generate(
|
| 848 |
+
self,
|
| 849 |
+
messages: list[dict[str, Any]],
|
| 850 |
+
tools: list[dict[str, Any]],
|
| 851 |
+
max_new_tokens: int,
|
| 852 |
+
temperature: float,
|
| 853 |
+
thinking: bool,
|
| 854 |
+
) -> tuple[str, dict[str, int]]:
|
| 855 |
+
prompt = render_qwen_xml_chat(messages, tools, thinking=thinking, add_generation_prompt=True)
|
| 856 |
+
|
| 857 |
+
def build_cmd(ctx_size: int) -> list[str]:
|
| 858 |
+
cmd = [
|
| 859 |
+
self.llama_cli,
|
| 860 |
+
"-m",
|
| 861 |
+
self.model_path,
|
| 862 |
+
"-p",
|
| 863 |
+
prompt,
|
| 864 |
+
"-n",
|
| 865 |
+
str(max_new_tokens),
|
| 866 |
+
"-c",
|
| 867 |
+
str(ctx_size),
|
| 868 |
+
"--temp",
|
| 869 |
+
str(max(0.0, temperature)),
|
| 870 |
+
"--no-display-prompt",
|
| 871 |
+
"-no-cnv",
|
| 872 |
+
"--simple-io",
|
| 873 |
+
"--no-warmup",
|
| 874 |
+
"-lv",
|
| 875 |
+
"1",
|
| 876 |
+
]
|
| 877 |
+
if self.threads > 0:
|
| 878 |
+
cmd.extend(["-t", str(self.threads)])
|
| 879 |
+
if self.gpu_layers >= 0:
|
| 880 |
+
cmd.extend(["-ngl", str(self.gpu_layers)])
|
| 881 |
+
if self.llama_lora:
|
| 882 |
+
cmd.extend(["--lora", self.llama_lora])
|
| 883 |
+
cmd.extend(self.extra_args)
|
| 884 |
+
return cmd
|
| 885 |
+
|
| 886 |
+
ctx_size = self.ctx_size
|
| 887 |
+
proc: subprocess.CompletedProcess[str] | None = None
|
| 888 |
+
for attempt in range(2):
|
| 889 |
+
try:
|
| 890 |
+
proc = subprocess.run(
|
| 891 |
+
build_cmd(ctx_size),
|
| 892 |
+
stdout=subprocess.PIPE,
|
| 893 |
+
stderr=subprocess.PIPE,
|
| 894 |
+
text=True,
|
| 895 |
+
errors="replace",
|
| 896 |
+
timeout=900,
|
| 897 |
+
check=False,
|
| 898 |
+
)
|
| 899 |
+
except FileNotFoundError as exc:
|
| 900 |
+
raise RuntimeError(f"llama.cpp CLI not found: {self.llama_cli}") from exc
|
| 901 |
+
|
| 902 |
+
if proc.returncode == 0:
|
| 903 |
+
self.ctx_size = ctx_size
|
| 904 |
+
break
|
| 905 |
+
|
| 906 |
+
error_text = proc.stderr[-4000:] or proc.stdout[-4000:]
|
| 907 |
+
match = re.search(r"prompt is too long \((\d+) tokens, max (\d+)\)", error_text)
|
| 908 |
+
if match and attempt == 0:
|
| 909 |
+
prompt_tokens = int(match.group(1))
|
| 910 |
+
needed = prompt_tokens + max_new_tokens + 128
|
| 911 |
+
next_ctx = max(ctx_size * 2, ((needed + 4095) // 4096) * 4096)
|
| 912 |
+
print(
|
| 913 |
+
f"[xref 9b] llama.cpp prompt exceeded ctx={ctx_size}; retrying with ctx={next_ctx}",
|
| 914 |
+
file=sys.stderr,
|
| 915 |
+
flush=True,
|
| 916 |
+
)
|
| 917 |
+
ctx_size = next_ctx
|
| 918 |
+
continue
|
| 919 |
+
|
| 920 |
+
raise RuntimeError(
|
| 921 |
+
"llama.cpp generation failed "
|
| 922 |
+
f"exit={proc.returncode}: {error_text}"
|
| 923 |
+
)
|
| 924 |
+
|
| 925 |
+
if proc is None:
|
| 926 |
+
raise RuntimeError("llama.cpp generation did not start")
|
| 927 |
+
if proc.returncode != 0:
|
| 928 |
+
error_text = proc.stderr[-4000:] or proc.stdout[-4000:]
|
| 929 |
+
raise RuntimeError(
|
| 930 |
+
"llama.cpp generation failed "
|
| 931 |
+
f"exit={proc.returncode}: {error_text}"
|
| 932 |
+
)
|
| 933 |
+
|
| 934 |
+
text = clean_llama_completion_output(proc.stdout)
|
| 935 |
+
if "<|im_end|>" in text:
|
| 936 |
+
text = text.split("<|im_end|>", 1)[0]
|
| 937 |
+
return text, {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
| 938 |
+
|
| 939 |
+
|
| 940 |
+
def build_messages(spec: str, sample_id: str, fmt: str, user_request: str) -> list[dict[str, Any]]:
|
| 941 |
+
system = (
|
| 942 |
+
spec.strip()
|
| 943 |
+
+ "\n\nRuntime rules:\n"
|
| 944 |
+
+ f"- You are analyzing staged sample `{sample_id}` only.\n"
|
| 945 |
+
+ f"- Detected format: {fmt}.\n"
|
| 946 |
+
+ "- The original path is hidden from the model to avoid path/name bias.\n"
|
| 947 |
+
+ "- Any tool path argument is ignored; tools are routed to the staged sample.\n"
|
| 948 |
+
+ "- Use static analysis only. Never ask to execute the sample.\n"
|
| 949 |
+
+ "- If evidence is insufficient, classify as unknown and state that closer disassembly with Ghidra or another disassembler is needed.\n"
|
| 950 |
+
+ "- For stripped, static, packed, encrypted, or sparse-string samples, prefer entropy, objdump/PE disassembly, and ghidra_summary before final_answer.\n"
|
| 951 |
+
+ "- Think internally, but do not include chain-of-thought in the final answer.\n"
|
| 952 |
+
)
|
| 953 |
+
user = (
|
| 954 |
+
f"{user_request.strip()}\n\n"
|
| 955 |
+
"Use the available reverse-engineering tools. Decide whether this sample is benign, malicious, hackware, or unknown. "
|
| 956 |
+
"When finished, call final_answer with the structured JSON verdict."
|
| 957 |
+
)
|
| 958 |
+
return [{"role": "system", "content": system}, {"role": "user", "content": user}]
|
| 959 |
+
|
| 960 |
+
|
| 961 |
+
def make_history_tool_call(tc: dict[str, Any], fallback_id: str) -> dict[str, Any]:
|
| 962 |
+
fn = tc.get("function", {})
|
| 963 |
+
args = _parse_args(fn.get("arguments"))
|
| 964 |
+
return {
|
| 965 |
+
"id": tc.get("id") or fallback_id,
|
| 966 |
+
"type": "function",
|
| 967 |
+
"function": {
|
| 968 |
+
"name": str(fn.get("name") or ""),
|
| 969 |
+
"arguments": args,
|
| 970 |
+
},
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
|
| 974 |
+
def analyze_one(
|
| 975 |
+
backend: Any,
|
| 976 |
+
source_path: Path,
|
| 977 |
+
sample_id: str,
|
| 978 |
+
run_dir: Path,
|
| 979 |
+
spec: str,
|
| 980 |
+
cfg: RuntimeConfig,
|
| 981 |
+
user_request: str,
|
| 982 |
+
) -> dict[str, Any]:
|
| 983 |
+
fmt = detect_format(source_path)
|
| 984 |
+
stage_dir = run_dir / "staged"
|
| 985 |
+
stage_dir.mkdir(parents=True, exist_ok=True)
|
| 986 |
+
suffix = ".elf" if fmt == "ELF" else ".exe" if fmt == "PE" else source_path.suffix
|
| 987 |
+
staged_path = (stage_dir / f"{sample_id}{suffix}").resolve()
|
| 988 |
+
shutil.copy2(source_path, staged_path)
|
| 989 |
+
|
| 990 |
+
tools = StaticTools(staged_path, fmt, cfg.obs_limit, cfg.ghidra_script_dir)
|
| 991 |
+
messages = build_messages(spec, sample_id, fmt, user_request)
|
| 992 |
+
answer: dict[str, Any] | None = None
|
| 993 |
+
error: str | None = None
|
| 994 |
+
usage: dict[str, int] = {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
| 995 |
+
tool_calls_seen = 0
|
| 996 |
+
transcript: list[dict[str, Any]] = list(messages)
|
| 997 |
+
|
| 998 |
+
try:
|
| 999 |
+
for turn in range(cfg.max_turns):
|
| 1000 |
+
if tool_calls_seen >= cfg.max_tool_calls:
|
| 1001 |
+
messages.append({"role": "user", "content": "Tool budget reached. Call final_answer now with your best JSON verdict."})
|
| 1002 |
+
transcript.append(messages[-1])
|
| 1003 |
+
|
| 1004 |
+
generated, step_usage = backend.generate(
|
| 1005 |
+
messages,
|
| 1006 |
+
tool_definitions(),
|
| 1007 |
+
cfg.max_tokens,
|
| 1008 |
+
cfg.temperature,
|
| 1009 |
+
cfg.thinking,
|
| 1010 |
+
)
|
| 1011 |
+
for key, value in step_usage.items():
|
| 1012 |
+
usage[key] = usage.get(key, 0) + int(value)
|
| 1013 |
+
|
| 1014 |
+
reasoning, visible = split_thinking(generated)
|
| 1015 |
+
tool_calls = parse_tool_calls(generated)
|
| 1016 |
+
assistant_content = strip_tool_xml(visible)
|
| 1017 |
+
assistant_msg: dict[str, Any] = {"role": "assistant", "content": assistant_content}
|
| 1018 |
+
if reasoning and cfg.save_reasoning:
|
| 1019 |
+
assistant_msg["reasoning_content"] = reasoning
|
| 1020 |
+
elif reasoning:
|
| 1021 |
+
assistant_msg["reasoning_content"] = ""
|
| 1022 |
+
if tool_calls:
|
| 1023 |
+
assistant_msg["tool_calls"] = [
|
| 1024 |
+
make_history_tool_call(tc, f"call_{turn}_{idx}")
|
| 1025 |
+
for idx, tc in enumerate(tool_calls)
|
| 1026 |
+
]
|
| 1027 |
+
messages.append(assistant_msg)
|
| 1028 |
+
transcript.append(dict(assistant_msg))
|
| 1029 |
+
|
| 1030 |
+
if not tool_calls:
|
| 1031 |
+
parsed = final_answer_from_args({"answer_json": assistant_content or generated})
|
| 1032 |
+
if parsed:
|
| 1033 |
+
answer = parsed
|
| 1034 |
+
break
|
| 1035 |
+
if turn < cfg.max_turns - 1:
|
| 1036 |
+
prompt = "Call final_answer now with your compact JSON verdict."
|
| 1037 |
+
messages.append({"role": "user", "content": prompt})
|
| 1038 |
+
transcript.append(messages[-1])
|
| 1039 |
+
continue
|
| 1040 |
+
break
|
| 1041 |
+
|
| 1042 |
+
stop = False
|
| 1043 |
+
for idx, tc in enumerate(tool_calls):
|
| 1044 |
+
fn = tc.get("function", {})
|
| 1045 |
+
name = str(fn.get("name") or "")
|
| 1046 |
+
args = _parse_args(fn.get("arguments"))
|
| 1047 |
+
tcid = tc.get("id") or f"call_{turn}_{idx}"
|
| 1048 |
+
if name in FINAL_TOOLS:
|
| 1049 |
+
answer = final_answer_from_args(args)
|
| 1050 |
+
msg = {"role": "tool", "tool_call_id": tcid, "name": name, "content": "submitted"}
|
| 1051 |
+
messages.append(msg)
|
| 1052 |
+
transcript.append(msg)
|
| 1053 |
+
stop = True
|
| 1054 |
+
break
|
| 1055 |
+
canonical = TOOL_ALIASES.get(name)
|
| 1056 |
+
if canonical is None:
|
| 1057 |
+
content = f"unknown tool {name}. Use the tools listed in the unified spec and final_answer."
|
| 1058 |
+
elif tool_calls_seen >= cfg.max_tool_calls:
|
| 1059 |
+
content = "tool budget exhausted; submit final_answer."
|
| 1060 |
+
else:
|
| 1061 |
+
tool_calls_seen += 1
|
| 1062 |
+
try:
|
| 1063 |
+
content = getattr(tools, canonical)(**args)
|
| 1064 |
+
except Exception as exc:
|
| 1065 |
+
content = f"tool error: {type(exc).__name__}: {exc}"
|
| 1066 |
+
msg = {
|
| 1067 |
+
"role": "tool",
|
| 1068 |
+
"tool_call_id": tcid,
|
| 1069 |
+
"name": name,
|
| 1070 |
+
"content": str(content)[: cfg.obs_limit],
|
| 1071 |
+
}
|
| 1072 |
+
messages.append(msg)
|
| 1073 |
+
transcript.append(msg)
|
| 1074 |
+
if stop:
|
| 1075 |
+
break
|
| 1076 |
+
except Exception as exc:
|
| 1077 |
+
error = f"{type(exc).__name__}: {exc}"
|
| 1078 |
+
|
| 1079 |
+
prediction = normalize_prediction(answer)
|
| 1080 |
+
result = {
|
| 1081 |
+
"sample_id": sample_id,
|
| 1082 |
+
"source_path": str(source_path),
|
| 1083 |
+
"staged_path": str(staged_path),
|
| 1084 |
+
"format": fmt,
|
| 1085 |
+
"prediction": prediction,
|
| 1086 |
+
"answer": answer,
|
| 1087 |
+
"error": error,
|
| 1088 |
+
"tools_used": tools.used_tools,
|
| 1089 |
+
"tool_calls": tool_calls_seen,
|
| 1090 |
+
"usage": usage,
|
| 1091 |
+
}
|
| 1092 |
+
if cfg.save_transcripts:
|
| 1093 |
+
result["transcript"] = transcript
|
| 1094 |
+
return result
|
| 1095 |
+
|
| 1096 |
+
|
| 1097 |
+
def write_outputs(results: list[dict[str, Any]], run_dir: Path) -> None:
|
| 1098 |
+
results_path = run_dir / "results.jsonl"
|
| 1099 |
+
with results_path.open("w", encoding="utf-8") as fh:
|
| 1100 |
+
for row in results:
|
| 1101 |
+
fh.write(json.dumps(row, ensure_ascii=True) + "\n")
|
| 1102 |
+
|
| 1103 |
+
counts: dict[str, int] = {}
|
| 1104 |
+
by_format: dict[str, dict[str, int]] = {}
|
| 1105 |
+
for row in results:
|
| 1106 |
+
pred = str(row.get("prediction", "unknown"))
|
| 1107 |
+
fmt = str(row.get("format", "unknown"))
|
| 1108 |
+
counts[pred] = counts.get(pred, 0) + 1
|
| 1109 |
+
by_format.setdefault(fmt, {})
|
| 1110 |
+
by_format[fmt][pred] = by_format[fmt].get(pred, 0) + 1
|
| 1111 |
+
summary = {
|
| 1112 |
+
"total": len(results),
|
| 1113 |
+
"predictions": counts,
|
| 1114 |
+
"by_format": by_format,
|
| 1115 |
+
"errors": sum(1 for row in results if row.get("error")),
|
| 1116 |
+
"completed_at": datetime.now(timezone.utc).isoformat(),
|
| 1117 |
+
}
|
| 1118 |
+
(run_dir / "summary.json").write_text(json.dumps(summary, indent=2) + "\n", encoding="utf-8")
|
| 1119 |
+
|
| 1120 |
+
lines = [
|
| 1121 |
+
"# AgentRE Triage Summary",
|
| 1122 |
+
"",
|
| 1123 |
+
f"Total samples: {len(results)}",
|
| 1124 |
+
f"Errors: {summary['errors']}",
|
| 1125 |
+
"",
|
| 1126 |
+
"## Predictions",
|
| 1127 |
+
"",
|
| 1128 |
+
]
|
| 1129 |
+
for label, count in sorted(counts.items()):
|
| 1130 |
+
lines.append(f"- {label}: {count}")
|
| 1131 |
+
lines += ["", "## Samples", ""]
|
| 1132 |
+
for row in results:
|
| 1133 |
+
lines.append(
|
| 1134 |
+
f"- `{row['sample_id']}` `{row['format']}` `{row['prediction']}` "
|
| 1135 |
+
f"tools={row['tool_calls']} path=`{row['source_path']}`"
|
| 1136 |
+
)
|
| 1137 |
+
answer = row.get("answer")
|
| 1138 |
+
if isinstance(answer, dict) and answer.get("summary"):
|
| 1139 |
+
lines.append(f" - {answer['summary']}")
|
| 1140 |
+
if row.get("error"):
|
| 1141 |
+
lines.append(f" - error: {row['error']}")
|
| 1142 |
+
(run_dir / "SUMMARY.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
| 1143 |
+
|
| 1144 |
+
|
| 1145 |
+
def main() -> int:
|
| 1146 |
+
parser = argparse.ArgumentParser(description="Analyze PE/ELF files with the AgentRE model and static tools.")
|
| 1147 |
+
parser.add_argument("target", help="File or directory to analyze.")
|
| 1148 |
+
parser.add_argument("--spec", default=str(DEFAULT_SPEC), help="Unified reverse-engineering spec.")
|
| 1149 |
+
parser.add_argument(
|
| 1150 |
+
"--backend",
|
| 1151 |
+
choices=["auto", "transformers", "llama-cpp"],
|
| 1152 |
+
default="auto",
|
| 1153 |
+
help="Inference backend. auto selects llama-cpp for .gguf models, otherwise transformers.",
|
| 1154 |
+
)
|
| 1155 |
+
parser.add_argument("--model", default=DEFAULT_BASE_MODEL, help="HF base model path/name or GGUF path for llama-cpp.")
|
| 1156 |
+
parser.add_argument("--adapter", default=DEFAULT_ADAPTER, help="Optional HF LoRA adapter path for transformers. Use '' to disable.")
|
| 1157 |
+
parser.add_argument("--output-dir", default="", help="Output directory. Defaults to runs/agentre_triage_<timestamp>.")
|
| 1158 |
+
parser.add_argument("--max-files", type=int, default=0, help="Directory mode limit; 0 means no limit.")
|
| 1159 |
+
parser.add_argument("--include-unknown", action="store_true", help="Include files that are not PE/ELF by magic bytes.")
|
| 1160 |
+
parser.add_argument("--max-turns", type=int, default=14)
|
| 1161 |
+
parser.add_argument("--max-tool-calls", type=int, default=12)
|
| 1162 |
+
parser.add_argument("--max-tokens", type=int, default=1200)
|
| 1163 |
+
parser.add_argument("--obs-limit", type=int, default=5000)
|
| 1164 |
+
parser.add_argument("--temperature", type=float, default=0.1)
|
| 1165 |
+
parser.add_argument("--dtype", default="auto", help="auto, bfloat16, float16, float32.")
|
| 1166 |
+
parser.add_argument("--device-map", default="auto")
|
| 1167 |
+
parser.add_argument("--llama-cli", default=DEFAULT_LLAMA_CLI, help="Path to llama.cpp llama-cli.")
|
| 1168 |
+
parser.add_argument("--ctx-size", type=int, default=32768, help="llama.cpp context size.")
|
| 1169 |
+
parser.add_argument("--threads", type=int, default=0, help="llama.cpp CPU threads; 0 lets llama.cpp choose.")
|
| 1170 |
+
parser.add_argument("--gpu-layers", type=int, default=-1, help="llama.cpp GPU layers; -1 leaves default.")
|
| 1171 |
+
parser.add_argument("--llama-lora", default="", help="Optional llama.cpp-compatible LoRA adapter. HF PEFT LoRA is not accepted here.")
|
| 1172 |
+
parser.add_argument("--llama-extra-arg", action="append", default=[], help="Extra raw argument passed to llama-cli. Repeatable.")
|
| 1173 |
+
parser.add_argument("--no-thinking", action="store_true", help="Disable Qwen thinking template flag.")
|
| 1174 |
+
parser.add_argument("--save-transcripts", action="store_true")
|
| 1175 |
+
parser.add_argument("--save-reasoning", action="store_true", help="Only meaningful with --save-transcripts.")
|
| 1176 |
+
parser.add_argument("--ghidra-script-dir", default=str(DEFAULT_GHIDRA_SCRIPT_DIR))
|
| 1177 |
+
parser.add_argument(
|
| 1178 |
+
"--request",
|
| 1179 |
+
default="Is this malicious or benign? Reverse engineer it and use the available tools.",
|
| 1180 |
+
help="User request inserted for each sample.",
|
| 1181 |
+
)
|
| 1182 |
+
parser.add_argument("--tool-smoke", action="store_true", help="Run static tools on the target and exit without loading the model.")
|
| 1183 |
+
args = parser.parse_args()
|
| 1184 |
+
|
| 1185 |
+
target = Path(args.target).expanduser().resolve()
|
| 1186 |
+
targets = collect_targets(target, include_unknown=args.include_unknown, max_files=args.max_files)
|
| 1187 |
+
if not targets:
|
| 1188 |
+
print(f"No PE/ELF targets found under {target}", file=sys.stderr)
|
| 1189 |
+
return 2
|
| 1190 |
+
|
| 1191 |
+
if args.output_dir:
|
| 1192 |
+
run_dir = Path(args.output_dir).expanduser().resolve()
|
| 1193 |
+
else:
|
| 1194 |
+
run_dir = (ROOT / "runs" / f"agentre_triage_{_now_stamp()}").resolve()
|
| 1195 |
+
run_dir.mkdir(parents=True, exist_ok=True)
|
| 1196 |
+
|
| 1197 |
+
spec = Path(args.spec).read_text(encoding="utf-8")
|
| 1198 |
+
cfg = RuntimeConfig(
|
| 1199 |
+
max_turns=args.max_turns,
|
| 1200 |
+
max_tool_calls=args.max_tool_calls,
|
| 1201 |
+
max_tokens=args.max_tokens,
|
| 1202 |
+
obs_limit=args.obs_limit,
|
| 1203 |
+
temperature=args.temperature,
|
| 1204 |
+
thinking=not args.no_thinking,
|
| 1205 |
+
save_transcripts=args.save_transcripts,
|
| 1206 |
+
save_reasoning=args.save_reasoning,
|
| 1207 |
+
ghidra_script_dir=Path(args.ghidra_script_dir).expanduser().resolve(),
|
| 1208 |
+
)
|
| 1209 |
+
|
| 1210 |
+
if args.tool_smoke:
|
| 1211 |
+
smoke_dir = run_dir / "tool_smoke"
|
| 1212 |
+
smoke_dir.mkdir(parents=True, exist_ok=True)
|
| 1213 |
+
for idx, path in enumerate(targets, 1):
|
| 1214 |
+
fmt = detect_format(path)
|
| 1215 |
+
staged = smoke_dir / f"sample_{idx:04d}{'.elf' if fmt == 'ELF' else '.exe' if fmt == 'PE' else path.suffix}"
|
| 1216 |
+
shutil.copy2(path, staged)
|
| 1217 |
+
tools = StaticTools(staged, fmt, cfg.obs_limit, cfg.ghidra_script_dir)
|
| 1218 |
+
print(f"== {path} ({fmt}) ==")
|
| 1219 |
+
print(tools.file())
|
| 1220 |
+
print(tools.strings()[:1200])
|
| 1221 |
+
return 0
|
| 1222 |
+
|
| 1223 |
+
adapter = args.adapter.strip() or None
|
| 1224 |
+
backend_name = args.backend
|
| 1225 |
+
if backend_name == "auto":
|
| 1226 |
+
backend_name = "llama-cpp" if str(args.model).lower().endswith(".gguf") else "transformers"
|
| 1227 |
+
print(f"[xref 9b] backend={backend_name} model={args.model}", flush=True)
|
| 1228 |
+
if adapter and backend_name == "transformers":
|
| 1229 |
+
print(f"[xref 9b] adapter={adapter}", flush=True)
|
| 1230 |
+
elif adapter and backend_name == "llama-cpp":
|
| 1231 |
+
print("[xref 9b] note: --adapter is ignored by llama-cpp; use a merged GGUF or --llama-lora", flush=True)
|
| 1232 |
+
print(f"[xref 9b] thinking={'on' if cfg.thinking else 'off'} targets={len(targets)} output={run_dir}", flush=True)
|
| 1233 |
+
if backend_name == "transformers":
|
| 1234 |
+
backend = LocalQwenBackend(args.model, adapter, args.dtype, args.device_map)
|
| 1235 |
+
else:
|
| 1236 |
+
backend = LlamaCppBackend(
|
| 1237 |
+
args.model,
|
| 1238 |
+
args.llama_cli,
|
| 1239 |
+
args.ctx_size,
|
| 1240 |
+
args.threads,
|
| 1241 |
+
args.gpu_layers,
|
| 1242 |
+
args.llama_lora.strip() or None,
|
| 1243 |
+
args.llama_extra_arg,
|
| 1244 |
+
)
|
| 1245 |
+
|
| 1246 |
+
results: list[dict[str, Any]] = []
|
| 1247 |
+
for idx, path in enumerate(targets, 1):
|
| 1248 |
+
sample_id = f"sample_{idx:04d}"
|
| 1249 |
+
print(f"[xref 9b] analyzing {sample_id} {path}", flush=True)
|
| 1250 |
+
result = analyze_one(backend, path, sample_id, run_dir, spec, cfg, args.request)
|
| 1251 |
+
results.append(result)
|
| 1252 |
+
print(
|
| 1253 |
+
f"[xref 9b] {sample_id} format={result['format']} prediction={result['prediction']} "
|
| 1254 |
+
f"tools={result['tool_calls']} error={result['error'] or ''}",
|
| 1255 |
+
flush=True,
|
| 1256 |
+
)
|
| 1257 |
+
write_outputs(results, run_dir)
|
| 1258 |
+
|
| 1259 |
+
write_outputs(results, run_dir)
|
| 1260 |
+
print(f"[xref 9b] summary={run_dir / 'SUMMARY.md'}", flush=True)
|
| 1261 |
+
return 0
|
| 1262 |
+
|
| 1263 |
+
|
| 1264 |
+
if __name__ == "__main__":
|
| 1265 |
+
raise SystemExit(main())
|
assets/xref-9b-banner.svg
ADDED
|
|
assets/xref-9b-eval-chart.svg
ADDED
|
|
release_manifest.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model_name": "xref-9b",
|
| 3 |
+
"release_type": "research_preview_gguf",
|
| 4 |
+
"created_at_utc": "2026-07-14T07:31:21.401747+00:00",
|
| 5 |
+
"artifacts": [
|
| 6 |
+
{
|
| 7 |
+
"path": "xref-9b-q4_k_m.gguf",
|
| 8 |
+
"sha256": "87f320a4edda407a00ddb005774640acac4c45882a9d03674e5f2c37e9dbafd6",
|
| 9 |
+
"recommended": true
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"path": "xref-9b-f16.gguf",
|
| 13 |
+
"sha256": "2e9438786b7aed9a9314629e811900616c1b4666a2f7cffa2f537fd566f238d8",
|
| 14 |
+
"recommended": false
|
| 15 |
+
}
|
| 16 |
+
],
|
| 17 |
+
"training_summary_public": [
|
| 18 |
+
"SFT on PE/ELF reverse-engineering instruction traces",
|
| 19 |
+
"IPO preference optimization over curated PE/ELF triage pairs"
|
| 20 |
+
],
|
| 21 |
+
"eval_summary": {
|
| 22 |
+
"mixed40_total": 40,
|
| 23 |
+
"sft_baseline_strict_accuracy": 0.55,
|
| 24 |
+
"sft_baseline_binary_accuracy": 0.625,
|
| 25 |
+
"ipo_strict_accuracy": 0.575,
|
| 26 |
+
"ipo_binary_accuracy": 0.575,
|
| 27 |
+
"ipo_release_workflow_strict_accuracy": 0.675,
|
| 28 |
+
"ipo_release_workflow_binary_accuracy": 0.675
|
| 29 |
+
},
|
| 30 |
+
"local_runtime_files": [
|
| 31 |
+
"agentre.py",
|
| 32 |
+
"agentre_triage.py",
|
| 33 |
+
"reverse_engineering_spec_unified.md",
|
| 34 |
+
"tools/ghidra_scripts/AgentRESummary.java"
|
| 35 |
+
]
|
| 36 |
+
}
|
requirements.txt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# xref-9b wrapper runtime dependencies
|
| 2 |
+
# The core chat workflow uses Python stdlib plus local command-line tools.
|
| 3 |
+
# Optional HF/Transformers path may require torch, transformers, and peft.
|
reverse_engineering_spec_unified.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AgentRE Unified Reverse Engineering Spec
|
| 2 |
+
|
| 3 |
+
## Purpose
|
| 4 |
+
|
| 5 |
+
You are a defensive reverse-engineering assistant. Analyze one binary with static tools, decide whether it is benign, malicious, hackware, or unknown, and submit one structured JSON verdict. The user may provide a single file or a directory of files; each analysis target is handled independently.
|
| 6 |
+
|
| 7 |
+
Do not execute the sample. Do not provide instructions for running malware. Ground every claim in tool output.
|
| 8 |
+
|
| 9 |
+
## Labels
|
| 10 |
+
|
| 11 |
+
- `benign`: ordinary non-malicious PE or ELF software.
|
| 12 |
+
- `malicious`: malware or clearly malicious implant behavior such as C2, reverse shell, loader/dropper, credential theft, persistence, destructive behavior, ransomware behavior, botnet behavior, worm behavior, or anti-analysis malware behavior.
|
| 13 |
+
- `hackware`: offensive or dual-use intrusion tooling, including scanners, exploit tooling, payload delivery, backdoor administration, credential attack tooling, or shell-access tooling.
|
| 14 |
+
- `unknown`: use only when static evidence is insufficient after reasonable tool use.
|
| 15 |
+
- When using `unknown`, explain what evidence is missing and recommend closer inspection with Ghidra or another disassembler, especially for stripped, static, packed, encrypted, or sparse-string binaries.
|
| 16 |
+
|
| 17 |
+
## Core Workflow
|
| 18 |
+
|
| 19 |
+
1. Start with `file` or `run_file` to identify PE/ELF format, architecture, linkage, and stripped status.
|
| 20 |
+
2. Use `strings` or `run_strings` early to find APIs, URLs, IPs, domains, paths, commands, mutexes, services, ransom notes, or suspicious blobs.
|
| 21 |
+
3. Use format-specific metadata tools:
|
| 22 |
+
- ELF: `readelf`, `objdump`, `nm`.
|
| 23 |
+
- PE: `pe_headers`, `pe_sections`, `pe_imports`, `pe_exports`, `pe_symbols`, `pe_disasm`.
|
| 24 |
+
4. Use disassembly before making behavior claims. Prefer targeted `disasm_func` or `run_disasm_func` when symbols name important functions.
|
| 25 |
+
5. Use `entropy`, `hexdump`, or `xxd` for packed, encrypted, encoded, or blob-like data.
|
| 26 |
+
6. Use `ghidra_summary` when binutils output is insufficient or when higher-level function/import/string summaries would help.
|
| 27 |
+
For stripped/static samples that loop on headers or have sparse strings, use `ghidra_summary` or disassembly before giving up.
|
| 28 |
+
7. Submit exactly one `final_answer` JSON object.
|
| 29 |
+
|
| 30 |
+
## Available Tools
|
| 31 |
+
|
| 32 |
+
All tools are static. Tool path arguments are optional; the runtime routes tools to the current staged sample.
|
| 33 |
+
|
| 34 |
+
Common tools:
|
| 35 |
+
|
| 36 |
+
- `file` / `run_file`: identify file type, architecture, linkage, and stripped status.
|
| 37 |
+
- `strings` / `run_strings`: extract printable strings.
|
| 38 |
+
- `entropy`: compute whole-file Shannon entropy.
|
| 39 |
+
- `hexdump`: dump bytes at an offset.
|
| 40 |
+
- `xxd`: alternate hex dump.
|
| 41 |
+
- `disasm_func` / `run_disasm_func`: disassemble one named function.
|
| 42 |
+
- `ghidra_summary`: run Ghidra headless analysis and return imports, strings, functions, and short instruction excerpts when Ghidra is installed.
|
| 43 |
+
- `final_answer` / `submit_answer`: submit the final JSON verdict.
|
| 44 |
+
|
| 45 |
+
ELF tools:
|
| 46 |
+
|
| 47 |
+
- `readelf` / `run_readelf`: inspect ELF headers, sections, symbols, program headers, and dynamic imports.
|
| 48 |
+
- `objdump` / `run_objdump`: disassemble or dump ELF sections.
|
| 49 |
+
- `nm`: list symbols.
|
| 50 |
+
|
| 51 |
+
PE tools:
|
| 52 |
+
|
| 53 |
+
- `pe_headers` / `run_pe_headers`: PE file header, optional header, data directories, subsystem, DLL characteristics.
|
| 54 |
+
- `pe_sections` / `run_pe_sections`: PE section table and entropy.
|
| 55 |
+
- `pe_imports` / `run_pe_imports`: imported DLLs and WinAPI symbols.
|
| 56 |
+
- `pe_exports` / `run_pe_exports`: export table and data directories.
|
| 57 |
+
- `pe_disasm` / `run_pe_disasm`: PE disassembly.
|
| 58 |
+
- `pe_symbols` / `run_pe_symbols`: PE symbols when present.
|
| 59 |
+
|
| 60 |
+
## PE Evidence Hints
|
| 61 |
+
|
| 62 |
+
Malicious PE evidence includes:
|
| 63 |
+
|
| 64 |
+
- C2 or beaconing: `InternetOpen`, `InternetConnect`, `HttpSendRequest`, `WinHttp*`, `WSAStartup`, `socket`, `connect`, hardcoded domains/IPs/URLs.
|
| 65 |
+
- Injection: `VirtualAllocEx`, `WriteProcessMemory`, `CreateRemoteThread`, `NtCreateThreadEx`, `QueueUserAPC`, `SetThreadContext`.
|
| 66 |
+
- Process hollowing: suspended process creation, `NtUnmapViewOfSection`, remote allocation/write, context change, resume.
|
| 67 |
+
- Persistence: service APIs, Run keys, scheduled task strings, startup-folder paths.
|
| 68 |
+
- Ransomware: recursive file traversal, file-extension targeting, ransom notes, crypto APIs, high-entropy encrypted payloads.
|
| 69 |
+
- Anti-analysis: `IsDebuggerPresent`, `CheckRemoteDebuggerPresent`, `NtQueryInformationProcess`, PEB checks, sandbox DLL checks, CPUID VM checks.
|
| 70 |
+
|
| 71 |
+
## ELF Evidence Hints
|
| 72 |
+
|
| 73 |
+
Malicious ELF evidence includes:
|
| 74 |
+
|
| 75 |
+
- C2 or botnet behavior: `socket`, `connect`, `send`, `recv`, `gethostbyname`, hardcoded domains/IPs, beacon loops.
|
| 76 |
+
- Shell or command execution: `/bin/sh`, `execve`, `system`, `popen`, shell command strings.
|
| 77 |
+
- Loader/dropper behavior: writes executable files, changes permissions with `chmod`, downloads via `wget`/`curl`, forks and execs payloads.
|
| 78 |
+
- Botnet/scanner behavior: attack/scanner function names, `/proc/net/tcp`, port scanning, watchdog use, single-instance locks, process killer routines.
|
| 79 |
+
- Anti-analysis: `ptrace`, anti-debug names, environment checks, process hiding, string/config decoding before network use.
|
| 80 |
+
|
| 81 |
+
## Final Answer Schema
|
| 82 |
+
|
| 83 |
+
Call `final_answer` exactly once with:
|
| 84 |
+
|
| 85 |
+
```json
|
| 86 |
+
{
|
| 87 |
+
"answer_json": {
|
| 88 |
+
"schema_version": "agentre_triage_v1",
|
| 89 |
+
"file_type": "ELF64",
|
| 90 |
+
"architecture": "x86-64",
|
| 91 |
+
"classification": "malicious",
|
| 92 |
+
"confidence": 0.85,
|
| 93 |
+
"summary": "One or two sentence verdict.",
|
| 94 |
+
"evidence": ["tool-grounded fact 1", "tool-grounded fact 2"],
|
| 95 |
+
"techniques": ["socket_connect", "xor_string_obfuscation"],
|
| 96 |
+
"network_indicators": [],
|
| 97 |
+
"decoded_strings": {},
|
| 98 |
+
"encoded_strings": false,
|
| 99 |
+
"encryption_details": {
|
| 100 |
+
"algorithm": null,
|
| 101 |
+
"key": null,
|
| 102 |
+
"key_storage": null
|
| 103 |
+
},
|
| 104 |
+
"anti_analysis": [],
|
| 105 |
+
"important_functions": []
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Rules:
|
| 111 |
+
|
| 112 |
+
- `classification` must be one of `benign`, `malicious`, `hackware`, `unknown`.
|
| 113 |
+
- Use JSON `null`, `false`, empty lists, or empty objects for absent findings.
|
| 114 |
+
- Keep evidence concise and tied to observed tool output.
|
| 115 |
+
- Do not include chain-of-thought in the final answer.
|
scripts/check_tools.sh
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
for tool in file strings readelf objdump nm; do
|
| 5 |
+
if command -v "$tool" >/dev/null 2>&1; then
|
| 6 |
+
echo "ok: $tool -> $(command -v "$tool")"
|
| 7 |
+
else
|
| 8 |
+
echo "missing: $tool"
|
| 9 |
+
fi
|
| 10 |
+
done
|
| 11 |
+
|
| 12 |
+
if command -v hexdump >/dev/null 2>&1; then
|
| 13 |
+
echo "ok: hexdump -> $(command -v hexdump)"
|
| 14 |
+
elif command -v xxd >/dev/null 2>&1; then
|
| 15 |
+
echo "ok: xxd -> $(command -v xxd)"
|
| 16 |
+
else
|
| 17 |
+
echo "missing: hexdump or xxd"
|
| 18 |
+
fi
|
| 19 |
+
|
| 20 |
+
if [[ -n "${GHIDRA_HEADLESS:-}" && -x "${GHIDRA_HEADLESS:-}" ]]; then
|
| 21 |
+
echo "ok: GHIDRA_HEADLESS -> $GHIDRA_HEADLESS"
|
| 22 |
+
elif command -v analyzeHeadless >/dev/null 2>&1; then
|
| 23 |
+
echo "ok: analyzeHeadless -> $(command -v analyzeHeadless)"
|
| 24 |
+
else
|
| 25 |
+
echo "optional missing: Ghidra analyzeHeadless"
|
| 26 |
+
fi
|
scripts/run_xref9b_chat.sh
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
if [[ $# -lt 1 ]]; then
|
| 5 |
+
echo "usage: $0 /path/to/binary [extra agentre.py args...]" >&2
|
| 6 |
+
exit 2
|
| 7 |
+
fi
|
| 8 |
+
|
| 9 |
+
TARGET="$1"
|
| 10 |
+
shift
|
| 11 |
+
|
| 12 |
+
MODEL="${XREF9B_GGUF:-$PWD/xref-9b-q4_k_m.gguf}"
|
| 13 |
+
LLAMA_CLI="${XREF9B_LLAMA_CLI:-llama-completion}"
|
| 14 |
+
|
| 15 |
+
python3 agentre.py chat "$TARGET" --model "$MODEL" --llama-cli "$LLAMA_CLI" --ctx-size "${XREF9B_CTX_SIZE:-65536}" --gpu-layers "${XREF9B_GPU_LAYERS:-99}" --max-turns "${XREF9B_MAX_TURNS:-8}" --max-tool-calls "${XREF9B_MAX_TOOL_CALLS:-16}" --max-tokens "${XREF9B_MAX_TOKENS:-1600}" --obs-limit "${XREF9B_OBS_LIMIT:-8000}" "$@"
|
scripts/run_xref9b_triage.sh
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env bash
|
| 2 |
+
set -euo pipefail
|
| 3 |
+
|
| 4 |
+
if [[ $# -lt 1 ]]; then
|
| 5 |
+
echo "usage: $0 /path/to/file_or_directory [extra agentre.py triage args...]" >&2
|
| 6 |
+
exit 2
|
| 7 |
+
fi
|
| 8 |
+
|
| 9 |
+
TARGET="$1"
|
| 10 |
+
shift
|
| 11 |
+
|
| 12 |
+
MODEL="${XREF9B_GGUF:-$PWD/xref-9b-q4_k_m.gguf}"
|
| 13 |
+
LLAMA_CLI="${XREF9B_LLAMA_CLI:-llama-completion}"
|
| 14 |
+
|
| 15 |
+
python3 agentre.py triage "$TARGET" --model "$MODEL" --llama-cli "$LLAMA_CLI" --ctx-size "${XREF9B_CTX_SIZE:-65536}" --gpu-layers "${XREF9B_GPU_LAYERS:-99}" --max-tool-calls "${XREF9B_MAX_TOOL_CALLS:-16}" --max-tokens "${XREF9B_MAX_TOKENS:-1600}" --obs-limit "${XREF9B_OBS_LIMIT:-8000}" "$@"
|
scripts/xref9b.env.example
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copy to scripts/xref9b.env and edit paths for your machine.
|
| 2 |
+
export XREF9B_GGUF="$PWD/xref-9b-q4_k_m.gguf"
|
| 3 |
+
export XREF9B_LLAMA_CLI="/path/to/llama.cpp/build/bin/llama-completion"
|
| 4 |
+
|
| 5 |
+
# Optional. Required for /ghidra and ghidra_summary.
|
| 6 |
+
export GHIDRA_HEADLESS="/path/to/ghidra/support/analyzeHeadless"
|
tools/ghidra_scripts/AgentRESummary.java
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Static Ghidra headless summary script for AgentRE.
|
| 2 |
+
// Usage from analyzeHeadless:
|
| 3 |
+
// -postScript AgentRESummary.java /path/to/output.txt
|
| 4 |
+
|
| 5 |
+
import ghidra.app.script.GhidraScript;
|
| 6 |
+
import ghidra.program.model.address.Address;
|
| 7 |
+
import ghidra.program.model.listing.Data;
|
| 8 |
+
import ghidra.program.model.listing.DataIterator;
|
| 9 |
+
import ghidra.program.model.listing.Function;
|
| 10 |
+
import ghidra.program.model.listing.FunctionIterator;
|
| 11 |
+
import ghidra.program.model.listing.Instruction;
|
| 12 |
+
import ghidra.program.model.listing.InstructionIterator;
|
| 13 |
+
import ghidra.program.model.listing.Listing;
|
| 14 |
+
import ghidra.program.model.mem.MemoryBlock;
|
| 15 |
+
import ghidra.program.model.symbol.Symbol;
|
| 16 |
+
import ghidra.program.model.symbol.SymbolIterator;
|
| 17 |
+
|
| 18 |
+
import java.io.File;
|
| 19 |
+
import java.io.FileWriter;
|
| 20 |
+
import java.io.PrintWriter;
|
| 21 |
+
|
| 22 |
+
public class AgentRESummary extends GhidraScript {
|
| 23 |
+
private String clean(String s) {
|
| 24 |
+
if (s == null) {
|
| 25 |
+
return "";
|
| 26 |
+
}
|
| 27 |
+
s = s.replace('\n', ' ').replace('\r', ' ');
|
| 28 |
+
if (s.length() > 240) {
|
| 29 |
+
return s.substring(0, 240) + "...";
|
| 30 |
+
}
|
| 31 |
+
return s;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
public void run() throws Exception {
|
| 35 |
+
String[] args = getScriptArgs();
|
| 36 |
+
if (args.length < 1) {
|
| 37 |
+
println("AgentRESummary requires an output path argument");
|
| 38 |
+
return;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
File outFile = new File(args[0]);
|
| 42 |
+
try (PrintWriter out = new PrintWriter(new FileWriter(outFile))) {
|
| 43 |
+
Listing listing = currentProgram.getListing();
|
| 44 |
+
|
| 45 |
+
out.println("== Program ==");
|
| 46 |
+
out.println("name: " + currentProgram.getName());
|
| 47 |
+
out.println("format: " + currentProgram.getExecutableFormat());
|
| 48 |
+
out.println("language: " + currentProgram.getLanguageID());
|
| 49 |
+
out.println("compiler: " + currentProgram.getCompilerSpec().getCompilerSpecID());
|
| 50 |
+
out.println("image_base: " + currentProgram.getImageBase());
|
| 51 |
+
|
| 52 |
+
out.println();
|
| 53 |
+
out.println("== Memory Blocks ==");
|
| 54 |
+
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
|
| 55 |
+
out.println(block.getName()
|
| 56 |
+
+ " start=" + block.getStart()
|
| 57 |
+
+ " end=" + block.getEnd()
|
| 58 |
+
+ " size=" + block.getSize()
|
| 59 |
+
+ " r=" + block.isRead()
|
| 60 |
+
+ " w=" + block.isWrite()
|
| 61 |
+
+ " x=" + block.isExecute());
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
out.println();
|
| 65 |
+
out.println("== External Symbols / Imports ==");
|
| 66 |
+
int count = 0;
|
| 67 |
+
SymbolIterator symbols = currentProgram.getSymbolTable().getExternalSymbols();
|
| 68 |
+
while (symbols.hasNext() && count < 600) {
|
| 69 |
+
Symbol sym = symbols.next();
|
| 70 |
+
out.println(sym.getName(true));
|
| 71 |
+
count++;
|
| 72 |
+
}
|
| 73 |
+
if (count >= 600) {
|
| 74 |
+
out.println("[truncated imports at 600]");
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
out.println();
|
| 78 |
+
out.println("== Strings ==");
|
| 79 |
+
count = 0;
|
| 80 |
+
DataIterator data = listing.getDefinedData(true);
|
| 81 |
+
while (data.hasNext() && count < 500) {
|
| 82 |
+
Data d = data.next();
|
| 83 |
+
Object value = null;
|
| 84 |
+
try {
|
| 85 |
+
value = d.getValue();
|
| 86 |
+
} catch (Throwable ignored) {
|
| 87 |
+
value = null;
|
| 88 |
+
}
|
| 89 |
+
if (value instanceof String) {
|
| 90 |
+
String text = clean((String) value);
|
| 91 |
+
if (text.length() >= 4) {
|
| 92 |
+
out.println(d.getAddress() + " " + text);
|
| 93 |
+
count++;
|
| 94 |
+
}
|
| 95 |
+
}
|
| 96 |
+
}
|
| 97 |
+
if (count >= 500) {
|
| 98 |
+
out.println("[truncated strings at 500]");
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
out.println();
|
| 102 |
+
out.println("== Functions ==");
|
| 103 |
+
count = 0;
|
| 104 |
+
FunctionIterator funcs = listing.getFunctions(true);
|
| 105 |
+
while (funcs.hasNext() && count < 220) {
|
| 106 |
+
Function f = funcs.next();
|
| 107 |
+
out.println(f.getEntryPoint() + " " + f.getName(true));
|
| 108 |
+
count++;
|
| 109 |
+
}
|
| 110 |
+
if (count >= 220) {
|
| 111 |
+
out.println("[truncated functions at 220]");
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
out.println();
|
| 115 |
+
out.println("== Function Instruction Excerpts ==");
|
| 116 |
+
int funcsPrinted = 0;
|
| 117 |
+
funcs = listing.getFunctions(true);
|
| 118 |
+
while (funcs.hasNext() && funcsPrinted < 40) {
|
| 119 |
+
Function f = funcs.next();
|
| 120 |
+
String name = f.getName(true).toLowerCase();
|
| 121 |
+
boolean interesting = name.contains("main")
|
| 122 |
+
|| name.contains("connect")
|
| 123 |
+
|| name.contains("socket")
|
| 124 |
+
|| name.contains("send")
|
| 125 |
+
|| name.contains("recv")
|
| 126 |
+
|| name.contains("exec")
|
| 127 |
+
|| name.contains("crypt")
|
| 128 |
+
|| name.contains("decrypt")
|
| 129 |
+
|| name.contains("attack")
|
| 130 |
+
|| name.contains("scan")
|
| 131 |
+
|| name.contains("loader")
|
| 132 |
+
|| name.contains("start");
|
| 133 |
+
if (!interesting && funcsPrinted >= 12) {
|
| 134 |
+
continue;
|
| 135 |
+
}
|
| 136 |
+
out.println();
|
| 137 |
+
out.println("-- " + f.getEntryPoint() + " " + f.getName(true) + " --");
|
| 138 |
+
InstructionIterator instrs = listing.getInstructions(f.getBody(), true);
|
| 139 |
+
int insnCount = 0;
|
| 140 |
+
while (instrs.hasNext() && insnCount < 45) {
|
| 141 |
+
Instruction insn = instrs.next();
|
| 142 |
+
Address addr = insn.getAddress();
|
| 143 |
+
out.println(" " + addr + " " + insn.toString());
|
| 144 |
+
insnCount++;
|
| 145 |
+
}
|
| 146 |
+
funcsPrinted++;
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
println("AgentRE summary written to " + outFile.getAbsolutePath());
|
| 151 |
+
}
|
| 152 |
+
}
|
xref-9b-f16.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2e9438786b7aed9a9314629e811900616c1b4666a2f7cffa2f537fd566f238d8
|
| 3 |
+
size 17920696640
|
xref-9b-q4_k_m.gguf
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:87f320a4edda407a00ddb005774640acac4c45882a9d03674e5f2c37e9dbafd6
|
| 3 |
+
size 5629108544
|