use wasm_encoder::reencode::{Reencode, RoundtripReencoder};
use wasm_encoder::{
BlockType, CodeSection, CustomSection, Function, FunctionSection, Instruction, RawSection,
TypeSection, ValType,
};
use wasmparser::{Parser, Payload};
use crate::imp::compiler::error::{CompilerError, Result};
const OBFUSCATION_MARKER: &[u8] =
b"digstore-obf-v1\x00opaque-predicates;bogus-code;control-flow-nops;instruction-substitution";
const BOGUS_FUNCTION_COUNT: u32 = 8;
const SUBSTITUTION_STRIDE: u32 = 3;
pub fn obfuscate(module_bytes: &[u8]) -> Result<Vec<u8>> {
let mut existing_type_count: u32 = 0;
for payload in Parser::new(0).parse_all(module_bytes) {
let payload = payload.map_err(|e| CompilerError::Validation(e.to_string()))?;
if let Payload::TypeSection(reader) = payload {
existing_type_count = reader.count();
}
}
let bogus_type_index = existing_type_count;
let mut reencoder = RoundtripReencoder;
let code = build_code_section(module_bytes, &mut reencoder)?;
let final_bytes = assemble(module_bytes, code, bogus_type_index)?;
Parser::new(0)
.parse_all(&final_bytes)
.try_for_each(|p| p.map(|_| ()))
.map_err(|e| CompilerError::Validation(e.to_string()))?;
Ok(final_bytes)
}
fn reencode_err(e: wasm_encoder::reencode::Error<core::convert::Infallible>) -> CompilerError {
CompilerError::Validation(format!("reencode failed: {e:?}"))
}
fn build_code_section(
module_bytes: &[u8],
reencoder: &mut RoundtripReencoder,
) -> Result<CodeSection> {
let mut code = CodeSection::new();
let mut func_ordinal: u32 = 0;
for payload in Parser::new(0).parse_all(module_bytes) {
let payload = payload.map_err(|e| CompilerError::Validation(e.to_string()))?;
if let Payload::CodeSectionEntry(body) = payload {
let f = transform_body(reencoder, &body, func_ordinal)?;
code.function(&f);
func_ordinal += 1;
}
}
for i in 0..BOGUS_FUNCTION_COUNT {
let mut f = Function::new(Vec::<(u32, ValType)>::new());
for _ in 0..(i + 1) {
f.instruction(&Instruction::Nop);
}
f.instruction(&Instruction::End);
code.function(&f);
}
Ok(code)
}
fn transform_body(
reencoder: &mut RoundtripReencoder,
body: &wasmparser::FunctionBody<'_>,
func_ordinal: u32,
) -> Result<Function> {
let mut locals = Vec::new();
for pair in body.get_locals_reader().map_err(parser_err)? {
let (cnt, ty) = pair.map_err(parser_err)?;
locals.push((cnt, reencoder.val_type(ty).map_err(reencode_err)?));
}
let mut f = Function::new(locals);
f.instruction(&Instruction::I32Const(1));
f.instruction(&Instruction::If(BlockType::Empty));
f.instruction(&Instruction::Nop);
f.instruction(&Instruction::End);
let mut reader = body.get_operators_reader().map_err(parser_err)?;
let mut op_index: u32 = 0;
let mut ops = Vec::new();
while !reader.eof() {
ops.push(reader.read().map_err(parser_err)?);
}
let last = ops.len().saturating_sub(1);
let mut const_index: u32 = 0;
for (i, op) in ops.into_iter().enumerate() {
if let wasmparser::Operator::I32Const { value: k } = op {
let do_subst = const_index.is_multiple_of(SUBSTITUTION_STRIDE);
const_index = const_index.wrapping_add(1);
if do_subst {
let seed = func_ordinal
.wrapping_mul(0x9E37_79B1)
.wrapping_add(const_index);
let a = seed.wrapping_mul(0x9E37_79B1) as i32;
let b = k.wrapping_sub(a);
f.instruction(&Instruction::I32Const(a));
f.instruction(&Instruction::I32Const(b));
f.instruction(&Instruction::I32Add);
op_index = op_index.wrapping_add(1);
continue;
}
}
let instr = reencoder.instruction(op).map_err(reencode_err)?;
f.instruction(&instr);
if i != last && (op_index.wrapping_add(func_ordinal)).is_multiple_of(2) {
f.instruction(&Instruction::Nop);
}
op_index = op_index.wrapping_add(1);
}
Ok(f)
}
fn parser_err(e: wasmparser::BinaryReaderError) -> CompilerError {
CompilerError::Validation(e.to_string())
}
fn assemble(module_bytes: &[u8], code: CodeSection, bogus_type_index: u32) -> Result<Vec<u8>> {
let mut module = wasm_encoder::Module::new();
let mut reencoder = RoundtripReencoder;
let mut code = Some(code);
for payload in Parser::new(0).parse_all(module_bytes) {
let payload = payload.map_err(|e| CompilerError::Validation(e.to_string()))?;
match payload {
Payload::TypeSection(reader) => {
let mut types = TypeSection::new();
reencoder
.parse_type_section(&mut types, reader)
.map_err(reencode_err)?;
types
.ty()
.function(Vec::<ValType>::new(), Vec::<ValType>::new());
module.section(&types);
}
Payload::ImportSection(r) => {
module.section(&RawSection {
id: 2,
data: &module_bytes[r.range()],
});
}
Payload::FunctionSection(reader) => {
let mut funcs = FunctionSection::new();
for ff in reader {
funcs.function(ff.map_err(|e| CompilerError::Validation(e.to_string()))?);
}
for _ in 0..BOGUS_FUNCTION_COUNT {
funcs.function(bogus_type_index);
}
module.section(&funcs);
}
Payload::TableSection(r) => {
module.section(&RawSection {
id: 4,
data: &module_bytes[r.range()],
});
}
Payload::MemorySection(r) => {
module.section(&RawSection {
id: 5,
data: &module_bytes[r.range()],
});
}
Payload::GlobalSection(r) => {
module.section(&RawSection {
id: 6,
data: &module_bytes[r.range()],
});
}
Payload::ExportSection(r) => {
module.section(&RawSection {
id: 7,
data: &module_bytes[r.range()],
});
}
Payload::StartSection { range, .. } => {
module.section(&RawSection {
id: 8,
data: &module_bytes[range],
});
}
Payload::ElementSection(r) => {
module.section(&RawSection {
id: 9,
data: &module_bytes[r.range()],
});
}
Payload::DataCountSection { range, .. } => {
module.section(&RawSection {
id: 12,
data: &module_bytes[range],
});
}
Payload::CodeSectionStart { .. } => {
if let Some(c) = code.take() {
module.section(&c);
}
}
Payload::CodeSectionEntry(_) => {}
Payload::DataSection(r) => {
module.section(&RawSection {
id: 11,
data: &module_bytes[r.range()],
});
}
Payload::CustomSection(r) => {
module.section(&RawSection {
id: 0,
data: &module_bytes[r.range()],
});
}
_ => {}
}
}
module.section(&CustomSection {
name: "digstore.obf".into(),
data: OBFUSCATION_MARKER.into(),
});
Ok(module.finish())
}
#[cfg(all(test, not(target_arch = "wasm32")))]
mod tests {
use super::*;
use wasmparser::{Validator, WasmFeatures};
fn template() -> Vec<u8> {
crate::imp::compiler::template::baked_template_bytes().to_vec()
}
fn count_nops(module_bytes: &[u8]) -> usize {
let mut count = 0usize;
for payload in Parser::new(0).parse_all(module_bytes) {
if let Payload::CodeSectionEntry(body) = payload.unwrap() {
let mut reader = body.get_operators_reader().unwrap();
while !reader.eof() {
if matches!(reader.read().unwrap(), wasmparser::Operator::Nop) {
count += 1;
}
}
}
}
count
}
fn count_i32_adds(module_bytes: &[u8]) -> usize {
let mut count = 0usize;
for payload in Parser::new(0).parse_all(module_bytes) {
if let Payload::CodeSectionEntry(body) = payload.unwrap() {
let mut reader = body.get_operators_reader().unwrap();
while !reader.eof() {
if matches!(reader.read().unwrap(), wasmparser::Operator::I32Add) {
count += 1;
}
}
}
}
count
}
fn count_i32_consts(module_bytes: &[u8]) -> usize {
let mut count = 0usize;
for payload in Parser::new(0).parse_all(module_bytes) {
if let Payload::CodeSectionEntry(body) = payload.unwrap() {
let mut reader = body.get_operators_reader().unwrap();
while !reader.eof() {
if matches!(
reader.read().unwrap(),
wasmparser::Operator::I32Const { .. }
) {
count += 1;
}
}
}
}
count
}
fn count_function_bodies(module_bytes: &[u8]) -> usize {
let mut count = 0usize;
for payload in Parser::new(0).parse_all(module_bytes) {
if let Payload::CodeSectionEntry(_) = payload.unwrap() {
count += 1;
}
}
count
}
#[test]
fn obfuscated_module_is_valid_wasm() {
let m = template();
let o = obfuscate(&m).expect("obfuscate ok");
let mut v = Validator::new_with_features(WasmFeatures::default());
v.validate_all(&o).expect("valid");
}
#[test]
fn obfuscation_inserts_real_nops_into_code_section() {
let m = template();
let before = count_nops(&m);
let o = obfuscate(&m).expect("ok");
let after = count_nops(&o);
assert!(
after > before,
"obfuscation must insert real nops (before={before}, after={after})"
);
}
#[test]
fn obfuscation_performs_real_instruction_substitution() {
let m = template();
let consts_before = count_i32_consts(&m);
let adds_before = count_i32_adds(&m);
let o = obfuscate(&m).expect("ok");
let consts_after = count_i32_consts(&o);
let adds_after = count_i32_adds(&o);
assert!(
adds_after > adds_before,
"instruction substitution must add real i32.add ops \
(adds before={adds_before}, after={adds_after})"
);
assert!(
consts_after > consts_before,
"instruction substitution must add real i32.const ops \
(consts before={consts_before}, after={consts_after})"
);
}
#[test]
fn obfuscation_appends_bogus_dead_functions() {
let m = template();
let before = count_function_bodies(&m);
let o = obfuscate(&m).expect("ok");
let after = count_function_bodies(&o);
assert!(
after > before,
"obfuscation must append bogus dead functions (before={before}, after={after})"
);
}
#[test]
fn obfuscation_changes_code_section_structurally_not_just_marker() {
fn code_section_bytes(module_bytes: &[u8]) -> Vec<u8> {
for payload in Parser::new(0).parse_all(module_bytes) {
if let Payload::CodeSectionStart { range, .. } = payload.unwrap() {
return module_bytes[range].to_vec();
}
}
Vec::new()
}
let m = template();
let o = obfuscate(&m).expect("ok");
assert_ne!(
code_section_bytes(&m),
code_section_bytes(&o),
"obfuscation must transform the code section, not just append a marker"
);
}
#[test]
fn obfuscation_is_deterministic() {
let m = template();
let a = obfuscate(&m).expect("a");
let b = obfuscate(&m).expect("b");
assert_eq!(
a, b,
"obfuscation must be byte-identical for identical input"
);
}
#[test]
fn obfuscation_changes_the_bytes() {
let m = template();
let o = obfuscate(&m).expect("ok");
assert_ne!(o, m, "obfuscation must alter the module");
}
#[test]
fn obfuscation_preserves_exports() {
let m = template();
let o = obfuscate(&m).expect("ok");
let t = crate::imp::compiler::template::load_template(&o).expect("re-parse");
for name in crate::imp::compiler::template::REQUIRED_EXPORTS {
assert!(t.has_export(name), "lost export {name}");
}
}
}