W1tn3ss
binary instrumentation, analysis, and patching framework
Install / Use
/learn @redthing1/W1tn3ssREADME
w1tn3ss
a cross-platform framework for binary analysis/instrumentation.
fully supports linux, macos, windows across x86, x64, and arm64.
built to enable flexible auditing, tracing, analysis, instrumentation of binaries across platforms.
highlights:
- record-and-replay (experimental): record the execution of a process, and replay the trace on any platform; with full time-travel
- code coverage: automatic gathering of code coverage/block hits for any instrumentable binary
- declarative patching: scriptable, ergonomic, declarative binary patching
features
- dynamic instrumentation tracers
- code coverage:
w1cov - record and replay
w1rewind/w1replay - call tracing:
w1xfer - scripting:
w1script - memory:
w1mem - process dump:
w1dump
- code coverage:
- signature scanning and binary patching (
p1ll/p1llx) - scriptable with js/lua
- reusable binary instrumentation infra
- cross-platform library injection:
w1nj3ct - cross-platform function hooking:
w1h00k - calling convention/abi modeling for many platforms
- cross-platform library injection:
build
initialize submodules:
git submodule update --init --recursive
build:
cmake -G Ninja -B build-release -DCMAKE_BUILD_TYPE=Release
cmake --build build-release --parallel
see doc/build.md for features and platform-specific instructions.
w1tool guide
this is a brief guide to using w1tool, a ready-to-use command-line for running tracers
coverage & tracing
code coverage helps us learn what code in a program gets run and how often. the w1cov tracer is purpose-built to collect detailed code coverage information, with only modest performance overhead.
the drcov format is ideal for coverage tracing, as it includes metadata about loaded modules. w1cov also supports collecting data in a superset of the drcov format, which also records hit counts of coverage units. this can be useful to record the execution frequency of a block.
my other project covtool provides a powerful tool for viewing, editing, and browsing coverage traces.
collect coverage in drcov format using w1cov:
# macos/linux
./build-release/bin/w1tool cover -s ./build-release/bin/samples/programs/simple_demo
# windows
.\build-release\bin\w1tool.exe cover -s .\build-release\bin\samples\programs\simple_demo.exe
output will resemble:
[w1cov.preload] [inf] coverage data export completed output_file=simple_demo_coverage.drcov
[w1cov.tracer] [inf] coverage collection completed coverage_units=59 modules=50 total_hits=71
the default block tracing mode is significantly more efficient than per-instruction tracing as it requires less frequent callback interruptions. however, qbdi detects basic blocks dynamically, so recorded block boundaries may differ from those detected by static analysis tools. this usually isn't an issue, as you can script your disassembler to fix any discrepancies when marking basic block coverage.
you can also trace coverage in the same drcov format by passing --inst to cover, which will use instruction callbacks.
for a more primitive form of tracing which simply records the instruction pointer, use w1trace:
# macos/linux
./build-release/bin/w1tool tracer -n w1trace -c output=simple_demo_trace.txt -s ./build-release/bin/samples/programs/simple_demo
# windows
.\build-release\bin\w1tool.exe tracer -n w1trace -c output=simple_demo_trace.txt -s .\build-release\bin\samples\programs\simple_demo.exe
real-time api call analysis
often it is valuable to learn what system library apis a program calls. for example, we can learn a lot about the behavior of a program by observing its calls to libc. the w1xfer tracer, powered by qbdi's ExecBroker mechanism, can intercept and observe calls from and returns back to instrumented code.
in addition to detecting calls crossing the instrumentation boundary, w1xfer resolves the symbols of these calls, and extracts function arguments based on platform-specific calling convention models. this allows for very rich interception and tracing of the arguments and return values of common library apis.
trace api calls in real time with w1xfer:
# macos/linux
./build-release/bin/w1tool -v tracer -n w1xfer -c analyze_apis=true -c output=test_transfers.jsonl -s ./build-release/bin/samples/programs/simple_demo
# windows
.\build-release\bin\w1tool.exe -v tracer -n w1xfer -c analyze_apis=true -c output=test_transfers.jsonl -s .\build-release\bin\samples\programs\simple_demo.exe
output will resemble:
registered platform conventions platform=aarch64 count=1
...
call=malloc(size=64) category=Heap module=libsystem_malloc.dylib
return=malloc() = 0x600003b982c0 raw_value=105553178755776 module=libsystem_malloc.dylib
...
call=puts(s="simple demo finished") category=I/O module=libsystem_c.dylib
simple demo finished
return=puts() = 10 raw_value=10 module=libsystem_c.dylib
call=intercept_exit(?) category= module=w1xfer_qbdipreload.dylib
as seen above, this can successfully intercept calls to many common libc apis!
scripting
w1tn3ss supports writing custom tracers in luajit through the w1script tracer.
scripts can hook various callbacks and directly access vm state, registers, and memory.
here's a simple instruction tracer:
local instruction_count = 0
local tracer = {}
local function on_instruction(vm, gpr, fpr)
instruction_count = instruction_count + 1
local pc = w1.reg.pc(gpr) or 0
local disasm = w1.inst.disasm(vm) or "<unknown>"
w1.log.info(w1.util.format_address(pc) .. ": " .. disasm)
return w1.enum.vm_action.CONTINUE
end
function tracer.init()
w1.on(w1.event.INSTRUCTION_PRE, on_instruction)
end
function tracer.shutdown()
w1.log.info("traced " .. instruction_count .. " instructions")
end
return tracer
run it:
# macos/linux
./build-release/bin/w1tool tracer -n w1script -c script=./scripts/w1script/instruction_tracer.lua -s ./build-release/bin/samples/programs/simple_demo
# windows
.\build-release\bin\w1tool.exe tracer -n w1script -c script=./scripts/w1script/instruction_tracer.lua -s .\build-release\bin\samples\programs\simple_demo.exe
this will produce a trace of disassembled instructions as they are executed.
see the example scripts, which demonstrate memory tracing, coverage collection, and api interception.
record and replay
the duo w1rewind + w1replay provides record/replay functionality.
traces can be captured with various levels of detail, trading performance/size for fidelity.
record a rewind trace:
./build-release/bin/w1tool rewind -s -o /tmp/trace.w1r -- ./build-release/bin/samples/programs/simple_demo
inspect:
./build-release/bin/w1replay inspect -t /tmp/trace.w1r --thread 1 --count 10
tips:
- to increase trace detail, use
--flow instruction --reg-deltas --mem-access reads_writes --mem-values - to capture stack bytes, use
--stack-window frame --stack-snapshot-interval 1 - run a gdb rsp server with
w1replay server -t <trace> --gdb 127.0.0.1:5555
p1ll guide
patching binaries is an essential part of a reversing or cracking workflow. p1ll is a portable signature scanning and patching library that can patch binaries statically on disk or dynamically in memory.
p1llx provides a nifty command line to run and inspect patches.
static patching
patch a binary on disk:
./build-release/bin/p1llx -vv cure -c ./patch_script.lua -i ./target_binary -o ./patched_binary
on macos, statically patched binaries require codesigning:
codesign -fs - ./patched_binary
the d0ct0r.py script provides intelligent patch development features; it automatically backs up the input file, and handles permissions and codesigning.
dynamic patching
patch a running process in memory:
# spawn new process
./build-release/bin/p1llx -vv poison -c ./patch_script.lua -s ./target_binary
# attach to existing process
./build-release/bin/p1llx -vv poison -c ./patch_script.lua -n target_binary
patch scripts
p1ll uses scripts to define signatures and patching. this is designed to be used through the declarative auto_cure api, which can define platform-specific signatures and patches.
example patch script:
-- validation signature
local SIG_DEMO_NAME = p1.sig(p1.str2hex("Demo Program"))
-- unique signature for this string
local SIG_ANGERY = p1.sig(p1.str2hex("Angery"), {single = true})
-- find a function by signature (optional module filter)
local SIG_CHECK_LICENSE_WIN_X64 = p1.sig([[
4885c0 -- test rax, rax
74?? -- je <offset>
b001 -- mov al, 1
]], {filter = "demo_program"})
-- patch: fall through the check by nopping it
local FIX_CHECK_LICENSE_WIN_X64 = [[
??????
9090 -- nop nop
????
]]
local meta = { -- declarative patch
name = "demo_program",
platforms = {"windows:x64"}, -- platforms supported by this patch
sigs = {
["*"] = { -- wildcard signatures are checked on all platforms
SIG_DEMO_NAME,
SIG_ANGERY,
}
},
patches = {
["windows:x64"] = { -- patch only on windows:x64
p1.patch(SIG_CHECK_LICENSE_WIN_X64, 0, FIX_CHECK_LICENSE_WIN_X64)
},
["*"] = { -- wildcard patches are used on all platforms
p1.patch(SIG_ANGERY, 0, p1.str2hex("Happey"))
}
}
}
function cure()
return p1.auto_cure(meta)
end
key concepts:
p1.sig(): define byte patterns (with??for wildcards)p1.patch(): specify signature, offset, and replacementmetatable: organize sigs and patches by platform
optional python bindings are available under src/p1ll/bindings/python. see the python bindings guide and samples under scripts/python.
p1ll is an excellent and powerful tool for binary modification!
see the [guide](./doc/p1l
