Cross-platform C++ library for patching and obfuscating code in compiled binaries. Code and binary parsing are logically separated in order to support as many binary format + architecture combinations as possible.
Stitch currently works with the following binary formats:
- PE
- Shellcode
Stitch's x86 capability is heavily built on top of zasm.
Each X86Function
provides a zasm::Assembler
instance that is pre-populated
with the original instructions as they were on disk. The original instructions can
be accessed with X86Function::GetOriginalCode
, and their positions within the assembler
can be accessed with X86Inst::GetPos
.
git clone https://github.com/badhive/stitch
cd stitch
cmake -DCMAKE_BUILD_TYPE=Release -B build .
cmake --build build/ --target stitch
Stitch allows for functions to be safely edited post-compilation. Operands for
position-relative instructions, such as jmp
s, are replaced with labels so that
the code can be easily modified and serialised without worry.
The main and intended use for Stitch is code obfuscation on a binary level. It handles the tedious task of injecting new data into files, so that operators can focus on more complex obfuscation techniques, including but not limited to VM-based obfuscation. Here's an example program that applies basic obfuscation (in the form of opaque predicates) to a function (specified by its absolute address).
#include "stitch/binary/pe.h"
#include "stitch/target/x86.h"
const std::vector regs = {
zasm::x86::rdi,
zasm::x86::rsi,
zasm::x86::rcx,
zasm::x86::rdx,
zasm::x86::r8,
zasm::x86::r9,
zasm::x86::r10,
};
auto& getRandomReg() {
auto& reg = regs[rand() % regs.size()];
return reg;
}
int main() {
srand(time(nullptr));
stitch::PE pe("pe_branching.bin");
const auto code = pe.OpenCode();
constexpr stitch::RVA fn_main = 0x00000001400015A1;
const auto fn = dynamic_cast<stitch::X86Function*>(code->EditFunction(
fn_main, ""));
fn->Instrument([](stitch::X86Function* fo, zasm::x86::Assembler& as) {
for (const stitch::X86Inst& inst : fo->GetOriginalCode()) {
const bool to_insert = rand() % 2;
const zasm::InstructionDetail& detail = inst.RawInst();
if (detail.getMnemonic() != zasm::x86::Mnemonic::Ret && to_insert) {
zasm::Label last_label = as.createLabel();
const auto& reg = getRandomReg();
as.setCursor(inst.GetPos());
as.pushf();
as.push(reg);
as.xor_(reg, zasm::Imm(rand()));
as.js(last_label);
as.jns(last_label);
as.bind(last_label);
as.pop(reg);
as.popf();
}
}
});
pe.SaveAs("target/pe_opaque_predicates.bin");
pe.Close();
}
Here's the function before the obfuscation is applied:
...and after:
- When the function address is supplied to
Code::EditFunction
, Stitch begins parsing the code at that address, while also doing basic control flow tracking by splitting it up into basic blocks - The code is parsed into a
zasm::x86::Assembler
instance and intra-function references are replaced with labels X86Function::Instrument
allows us to modify the code, making use of an assembler that has been populated with the original codeX86Function::Finish
updates PE relocation info, assembles and writes the code to a new section with a 16-byte alignment. The memory ranges previously occupied by the function are patched out and a jmp to the new code is inserted in its place.