342 lines
13 KiB
Rust
342 lines
13 KiB
Rust
/// Warlock's Stave — egui GUI Demo
|
|
///
|
|
/// Auto-steps through a realistic x86-64 instruction stream, feeding each
|
|
/// frame through the core analysis pipeline (branch prediction, dictionary
|
|
/// scanning, heatmap accumulation) and rendering the results on the 4-quarter
|
|
/// hex grid workspace.
|
|
///
|
|
/// Run: cargo run --bin gui_demo
|
|
///
|
|
/// Controls:
|
|
/// Space — toggle auto-step
|
|
/// Right — single step forward
|
|
/// R — reset demo state
|
|
/// Esc / Q — quit
|
|
|
|
use eframe::egui::{self, Color32, RichText};
|
|
use stave_core::stave_ui::{
|
|
AxialCoordinate, VisualCellMetrics, WarlockAppWorkspace,
|
|
};
|
|
use stave_core::{BranchEvaluator, DictionaryRule, MemoryScanner};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Instruction stream: realistic x86-64 bytes with hex + ASM labels
|
|
// ---------------------------------------------------------------------------
|
|
const STREAM: &[(&[u8], &str, &str)] = &[
|
|
(&[0x48, 0x31, 0xC0], "48 31 C0 ", "xor rax, rax"),
|
|
(&[0x48, 0x89, 0xC3], "48 89 C3 ", "mov rbx, rax"),
|
|
(&[0x0F, 0x84, 0x10, 0x00, 0x00, 0x00], "0F 84 10 00 00 00 ", "je +0x10"),
|
|
(&[0xE9, 0x05, 0x00, 0x00, 0x00], "E9 05 00 00 00 ", "jmp +0x05"),
|
|
(&[0x48, 0x83, 0xC4, 0x28], "48 83 C4 28 ", "add rsp, 0x28"),
|
|
(&[0x48, 0x8D, 0x05, 0x00, 0x00, 0x00, 0x00], "48 8D 05 00 00 00 00 ", "lea rax, [rip]"),
|
|
(&[0x0F, 0x85, 0x20, 0x00, 0x00, 0x00], "0F 85 20 00 00 00 ", "jne +0x20"),
|
|
(&[0x48, 0x31, 0xFF], "48 31 FF ", "xor rdi, rdi"),
|
|
// This one contains "http://" — dictionary scanner will flag it
|
|
(&[0x68, 0x74, 0x74, 0x70, 0x3A, 0x2F, 0x2F], "68 74 74 70 3A 2F 2F ", "push 0x2F2F7074_7474_7868"),
|
|
(&[0xC3], "C3 ", "ret"),
|
|
// Loop back: second pass re-visits cells (heatmap intensity increases)
|
|
(&[0x48, 0x31, 0xC0], "48 31 C0 ", "xor rax, rax"),
|
|
(&[0x0F, 0x84, 0x10, 0x00, 0x00, 0x00], "0F 84 10 00 00 00 ", "je +0x10"),
|
|
(&[0x48, 0x89, 0xE5], "48 89 E5 ", "mov rbp, rsp"),
|
|
(&[0x48, 0x83, 0xEC, 0x20], "48 83 EC 20 ", "sub rsp, 0x20"),
|
|
(&[0xE8, 0x00, 0x00, 0x00, 0x00], "E8 00 00 00 00 ", "call +0x00"),
|
|
];
|
|
|
|
/// Hex positions for each instruction — laid out in a visible cluster.
|
|
/// When the stream loops, cells get revisited and their intensity grows.
|
|
const HEX_POS: &[(i32, i32)] = &[
|
|
( 0, 0), ( 1, 0), ( 2, 0), ( 3, 0), ( 2, -1),
|
|
( 1, -1), ( 0, -1), (-1, -1), (-1, 0), ( 0, 1),
|
|
// Second pass — overlaps first pass cells
|
|
( 0, 0), ( 2, 0), ( 1, 1), ( 2, 1), ( 3, 1),
|
|
];
|
|
|
|
const COLOR_NEON: Color32 = Color32::from_rgb(0, 230, 180);
|
|
const COLOR_ACCENT: Color32 = Color32::from_rgb(0, 150, 255);
|
|
const COLOR_DIM: Color32 = Color32::from_rgb(120, 120, 130);
|
|
const COLOR_BG: Color32 = Color32::from_rgb(10, 10, 12);
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Demo application state
|
|
// ---------------------------------------------------------------------------
|
|
struct GuiDemo {
|
|
workspace: WarlockAppWorkspace,
|
|
step_index: usize,
|
|
auto_step: bool,
|
|
interval_ms: u64,
|
|
last_step: std::time::Instant,
|
|
total_steps: u64,
|
|
branch_log: Vec<(u64, String, String)>,
|
|
dict_log: Vec<(u64, String)>,
|
|
}
|
|
|
|
impl GuiDemo {
|
|
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
|
let mut ws = WarlockAppWorkspace::new();
|
|
// Clear seed data — we'll populate from the instruction stream
|
|
ws.cell_database.clear();
|
|
ws.register_states.clear();
|
|
ws.disassembly_buffer.clear();
|
|
|
|
Self {
|
|
workspace: ws,
|
|
step_index: 0,
|
|
auto_step: true,
|
|
interval_ms: 350,
|
|
last_step: std::time::Instant::now(),
|
|
total_steps: 0,
|
|
branch_log: Vec::new(),
|
|
dict_log: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Feed one instruction through the analysis pipeline.
|
|
fn do_step(&mut self) {
|
|
let idx = self.step_index % STREAM.len();
|
|
let (bytes, hex, asm) = STREAM[idx];
|
|
let rip = 0x7FF0 + idx as u64 * 16;
|
|
// Alternate EFLAGS to exercise conditional / not-taken paths
|
|
let eflags: u32 = match idx % 4 {
|
|
0 => 0x202, // ZF=1
|
|
1 => 0x246, // ZF=1, SF=1
|
|
2 => 0x282, // ZF=0 (je won't be taken)
|
|
_ => 0x242, // ZF=1, SF=1
|
|
};
|
|
|
|
// --- Branch evaluation ---
|
|
let branch = BranchEvaluator::evaluate_execution_flow(bytes, rip, eflags);
|
|
let branch_str = format!("{:?}", branch);
|
|
|
|
// --- Dictionary scan ---
|
|
let dict_hits = MemoryScanner::scan_buffer(
|
|
bytes,
|
|
&DictionaryRule::get_default_hunting_library(),
|
|
);
|
|
|
|
// --- Update heatmap cell ---
|
|
let pos = HEX_POS[idx];
|
|
let coord = AxialCoordinate { q: pos.0, r: pos.1 };
|
|
let entry = self
|
|
.workspace
|
|
.cell_database
|
|
.entry(coord)
|
|
.or_insert(VisualCellMetrics {
|
|
total_hits: 0,
|
|
is_breakpoint: asm.starts_with("je") || asm.starts_with("jne"),
|
|
address_reference: rip,
|
|
});
|
|
entry.total_hits += 1;
|
|
|
|
// --- Disassembly buffer ---
|
|
self.workspace
|
|
.disassembly_buffer
|
|
.push((rip, hex.to_string(), asm.to_string()));
|
|
if self.workspace.disassembly_buffer.len() > 60 {
|
|
self.workspace.disassembly_buffer.remove(0);
|
|
}
|
|
|
|
// --- Register states (simulated) ---
|
|
let rip_val = rip + bytes.len() as u64;
|
|
let rax_val = match asm {
|
|
"xor rax, rax" => 0,
|
|
_ => 0xDEAD_0000_0000_0000 + self.total_steps as u64,
|
|
};
|
|
let rsp_val = 0x7FFF_FFFF_E000 - self.total_steps as u64 * 0x28;
|
|
|
|
self.workspace
|
|
.register_states
|
|
.insert("RIP".to_string(), (rip_val, true));
|
|
self.workspace
|
|
.register_states
|
|
.insert("RAX".to_string(), (rax_val, asm.contains("rax")));
|
|
self.workspace
|
|
.register_states
|
|
.insert("RBX".to_string(), (0x0000_7FFF_E000, asm.contains("rbx")));
|
|
self.workspace
|
|
.register_states
|
|
.insert("RSP".to_string(), (rsp_val, asm.contains("rsp")));
|
|
self.workspace
|
|
.register_states
|
|
.insert("RDI".to_string(), (0, asm.contains("rdi")));
|
|
self.workspace
|
|
.register_states
|
|
.insert("RBP".to_string(), (rsp_val, asm.contains("rbp")));
|
|
self.workspace
|
|
.register_states
|
|
.insert("EFLAGS".to_string(), (eflags as u64, true));
|
|
|
|
// --- Logging ---
|
|
if !branch_str.contains("NonControlFlow") {
|
|
self.branch_log
|
|
.push((rip, asm.to_string(), branch_str));
|
|
if self.branch_log.len() > 30 {
|
|
self.branch_log.remove(0);
|
|
}
|
|
}
|
|
for dm in &dict_hits {
|
|
self.dict_log
|
|
.push((rip, format!("[{}] {}", dm.label, asm)));
|
|
if self.dict_log.len() > 20 {
|
|
self.dict_log.remove(0);
|
|
}
|
|
}
|
|
|
|
self.step_index += 1;
|
|
self.total_steps += 1;
|
|
}
|
|
|
|
/// Render a compact control bar above the workspace.
|
|
fn render_controls(&mut self, ctx: &egui::Context) {
|
|
egui::TopBottomPanel::top("controls").show(ctx, |ui| {
|
|
ui.set_height(36.0);
|
|
ui.horizontal(|ui| {
|
|
ui.style_mut().visuals.panel_fill = COLOR_BG;
|
|
|
|
// Play / Pause
|
|
let label = if self.auto_step { "Pause" } else { "Play" };
|
|
if ui.button(label).clicked() {
|
|
self.auto_step = !self.auto_step;
|
|
}
|
|
|
|
// Single step
|
|
if ui.button("Step").clicked() {
|
|
self.do_step();
|
|
}
|
|
|
|
// Speed slider
|
|
ui.label(RichText::new("Speed:").color(COLOR_DIM));
|
|
ui.add(egui::Slider::new(&mut self.interval_ms, 50..=1000).suffix("ms"));
|
|
|
|
// Reset
|
|
if ui.button("Reset").clicked() {
|
|
self.step_index = 0;
|
|
self.total_steps = 0;
|
|
self.workspace.cell_database.clear();
|
|
self.workspace.register_states.clear();
|
|
self.workspace.disassembly_buffer.clear();
|
|
self.branch_log.clear();
|
|
self.dict_log.clear();
|
|
}
|
|
|
|
ui.separator();
|
|
|
|
// Status
|
|
ui.label(
|
|
RichText::new(format!(
|
|
"Frame {} | Cells {} | Dict hits {}",
|
|
self.total_steps,
|
|
self.workspace.cell_database.len(),
|
|
self.dict_log.len(),
|
|
))
|
|
.color(COLOR_NEON),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
/// Render the live log panel below the workspace.
|
|
fn render_log_panel(&self, ctx: &egui::Context) {
|
|
egui::TopBottomPanel::bottom("log").show(ctx, |ui| {
|
|
ui.set_height(100.0);
|
|
ui.style_mut().visuals.panel_fill = COLOR_BG;
|
|
ui.vertical(|ui| {
|
|
ui.horizontal(|ui| {
|
|
ui.label(
|
|
RichText::new("BRANCH EVALUATOR LOG")
|
|
.strong()
|
|
.color(COLOR_ACCENT),
|
|
);
|
|
ui.separator();
|
|
ui.label(
|
|
RichText::new("DICTIONARY SCANNER LOG")
|
|
.strong()
|
|
.color(Color32::from_rgb(255, 200, 50)),
|
|
);
|
|
});
|
|
egui::ScrollArea::vertical()
|
|
.max_height(72.0)
|
|
.show(ui, |ui| {
|
|
ui.horizontal(|ui| {
|
|
// Branch log (left half)
|
|
ui.vertical(|ui| {
|
|
for (rip, asm, outcome) in &self.branch_log {
|
|
ui.monospace(
|
|
RichText::new(format!(
|
|
" {:#010X} {} -> {}",
|
|
rip, asm, outcome
|
|
))
|
|
.color(Color32::from_rgb(180, 220, 255)),
|
|
);
|
|
}
|
|
});
|
|
ui.separator();
|
|
// Dict log (right half)
|
|
ui.vertical(|ui| {
|
|
for (rip, msg) in &self.dict_log {
|
|
ui.monospace(
|
|
RichText::new(format!(
|
|
" {:#010X} {}",
|
|
rip, msg
|
|
))
|
|
.color(Color32::from_rgb(255, 220, 100)),
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
impl eframe::App for GuiDemo {
|
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
|
// Auto-step timer
|
|
if self.auto_step {
|
|
if self.last_step.elapsed().as_millis() as u64 >= self.interval_ms {
|
|
self.do_step();
|
|
self.last_step = std::time::Instant::now();
|
|
}
|
|
ctx.request_repaint_after(std::time::Duration::from_millis(50));
|
|
}
|
|
|
|
// Keyboard shortcuts
|
|
ctx.input(|i| {
|
|
if i.key_pressed(egui::Key::Space) {
|
|
self.auto_step = !self.auto_step;
|
|
}
|
|
if i.key_pressed(egui::Key::ArrowRight) {
|
|
self.do_step();
|
|
}
|
|
if i.key_pressed(egui::Key::R) {
|
|
self.step_index = 0;
|
|
self.total_steps = 0;
|
|
self.workspace.cell_database.clear();
|
|
self.workspace.register_states.clear();
|
|
self.workspace.disassembly_buffer.clear();
|
|
self.branch_log.clear();
|
|
self.dict_log.clear();
|
|
}
|
|
});
|
|
|
|
// Render: controls (top) → workspace (center) → log (bottom)
|
|
self.render_controls(ctx);
|
|
self.workspace.update_workspace_layout(ctx);
|
|
self.render_log_panel(ctx);
|
|
}
|
|
}
|
|
|
|
fn main() -> eframe::Result<()> {
|
|
let options = eframe::NativeOptions {
|
|
viewport: egui::ViewportBuilder::default()
|
|
.with_title("Warlock's Stave — GUI Demo")
|
|
.with_inner_size([1280.0, 860.0])
|
|
.with_min_inner_size([800.0, 500.0]),
|
|
..Default::default()
|
|
};
|
|
eframe::run_native(
|
|
"Warlock's Stave",
|
|
options,
|
|
Box::new(|cc| Box::new(GuiDemo::new(cc))),
|
|
)
|
|
} |