This blog post is aimed at manipulating the PEB’s LoaderDataModule lists in Windows. These lists catalog all loaded libraries within a process, providing an avenue for detecting our loaded module. Notably, the current implementation is tailored for x86 architecture, with future plans for x64 compatibility.

Microsoft Windows PEB Documentation

Disclaimer

This is strictly intended for educational purposes and does not endorse the development of malicious software.

Windows Structs

To streamline development, comprehensive Windows structs have been sourced from undocumented.ntinternals.net, offering detailed insights into Windows internals.

Obtaining the PEB Struct

Accessing the PEB* structure is facilitated by the __readfsdword function from intrin.h. Invocation with an offset of 0x30 and appropriate casting yields the desired result.

1
auto pPeb = (CPEB*)__readfsdword(0x30);

Alternatively, inline assembly can achieve the same result for those averse to using MS intrinsics:

1
2
3
4
5
6
7
8
CPEB* pPeb;
__asm
{
    PUSH EAX;
    MOV EAX, DWORD PTR FS:[0x30];
    MOV pPeb, EAX;
    POP EAX;
}

Enumerating the Module List

The PEB encompasses three linked lists, each containing identical information but ordered differently. Concealing a module necessitates patching all three lists. Adjustments to the second and third lists involve subtracting sizeof(LIST_ENTRY) and sizeof(LIST_ENTRY) * 2 from the pointer to encompass the entire structure.

1
2
3
auto moduleList = pPeb->LoaderData->InLoadOrderModuleList;              // ptrOffset = 0
auto moduleList = pPeb->LoaderData->InMemoryOrderModuleList;            // ptrOffset = 8
auto moduleList = pPeb->LoaderData->InInitializationOrderModuleList;    // ptrOffset = 16

How to enumerate such a linked list:

1
2
3
4
for (auto pListEntry = moduleList.Flink; pListEntry->Flink != moduleList.Flink; pListEntry = pListEntry->Flink)
{
    auto pLdrDataEntry = (CLDR_MODULE*)((uintptr_t)pListEntry - ptrOffset);
}

Linked list structure as ASCII art:

1
2
3
                   +-------+ Flink -> +-------+ Flink -> +-------+ Flink -> ( Mod 1 )
                   | Mod 1 |          | Mod 2 |          | Mod 3 |
( Mod 3 ) <- Flink +-------+ <- Blink +-------+ <- Blink +-------+

Patching the Linked List

Patching the linked list involves manipulating the module’s pointers within the list. This process includes setting pointers to neighboring modules and ensuring coherence within the list structure.

1
2
3
pModule->x.Flink                                // get the next module (Module 3)
pModule->x.Flink->Blink                         // get the pointer to the previous module (Module 2)
pModule->x.Flink->Blink = pModule->x.Blink;     // set it to the blink of our main module, which points to Module 1

And now reverse (because its a double linked list)

1
2
3
pModule->x.Blink                                // get the previous module (Module 1)
pModule->x.Blink->Flink                         // get the pointer to the next module (Module 2)
pModule->x.Blink->Flink = pModule->x.Flink;     // set it to the flink of our main module, which points to Module 3

Repeat that for the other two linked lists and we’ve successfully patched it

1
2
3
4
5
6
7
8
pModule->InLoadOrderModuleList.Flink->Blink = pModule->InLoadOrderModuleList.Blink;
pModule->InLoadOrderModuleList.Blink->Flink = pModule->InLoadOrderModuleList.Flink;

pModule->InMemoryOrderModuleList.Flink->Blink = pModule->InMemoryOrderModuleList.Blink;
pModule->InMemoryOrderModuleList.Blink->Flink = pModule->InMemoryOrderModuleList.Flink;

pModule->InInitializationOrderModuleList.Flink->Blink = pModule->InInitializationOrderModuleList.Blink;
pModule->InInitializationOrderModuleList.Blink->Flink = pModule->InInitializationOrderModuleList.Flink;

Next Steps After Patching

While removal from the linked list conceals the module’s presence, detection via its PE Header remains possible. Erasure of the original list entry and the module’s PE Header is essential to eliminate traces of its existence.

How to erase the original list entry:

