恶意软件开发简介
什么是恶意软件?
恶意软件是一种专门设计用于执行恶意操作的软件,例如未经授权访问机器或从机器窃取敏感数据。“恶意软件”一词通常与非法或犯罪行为有关,但它也可以被道德黑客(如渗透测试人员和红队成员)用于对组织进行授权安全评估。影域实验室 假定参加本课程的用户将仅将所学知识用于道德和法律目的。任何其他用途都可能导致刑事指控,影域实验室 对此不承担任何责任。
为什么要学习恶意软件开发?
人们想要学习恶意软件开发的原因有很多。从攻击性安全的角度来看,测试人员通常需要针对客户的环境执行某些恶意任务。在参与使用的工具类型方面,测试人员通常有三个主要选择:
- 开源工具 (OST) - 这些工具通常由安全供应商签名,并在任何受到良好保护或成熟的组织中检测到。在进行攻击性安全评估时,它们并不总是可靠的。
- 购买工具 - 预算较大的团队通常会选择购买工具,以节省参与期间的宝贵时间。与自定义工具类似,这些工具通常是闭源的,并且更有可能逃避安全解决方案。
- 开发自定义工具 - 由于这些工具是定制的,因此它们尚未经过安全供应商的分析或签名,这为攻击团队在检测方面提供了优势。这就是恶意软件开发知识对于更成功的攻击性安全评估至关重要的地方。
应该使用什么编程语言?
从技术上讲,任何编程语言都可用于构建恶意软件,例如 Python、PowerShell、C#、C、C++ 和 Go。话虽如此,在恶意软件开发方面,某些编程语言比其他编程语言更胜一筹,原因有几个,通常归结为以下几点:
- 某些编程语言更难进行逆向工程。确保防御者对恶意软件的行为方式了解有限,这始终是攻击者的目标之一
- 某些编程语言需要目标系统具备先决条件。例如,执行 Python 脚本需要目标机器上存在解释器。如果机器上没有 Python 解释器,就不可能执行基于 Python 的恶意软件。
- 根据编程语言的不同,生成的文件大小会有所不同。
高级编程语言与低级编程语言
过去几年,Windows 恶意软件开发领域发生了变化,现在高度专注于规避基于主机的安全解决方案,例如防病毒 (AV) 和端点检测与响应 (EDR)。随着技术的进步,构建执行可疑命令或执行“类似恶意软件”操作的恶意软件已不再足够。
影域 Academy 将教您构建可用于实际交战的规避恶意软件。这些模块还将调用 非 opsec 操作或可能让您的恶意软件被安全解决方案或蓝队检测到的操作。
恶意软件开发生命周期
从根本上讲,恶意软件是一种旨在执行某些操作的软件。成功的软件实施需要一个称为软件开发生命周期 (SDLC) 的过程。同样,精心构建的复杂恶意软件将需要一个定制版本的 SDLC,称为恶意软件开发生命周期 (MDLC)。
虽然 MDLC 不一定是一个正式的过程,但它在 影域 Academy 中用于让读者轻松了解开发过程。MDLC 包含 5 个主要阶段:
- 开发 - 开始开发或改进恶意软件中的功能。
- 测试 - 执行测试以发现迄今为止开发的代码中隐藏的错误。
- 离线 AV/EDR 测试 - 针对尽可能多的安全产品运行开发的恶意软件。重要的是离线进行测试以确保不会将样本发送给安全供应商。使用 Microsoft Defender,可以通过禁用自动样本提交和云交付保护选项来实现这一点。
- 在线 AV/EDR 测试 - 使用互联网连接对安全产品运行开发的恶意软件。云引擎通常是 AV/EDR 中的关键组件,因此针对这些组件测试恶意软件对于获得更准确的结果至关重要。请谨慎行事,因为此步骤可能会导致样本被发送到安全解决方案的云引擎。
- IoC(入侵指标)分析 - 在此阶段,您将成为威胁猎手或恶意软件分析师。分析恶意软件并提取可能用于检测或签名恶意软件的 IoC。
- 返回步骤 1。
所需工具
介绍
在开始恶意软件开发之前,需要通过安装恶意软件开发和逆向工程工具来准备开发工作区。这些工具将帮助开发和分析恶意软件,并将在整个模块中使用。
逆向工程工具
提到的几种工具更侧重于逆向工程而不是开发。对构建的恶意软件进行逆向工程对于充分了解其内部工作原理以及了解恶意软件分析师在检查恶意软件时会看到什么至关重要。
要安装的工具
安装以下工具:
- Visual Studio - 这是进行编码和编译过程的开发环境。安装 C/C++ 运行时。
- x64dbg - x64dbg 是一个调试器,将在整个模块中使用,以了解开发的恶意软件的内部情况。
- PE-Bear - PE-bear 是一个用于 PE 文件的多平台逆向工具。它还将用于评估开发的恶意软件并寻找可疑指标。
- Process Hacker 2 - Process Hacker 是一个功能强大的多用途工具,可帮助监控系统资源、调试软件和检测恶意软件。
- Msfvenom - Msfvenom 是一个命令行界面工具,用于创建、操作和输出有效载荷。
Visual Studio
Visual Studio 是 Microsoft 开发的集成开发环境 (IDE)。它用于开发各种软件,例如 Web 应用程序、Web 服务和计算机程序。它还附带用于构建和测试应用程序的开发和调试工具。Visual Studio 将是本课程中用于开发的主要 IDE。
x64dbg
x64dbg 是一款适用于 x64 和 x86 Windows 二进制文件的开源调试实用程序。它用于分析和调试用户模式应用程序和内核模式驱动程序。它提供了一个图形用户界面,允许用户检查和分析其程序的状态并查看内存内容、汇编指令和寄存器值。使用 x64dbg,用户可以设置断点、查看堆栈和堆数据、逐步执行代码以及读取和写入内存值。
主“CPU”选项卡有 4 个屏幕:
- 反汇编(左上角):此窗口显示应用程序正在执行的汇编指令。
- 转储(左下角):此窗口显示正在调试的应用程序的内存内容。
- 寄存器(右上角):此窗口显示 CPU 寄存器的值。
- 堆栈(右下角):此窗口显示堆栈的内容。
- 其余选项卡也提供了有用的信息,但它们将在使用时在模块中讨论。
PE-Bear
PE-Bear 是一款免费的开源工具,旨在帮助恶意软件分析师和逆向工程师快速轻松地分析 Windows 可移植可执行 (PE) 文件。它有助于分析和可视化 PE 文件的结构,查看每个模块的导入和导出,并执行静态分析以检测异常和可能的恶意代码。PE-bear 还包括 PE 标头和节验证等功能,以及十六进制编辑器。
Process Hacker
Process Hacker 是一款用于查看和操作 Windows 上的进程和服务的开源工具。它与任务管理器类似,但提供更多信息和高级功能。它可用于终止进程和服务、查看详细的进程信息和统计信息、设置进程优先级等。在分析正在运行的进程以查看已加载的 DLL 和内存区域等项目时,Process Hacker 非常有用。
Msfvenom
Msfvenom 是一个 Metasploit 框架独立有效载荷生成器,允许用户生成各种类型的有效载荷。这些有效载荷将由本课程中创建的恶意软件使用。
Pre-Built Virtual Machines
有预构建的虚拟机可供使用。这些虚拟机已预先安装所有必需的工具和课程代码。请注意,Microsoft Defender 已针对“C:UsersMALDEV01DesktopMaldev-code”文件夹预先配置了排除项。
- VMware - Download VM
- SHA256: 01ac8ef7078e0c263af10123d54a065b0d74f6d6bf4ea41c0a31c90105a40ba6
- VirtualBox - Download VM
- SHA256: 95774225f6e9265ac147e1a6af8a5ae488d833e6dd75debd13f3a38fa4f2de24
3D Accelerated Graphics
确保虚拟机设置已禁用 3D 加速图形,因为这可能会导致虚拟机冻结或滞后。
Changelog
部署虚拟机后,部分代码已更新。为确保您拥有最新的代码示例,请从以下模块重新安装代码片段:
- Bypassing AVs
编码基础
介绍
如前所述,本课程要求对 C 有基本的了解。话虽如此,由于本课程中有几个概念非常重要,因此将在此提及。
结构
结构或结构体是用户定义的数据类型,允许程序员将不同数据类型的相关数据项分组为一个单元。结构体可用于存储与特定对象相关的数据。结构体有助于以易于访问和操作的方式组织大量相关数据。结构体中的每个项目称为“成员”或“元素”,这些术语在课程中可互换使用。
使用 Windows API 时常见的情况是,某些 API 需要填充结构作为输入,而其他 API 则采用声明的结构并填充它。下面是 THREADENTRY32
结构体的示例,此时无需了解成员的用途。
typedef struct tagTHREADENTRY32 {
DWORD dwSize; // Member 1
DWORD cntUsage; // Member 2
DWORD th32ThreadID;
DWORD th32OwnerProcessID;
LONG tpBasePri;
LONG tpDeltaPri;
DWORD dwFlags;
THREADENTRY32;
声明结构
本课程中使用的结构通常使用 typedef
关键字来声明,以赋予结构别名。例如,下面的结构使用名称 _STRUCTURE_NAME
创建,但 typedef
添加了另外两个名称 STRUCTURE_NAME
和 *PSTRUCTURE_NAME
。
typedef struct _STRUCTURE_NAME {
// structure elements
} STRUCTURE_NAME, *PSTRUCTURE_NAME;STRUCTURE_NAME
别名指的是结构名称,而 PSTRUCTURE_NAME
表示指向该结构的指针。微软通常使用 P
前缀来表示指针类型。
初始化结构
初始化结构将根据初始化实际结构类型还是指向结构的指针而有所不同。继续前面的示例,使用 _STRUCTURE_NAME
或 STRUCTURE_NAME
初始化结构是相同的,如下所示。
STRUCTURE_NAME struct1 = { 0 }; // The '{ 0 }' part, is used to initialize all the elements of struct1 to zero
// OR
_STRUCTURE_NAME struct2 = { 0 }; // The '{ 0 }' part, is used to initialize all the elements of struct2 to zero
初始化结构指针“PSTRUCTURE_NAME”时,情况有所不同。
PSTRUCTURE_NAME structpointer = NULL;
初始化和访问结构成员
结构的成员可以直接通过结构初始化,也可以通过指向结构的指针间接初始化。在下面的例子中,结构“struct1”有两个成员,“ID”和“Age”,通过点运算符(“。”)直接初始化。
typedef struct _STRUCTURE_NAME {
int ID;
int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;
STRUCTURE_NAME struct1 = { 0 }; // initialize all elements of struct1 to zero
struct1.ID = 1470; // initialize the ID element
struct1.Age = 34; // initialize the Age element
初始化成员的另一种方法是使用指定初始化语法,其中可以指定要初始化结构中的哪些成员。
typedef struct _STRUCTURE_NAME {
int ID;
int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;
STRUCTURE_NAME struct1 = { .ID = 1470, .Age = 34}; // initialize both the ID and the Age elements
另一方面,通过指针访问和初始化结构是通过箭头运算符(->
)完成的。
typedef struct _STRUCTURE_NAME {
int ID;
int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;
STRUCTURE_NAME struct1 = { .ID = 1470, .Age = 34};
PSTRUCTURE_NAME structpointer = &struct1; // structpointer is a pointer to the 'struct1' structure
// Updating the ID member
structpointer->ID = 8765;
printf("The structure's ID member is now : %d n", structpointer->ID);
箭头运算符可以转换为点格式。例如,structpointer->ID
等同于 (*structpointer).ID
。也就是说,structurepointer
被取消引用,然后直接访问。
枚举
枚举或枚举数据类型用于定义一组命名常量。要创建枚举,请使用 enum
关键字,后跟枚举的名称和标识符列表,每个标识符代表一个命名常量。编译器会自动为常量分配值,从 0 开始,每个后续常量的值增加 1。在本课程中,可以看到枚举表示特定数据、错误代码或返回值的状态。
枚举的一个例子是包含 7 个常量的“工作日”列表。在下面的例子中,星期一的值为 0,星期二的值为 1,依此类推。需要注意的是,不能使用点 (.) 运算符修改或访问枚举列表。而是直接使用其命名常量值访问每个元素。
enum Weekdays {
Monday, // 0
Tuesday, // 1
Wednesday, // 2
Thursday, // 3
Friday, // 4
Saturday, // 5
Sunday // 6
};
// Defining a "Weekdays" enum variable
enum Weekdays EnumName = Friday; // 4
// Check the value of "EnumName"
switch (EnumName){
case Monday:
printf("Today Is Monday !n");
break;
case Tuesday:
printf("Today Is Tuesday !n");
break;
case Wednesday:
printf("Today Is Wednesday !n");
break;
case Thursday:
printf("Today Is Thursday !n");
break;
case Friday:
printf("Today Is Friday !n");
break;
case Saturday:
printf("Today Is Saturday !n");
break;
case Sunday:
printf("Today Is Sunday !n");
break;
default:
break;
}
Union
在 C 编程语言中,Union 是一种允许在同一内存位置存储各种数据类型的数据类型。联合提供了一种将单个内存位置用于多种用途的有效方法。联合并不常用,但可以在 Windows 定义的结构中看到。以下代码说明了如何在 C 中定义联合:
union ExampleUnion {
int IntegerVar;
char CharVar;
float FloatVar;
};
ExampleUnion
可以将 char
、int
和 float
数据类型存储在同一个内存位置。要在 C 中访问联合的成员,可以使用点运算符,类似于结构中使用的点运算符。
需要注意的是,在联合中,为任何成员分配新值也会更改所有其他成员的值,因为它们共享同一个内存位置来存储其数据。此外,为联合分配的内存等于其最大成员的大小。
按位运算符
按位运算符是操作二进制值的各个位的运算符,对每个相应的位位置执行运算。按位运算符如下所示:
- Right shift (
>>
) - Left shift (
<<
) - Bitwise OR (
|
) - Bitwise AND (
&
) - Bitwise XOR (
^
) - Bitwise NOT (
~
)
右移和左移
右移(“>>”)和左移(“<<”)运算符分别用于将二进制数的位向右和向左移动指定位数。
右移将丢弃最右边的位数(指定值),并在左侧插入相同数量的零位。例如,下图显示“10100111”向右移动“2”,变为“00101001”。
另一方面,左移会丢弃最左边的位,并从右侧插入相同数量的零位。例如,下图显示“10100111”左移“2”,变为“10011100”。
按位或
按位或运算是一种涉及位级两个二进制值的逻辑运算。它根据第二个操作数的相应位评估第一个操作数的每个位,从而生成一个新的二进制值。新的二进制值在原始值中对应位中的一个或两个为 1 的任何位位置都包含 1。
下表表示具有所有可能输入位的按位或输出。
按位与
按位与运算是一种涉及位级两个二进制值的逻辑运算。仅当两个输入操作数的相应位均为 1 时,此运算才会将新二进制值的位设置为 1。
下表表示具有所有可能输入位的按位与输出。
按位异或
按位异或运算(也称为排他或)是一种涉及位级两个二进制值的逻辑运算。如果只有一个位为 1,则每个位置的结果为 1。相反,如果两个位均为 0 或 1,则输出为 0。
下表表示具有所有可能输入位的按位异或输出。
按位非
按位非运算取一个二进制数并翻转其所有位。换句话说,它将所有 0 变为 1,将所有 1 变为 0。下表表示按位非输出以及所有可能的输入位。
按值传递
按值传递是一种将参数传递给函数的方法,其中参数是对象值的副本。这意味着当参数按值传递时,对象的值会被复制,并且函数只能修改其对象值的本地副本,而不能修改原始对象本身。
int add(int a, int b)
{
int result = a + b;
return result;
}
int main()
{
int x = 5;
int y = 10;
int sum = add(x, y); // x and y are passed by value
return 0;
}
通过引用传递
通过引用传递是一种将参数传递给函数的方法,其中参数是指向对象的指针,而不是对象值的副本。这意味着当通过引用传递参数时,传递的是对象的内存地址而不是对象的值。然后函数可以直接访问和修改对象,而无需创建对象的本地副本。
void add(int *a, int *b, int *result)
{
int A = *a; // A is now the same value of a passed in from the main function
int B = *b; // B is now the same value of b passed in from the main function
*result = B + A;
}
int main()
{
int x = 5;
int y = 10;
int sum = 0;
add(&x, &y, &sum);
// 'sum' now is 15
return 0;
}
Windows 体系结构Windows Architecture
简介
本模块介绍 Windows 架构以及 Windows 进程和应用程序内部发生的事情。
Windows 架构
运行 Windows 操作系统的机器内的处理器可以在两种不同的模式下运行:用户模式和内核模式。应用程序在用户模式下运行,操作系统组件在内核模式下运行。当应用程序想要完成一项任务(例如创建文件)时,它无法自行完成。唯一可以完成任务的实体是内核,因此应用程序必须遵循特定的函数调用流程。下图显示了此流程的高级版本。
- 用户进程 - 用户执行的程序/应用程序,例如记事本、Google Chrome 或 Microsoft Word。
- 子系统 DLL - 包含由用户进程调用的 API 函数的 DLL。例如,
kernel32.dll
导出 CreateFile Windows API (WinAPI) 函数,其他常见子系统 DLL 包括ntdll.dll
、advapi32.dll
和user32.dll
。 - Ntdll.dll - 系统范围的 DLL,是用户模式下可用的最低层。这是一个特殊的 DLL,用于创建从用户模式到内核模式的转换。这通常称为本机 API 或 NTAPI。
- 执行内核 - 这就是所谓的 Windows 内核,它调用内核模式中可用的其他驱动程序和模块来完成任务。Windows 内核部分存储在“C:WindowsSystem32”下名为“ntoskrnl.exe”的文件中。
函数调用流程
下图显示了创建文件的应用程序示例。首先,用户应用程序调用 CreateFile
WinAPI 函数,该函数在 kernel32.dll
中可用。Kernel32.dll
是一个关键的 DLL,它将应用程序暴露给 WinAPI,因此可以看到大多数应用程序都加载了它。接下来, CreateFile
调用其等效的 NTAPI 函数 NtCreateFile
,该函数通过 ntdll.dll
提供。Ntdll.dll
然后执行汇编 sysenter
(x86) 或 syscall
(x64) 指令,将执行转移到内核模式。然后使用内核 NtCreateFile
函数调用内核驱动程序和模块来执行请求的任务。
函数调用流程示例
此示例显示了通过调试器发生的函数调用流程。这是通过将调试器附加到通过 CreateFileW
Windows API 创建文件的二进制文件来完成的。
用户应用程序调用 CreateFileW
WinAPI。
接下来,“CreateFileW”调用其等效的 NTAPI 函数“NtCreateFile”。
最后,“NtCreateFile”函数使用“syscall”汇编指令从用户模式转换到内核模式。然后内核将创建该文件。
直接调用本机 API (NTAPI)
需要注意的是,应用程序可以直接调用系统调用(即 NTDLL 函数),而无需通过 Windows API。Windows API 只是充当本机 API 的包装器。话虽如此,本机 API 更难使用,因为它没有被 Microsoft 正式记录。此外,Microsoft 建议不要使用本机 API 函数,因为它们可能随时更改,而不会发出警告。
未来的模块将探讨直接调用本机 API 的好处。
原文始发于微信公众号(影域实验室):《从0-1开始学免杀》系列第一节-非卖课!!!
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论