首先得先了解一个结构:

1.TEB、PEB

  • TEB:

TEB线程环境块:每个线程都有一个对应的TEB

  • PEB:

PEB进程环境块:每个进程在创建时都会有一个对应的PEB,它储存了该进程的全局状态和环境信息

这里不进行详细介绍,只了解需要用到的东西。

(1)TEB结构体

image-20241021111145291

32位操作系统下,TEB结构体中偏移0x30的值是PEB结构体的地址,指向PEB结构体。我们可以通过这个获取到PEB的地址

(2)PEB结构体

PEB 结构体中用到的就是这个PPEB_LDR_DATA Ldr;,偏移量是0xC,用这个可以直接获取到LDR_DATA结构体的地址。

image-20241021111328011

(3)PEB_LDR_DATA结构体

我们来看看这个PEB_LDR_DATA结构体是什么:

1
2
3
4
5
6
7
8
typedef struct _PEB_LDR_DATA {
ULONG Length;
BOOL Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

前三个不用管,先看看这个LIST_ENTRY是什么东西:

1
2
3
4
typedef struct _LIST_ENTRY {
struct_LIST_ENTRY *Flink;
struct_LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY,*RESTRICTED_POINTER;

这个就是给双向链表,Flink指向下一个结构,Blink指向上一个结构

然后再来说说后三个变量是什么:

  • InLoadOrderModuleList : 载入顺序排序的dll
  • InMemoryOrderModuleList : 内存排序的dll
  • InInitializationOrderModuleList : 初始化排序的dll
(4)LDR_DATA_TABLE_ENTRY结构体

接下来再讲一下DLL的信息是如何被存储的,每个模块的信息都被存储在一个LDR_DATA_TABLE_ENTRY结构体当中:

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
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONGFlags;
USHORT LoadCount;
USHORT TlsIndex;
union
{
LIST_ENTRY HashLinks;
struct
{
PVOID SectionPointer;
ULONG CheckSum;
};
};
union
{
ULONG TimeDateStamp;
PVOID LoadedImports;
};
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY,*PLDR_DATA_TABLE_ENTRY;

可以看到这个结构体当中也有PEB_LDR_DATA中的三个结构。那么这两个不同的结构是怎么练习起来的呢?可以看这张图:

image-20241021112816635

就是通过这几个结构中的双向链表联系起来的,知道这个后我们就可以通过遍历来获取到想要的dll的信息了。

2.获取TEB、PEB

那我们如何来获取这几个结构呢?32位操作系统下,TEB的首地址就是fs:[0x0]那么知道这个了,我们就可以通过内联汇编的方式来获取到这几个结构体的值

1
2
3
4
5
6
7
8
_asm{
mov eax , fs:[0x30] // PEB
mov eax , [eax + 0xC] // PEB_LDR_DATA
add eax , 0x0C // LDR->InLoadOrderLinks
mov pBeg , eax // LDR的InLoadOrderLinks
mov eax , [eax] // LDR_DATA_TABLE_ENTRY
mov pPLD , eax // LDR_DATA_TABLE_ENTRY的InLoadOrderLinks
}

此时pBeg就是PEB_LDR_DATA的地址,pPLD就是第一个dll的InLoadOrderLinks,因为这个是双向循环链表,那么只要遍历这个链表就会回到pBeg,所以我们可以使用循环来搜寻我们想要的dll的信息

1
2
3
4
5
while(pBeg != pPLD)
{
·······
pPLD = (LDR_DATA_TABLE_ENTRY*)pPLD->InLoadOrderLinks.Flink;
}

3.导入表隐藏

在我们编写的ShellCode中,直接调用系统API就很容易被杀掉,比如当我们需要将ShellCode写到进程中时必须要使用VirtualAllocCreateThreadWriteProcessMemory···这些api肯定会被重点监控,此时我们可以通过GetProcAddress函数来获取这些函数地址,这样导入表中就没有了这些函数名称。但是也会有LoadLibraryGetProcAddress这些个函数名,这时候就得用上面的知识通过PEB,获取到 dll 的加载的地址,再通过导出表获得函数,这样导入表中就不会有这些函数的名称,从而完成了导入表隐藏函数名。

4.代码实现

(1)32位架构代码

32位程序是可以直接用_asm内联汇编的,使用比较方便

使用到的结构体: PEB_LDR_DATALDR_DATA_TABLE_ENTRYUNICODE_STRING这可以让后续访问更加简单

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
typedef HMODULE(WINAPI* PLOADLIBRARYA)(LPCSTR);
typedef DWORD(WINAPI* PGETPROCADDRESS)(HMODULE, LPCSTR);
typedef int(WINAPI* PMESSAGEBOX)(HWND, LPCTSTR, LPCTSTR, UINT);

// 存储dll的名字
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;


// PEB_LDR_DATA结构体
typedef struct _PEB_LDR_DATA
{
DWORD Length;
bool Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList; // 主要使用这个双向链表
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList; // dll加载顺序,指向第一个_LDR_DATA_TABLE_ENTRY
} PEB_LDR_DATA, * PPEB_LDR_DATA;

typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
UINT32 SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
UINT32 Flags;
USHORT LoadCount;
USHORT TlsIndex;
LIST_ENTRY HashLinks;
PVOID SectionPointer;
UINT32 CheckSum;
UINT32 TimeDateStamp;
PVOID LoadedImports;
PVOID EntryPointActivationContext;
PVOID PatchInformation;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

全局变量定义:

1
2
3
4
// 主要获取的是kernel32中的LoadLibrary 和 GetProcAddress这两个API
typedef HMODULE(WINAPI* PLOADLIBRARYA)(LPCSTR);
typedef DWORD(WINAPI* PGETPROCADDRESS)(HMODULE, LPCSTR);
typedef int(WINAPI* PMESSAGEBOX)(HWND, LPCTSTR, LPCTSTR, UINT);

获取kernel32.dll的基址:

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
DWORD WINAPI GetKernel32Base()
{
PLOADLIBRARYA pLoadLibraryA = NULL;
PGETPROCADDRESS pGetProcAddress = NULL;
PMESSAGEBOX pMessageBox = NULL;

PLDR_DATA_TABLE_ENTRY pPLD; // 移动指针
PLDR_DATA_TABLE_ENTRY pBeg; // LDR-哨兵

// 用于判断dll名是否相等
PCHAR pFirst = NULL;
PCHAR pLast = NULL;

DWORD ret = 0, i = 0;
DWORD dwKernelBase = 0;

TCHAR szKernel32[] = L"KERNEL32.DLL";
char szGetProcAddress[] = "GetProcAddress";
char szLoadLibraryA[] = "LoadLibraryA";

_asm {
mov eax, fs: [0x30] // PEB
mov eax, [eax + 0xC] // LDR
add eax, 0x0C
mov pBeg, eax // LDR-哨兵
mov eax, [eax]
mov pPLD, eax
}

// Find kerenl
while (pPLD != pBeg)
{
pLast = (PCHAR)pPLD->BaseDllName.Buffer;
pFirst = (PCHAR)szKernel32;
int flag = 1;
int Length = pPLD->BaseDllName.Length;

for (int i = 0; i < Length; i++)
{
if (*pLast != *pFirst)
{
flag = 0;
break;
}
}

if (flag == 1) break;
pPLD = (PLDR_DATA_TABLE_ENTRY)pPLD->InLoadOrderLinks.Flink;
}
return (DWORD)pPLD->DllBase;
}

获取kernel32导出表,获取GetProcAddressLoadLibrary地址。

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
VOID GetAPI(DWORD dwKernelBase)
{
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)dwKernelBase;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)((DWORD)pDos + pDos->e_lfanew);
PIMAGE_FILE_HEADER pFile = (PIMAGE_FILE_HEADER)((DWORD)pNt + 4);
PIMAGE_OPTIONAL_HEADER pOption = (PIMAGE_OPTIONAL_HEADER)((DWORD)pFile + 20);
PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((DWORD)pOption + pFile->SizeOfOptionalHeader);
PIMAGE_EXPORT_DIRECTORY pExportDir = (PIMAGE_EXPORT_DIRECTORY)(dwKernelBase + pOption->DataDirectory[0].VirtualAddress);

// 导出名称表地址:
PDWORD pAddOfName_RVA = (PDWORD)(dwKernelBase + pExportDir->AddressOfNames);
// 序号导出表地址
PWORD pAddOfOrd_RVA = (PWORD)(dwKernelBase + pExportDir->AddressOfNameOrdinals);
// 函数地址表地址
PDWORD pAddOfFun_RVA = (PDWORD)(dwKernelBase + pExportDir->AddressOfFunctions);

DWORD dwCnt = 0;
char* pFinded = NULL, * pSrc = szGetProcAddress;

for (; dwCnt < pExportDir->NumberOfNames; dwCnt++)
{
pFinded = (char*)(pAddOfName_RVA[dwCnt] + dwKernelBase);
while (*pFinded && *pFinded == *pSrc)
pFinded++, pSrc++;
if (*pFinded == *pSrc)
{
pGetProcAddress = (PGETPROCADDRESS)(pAddOfFun_RVA[pAddOfOrd_RVA[dwCnt]] + dwKernelBase);
break;
}
pSrc = szGetProcAddress;
}
if (!pGetProcAddress)
{
printf("GetProcAddress获取错误\n");
return 0;
}
pLoadLibraryA = (PLOADLIBRARYA)pGetProcAddress((HMODULE)dwKernelBase, "LoadLibraryA");
}