1
2
3
4
5
__stosb((unsigned char*)pModule, 0, sizeof(CLDR_MODULE));
// or
memset(pModule, 0, sizeof(CLDR_MODULE));
// or
ZermoMemory(pModule, sizeof(CLDR_MODULE))

How to erase the PE Header:

1
2
3
4
5
6
7
8
void* baseAddress = pModule->BaseAddress;
size_t headerSize = ((IMAGE_NT_HEADERS*)(baseAddress + (IMAGE_DOS_HEADER*)baseAddress)->e_lfanew)->OptionalHeader.SizeOfHeaders

__stosb((unsigned char*)baseAddress, 0, headerSize);
// or
memset(baseAddress, 0, headerSize);
// or
ZermoMemory(baseAddress, headerSize)

By erasing the old list entry and the module’s PE Header, the module’s name and identifying signatures are effectively gone, rendering it significantly more challenging to detect through traditional means such as PE Header signatures. However, it’s crucial to note that while the module itself may be concealed, the allocated memory space remains accessible. Thus, if the module contains recognizable code patterns or functionalities known to those seeking it, detection remains a possibility. Caution is advised when implementing such techniques, as skilled adversaries may still uncover concealed modules through thorough analysis.

Source Code

main.hpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
#ifndef _MAIN_H
#define _MAIN_H

#define NOMINMAX
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <intrin.h>
#include <psapi.h>

#include <iostream>
#include <iomanip>
#include <vector>

struct CUNICODE_STRING
{
    unsigned short Length;
    unsigned short MaximumLength;
    wchar_t* Buffer;
};

struct CPEB_LDR_DATA
{
    unsigned long Length;
    unsigned char Initialized;
    void* SsHandle;
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
};

struct CPEB
{
    unsigned char InheritedAddressSpace;
    unsigned char ReadImageFileExecOptions;
    unsigned char BeingDebugged;
    unsigned char Spare;
    void* Mutant;
    void* ImageBaseAddress;
    CPEB_LDR_DATA* LoaderData;
    void* ProcessParameters;
    void* SubSystemData;
    void* ProcessHeap;
    void* FastPebLock;
    void* FastPebLockRoutine;
    void* FastPebUnlockRoutine;
    unsigned long EnvironmentUpdateCount;
    void** KernelCallbackTable;
    void* EventLogSection;
    void* EventLog;
    void* FreeList;
    unsigned long TlsExpansionCounter;
    void* TlsBitmap;
    unsigned long TlsBitmapBits[0x2];
    void* ReadOnlySharedMemoryBase;
    void* ReadOnlySharedMemoryHeap;
    void** ReadOnlyStaticServerData;
    void* AnsiCodePageData;
    void* OemCodePageData;
    void* UnicodeCaseTableData;
    unsigned long NumberOfProcessors;
    unsigned long NtGlobalFlag;
    unsigned char Spare2[0x4];
    LARGE_INTEGER CriticalSectionTimeout;
    unsigned long HeapSegmentReserve;
    unsigned long HeapSegmentCommit;
    unsigned long HeapDeCommitTotalFreeThreshold;
    unsigned long HeapDeCommitFreeBlockThreshold;
    unsigned long NumberOfHeaps;
    unsigned long MaximumNumberOfHeaps;
    void*** ProcessHeaps;
    void* GdiSharedHandleTable;
    void* ProcessStarterHelper;
    void* GdiDCAttributeList;
    void* LoaderLock;
    unsigned long OSMajorVersion;
    unsigned long OSMinorVersion;
    unsigned long OSBuildNumber;
    unsigned long OSPlatformId;
    unsigned long ImageSubSystem;
    unsigned long ImageSubSystemMajorVersion;
    unsigned long ImageSubSystemMinorVersion;
    unsigned long GdiHandleBuffer[0x22];
    unsigned long PostProcessInitRoutine;
    unsigned long TlsExpansionBitmap;
    unsigned char TlsExpansionBitmapBits[0x80];
    unsigned long SessionId;
};

struct CLDR_MODULE
{
    LIST_ENTRY InLoadOrderModuleList;
    LIST_ENTRY InMemoryOrderModuleList;
    LIST_ENTRY InInitializationOrderModuleList;
    void* BaseAddress;
    void* EntryPoint;
    unsigned long SizeOfImage;
    CUNICODE_STRING FullDllName;
    CUNICODE_STRING BaseDllName;
    unsigned long Flags;
    short LoadCount;
    short TlsIndex;
    LIST_ENTRY HashTableEntry;
    unsigned long TimeDateStamp;
};

