The definitive open-source binary analysis workstation for the modern researcher.
Zero bloat. Zero vendor lock-in. Maximum signal.
Most likely, this program answers the question: what if hex editors were written from scratch in 2026?
EUVA is a WPF/C# native application that operates directly on the binary layer. No heavy runtime frameworks handling PE parsing. No scripting interpreters bolted on as afterthoughts. No 200-MB installs for features you'll never use. What EUVA provides instead:
- A Memory-Mapped File engine that scales to arbitrarily large binaries with zero heap pressure
- A structured PE decomposition layer that turns raw bytes into a navigable semantic tree
- A Dirty Tracking system that achieves nanosecond-precision change verification directly in the renderer
- A built-in x86 assembler that compiles instructions to opcodes inline, with automatic relative offset resolution
- A scriptable patching DSL (
.euvformat) with live file-watch execution - A plugin-extensible detector pipeline for packer/protector identification
- A fully themeable rendering layer with a 30-token color palette and hot-reload support
This program is under active development. Experimental builds may contain bugs or lead to unexpected behavior. Use with caution.
This software is provided "as is", without warranty of any kind. EUVA is a high-precision instrument designed for educational purposes and security research. The author is not responsible for any system instability, data loss, or legal consequences resulting from the misuse of this tool. By using EUVA, you acknowledge that: You are solely responsible for your actions. You understand the risks of modifying binary files and process memory. You will respect the laws and regulations of your jurisdiction.
EUVA does not load files into byte[] arrays. It maps them directly through the OS virtual memory system via System.IO.MemoryMappedFiles. This is a fundamental architectural choice, not an optimization.
_mmf = MemoryMappedFile.CreateFromFile(
filePath, FileMode.Open, null, 0,
MemoryMappedFileAccess.ReadWrite);
_accessor = _mmf.CreateViewAccessor(0, 0, MemoryMappedFileAccess.ReadWrite);What this means in practice:
| Concern | HxD/010 Editor approach | EUVA approach |
|---|---|---|
| 1 GB binary | Full allocation or paged chunking | Kernel handles paging transparently |
| Write operation | Copy-on-write to a temp buffer | Direct _accessor.Write(offset, value) |
| Flush to disk | Serialization pass | _accessor.Flush() — OS flushes dirty pages |
| Memory pressure | Scales with file size | Scales with viewport, not file size |
The renderer operates in a fully virtualized coordinate system. _currentScrollLine is the only state separating screen-space from file-space:
long firstVisibleLine = _currentScrollLine;
long lastVisibleLine = Math.Min(firstVisibleLine + visibleLines, totalLines);
for (long line = firstVisibleLine; line < lastVisibleLine; line++)
{
long offset = line * _bytesPerLine;
double y = (line - firstVisibleLine) * _lineHeight + 25;
DrawLine(dc, offset, y, offsetColumnWidth, asciiColumnStart);
}A 4 GB binary and a 4 KB binary render with identical overhead.
Scroll Acceleration:
Mouse wheel input is multiplied by keyboard modifier state, providing three-speed navigation:
int multiplier = 1;
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Control)) multiplier = 100;
else if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift)) multiplier = 1000;
int linesToScroll = (e.Delta > 0 ? -3 : 3) * multiplier;Default: 3 lines/tick. Ctrl: 300 lines/tick. Shift: 3000 lines/tick.
Dirty Tracking is EUVA's mechanism for instant, zero-latency verification of binary modifications. It is not a diff engine. It is a live coordinate index.
private readonly HashSet<long> _modifiedOffsets = new();
public void WriteByte(long offset, byte value)
{
_accessor.Write(offset, value);
lock (_modifiedOffsets)
{
_modifiedOffsets.Add(offset);
}
}Every write operation — whether from the script engine, manual editing, or assembler output — registers the exact file offset in _modifiedOffsets. The renderer queries this set for every byte it draws:
if (_modifiedOffsets.Contains(byteOffset))
{
var modBackground = new SolidColorBrush(
Color.FromArgb(80, 255, 0, 128)); // Hot-pink overlay
dc.DrawRectangle(modBackground, null,
new Rect(x - 2, y - 2, _charWidth * 2.5, _lineHeight));
}Modified bytes receive a distinct visual overlay in the same render pass as unmodified bytes. There is no diff phase, no second pass, no "compare mode." The moment a byte is written, it is visually distinct from the surrounding data.
Change Navigation:
F3 invokes JumpToNextChange(), which walks _modifiedOffsets in ascending order from the current selection, wrapping to the minimum offset when exhausted:
var nextChange = _modifiedOffsets
.Where(o => o > startSearchFrom)
.OrderBy(o => o)
.Cast<long?>()
.FirstOrDefault();
if (nextChange == null)
nextChange = _modifiedOffsets.Min();This enables rapid verification workflows: run a script patch, press F3 repeatedly to audit every modified byte individually, verify against expected values in the Inspector.
PEMapper implements IBinaryMapper and produces a fully navigable BinaryStructure tree from raw PE binary data. Parsing is delegated to AsmResolver for header extraction, but the semantic decomposition, region mapping, and tree construction are native EUVA logic.
Parse Pipeline:
Parse(ReadOnlySpan<byte> data)
├── ParseDosHeader() → IMAGE_DOS_HEADER node + DOS region
├── ParseNtHeaders()
│ ├── ParseFileHeader() → IMAGE_FILE_HEADER node
│ └── ParseOptionalHeader() → IMAGE_OPTIONAL_HEADER node
├── ParseSections() → IMAGE_SECTION_HEADER nodes + Code/Data regions
└── ParseDataDirectories() → Import/Export directory nodes
Section Region Coloring Logic:
Section type is detected from PE characteristics flags and mapped directly to a WPF Color:
var color = section.IsContentCode ? Colors.LightGreen :
section.IsContentInitializedData ? Colors.LightBlue :
section.IsContentUninitializedData? Colors.LightGray :
Colors.LightYellow;Reflection-Based Field Resolution:
PE header fields vary across library versions and AsmResolver API surfaces. PEMapper uses a multi-candidate reflection probe to resolve properties robustly without hard-coded field names:
private static object? GetNestedMemberValue(object? obj, params string[] names)
{
foreach (var name in names)
{
// Walk dotted paths (e.g., "Header.PointerToRawData")
// Try Property → fall back to Field
// Return first successful resolution
}
return null;
}Calling sites provide ordered fallback lists:
var rawPtrVal = GetNestedMemberValue(section,
"Header.PointerToRawData", // AsmResolver 5.x path
"PointerToRawData", // flat property
"Offset"); // final fallbackThis makes PEMapper resilient to upstream API changes without requiring conditional compilation.
The DataRegion Model:
Every parsed structural element is materialized as a DataRegion with precise byte boundaries:
public class DataRegion
{
public long Offset { get; init; }
public long Size { get; init; }
public string Name { get; init; }
public RegionType Type { get; init; }
public Color HighlightColor { get; init; }
public int Layer { get; init; }
public BinaryStructure? LinkedStructure { get; init; }
public bool Contains(long offset) => offset >= Offset && offset < Offset + Size;
}RegionType covers the full PE taxonomy:
| Value | Semantic |
|---|---|
Header |
DOS/NT header areas |
Code |
Executable sections |
Data |
Initialized data sections |
Import |
Import Address Table region |
Export |
Export Directory region |
Resource |
.rsrc section |
Relocation |
.reloc section |
Debug |
Debug directory data |
Overlay |
Post-EOF appended data |
Signature |
Authenticode signature region |
Unknown |
Unclassified regions |
The Inspector panel provides simultaneous multi-type interpretation of any selected file offset. All reads go directly through _accessor.ReadArray() — no intermediate copies, no allocation.
Supported Type Matrix:
| Type | Size (bytes) | Implementation | Notes |
|---|---|---|---|
Int8 / UInt8 |
1 | (sbyte)b | b |
Binary bit-string display included |
Int16 / UInt16 |
2 | BitConverter.ToUInt16 |
|
DOS Date |
2 | Bitfield decode | year = ((v >> 9) & 0x7F) + 1980 |
DOS Time |
2 | Bitfield decode | hour = v >> 11, min = (v >> 5) & 0x3F, sec = (v & 0x1F) * 2 |
Int24 / UInt24 |
3 | Manual byte construction | b[0] | (b[1] << 8) | (b[2] << 16) |
Int32 / UInt32 |
4 | BitConverter.ToUInt32 |
|
Single (float32) |
4 | BitConverter.ToSingle |
IEEE 754 |
time_t (32-bit) |
4 | DateTimeOffset.FromUnixTimeSeconds(v) |
Unix epoch |
Int64 / UInt64 |
8 | BitConverter.ToUInt64 |
|
Double (float64) |
8 | BitConverter.ToDouble |
IEEE 754 |
FILETIME |
8 | DateTime.FromFileTime((long)v) |
100ns intervals since 1601-01-01 |
OLETIME |
8 | DateTime.FromOADate(double) |
COM Automation date |
GUID / UUID |
16 | new Guid(b).ToString("B") |
RFC 4122 format |
ULEB128 |
Variable | Streaming decode (max 10 bytes) | DWARF/WebAssembly encoding |
SLEB128 |
Variable | Sign-extended streaming decode | Signed variant |
DOS Timestamp Bitfield Decoding:
The MS-DOS FAT timestamp encoding packs date and time into two 16-bit words with the following layout:
Date word (16 bits):
[15:9] Year offset from 1980 (0–119 → 1980–2099)
[8:5] Month (1–12)
[4:0] Day (1–31)
Time word (16 bits):
[15:11] Hours (0–23)
[10:5] Minutes (0–59)
[4:0] 2-second intervals (0–29 → 0–58 seconds)
public static string ToDosDate(ushort v) =>
$"{((v >> 9) & 0x7F) + 1980:D4}-{(v >> 5) & 0x0F:D2}-{v & 0x1F:D2}";
public static string ToDosTime(ushort v) =>
$"{v >> 11:D2}:{(v >> 5) & 0x3F:D2}:{(v & 0x1F) * 2:D2}";ULEB128 / SLEB128 Streaming Decoder:
Variable-length encodings (used in DWARF debug info, WebAssembly modules, and Android DEX) are decoded by streaming 7-bit groups with continuation bits:
public static (long value, int size) ReadLEB128(byte[] data, bool signed)
{
long result = 0; int shift = 0; int pos = 0;
while (pos < data.Length)
{
byte b = data[pos++];
result |= (long)(b & 0x7F) << shift;
shift += 7;
if ((b & 0x80) == 0)
{
// Signed: sign-extend if the final group's MSB is set
if (signed && (shift < 64) && ((b & 0x40) != 0))
result |= -(1L << shift);
break;
}
}
return (result, pos);
}Endianness Toggle:
All multi-byte reads respect the current endian mode (IsLittleEndian). Toggling via BtnEndian_Click immediately re-parses the current selection:
byte[] GetLE(int count) {
var b = GetRaw(count);
if (!IsLittleEndian) Array.Reverse(b);
return b;
}A zero-allocation, ReadOnlySpan<byte>-native pattern matching engine with wildcard support.
Pattern Format:
Patterns are space-delimited hex byte strings. ?? or ? denotes a wildcard position that matches any byte value.
"55 50 58 30" // exact match: UPX0
"60 BE ?? ?? ?? ?? 8D BE ?? ?? ?? ??" // UPX entry stub with wildcards
"B8 ?? ?? ?? ?? B9 ?? ?? ?? ?? 50 51 E8" // Themida entry
Parser:
private static PatternByte[] ParsePattern(string pattern)
{
var parts = pattern.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var result = new PatternByte[parts.Length];
for (int i = 0; i < parts.Length; i++)
result[i] = (parts[i] == "??" || parts[i] == "?")
? new PatternByte { IsWildcard = true }
: new PatternByte { Value = Convert.ToByte(parts[i], 16) };
return result;
}Matcher:
private static bool MatchesPattern(ReadOnlySpan<byte> data, PatternByte[] pattern)
{
for (int i = 0; i < pattern.Length; i++)
if (!pattern[i].IsWildcard && data[i] != pattern[i].Value)
return false;
return true;
}The outer scan loop slices ReadOnlySpan<byte> at each candidate position — no allocations, no copies.
Shannon Entropy Calculator:
public static double CalculateEntropy(ReadOnlySpan<byte> data)
{
Span<int> frequencies = stackalloc int[256];
foreach (byte b in data) frequencies[b]++;
double entropy = 0.0;
double len = data.Length;
for (int i = 0; i < 256; i++)
{
if (frequencies[i] == 0) continue;
double p = frequencies[i] / len;
entropy -= p * Math.Log2(p);
}
return entropy; // Bits per byte, range [0.0, 8.0]
}Entropy thresholds used by the detector pipeline:
| Threshold | Interpretation |
|---|---|
< 5.0 |
Uncompressed / sparse data |
5.0 – 7.0 |
Normal executable code |
> 7.0 |
Compressed / encrypted (UPX) |
> 7.5 |
Heavy obfuscation (Themida/WinLicense) |
The detection subsystem is a priority-ordered, async plugin chain. Each registered IDetector receives the full file buffer and the parsed BinaryStructure tree, then produces a DetectionResult with a confidence score in [0.0, 1.0].
Registration and Sorting:
public void RegisterDetector(IDetector detector)
{
_detectors.Add(detector);
_detectors.Sort((a, b) => a.Priority.CompareTo(b.Priority));
}Lower priority numbers run first. Built-in detectors:
| Detector | Priority | Detection Strategy |
|---|---|---|
ThemidaDetector |
5 | Signature scan + section name check + Import Table anomaly + entropy |
UPXDetector |
10 | Signature scan (UPX0/1/!) + entry stub pattern + section names + entropy |
FSGDetectorPlugin |
15 | 3-version signature scan + section sizing heuristics + import anomaly |
Confidence Scoring — UPX Example:
| Evidence | Confidence Contribution |
|---|---|
| Any UPX signature match | +0.40 |
Section named UPX0 or UPX1 |
+0.40 |
Section named .UPX0 or .UPX1 |
+0.30 |
| Entropy > 7.0 | +0.20 |
| Total (capped at 1.0) | Max 1.00 |
Plugin System:
Third-party detectors can be distributed as standalone .dll files and loaded at runtime:
public void LoadFromDirectory(string pluginDirectory)
{
var dllFiles = Directory.GetFiles(pluginDirectory, "*.dll",
SearchOption.AllDirectories);
foreach (var dllFile in dllFiles)
{
var assembly = Assembly.LoadFrom(dllFile);
LoadFromAssembly(assembly);
}
}Any class implementing IDetector (or IDetectorPlugin for richer metadata) is automatically instantiated and registered. IDetectorPlugin adds lifecycle hooks:
public interface IDetectorPlugin : IDetector
{
PluginMetadata Metadata { get; }
void Initialize();
void Cleanup();
}EUVA includes a proprietary x86 assembler compiled entirely from scratch in C#. No NASM. No Keystone. No external assembly library of any kind. The assembler translates mnemonic strings to raw opcode byte sequences inline, with full support for automatic relative offset calculation.
private static readonly Dictionary<string, byte> Regs = new() {
{ "eax", 0 }, { "ecx", 1 }, { "edx", 2 }, { "ebx", 3 },
{ "esp", 4 }, { "ebp", 5 }, { "esi", 6 }, { "edi", 7 }
};private static readonly Dictionary<string, byte> Ops = new() {
{ "add", 0x01 }, { "or", 0x09 }, { "and", 0x21 },
{ "sub", 0x29 }, { "xor", 0x31 }, { "cmp", 0x39 },
{ "jmp", 0xE9 }, { "mov_eax", 0xB8 }
};Encoding: 0x90
Length: 1 byte
if (mnemonic == "nop") return new byte[] { 0x90 };Encoding: 0xC3
Length: 1 byte
This is the most architecturally significant instruction in the assembler. jmp requires the computation of a signed 32-bit relative displacement — the delta between the target address and the instruction's post-execution program counter.
Encoding formula:
rel32 = target_address - (current_address + 5)
The +5 accounts for the 5-byte length of the jmp rel32 instruction itself (1 opcode byte + 4 displacement bytes). The CPU's instruction pointer advances past the full instruction before the relative offset is applied.
if (mnemonic == "jmp" && tokens.Length == 2)
{
if (long.TryParse(tokens[1], out long targetAddr))
{
int relativeOffset = (int)(targetAddr - (currentAddr + 5));
byte[] offsetBytes = BitConverter.GetBytes(relativeOffset);
byte[] result = new byte[5];
result[0] = 0xE9; // jmp rel32
Buffer.BlockCopy(offsetBytes, 0, result, 1, 4);
return result;
}
}Example: Injecting a jmp from address 0x00401000 to 0x00402000:
rel32 = 0x00402000 - (0x00401000 + 5)
= 0x00402000 - 0x00401005
= 0x00000FFB
Encoding: E9 FB 0F 00 00
This calculation is critical for cross-section jumps. When the script engine injects a trampoline from .text to injected code in .data, the absolute target address must be resolved at assembly time and the relative displacement recalculated. EUVA performs this automatically.
Encodes the "Move Immediate to Register" short form (opcode B8+rd):
if (mnemonic == "mov" && tokens.Length == 3)
{
if (Regs.TryGetValue(tokens[1], out byte regIdx)
&& int.TryParse(tokens[2], out int val))
{
byte[] result = new byte[5];
result[0] = (byte)(0xB8 + regIdx); // B8 = MOV EAX; B9 = MOV ECX; etc.
Buffer.BlockCopy(BitConverter.GetBytes(val), 0, result, 1, 4);
return result;
}
}| Register | Opcode |
|---|---|
eax |
B8 <imm32> |
ecx |
B9 <imm32> |
edx |
BA <imm32> |
ebx |
BB <imm32> |
esp |
BC <imm32> |
ebp |
BD <imm32> |
esi |
BE <imm32> |
edi |
BF <imm32> |
Two-operand ALU instructions are encoded using the ModRM byte. In register-to-register mode, ModRM has the format 11 src dst (mod=11b, indicating direct register operands):
ModRM = 0xC0 | (src << 3) | dest
if (Ops.ContainsKey(mnemonic) && tokens.Length == 3)
{
if (Regs.TryGetValue(tokens[1], out byte dest)
&& Regs.TryGetValue(tokens[2], out byte src))
{
byte modRM = (byte)(0xC0 + (src << 3) + dest);
return new byte[] { Ops[mnemonic], modRM };
}
}ModRM encoding example — xor eax, eax (the canonical zero idiom):
dest = Regs["eax"] = 0
src = Regs["eax"] = 0
ModRM = 0xC0 | (0 << 3) | 0 = 0xC0
Encoding: 31 C0
xor ebx, ecx:
dest = Regs["ebx"] = 3
src = Regs["ecx"] = 1
ModRM = 0xC0 | (1 << 3) | 3 = 0xCB
Encoding: 31 CB
| Mnemonic | Operands | Opcode | Length | Notes |
|---|---|---|---|---|
nop |
— | 90 |
1 | No-op |
ret |
— | C3 |
1 | Near return |
jmp |
imm_addr |
E9 rel32 |
5 | Relative offset auto-calculated |
mov |
reg, imm32 |
B8+rd imm32 |
5 | Register immediate |
add |
reg, reg |
01 /r |
2 | ModRM register-register |
or |
reg, reg |
09 /r |
2 | |
and |
reg, reg |
21 /r |
2 | |
sub |
reg, reg |
29 /r |
2 | |
xor |
reg, reg |
31 /r |
2 | |
cmp |
reg, reg |
39 /r |
2 | Flags only, no write |
EUVA scripts are structured text files with .euv extension. The script engine parses and executes them either on-demand (F5) or automatically via file-system watch when the file changes on disk.
Scripts execute in a two-phase pipeline:
- Parse phase: The engine reads all lines, resolves method declarations, and builds
MethodContainerobjects with body line lists andclinkexport declarations. - Execute phase:
FinalizeMethod()iterates over each method's body, callingExecuteCommand()per line. Variables are scoped: local to the method, with optional export to global scope viaclink.
# Comments use # or //
start; # Required: marks begin of executable body
public: # Access modifier for following method
_createMethod(Name) {
# method body
}
private: # Private methods cannot export via clink
_createMethod(Internal) {
# body
}
end; # Required: execution aborted if missing
The engine enforces the end; sentinel:
if (!isTerminated) throw new Exception("FATAL: No 'end;' flag! Execution aborted.");Declares a named method block. Methods are the primary unit of logical grouping. A method may be public: (can export symbols via clink) or private: (local execution only).
public:
_createMethod(GodMode) {
find(MyFunc = 48 83 EC 28 41 B9 01)
MyFunc : nop
clink: [MyFunc]
}
Execution flow:
FinalizeMethod() runs each body line through ExecuteCommand(), then processes clink exports:
if (method.Access == "public") {
foreach (var exportName in method.Clinks.Keys) {
if (localScope.TryGetValue(exportName, out long addr)) {
string globalName = $"{method.Name}.{exportName}";
globalScope[globalName] = addr;
} else {
throw new Exception($"Unknown: '{exportName}' not defined in {method.Name}!");
}
}
}After finalization, exported symbols are accessible to subsequent methods as MethodName.SymbolName in the global scope.
Scans the loaded binary for the first occurrence of a byte pattern and assigns the file offset to a local variable.
find(MyFunc = 48 83 EC 28 41 B9 01)
Pattern format: space-separated hex bytes. ?? is a wildcard matching any byte value.
find(StubEntry = 60 BE ?? ?? ?? ?? 8D BE ?? ?? ?? ??)
On match, MyFunc is set to the file offset of the first byte of the pattern. On no match, the variable is set to -1. The console logs the result:
[Found] MyFunc at 0x00401A30
Implementation:
private long FindSignature(string pattern)
{
var p = pattern.Split(' ')
.Select(b => b == "??" ? (byte?)null : Convert.ToByte(b, 16))
.ToArray();
for (long i = 0; i < HexView.FileLength - p.Length; i++) {
bool m = true;
for (int j = 0; j < p.Length; j++)
if (p[j] != null && HexView.ReadByte(i + j) != p[j]) { m = false; break; }
if (m) return i;
}
return -1;
}Assigns a computed value to a local variable. The right-hand side is evaluated by ParseMath(), which supports hex literals, arithmetic operators, and variable substitution.
set(PatchBase = MyFunc + 0x10)
set(NopEnd = PatchBase + 4)
set(NewTarget = 0x00405000)
The core write instruction. The address expression is on the left of :, the payload on the right.
Syntax:
<address_expr> : <payload>
Payload types:
1. Assembly mnemonics (assembled inline by AsmLogic.Assemble):
MyFunc : nop
(MyFunc + 1) : nop
(MyFunc + 4) : mov eax, 999
(MyFunc + 9) : jmp 0x00405000
(MyFunc + 14): xor eax, eax
2. Raw hex bytes:
0x00401000 : 90 90 90 90
3. ASCII string literals:
0x00402000 : "Hello, World!"
The engine tries assembly first, then string parsing, then raw hex:
bytes = AsmLogic.Assemble(dataPart, addr);
if (bytes == null && dataPart.Contains("\""))
bytes = Encoding.ASCII.GetBytes(Regex.Match(dataPart, "\"(.*)\"").Groups[1].Value);
if (bytes == null)
bytes = ParseBytes(dataPart);Reads bytes at the given address and halts the patch if they do not match. Used to verify pre-conditions before writing.
check MyFunc : 48 83 EC 28
If the bytes at MyFunc do not equal 48 83 EC 28, the command returns early without executing subsequent patch lines.
Declares which local variables should be exported from the current method to the global scope after execution. Only valid inside public: methods.
public:
_createMethod(Scanner) {
find(EntryPoint = 55 8B EC)
find(ExitPoint = C9 C3)
clink: [EntryPoint, ExitPoint]
}
_createMethod(Patcher) {
# Access exported symbols as Scanner.EntryPoint, Scanner.ExitPoint
set(target = Scanner.EntryPoint + 0x20)
target : nop
}
All address expressions in EUVA scripts pass through ParseMath(), a full arithmetic evaluator supporting variable substitution, hex literals, and standard operators.
Processing pipeline:
- Replace
.or()withlastAddress(the post-write cursor from the previous patch operation) - Substitute all known variable names with their decimal string representations (longest-key-first to prevent partial matches)
- Convert
0xNNNhex literals to decimal - Evaluate the resulting expression using
System.Data.DataTable.Compute()
private long ParseMath(string expr, long lastAddr, Dictionary<string, long> effectiveScope)
{
string formula = expr.Trim().Replace(" ", "");
if (formula == "." || formula == "()") return lastAddr;
// Substitute variables (longest first to avoid partial matches)
var sortedKeys = effectiveScope.Keys.OrderByDescending(k => k.Length).ToList();
foreach (var key in sortedKeys) {
string pattern = @"\b" + Regex.Escape(key) + @"\b";
formula = Regex.Replace(formula, pattern, effectiveScope[key].ToString("D"));
}
// Hex literal conversion
formula = Regex.Replace(formula, @"0x([0-9A-Fa-f]+)", m =>
long.Parse(m.Groups[1].Value, NumberStyles.HexNumber).ToString());
return Convert.ToInt64(new DataTable().Compute(formula, null));
}Examples:
| Expression | Resolves to |
|---|---|
MyFunc |
Address found by find() |
MyFunc + 4 |
MyFunc plus 4 |
(MyFunc + 1) |
Parenthesized arithmetic |
0x00401000 |
Absolute hex address |
MyFunc + 0x10 |
Mixed variable + hex |
. |
lastAddress (cursor from previous write) |
Scanner.EntryPoint + 8 |
Cross-method exported symbol |
This is the canonical test script shipped with EUVA:
start;
public:
_createMethod(GodMode) {
# Locate the function prologue in the binary
find(MyFunc = 48 83 EC 28 41 B9 01)
# NOP out the first 4 bytes of the function
MyFunc : nop
(MyFunc + 1) : nop
(MyFunc + 2) : nop
(MyFunc + 3) : nop
# mov eax, 999 — force a known return value
(MyFunc + 4) : mov eax, 999
# Export MyFunc address for use by other methods
clink: [MyFunc]
}
end;
Execution trace:
[Found] MyFunc at 0x00401A30
[Write] 0x00401A30 ← 90 (nop)
[Write] 0x00401A31 ← 90 (nop)
[Write] 0x00401A32 ← 90 (nop)
[Write] 0x00401A33 ← 90 (nop)
[Write] 0x00401A34 ← BF E7 03 00 00 (mov edi, 999)
[Link] GodMode.MyFunc -> 0x401A30
EUVA monitors the active script file for changes using FileSystemWatcher:
_scriptWatcher = new FileSystemWatcher(dir) {
Filter = file,
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size
| NotifyFilters.FileName | NotifyFilters.CreationTime,
EnableRaisingEvents = true
};
_scriptWatcher.Changed += OnScriptUpdated;On any change event, the engine waits 400ms (debounce), then re-executes the full script. Save the file in your editor → EUVA re-patches the binary immediately. This enables a tight iterative loop: write a patch, save, see the Dirty Track overlay update, verify in the Inspector, refine, repeat.
F5 forces immediate re-execution without waiting for a file change.
EUVA's visual presentation is fully controlled by .themes files — plain-text palettes defining 30 canonical UI color tokens.
| Token | Default (R,G,B,A) | Used In |
|---|---|---|
Background |
30,30,30,255 |
Main window |
Sidebar |
37,37,38,255 |
Left panel |
Toolbar |
45,45,48,255 |
Menu bar |
Border |
62,62,66,255 |
Panel borders |
Hex_Background |
30,30,30,255 |
HexView canvas |
Hex_ByteActive |
173,216,230,255 |
Non-null byte text |
Hex_ByteNull |
80,80,80,255 |
Zero byte text |
Hex_ByteSelected |
255,255,0,255 |
Selected byte |
Hex_AsciiPrintable |
144,238,144,255 |
Printable ASCII chars |
Hex_AsciiNonPrintable |
100,100,100,255 |
Non-printable chars |
TreeIconSection |
86,156,214,255 |
Section nodes |
TreeIconField |
78,201,176,255 |
Field nodes |
PropertyKey |
156,220,254,255 |
Inspector labels |
PropertyValue |
206,145,120,255 |
Inspector values |
ConsoleError |
244,71,71,255 |
Error log lines |
ConsoleSuccess |
106,153,85,255 |
Success log lines |
# EUVA Color Palette
# Format: TokenName = R , G , B , A
# A = 255 means fully opaque
Background = 18 , 18 , 18 , 255
Hex_Background = 18 , 18 , 18 , 255
Hex_ByteActive = 200, 200, 255, 255
Hex_ByteSelected = 255, 200, 0, 255
Hex_AsciiPrintable = 100, 255, 100, 255
ConsoleError = 255, 80, 80, 255
Parser rules (No-Hysteria mode):
#starts an inline comment; everything after it on the line is ignored- Blank and comment-only lines are silently skipped
- Malformed lines log an error and are skipped — the rest of the file continues loading
- Channel values outside
[0, 255]are rejected per-line without aborting the file - Every successfully parsed token is injected as both a
Colorand a frozenSolidColorBrushintoApplication.Current.Resources, enablingDynamicResourcebindings throughout the entire WPF tree
Hotkeys are defined in plain-text .htk files:
# EUVA Hotkey Configuration
# Format: Action = Modifier + Key
NavInspector = Alt + D1
NavSearch = Alt + D2
NavDetections = Alt + D3
NavProperties = Alt + D4
CopyHex = Control + C
CopyCArray = Control + Shift + C
CopyPlainText = Ctrl+Alt+C
Available actions:
| Action | Default Binding | Effect |
|---|---|---|
NavInspector |
Alt+1 |
Switch to Inspector tab |
NavSearch |
Alt+2 |
Switch to Search tab, focus input |
NavDetections |
Alt+3 |
Switch to Detections tab |
NavProperties |
Alt+4 |
Switch to Properties tab |
CopyHex |
Ctrl+C |
Copy selection as hex string |
CopyCArray |
Ctrl+Shift+C |
Copy selection as C byte array |
CopyPlainText |
Ctrl+Alt+C |
Copy selection as decoded text |
EUVA's ASCII panel decodes bytes using a pre-computed lookup table initialized for any Windows code page:
public void InitializeAsciiTable(int codePage)
{
var encoding = Encoding.GetEncoding(codePage);
byte[] allBytes = new byte[256];
for (int i = 0; i < 256; i++) allBytes[i] = (byte)i;
string decoded = encoding.GetString(allBytes);
// Populate _asciiLookupTable[256]
}Supported code pages (menu-selectable):
| Code Page | Encoding |
|---|---|
| 28591 | ISO-8859-1 (Latin-1), default |
| 1251 | Windows Cyrillic |
| 1252 | Windows Western |
| 65001 | UTF-8 |
| Any valid Windows code page | Via Encoding.GetEncoding(codePage) |
The table is regenerated on encoding change and the viewport invalidated immediately.
A secondary render mode that streams raw binary files as grayscale ASCII art video at 60 FPS, using the hex viewport as a canvas. Brightness is mapped through a 10-character density ramp:
private readonly string _videoRamp = " .:-=+*#%@";
int rampIndex = value * (_videoRamp.Length - 1) / 255;
displayChar = _videoRamp[rampIndex];Byte value 0x00 maps to space (black). Byte value 0xFF maps to @ (white). The ASCII panel effectively becomes a 24×N-row grayscale display.
The engine uses CompositionTarget.Rendering for frame delivery, synchronized to the WPF composition clock, and reads raw frames sequentially from a FileStream with FileOptions.SequentialScan:
int bytesRead = _rawVideoStream.Read(_frameBuffer, 0, _videoTotalSize);
if (bytesRead < _videoTotalSize)
{
_rawVideoStream.Position = 0; // Loop
return;
}
HexView.SetMediaFrame(_frameBuffer);| Component | Requirement |
|---|---|
| Runtime | .NET 8.0 Windows |
| Language | C# 12.0 |
| UI Framework | WPF (net8.0-windows) |
| Nullable | Enabled |
| PE Parsing | AsmResolver 5.5.1 |
| Architecture | x64 |
| OS | Windows 10 / 11 |
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<LangVersion>12.0</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>Implement IDetectorPlugin and distribute as a .dll:
public class MyPackerDetector : IDetectorPlugin
{
public string Name => "MyPacker Detector";
public string Version => "1.0.0";
public int Priority => 20;
public PluginMetadata Metadata => new()
{
Author = "Your Name",
Description = "Detects MyPacker v1.x",
SupportedPackers = new List<string> { "MyPacker 1.0" }
};
public void Initialize() { }
public void Cleanup() { }
public bool CanAnalyze(BinaryStructure structure) =>
structure.Type == "Root" && structure.Name == "PE File";
public async Task<DetectionResult?> DetectAsync(
ReadOnlyMemory<byte> fileData, BinaryStructure structure)
{
return await Task.Run(() =>
{
var signatures = SignatureScanner.FindPattern(
fileData.Span, "60 ?? ?? ?? E8 00 00 00 00", "MyPacker Stub");
if (signatures.Count == 0) return null;
return new DetectionResult
{
Name = "MyPacker",
Type = DetectionType.Packer,
Confidence = 0.85,
Signatures = signatures,
DetectorName = Name
};
});
}
}Drop the compiled .dll into the plugins directory. EUVA loads it automatically at startup.
Never worry about corrupting a binary again. EUVA HexEngine features a robust, multi-level undo system:
- Step-by-Step Rollback (
Ctrl + Z): Undo individual byte patches one by one. - Session Rollback (
Ctrl + Shift + Z): Revert all changes made during the entire script execution session
EUVA is free software released under the GNU General Public License v3.0.
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2026 pumpkin-bit (falker) & EUVA Contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
---
EngineUnpacker Visual Analyzer (EUVA)
Professional PE Static Analysis Tool
Educational tool for reverse engineering research.
Use responsibly and in accordance with applicable laws.
EUVA — built for researchers who read hex for fun.