main函数

1
2
3
4
5
6
7
8
9
int main()
{
DWORD dwKernel32Base = GetKernel32Base();
GetAPI(dwKernel32Base);
HMODULE hUser = pLoadLibraryA("user32.dll");
pMessageBox = (PMESSAGEBOX)pGetProcAddress(hUser, "MessageBoxW");

pMessageBox(NULL, TEXT("隐藏导入表成功"), TEXT("[Successed]"), MB_OK);
}

这样就可以在导入表中将敏感的API隐藏起来了,用dumpbin查看一下导入表:

1
dumpbin /imports Test1.exe

image-20241022133150810

可以看见messagebox弹出来了,而且导入表中根本没有LoadLibrary的API,GetProcAddress可能是系统函数调用了。完成32位下的导入表隐藏。

(2)64位架构

注意:

  • 在x64架构下,gs:[0x30]寄存器在ring3指向TEB的结构,TEB + 60处指向PEB结构,PEB+0x18处指向PEB_LDR_DATA结构,PEB_LDR_DATA+0x30处为InInitializationOrderModuleList。
  • x64架构下无法使用内联汇编,只能用联合编译进行插入汇编代码

这里我们需要这样取PEB结构的地址:

先创建一个.asm的文件在里面这么写:

1
2
3
4
5
6
.CODE
GetPeb PROC
mov rax,[60h]
ret
GetPeb ENDP
END