/// <summary>
/// Returns a list of all the modules that are inside the linked lists in the PEB
/// </summary>
/// <param name="tableId">Can be ignored unless you know why you want to use another table. 0 for LoadOrder, 8 for MemoryOrder, 16 for InitOrder</param>
/// <returns>A list containing all the loaded modules from the PEB</returns>
std::vector<CLDR_MODULE*> GetPebModules(int tableId = 0) noexcept;

/// <summary>
/// Returns a pointer to the loader module with the given name, note that this is case sensitive
/// </summary>
/// <param name="moduleName">Module name</param>
/// <returns>Pointer to the loader module, nullptr if no module was found</returns>
CLDR_MODULE* GetPebModuleByName(std::wstring moduleName) noexcept;

/// <summary>
/// Returns a pointer to the module's BaseAddress with the given name, note that this is case sensitive
/// </summary>
/// <param name="moduleName">Module name</param>
/// <returns>Pointer to the module's BaseAddress, nullptr if no module was found</returns>
void* GetPebModuleBase(std::wstring moduleName) noexcept;

/// <summary>
/// Hides/Erases a module from the PEB
/// </summary>
/// <param name="pModule">Pointer to the Loader Module</param>
/// <param name="erasePeHeader">Should we erase the PE Header from memory to prevent anyone spotting our image using it? (default: true)</param>
/// <param name="eraseListEntry">Should we erase the remove linked list entry from memory? (default: true)</param>
void HideModuleFromPeb(CLDR_MODULE* pModule, bool erasePeHeader = true, bool eraseListEntry = true) noexcept;

/// <summary>
/// Alter the modules name in the PEB, the new name can only be as long as the original or shorter.
/// </summary>
/// <param name="pModule">Pointer to the Loader Module</param>
/// <param name="fakeModuleName">The new "fake" name</param>
void FakeModuleNameInPeb(CLDR_MODULE* pModule, std::wstring fakeModuleName) noexcept;

/// <summary>
/// Alter the modules name in the PEB, the new path can only be as long as the original or shorter.
/// </summary>
/// <param name="pModule">Pointer to the Loader Module</param>
/// <param name="fakeModulePath">The new "fake" path</param>
void FakeModulePathInPeb(CLDR_MODULE* pModule, std::wstring fakeModulePath) noexcept;

/// <summary>
/// Alter the modules SizeOfImage field to prevent a successful image dump
/// </summary>
/// <param name="pModule">Pointer to the Loader Module</param>
/// <param name="newSize">The new size, maybe select a very large or very small number to foll any dumpers</param>
void FakeModuleSizeInPeb(CLDR_MODULE* pModule, size_t newSize) noexcept;

/// <summary>
/// Alter the modules BaseAddress field to prevent a successful image dump
/// </summary>
/// <param name="pModule">Pointer to the Loader Module</param>
/// <param name="newBase">The new base address</param>
void FakeModuleBaseAddressInPeb(CLDR_MODULE* pModule, void* newBase) noexcept;

/// <summary>
/// Patches a memory section to we writeable, modifies the memory and re-protects it with the old protection.
/// </summary>
/// <param name="destination">Memory to patch</param>
/// <param name="value">Value to set the memory to</param>
/// <param name="size">Size of the patch</param>
/// <param name="access">Memory access, defaults to PAGE_EXECUTE_READWRITE</param>
__forceinline void PatchMemSet(unsigned char* destination, int value, unsigned int size, unsigned long access = PAGE_EXECUTE_READWRITE) noexcept
{
    unsigned long oldProtection;
    VirtualProtect(destination, size, access, &oldProtection);

    __stosb(destination, value, size);

    unsigned long newOldProtection;
    VirtualProtect(destination, size, oldProtection, &newOldProtection);
};

/// <summary>
/// MS code to print all active modules from the active process
/// </summary>
void PrintModules();

#endif

main.cpp

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
#include "Main.hpp"

