Saturday 19 March 2022

Detecting Inheritance


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