在这个文件的属性页中选择

image-20241023202837647

1
2
命令行:ml64 /Fo $(IntDir)%(fileName).obj /c %(fileName).asm
输出: $(IntDir)%(fileName).obj

image-20241023202935493

然后在.cpp中声明:

image-20241023203007542

这样就就可以通过GetPeb()获取到PEB结构的地址了。

代码实现:

定义一些变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} UNICODE_STRING, * PUNICODE_STRING;

// 全局变量
PLONG64 pPEB ;

// 全局函数
PLONG64 GetInInitializationOrderModuleListAddr();
LONG64 GetKernel32Base();
LONG64 GetgetProcAddress(LONG64 Kernel32Base);

// 自己导入的函数
typedef LONG64(WINAPI* PGETPROCADDRESS)(HMODULE, LPCSTR);
typedef HMODULE(WINAPI* PLOADLIBRARYA)(LPCSTR);
typedef int(WINAPI* PMESSAGEBOX)(HWND, LPCTSTR, LPCTSTR, UINT);

GetInInitializationOrderModuleListAddr()函数

1
2
3
4
5
6
7
PLONG64 GetInInitializationOrderModuleListAddr()
{
pPEB = (PLONG64)GetPeb(); // peb
PLONG64 Ldr = (PLONG64) * (PLONG64)((LONG64)pPEB + 0x18); // ldr
PLONG64 InInitializationOrderModuleListAddr = (PLONG64)((LONG64)Ldr + 0x30);
return InInitializationOrderModuleListAddr;
}