int main()
{
    // sample dll to mess around with, it doesn't crash if your mess with it
#ifdef _DEBUG
    std::wstring moduleName(L"ucrtbased.dll");
#else
    std::wstring moduleName(L"ucrtbase.dll");
#endif

    CLDR_MODULE* pModule = GetPebModuleByName(moduleName);

    if (pModule)
    {
        std::wcout << ">> Found module \"" << moduleName << "\" PEB entry at: 0x" << std::hex << pModule << std::dec << std::endl << std::endl;
        PrintModules();

        // save the old base address
        void* oldBaseAddress = GetPebModuleBase(moduleName);

        std::wcout << std::endl << ">> Faking Module Data" << std::endl << std::endl;
        FakeModulePathInPeb(pModule, L"C:\\Angus\\");
        FakeModuleNameInPeb(pModule, L"Gammel.mp3");
        FakeModuleBaseAddressInPeb(pModule, (void*)0x1337);
        FakeModuleSizeInPeb(pModule, 0x4711);

        PrintModules();

        // restore the old base, otherwise we can't erase the pe header 
        FakeModuleBaseAddressInPeb(pModule, oldBaseAddress);

        std::wcout << std::endl << ">> Hiding Module" << std::endl << std::endl;
        HideModuleFromPeb(pModule, true, true);

        PrintModules();
        return 0;
    }

    std::wcout << ">> Unable to find module named: \"" << moduleName << std::endl;
    PrintModules();
    return 1;
}

std::vector<CLDR_MODULE*> GetPebModules(int tableId) noexcept
{
    if (tableId < 0) tableId = 0;
    if (tableId > 2) tableId = 2;

    // CPEB* pPeb;
    // __asm 
    // {
    //     PUSH EAX;
    //     MOV EAX, DWORD PTR FS:[0x30];
    //     MOV pPeb, EAX;
    //     POP EAX;
    // }

    auto pPeb = (CPEB*)__readfsdword(0x30);
    auto pPebLdrData = pPeb->LoaderData;

    auto moduleList = tableId == 0 ? pPebLdrData->InLoadOrderModuleList : tableId == 1 ? pPebLdrData->InMemoryOrderModuleList : pPebLdrData->InInitializationOrderModuleList;
    auto ptrOffset = tableId * sizeof(LIST_ENTRY); // 0 for LoadOrder, 8 for MemoryOrder, 16 for InitOrder

    std::vector<CLDR_MODULE*> dllInfo;

    for (auto pListEntry = moduleList.Flink; pListEntry->Flink != moduleList.Flink; pListEntry = pListEntry->Flink)
    {
        auto pLdrDataEntry = (CLDR_MODULE*)((uintptr_t)pListEntry - ptrOffset);
        dllInfo.push_back(pLdrDataEntry);
    }

    return dllInfo;
}

CLDR_MODULE* GetPebModuleByName(std::wstring moduleName) noexcept
{
    for (auto const& pModule : GetPebModules())
    {
        // get the peb entry by name
        if (std::wstring(pModule->BaseDllName.Buffer) == moduleName)
        {
            return pModule;
        }
    }

    return nullptr;
}

void* GetPebModuleBase(std::wstring moduleName) noexcept
{
    for (auto const& pModule : GetPebModules())
    {
        // get the peb entry by name
        if (std::wstring(pModule->BaseDllName.Buffer) == moduleName)
        {
            return pModule->BaseAddress;
        }
    }

    return nullptr;
}

