Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/analysis/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ tracing-subscriber.workspace = true
hex.workspace = true
thiserror.workspace = true
chrono.workspace = true
futures-util = "0.3"

alloy = "1.1"
heimdall-decompiler = { git = "https://github.com/Jon-Becker/heimdall-rs", tag = "0.9.0" }
Expand Down
22 changes: 21 additions & 1 deletion crates/analysis/src/decompile_diff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@
pub mod parser;

use alloy::primitives::Bytes;
use futures_util::FutureExt;
use heimdall_decompiler::DecompilerArgsBuilder;
use imara_diff::{Diff, InternedInput, Interner, Token, UnifiedDiffPrinter};
use owo_colors::OwoColorize;
use std::any::Any;
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::panic::AssertUnwindSafe;

/// Errors that can occur during decompile diff analysis.
#[derive(thiserror::Error, Debug)]
Expand All @@ -29,6 +32,10 @@ pub enum DecompileDiffError {
/// Decompilation produced no source output.
#[error("decompilation produced no source output")]
NoSource,

/// The decompiler panicked while processing bytecode.
#[error("decompiler panic: {0}")]
DecompilerPanic(String),
}

/// A single replacement hunk representing lines removed and added.
Expand Down Expand Up @@ -401,10 +408,23 @@ pub async fn decompile(target: Bytes) -> Result<String, DecompileDiffError> {
.include_solidity(true)
.build()
.unwrap();
let result = heimdall_decompiler::decompile(args).await?;
let result = AssertUnwindSafe(heimdall_decompiler::decompile(args))
.catch_unwind()
.await
.map_err(|payload| DecompileDiffError::DecompilerPanic(panic_payload(payload)))??;
result.source.ok_or(DecompileDiffError::NoSource)
}

fn panic_payload(payload: Box<dyn Any + Send>) -> String {
if let Some(message) = payload.downcast_ref::<String>() {
return message.clone();
}
if let Some(message) = payload.downcast_ref::<&'static str>() {
return (*message).to_string();
}
"unknown panic payload".to_string()
}

// ============================================================================
// Structured Diff Types and Implementation
// ============================================================================
Expand Down
57 changes: 2 additions & 55 deletions crates/analysis/src/obfuscation.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
use azoth_core::seed::Seed;
use azoth_transform::{
Transform,
jump_address_transformer::JumpAddressTransformer,
obfuscator::{ObfuscationConfig, obfuscate_bytecode},
opaque_predicate::OpaquePredicate,
shuffle::Shuffle,
};
use azoth_transform::obfuscator::{ObfuscationConfig, obfuscate_bytecode};
use chrono::{DateTime, Utc};
use hex::FromHexError;
use serde::Serialize;
Expand All @@ -16,13 +10,6 @@ use std::{
};
use thiserror::Error as ThisError;

/// Default passes applied to each obfuscation run.
///
/// Leaving this empty means the analysis reuses the obfuscator's native defaults
/// (dispatcher when detected plus any user-specified transforms) instead of
/// forcing deprecated transforms such as Shuffle.
pub const DEFAULT_PASSES: &str = "";

/// Configuration for running an obfuscation analysis experiment.
#[derive(Debug, Clone)]
pub struct AnalysisConfig<'a> {
Expand Down Expand Up @@ -341,8 +328,6 @@ pub enum AnalysisError {
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("invalid transform pass: {0}")]
InvalidPass(String),
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("format error: {0}")]
Expand All @@ -357,7 +342,6 @@ pub async fn analyze_obfuscation(
return Err(AnalysisError::EmptyIterations);
}

