Elements of Modern Malware Design, Part 1
This post marks the beginning of my blog and this series. To commemorate this wondrous occasion, I will start by discussing the importance of RAII-based allocators in modern malware design.
The motivation
A common shortcoming of C2 products is the potential for data leakage through memory allocations. The data from named pipes, sockets, and other sources has been stored on the heap in clear text for as long as these products have existed. The commonly adopted solution to this issue is heap encryption, which involves encrypting not only the reflective DLL/PIC during sleep but also the implant and/or process heap.
It is important to note that the heap manager does not zero out the memory when a heap allocation is freed. Instead, the memory is marked as available. Consequently, data that has been freed remains susceptible to compromise until overwritten by another allocation.
Out of sight, out of mind
To ensure that data lives on the heap for as short a time as possible, we can use a RAII-based allocator, where the destructor zeroes the allocation before it is freed.
The example below showcases the Buffer
class, which employs RtlAllocateHeap
to allocate memory on an implant-created heap ImplantHeap
. When this class goes out of scope and the destructor is called, a memset
call guarantees the absence of memory leaks. To allow the potential of this method to shine through, I have also implemented a resize
function member that uses RtlReAllocateHeap
to resize the allocation.
template <typename Type>
class Buffer
{
public:
Buffer(IN CONST UINT InitialSize) : Size{ InitialSize }, Allocation{ static_cast<Type>(RtlAllocateHeap(ImplantHeap, HEAP_ZERO_MEMORY, Size)) }
{
if(Allocation == nullptr)
{
throw STATUS_INSUFFICIENT_RESOURCES;
}
}
// We need a custom copy constructor to prevent double frees.
Buffer(CONST Buffer& Copy) : Size{ Copy.Size }, Allocation{ static_cast<Type>(RtlAllocateHeap(ImplantHeap, HEAP_ZERO_MEMORY, Size)) }
{
if(Allocation == nullptr)
{
throw STATUS_INSUFFICIENT_RESOURCES;
}
__builtin_memcpy(Allocation, Copy.Allocation, Size);
}
CONST Type& data() CONST
{
return Allocation;
}
CONST UINT& size() CONST
{
return Size;
}
BOOLEAN resize(IN CONST UINT NewSize)
{
PVOID NewAllocation{ RtlReAllocateHeap(ImplantHeap, HEAP_ZERO_MEMORY, Allocation, NewSize) };
if(NewAllocation == nullptr)
{
return FALSE;
}
Allocation = static_cast<Type>(NewAllocation);
Size = NewSize;
return TRUE;
}
~Buffer()
{
__builtin_memset(Allocation, NULL, Size);
RtlFreeHeap(ImplantHeap, HEAP_ZERO_MEMORY, Allocation);
}
private:
UINT Size { NULL };
Type Allocation{ nullptr };
};
A more elegant solution would be to create a custom allocator class and use standard library containers.
template <class Type>
class CustomAllocator
{
public:
typedef Type value_type;
typedef Type* pointer;
typedef Type& reference;
typedef CONST Type* const_pointer;
typedef CONST Type& const_reference;
CustomAllocator() = default;
template<class U> explicit CustomAllocator(CONST CustomAllocator<U>&) noexcept {}
template<class U> BOOLEAN operator == (CONST CustomAllocator<U>&) CONST noexcept
{
return TRUE;
}
template<class U> BOOLEAN operator != (CONST CustomAllocator<U>&) CONST noexcept
{
return FALSE;
}
[[nodiscard]] Type* allocate(IN SIZE_T Size) CONST;
STATIC VOID deallocate(IN Type* Allocation, IN SIZE_T Size) noexcept;
};
template <class Type>
Type* CustomAllocator<Type>::allocate(IN CONST SIZE_T Size) CONST
{
if (Size == 0)
{
return nullptr;
}
CONST AUTO Allocation = static_cast<Type*>(HeapAlloc(ImplantHeap, HEAP_ZERO_MEMORY, Size * sizeof(Type)));
if (Allocation == nullptr)
{
throw STATUS_INSUFFICIENT_RESOURCES; // Not ideal, but eh
}
return Allocation;
}
template<class Type>
VOID CustomAllocator<Type>::deallocate(IN Type* CONST Allocation, IN CONST SIZE_T Size) noexcept
{
__builtin_memset(Allocation, 0, Size * sizeof(Type));
HeapFree(ImplantHeap, 0, Allocation);
}
Subsequently,
template <typename Type>
using CustomVector = std::vector<Type, CustomAllocator<Type>>;
int main()
{
// 0x1000 bytes allocated on the implant heap, and nulled when the object goes out of scope
CustomVector<BYTE> Buffer(0x1000);
// Now you can use all std::vector methods without having to write them yourself
SIZE_T Size = Buffer.size();
}
What this does not cover are allocations made by external libraries such as WinHTTP, as they usually allocate on the process heap. To combat this, we can hook GetProcessHeap
and have it return the heap created by our implant. In addition, a hook on RtlFreeHeap
allows us to set that memory to zero; out of sight, out of mind.