LONG64 GetKernel32Base()函数

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
LONG64 GetKernel32Base()
{
PLONG64 InitAddr = GetInInitializationOrderModuleListAddr();
TCHAR szKernel32[] = L"KERNEL32.DLL";
PBYTE Target = (PBYTE)szKernel32;
PLONG64 head = InitAddr;
PLONG64 Point = (PLONG64)*InitAddr;
while (head != Point)
{
PUNICODE_STRING pString = (PUNICODE_STRING)((LONG64)Point + 0x38);
PCHAR szCheck = (PCHAR)pString->Buffer;
int flag = 1;
for (int i = 0; i < pString->Length; i++)
{
if (szCheck[i] != Target[i])
{
flag = 0;
break;
}
}
if (flag == 1) break;
Point = (PLONG64)(*Point);
}
return *(PLONG64)((LONG64)Point + 0x10);
}

LONG64 GetgetProcAddress(LONG64 Kernel32Base)函数

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
LONG64 GetgetProcAddress(LONG64 Kernel32Base)
{

PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)Kernel32Base;
PIMAGE_NT_HEADERS64 pNt = (PIMAGE_NT_HEADERS64)(Kernel32Base + pDos->e_lfanew);
PIMAGE_FILE_HEADER pFile = (PIMAGE_FILE_HEADER)((LONG64)pNt + 4);
PIMAGE_OPTIONAL_HEADER64 pOpt = (PIMAGE_OPTIONAL_HEADER64)((LONG64)pFile + 20);
PIMAGE_DATA_DIRECTORY pDir = pOpt->DataDirectory;

PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(pDir[0].VirtualAddress + Kernel32Base);

CHAR szGetProcAddr[] = "GetProcAddress";
PDWORD NameOfAddr = (PDWORD)(pExport->AddressOfNames + Kernel32Base);
PDWORD FunOfAddr = (PDWORD)(pExport->AddressOfFunctions + Kernel32Base);
PWORD OrdOfAddr = (PWORD)(pExport->AddressOfNameOrdinals + Kernel32Base);

for (int i = 0; i < pExport->NumberOfNames; i++)
{
PCHAR Target = szGetProcAddr;
PCHAR getProc = (PCHAR)(NameOfAddr[i] + Kernel32Base);
int flag = 1;
while (*Target)
{
if (*Target != *getProc) {
flag = 0;
break;
}
Target++;
getProc++;
}
if (flag)
{
return (LONG64)(FunOfAddr[OrdOfAddr[i]] + Kernel32Base);
}
}
return -1;
}

main函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main()
{
LONG64 kernel32base = GetKernel32Base();
PGETPROCADDRESS pGetProcAddr = (PGETPROCADDRESS)GetgetProcAddress(kernel32base);
PLOADLIBRARYA pLoadLibraryA = NULL;
pLoadLibraryA = (PLOADLIBRARYA)pGetProcAddr((HMODULE)kernel32base, "LoadLibraryA");
if (!pLoadLibraryA)
{
printf("error");
return 0;
}
HMODULE hUser32 = pLoadLibraryA("User32.dll");
if (!hUser32) {
printf("error-user32.dll\n");
return 0;
}
PMESSAGEBOX pMessageBox = NULL;
pMessageBox = (PMESSAGEBOX)pGetProcAddr(hUser32, "MessageBoxW");
pMessageBox(NULL, L"good!!", L"[SUCCESS]", 0);
return 0;
}

这样就可以执行弹框了,再看看.exe导入表中有没有那几个敏感的API。

image-20241023205004356

image-20241023205402040

可以发现这里已经没有了GetProcAddressLoadLibrary这两个函数了,说明我们导入表隐藏成功了。

通过导入表隐藏的方式就可以把各种敏感的API隐藏起来。接着我们还可以将ShellCode存储到资源节当中通过这种方式拿出来用:

1
2
3
4
5
6
7
8
9
int main()
{
HRSRC Res = FindResource(NULL, MAKEINTRESOURCE(IDR_BIN1), L"BIN1");
DWORD Size = SizeofResource(NULL, Res);
HGLOBAL Load = LoadResource(NULL, Res);
void* buffer = VirtualAlloc(0, Size, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(buffer, Load, Size);
((void(*)())buffer)();
}
  • FindResource : 获取资源句柄
  • SizeofResource : 如果函数成功,则返回值为资源中的字节数。
  • LoadResource : 返回HGLOBAL类型的资源类型句柄(就是个地址)

然后申请一个可读可写可执行的空间,将ShellCode贴上去,再通过函数指针运行这个ShellCode。