247 lines
11 KiB
Plaintext
247 lines
11 KiB
Plaintext
|
Metasm, the Ruby assembly manipulation suite
|
||
|
============================================
|
||
|
|
||
|
* sample scripts in samples/ -- read comments at the beginning of the files
|
||
|
* all files are licensed under the terms of the LGPL
|
||
|
|
||
|
Author: Yoann Guillot <john at ofjj.net>
|
||
|
|
||
|
|
||
|
Basic overview:
|
||
|
|
||
|
Metasm allows you to interact with executables formats (ExeFormat):
|
||
|
PE, ELF, Mach-O, Shellcode, etc.
|
||
|
There are three approaches to an ExeFormat:
|
||
|
- compiling one up, from scratch
|
||
|
- decompiling an existing format
|
||
|
- manipulating the file structure
|
||
|
|
||
|
|
||
|
Assembly:
|
||
|
|
||
|
When compiling, you start from a source text (ruby String, consisting
|
||
|
mostly in a sequence of instructions/data/padding directive), that is parsed.
|
||
|
|
||
|
The string is handed to a Preprocessor instance (which handles #if, #ifdef,
|
||
|
#include, #define, /* */ etc, should be 100% compatible with gcc -E), which is
|
||
|
encapsulated in an AsmPreprocessor for assembler sources (to handles asm macro
|
||
|
definitions, equ and asm ';' comments).
|
||
|
The interface to do that is ExeFormat#parse(text[, filename, lineno]) or
|
||
|
ExeFormat.assemble (which calls .new, #parse and #assemble).
|
||
|
|
||
|
The (Asm)Preprocessor returns tokens to the ExeFormat, which parses them as Data,
|
||
|
Padding, Labels or parser directives. Parser directives always start with a dot.
|
||
|
They can be generic (.pad, .offset...) or ExeFormat-specific (.section,
|
||
|
.import, .entrypoint...). They are handled by #parse_parser_instruction().
|
||
|
If the ExeFormat does not recognize a word, it is handed to its CPU instance,
|
||
|
which is responsible for parsing Instructions (or raise an exception).
|
||
|
All those tokens are stored in one or more arrays in the @source attribute of
|
||
|
the ExeFormat (Shellcode's @source is an Array, for PE/ELF it is a hash
|
||
|
[section name] => [Array of parsed data])
|
||
|
Every immediate value can be an arbitrary Expression (see later).
|
||
|
|
||
|
You can then assemble the source to binary sections using ExeFormat#assemble.
|
||
|
|
||
|
Once the section binaries are available, the whole binary executable can be
|
||
|
written to disk using ExeFormat#encode_file(filename).
|
||
|
|
||
|
PE and ELF include an autoimport feature that allows automatic creation of
|
||
|
import-related data for known OS-specific functions (e.g. unresolved calls to
|
||
|
'strcpy' will generate data so that the binary is linked against the libc
|
||
|
library at runtime).
|
||
|
|
||
|
The samples/{exe,pe,elf}encode.rb can take an asm source file as argument
|
||
|
and compile it to a working executable.
|
||
|
|
||
|
The CPU classes are responsible for parsing and encoding individual
|
||
|
instructions. The current Ia32 parser uses the Intel syntax (e.g. mov eax, 42).
|
||
|
The generic parser recognizes labels as a string at the beginning of a line
|
||
|
followed by a colon (e.g. 'some_label:'). GCC-style local labels may be used
|
||
|
(e.g. '1:', refered to using '1b' (backward) or '1f' (forward) ; may be
|
||
|
redefined as many times as needed.)
|
||
|
Data are specified using 'db'-style notation (e.g. 'dd 42h', 'db "blabla", 0')
|
||
|
See samples/asmsyntax.rb
|
||
|
|
||
|
|
||
|
EncodedData:
|
||
|
|
||
|
In Metasm all binary data is stored as an EncodedData.
|
||
|
EncodedData has 3 main attributes:
|
||
|
- #data which holds the raw binary data (generally a ruby String, but see
|
||
|
VirtualString)
|
||
|
- #export which is a hash associating an export name (label name) to an offset
|
||
|
within #data
|
||
|
- #reloc which is a hash whose keys are offsets within #data, and whose values
|
||
|
are Relocation objects.
|
||
|
A Relocation object has an endianness (:little/:big), a type (:u32 for unsigned
|
||
|
32bits) and a target (the intended value stored here).
|
||
|
The target is an arbitrary arithmetic/logic Expression.
|
||
|
|
||
|
EncodedData also has a #virtsize (for e.g. .bss sections), and a #ptr (internal
|
||
|
offset used when decoding things)
|
||
|
|
||
|
You can fixup an EncodedData, with a Hash variable name => value (value should
|
||
|
be an Expression or a numeric value). When you do that, each relocation's target
|
||
|
is bound using the binding, and if the result is calculable (no external variable
|
||
|
name used in the Expression), the result is encoded using the relocation's
|
||
|
size/sign/endianness information. If it overflows (try to store 128 in an 8bit
|
||
|
signed relocation), an EncodeError exception is raised. Use the :a32 type to
|
||
|
allow silent overflow truncating.
|
||
|
If the relocation's target is not numeric, the target is unchanged if you use
|
||
|
EncodedData#fixup, or it is replaced with the bound target with #fixup! .
|
||
|
|
||
|
|
||
|
Disassembly:
|
||
|
|
||
|
The disassembler needs a decoded ExeFormat (to be able to say what data is at
|
||
|
which virtual address) and an entrypoint (a virtual address or export name).
|
||
|
It can then start to disassemble instructions. When it encounters an
|
||
|
Opcode marked as :setip, it asks the CPU for the jump destination (an
|
||
|
Expression that may involve register values, for e.g. jmp eax), and backtraces
|
||
|
instructions until it finds the numeric value.
|
||
|
|
||
|
The walking generates an InstructionBlock graph. Each block holds a list of
|
||
|
DecodedInstruction, and pointers to the next/previous block.
|
||
|
|
||
|
The disassembler also traces data accesses by instructions, and stores Xrefs
|
||
|
every time.
|
||
|
The backtrace parameters can be tweaked, and may be specifically changed for
|
||
|
:r/:w backtraces (instruction memory xrefs, using #backtrace_maxblocks_data)
|
||
|
When an Expression is backtracked, each walked block is marked so that loops
|
||
|
are detected, and so that if a new code path is found to an existing block,
|
||
|
backtraces can be resumed using this new path.
|
||
|
|
||
|
The disassembler stores decoded instructions in #decoded, which is a hash
|
||
|
associating an address (Integer or Expression, #normalized) to a
|
||
|
DecodedInstruction.
|
||
|
|
||
|
The disassembler makes very few assumptions, and in particular does not
|
||
|
suppose that functions will return ; they will only if the backtrace of the
|
||
|
'ret' instructions is conclusive. This is quite powerful, but also implies
|
||
|
that any error in the backtracking process can lead to a full stop ; and also
|
||
|
means that the disassembler is quite slow.
|
||
|
|
||
|
When a subfunction is found, a special DecodedFunction is created, which holds
|
||
|
a summary of the function's effects (like a DecodedInstruction on steroids).
|
||
|
This allows the backtracker to 'step over' subfunctions, which greatly improves
|
||
|
speed. The DecodedFunctions may be callback-based, to allow a very dynamic
|
||
|
behaviour.
|
||
|
External function calls create dedicated DecodedFunctions, which holds some
|
||
|
API information (e.g. stack fixup information, basic parameter accesses...)
|
||
|
This information may be derived from a C header parsed beforehand.
|
||
|
If no C function prototype is available, a special default entry is used,
|
||
|
which supposes that the function has a standard ABI.
|
||
|
|
||
|
Ia32 implements a specific :default entry, which handles automatic stack fixup
|
||
|
resolution, by assuming that the last 'call' instruction returns. This may lead
|
||
|
to unexpected results, for maximum accuracy a C header holding information for
|
||
|
all external functions is recommanded (see samples/factorize-headers-peimports
|
||
|
for a script to generate such a header from a full Visual Studio installation
|
||
|
and the program import directory).
|
||
|
|
||
|
Ia32 also implements a specific GetProcAddress/dlsym callback, that will
|
||
|
yield the correct return value if the parameters can be backtraced.
|
||
|
|
||
|
The scripts implementing a full disassembler are samples/disassemble{-gtk}.rb
|
||
|
See the comments for the GTK key bindings.
|
||
|
|
||
|
|
||
|
ExeFormat manipulation:
|
||
|
|
||
|
You can encode/decode an ExeFormat (ie decode sections, imports, headers etc)
|
||
|
|
||
|
Constructor: ExeFormat.decode_file(str), ExeFormat.decode_file_header(str)
|
||
|
Methods: ExeFormat#encode_file(filename), ExeFormat#encode_string
|
||
|
|
||
|
PE and ELF files have a LoadedPE/LoadedELF counterpart, that is able to work
|
||
|
memory-mmaped versions of those formats (for e.g. debugging running processes.)
|
||
|
|
||
|
|
||
|
VirtualString:
|
||
|
|
||
|
A VirtualString is a String-like object: you can read and may rewrite slices of
|
||
|
it. It can be used as EncodedData#data, and thus allows virtualization
|
||
|
of most Metasm algorithms.
|
||
|
You cannot change a VirtualString length.
|
||
|
Taking a slice of a VirtualString can return either a String (for small sizes)
|
||
|
or another VirtualString (a 'window' into the other). You can force getting a
|
||
|
small VirtualString using the #dup(offset, length) method.
|
||
|
Any unimplemented method called on it is forwarded to a frozen String which is
|
||
|
a full copy of the VirtualString (should be avoided if possible, the underlying
|
||
|
string may be very big & slow to access).
|
||
|
|
||
|
There are currently 3 VirtualStrings implemented:
|
||
|
- VirtualFile, whichs loads a file by page-sized chunks on demand,
|
||
|
- WindowsRemoteString, which maps another process' virtual memory (uses the
|
||
|
windows debug api through WinDbg)
|
||
|
- LinuxRemoteString, which maps another process' virtual memory (need ptrace
|
||
|
rights, memory reading is done using /proc/pid/mem)
|
||
|
|
||
|
The Win/Lin version are quite powerful, and allow things like live process
|
||
|
disassembly/patching easily (use LoadedPE/LoadedELF as ExeFormat)
|
||
|
|
||
|
|
||
|
Debugging:
|
||
|
|
||
|
Metasm includes a few interfaces to allow live debugging.
|
||
|
The WinOS and LinOS classes offer access to the underlying OS processes (e.g.
|
||
|
OS.current.find_process('foobar') will retrieve a running process with foobar
|
||
|
in its filename ; then process.mem can be used to access its memory.)
|
||
|
|
||
|
The Windows and Linux debugging APIs (x86 only) have a basic ruby interface
|
||
|
(PTrace32, extended in samples/rubstop.rb ; and WinDBG, a simple mapping of the
|
||
|
windows debugging API) ; those will be more worked on/integrated in the future.
|
||
|
|
||
|
A linux console debugging interface is available in samples/lindebug.rb ; it
|
||
|
uses a SoftICE-like look and feel.
|
||
|
This interface can talk to a gdb-server through samples/gdbclient.rb ; use
|
||
|
[udp:]<host:port> as target.
|
||
|
|
||
|
The disassembler scripts allow live process interaction by using as target
|
||
|
'live:<pid or part of filename>'.
|
||
|
|
||
|
|
||
|
C Parser:
|
||
|
|
||
|
Metasm includes a hand-written C Parser.
|
||
|
It handles all the constructs i am aware of, except for the '[1..2]' array
|
||
|
initializer specificator, and hex floats:
|
||
|
- static const L"bla"
|
||
|
- variable arguments
|
||
|
- incomplete types
|
||
|
- __attributes__(()), __declspec()
|
||
|
- #pragma once
|
||
|
- #pragma pack
|
||
|
- C99 declarators
|
||
|
- Nested functions
|
||
|
- __int8 etc native types
|
||
|
- Label addresses (&&label)
|
||
|
Also note that all those things are parsed, but most of them will fail to
|
||
|
compile on the Ia32 backend (the only one implemented so far.)
|
||
|
|
||
|
When you parse a C String using C::Parser.parse(text), you receive a Parser
|
||
|
object. It holds a #toplevel fields, which is a C::Block, and holds #structs,
|
||
|
#symbols and #statements. The top-level functions are found in the #symbol hash
|
||
|
whose keys are the symbol names, associated to a C::Variable object holding
|
||
|
the functions. The function parameter/attributes are accessible through
|
||
|
func.type, and the code is in func.initializer, which is itself a C::Block.
|
||
|
Under it you'll find a tree-like structure of C::Statements (If, While, Asm...)
|
||
|
|
||
|
A C::Parser may be #precompiled to transform it into a simplified version that
|
||
|
is easier to compile: typedefs are removed, control sequences are transformed
|
||
|
in if () goto ; etc.
|
||
|
|
||
|
The prefered way to create a C::Parser is through a CPU#new_cparser, that
|
||
|
will correctly initialize the type sizes (is long 4 or 8 bytes? etc) ; and
|
||
|
may define preprocessor macros needed to correctly parse standard headers.
|
||
|
Vendor-specific headers may need to use either #pragma prepare_visualstudio
|
||
|
(to parse the Microsoft Visual Studio headers) or prepare_gcc (for gcc), the
|
||
|
latter may be auto-detected (or may not).
|
||
|
Vendor headers tested are VS2003 (incl. DDK) and gcc4 ; ymmv.
|
||
|
|
||
|
Currently the CPU#compilation of a C code will generate an asm source (text),
|
||
|
which may then be parsed & assembled to binary code.
|
||
|
|
||
|
See ExeFormat#compile_c, and samples/{exe,pe,elf}encode.rb
|
||
|
|