Chapter I: General concepts
void* operator new(size_t size);
§§ Debug builds usually fill the allocated space with a predefined pattern.
(i.e: 0xbaadf00d for HeapAlloc())
§§ Initialization is deferred until a constructor is called.
To invoke a method, an instance should be explicitly passed as 1ˢᵗ argument.
A* a; // Allocate an A object and invoke // the default constructor A::A(). a = new A(); delete a; // Only reserve enough space. a = static_cast<A*>(operator new(sizeof(A)));
Now, classes with virtual methods increase in size and point to a Dispatch table.
class A; class B: public A; + - - - - - + - - - - - - - A : | Dispatch* | ...Properties + - - - - - + - - - - - - - B _/ Replace? virtual table and append its own properties.
A pointer to a constructor can't be obtained normally, explicitly calling
a constructor(A::A) is not possible, but placement new can instantiate
a given address instead of allocating memory.
int main() { A* a; a = static_cast<A*>(operator new(sizeof(A))); // Placement new would call the // constructor on our behalf. new(a) A; return 0; }
// g++ bad.cxx -o bad -fpermissive -w // Relative Call #define CALLSZ 5 #define CALLIN 1 // void (*Constructor)(void) = nullptr; void locateConstructor() { // This procedure is nasty, gets the // return-address, acquiring (&A::A) // by parsing a CALL instruction. char* ret; ret = __builtin_return_address(1); ret = __builtin_extract_return_addr(ret); ret += *(uint32_t*)(ret - CALLSZ + CALLIN); if (Constructor) return; Constructor = *(void(**)())&ret; }
| main() | | A::A() | | locateConstructor() | | CALL A::A() | | CALL locateConstructor() | | ... | * Frame(1) * Frame(0) / \__ _ _ _ __ _ __ _ __ _ _ __ __ _ _ ___/ - - - - - - - - - - - - - -- - - -
asm("movq %0, %%rcx\r\n" :: "r" (a)); ::Constructor();
To overwrite virtual methods, make an object point to a different table.
There's a catch however, a legitimate table is usually stored on compile
time in a read-only section, this can be used to detect modification.
A workaround is to change page protection (VirtualProtect/mprotect):
void(**f)(void*); f = (void(**)(void*)) VirtualAlloc(nullptr, sizeof(void*), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); f[0] = (void(*)(void*)) &Z::m; VirtualProtect(f, sizeof(void*), PAGE_READONLY, nullptr);
Chapter II: Inheritance
To avoid redundancy and code-duplication, it is permitted to inherit
from a base-class and share its functionality and properties:
class Z { protected: int z; public: Z() = default; virtual void m() {} }; // class A: public Z { private: int a; public: A() {} void m() override {} };
+ - - - - - + - + Z | Dispatch* | z | + - - - - - + - + - + A | Dispatch* | z | a | A::A() // Extended version Z::Z() Dispatch* modification ...inline A::A()
Multiple-inheritance is possible, but can lead to ambiguity for virtual
functions (i.e: Diamond inheritance).
class Z; class A: public Z; class B: public Z; class C: public A, public B; ( Z ) / \ ( A ) ( B ) \ / ( C )
C::C() // Extended version A::A() // new(c) A; B::B() // new((uint8_t*)c + sizeof(A)) B; Dispatch* modification ...inline C::C()
The resulting instance holds 2 distinct pointers and duplicate Z members:
- (C Dispatch*) at 0
- (B Dispatch*) at 16
std::cout << (A*)c << '\n'; // 0x25C2560 std::cout << (B*)c << '\n'; // 0x25C2570 std::cout << std::flush;
Chapter III: Polymorphism
The superclass is part of a subclass (i.e: every A is also a Z), the
dispatch table remains untouched after casting, and will still hold
virtual methods of the subclass (Upcasting).
A* a = new A; Z* z = a;
For non-virtual methods, they'll be taken from the superclass, and
it's also possible to do the reverse operation (Downcasting) but it
can lead to type-confusion bugs.
Chapter IV: Implementation
To locate constructors and examine their hierarchy, this modules will be used:
- pefile (for parsing the PE file)
- capstone (for disassembling instructions)
- networkx (for graph construction)
- struct (for packing/unpacking binary data)
The first step would be obtaining addresses to profile, either with an incremental-
search for function prologue (₃₂&H558BEC, ₆₄&H554889E5) or providing debug
symbols during compilation (i.e: MinGW in .pdata).
import matplotlib.pyplot as plt import networkx import capstone import pefile import struct #### //////////////////////////////////// ### Locating virtual constructors in a PE ### file plus visualizing their hierarchy ### with a planar graph. //////////////// #### //////////////////////////////////// pe = pefile.PE('Virtual.exe') fileData = pe.get_memory_mapped_image() sectionIndex = next( i for i, section in enumerate(pe.sections) if b'.pdata' in section.Name) pdataSection = pe.sections[sectionIndex] pdataStart = pdataSection.VirtualAddress pdataEnd = pdataStart + pdataSection.Misc_VirtualSize pdataContent = fileData[pdataStart:pdataEnd] cs = capstone.Cs(capstone.CS_ARCH_X86, capstone.CS_MODE_64) cs.detail = True ### //////////
#### ///////// def badSignature(sequence, prologue): return int.from_bytes(sequence, byteorder='big', signed=False) != prologue
def locateLea(instruction): global intermediateReg operands = instruction.operands if intermediateReg or \ operands[1].mem.base != capstone.x86.X86_REG_RIP: return intermediateReg = operands[0].reg def locateVirtual(instruction): global thisOffset, intermediateReg, isConstructor operands = instruction.operands if thisOffset < 0 or \ intermediateReg is None or \ operands[0].type != capstone.x86.X86_OP_MEM or \ operands[1].type != capstone.x86.X86_OP_REG: return False isConstructor = int.__eq__(operands[1].reg, intermediateReg) return True def locateMethod(instruction): global thisOffset operands = instruction.operands if operands[0].type != capstone.x86.X86_OP_MEM or \ operands[1].type != capstone.x86.X86_OP_REG: return False destination = operands[0].mem.base source = operands[1].reg if destination != capstone.x86.X86_REG_RBP: return False thisOffset = operands[0].mem.disp return True def fixedAddress(pe, rva): return hex(pe.OPTIONAL_HEADER.ImageBase + rva) ### //////////
#### ///////// constructorDict = dict() for i in range(0, len(pdataContent), 12): functionAddress = struct.unpack('<I', pdataContent[i:i+4])[0] functionAlign = struct.unpack('<I', pdataContent[i+4:i+8])[0] functionUnwind = struct.unpack('<I', pdataContent[i+8:i+12])[0] functionData = fileData[functionAddress:] if badSignature(functionData[:4], 0x554889E5): continue ### intermediateReg = None thisOffset = -1 isConstructor = False callTargets = list() ### for instruction in cs.disasm(functionData, 0): if str.__eq__(instruction.mnemonic, 'ret'): break elif str.__eq__(instruction.mnemonic, 'lea'): locateLea(instruction) elif str.__eq__(instruction.mnemonic, 'mov'): locateMethod(instruction) locateVirtual(instruction) elif str.__eq__(instruction.mnemonic, 'call'): relativeOffset = instruction.operands[0].imm relativeOffset += functionAddress callTargets.append(fixedAddress(pe, relativeOffset)) ### if isConstructor: constructorAddress = fixedAddress(pe, functionAddress) constructorDict[constructorAddress] = callTargets Graph = networkx.DiGraph(constructorDict) networkx.draw_planar(Graph, node_color = 'w', node_shape = 'd', alpha = 0.4, node_size = 2048, with_labels = True) Figure = plt.gcf() Figure.canvas.manager.set_window_title('Inheritance graph') plt.show()
Yielding the following as a result:
No comments:
Post a Comment