void HideModuleFromPeb(CLDR_MODULE* pModule, bool erasePeHeader, bool eraseListEntry) noexcept
{
    /*
                       Swap
                       +---------------------v
        +----------+ Flink -> +----------+ Flink -> +----------+
        | Module 1 |          | Module 2 |          | Module 3 |
        +----------+ <- Blink +----------+ <- Blink +----------+
                          ^---------------------+
                                             Swap

        Step by Step guide:

        pModule is Module 2

        pModule->x.Flink                                // get the next module (Module 3)
        pModule->x.Flink->Blink                         // get the pointer to the previous module (Module 2)
        pModule->x.Flink->Blink = pModule->x.Blink;     // set it to the blink of our main module, which points to Module 1

        and now reverse (because its a double linked list)

        pModule->x.Blink                                // get the previous module (Module 1)
        pModule->x.Blink->Flink                         // get the pointer to the next module (Module 2)
        pModule->x.Blink->Flink = pModule->x.Flink;     // set it to the flink of our main module, which points to Module 3

        repeat it for the other two linked lists and we've successfully patched it
    */

    pModule->InLoadOrderModuleList.Flink->Blink = pModule->InLoadOrderModuleList.Blink;
    pModule->InLoadOrderModuleList.Blink->Flink = pModule->InLoadOrderModuleList.Flink;

    pModule->InMemoryOrderModuleList.Flink->Blink = pModule->InMemoryOrderModuleList.Blink;
    pModule->InMemoryOrderModuleList.Blink->Flink = pModule->InMemoryOrderModuleList.Flink;

    pModule->InInitializationOrderModuleList.Flink->Blink = pModule->InInitializationOrderModuleList.Blink;
    pModule->InInitializationOrderModuleList.Blink->Flink = pModule->InInitializationOrderModuleList.Flink;

    if (erasePeHeader)
    {
        uintptr_t baseAddress = (uintptr_t)pModule->BaseAddress;
        PatchMemSet((unsigned char*)baseAddress, 0, (((IMAGE_NT_HEADERS*)baseAddress + ((IMAGE_DOS_HEADER*)baseAddress)->e_lfanew))->OptionalHeader.SizeOfHeaders);
    }

    if (eraseListEntry)
    {
        __stosb((unsigned char*)pModule, 0, sizeof(CLDR_MODULE));
    }
}

void FakeModuleNameInPeb(CLDR_MODULE* pModule, std::wstring fakeModuleName) noexcept
{
    // we dont want to resize this string lmao, limit it to the original lenght
    unsigned short newSize = std::min(pModule->BaseDllName.MaximumLength, (unsigned short)(fakeModuleName.size() * sizeof(wchar_t)));

    __movsb((unsigned char*)pModule->BaseDllName.Buffer, (unsigned char*)fakeModuleName.c_str(), newSize + sizeof(wchar_t));
    pModule->BaseDllName.Length = newSize;
}

void FakeModulePathInPeb(CLDR_MODULE* pModule, std::wstring fakeModulePath) noexcept
{
    // we dont want to resize this string lmao, limit it to the original lenght
    unsigned short newSize = std::min(pModule->FullDllName.MaximumLength, (unsigned short)(fakeModulePath.size() * sizeof(wchar_t)));

    __movsb((unsigned char*)pModule->FullDllName.Buffer, (unsigned char*)fakeModulePath.c_str(), newSize);

    // set the new string lenght and adjust the name offset
    pModule->FullDllName.Length = newSize + pModule->BaseDllName.Length;
    pModule->BaseDllName.Buffer = (wchar_t*)((unsigned char*)pModule->FullDllName.Buffer + newSize);
}

void FakeModuleSizeInPeb(CLDR_MODULE* pModule, size_t newSize) noexcept
{
    // should be doable without a function, but you get the idea
    pModule->SizeOfImage = newSize;
}

void FakeModuleBaseAddressInPeb(CLDR_MODULE* pModule, void* newBase) noexcept
{
    // should be doable without a function, but you get the idea
    pModule->BaseAddress = newBase;
}

void PrintModules()
{
    HMODULE hMods[1024];
    unsigned long cbNeeded;

    void* hProcess = GetCurrentProcess();

    if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded))
    {
        for (size_t i = 0; i < cbNeeded / sizeof(HMODULE); ++i)
        {
            wchar_t szModName[MAX_PATH];
            wchar_t szModFilename[MAX_PATH];

            if (GetModuleBaseName(hProcess, hMods[i], szModName, MAX_PATH)
                && GetModuleFileNameEx(hProcess, hMods[i], szModFilename, MAX_PATH))
            {
                MODULEINFO moduleInfo;
                GetModuleInformation(hProcess, hMods[i], &moduleInfo, cbNeeded);

                std::wcout << ">> " << std::setw(20) << szModName;
                std::wcout << " | Base: 0x" << std::setfill(L'0') << std::setw(8) << std::hex << moduleInfo.lpBaseOfDll;
                std::wcout << " | Size: 0x" << std::setw(8) << moduleInfo.SizeOfImage << std::dec;
                std::wcout << std::setfill(L' ') << " | " << szModFilename << std::endl;
            }
        }
    }

    CloseHandle(hProcess);
}