let passes = parse_passes(DEFAULT_PASSES)?;
let original_bytes = hex_to_bytes(config.original_bytecode)?;
let mut sequence_lengths = Vec::with_capacity(config.iterations);
let mut sequence_counter: HashMap<Vec<u8>, usize> = HashMap::new();
Expand All @@ -374,7 +358,7 @@ pub async fn analyze_obfuscation(
let seed_hex = seed.to_hex();
let mut obfuscation_config = ObfuscationConfig::with_seed(seed.clone());
obfuscation_config.preserve_unknown_opcodes = true;
obfuscation_config.transforms = passes.iter().map(|p| p.build()).collect();
obfuscation_config.transforms = ObfuscationConfig::default().transforms;

match obfuscate_bytecode(
config.original_bytecode,
Expand Down Expand Up @@ -676,43 +660,6 @@ fn truncate_hex(input: &str, max_len: usize) -> String {
}
}

fn parse_passes(passes: &str) -> Result<Vec<TransformSpec>, AnalysisError> {
let mut specs = Vec::new();
if passes.trim().is_empty() {
return Ok(specs);
}
for raw in passes.split(',') {
let name = raw.trim();
if name.is_empty() {
continue;
}
let spec = match name {
"shuffle" => TransformSpec::Shuffle,
"opaque_pred" | "opaque_predicate" => TransformSpec::OpaquePredicate,
"jump_transform" | "jump_addr" => TransformSpec::JumpTransform,
other => return Err(AnalysisError::InvalidPass(other.to_string())),
};
specs.push(spec);
}
Ok(specs)
}

enum TransformSpec {
Shuffle,
OpaquePredicate,
JumpTransform,
}

impl TransformSpec {
fn build(&self) -> Box<dyn Transform> {
match self {
TransformSpec::Shuffle => Box::new(Shuffle),
TransformSpec::OpaquePredicate => Box::new(OpaquePredicate::new()),
TransformSpec::JumpTransform => Box::new(JumpAddressTransformer::new()),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Options:
- `-D, --deployment <BYTECODE>` - Input deployment bytecode (required)
- `-R, --runtime <BYTECODE>` - Input runtime bytecode (required)
- `--seed <value>` - Cryptographic seed for deterministic obfuscation
- `--passes <list>` - Comma-separated list of transforms (default: `arithmetic_chain,push_split,slot_shuffle,string_obfuscate,cluster_shuffle`)
- `--passes <list>` - Comma-separated list of transforms (default: `string_obfuscate,constant_mask,arithmetic_chain,push_split,slot_shuffle,cluster_shuffle`)
- `--emit <file>` - Path to write gas/size report as JSON
- `--emit-debug <PATH>` - Path to emit detailed CFG trace debug report as JSON
- `--tui` - Launch TUI to view debug trace after obfuscation
Expand Down
3 changes: 1 addition & 2 deletions crates/cli/src/commands/analyze.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::commands::{obfuscate::read_input, ObfuscateError};
use crate::commands::obfuscate::read_input;
use async_trait::async_trait;
use azoth_analysis::obfuscation::{analyze_obfuscation, AnalysisConfig, AnalysisError};
use clap::Args;
Expand Down Expand Up @@ -111,7 +111,6 @@ fn map_analysis_error(err: AnalysisError) -> Box<dyn Error> {
AnalysisError::UnknownOpcodes { count } => Box::new(std::io::Error::other(format!(
"analysis aborted due to {count} unknown opcode(s)"
))),
AnalysisError::InvalidPass(name) => Box::new(ObfuscateError::InvalidPass(name)),
AnalysisError::ObfuscationFailure { source, .. } => source,
AnalysisError::Io(err) => Box::new(err),
AnalysisError::Fmt(err) => Box::new(err),
Expand Down
2 changes: 1 addition & 1 deletion crates/cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub mod tui;
use thiserror::Error;

pub const DEFAULT_PASSES: &str =
"arithmetic_chain, push_split, slot_shuffle, string_obfuscate, cluster_shuffle";
"string_obfuscate, constant_mask, arithmetic_chain, push_split, slot_shuffle, cluster_shuffle";

/// Errors that can occur during obfuscation.
#[derive(Debug, Error)]
Expand Down
3 changes: 3 additions & 0 deletions crates/cli/src/commands/obfuscate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ pub(crate) fn build_passes(list: &str) -> Result<Vec<Box<dyn Transform>>, Box<dy
"arithmetic_chain" => Ok(Box::new(
azoth_transform::arithmetic_chain::ArithmeticChain::new(),
) as Box<dyn Transform>),
"constant_mask" | "literal_mask" => Ok(Box::new(
azoth_transform::constant_mask::ConstantMask::new(),
) as Box<dyn Transform>),
"push_split" => {
Ok(Box::new(azoth_transform::push_split::PushSplit::new()) as Box<dyn Transform>)
}
Expand Down
5 changes: 5 additions & 0 deletions crates/core/src/cfg_ir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ pub struct CfgIrBundle {
/// rewrite every AC-emitted offset PUSH so CODECOPY still points into
/// the appended data section.
pub ac_runtime_length_estimate: Option<usize>,
/// Map of original runtime byte offsets for immutable placeholders to
/// XOR keys used by transforms that mask the placeholder. Init-code
/// immutable writes must store `value XOR key` at these offsets.
pub immutable_masks: HashMap<usize, Vec<u8>>,
}

impl CfgIrBundle {
Expand Down Expand Up @@ -1679,6 +1683,7 @@ pub fn build_cfg_ir(
dispatcher_blocks: HashSet::new(),
arithmetic_chain_data: None,
ac_runtime_length_estimate: None,
immutable_masks: HashMap::new(),
};
let body_blocks = bundle
.cfg
Expand Down
Loading
Loading