0%
毅种循环

模块 4 - 编程基础

恶意软件开发课程 - 编程基础


模块 4 - 编程基础#

[!IMPORTANT] 本知识库声明

本知识库由本人整理自互联网 MalDev Academy 泄露资源,并由本人手动翻译为中文,过程中增加了大量关键技术提示实践心得

  1. 内容完整性:并未修改任何核心代码与技术逻辑,仅做汉化与注释加强。
  2. 版权归属:原始知识产权归原作者/官方所有。
  3. 支持正版:本仓库仅供内部学习交流,如果您有经济能力,请务必支持正版课程
  4. 权利申诉:如相关内容侵犯了您的权益,请联系我,我将立即核实并删除。

编程基础#

简介#

如前所述,本课程要求具备 C 语言的基础理解作为先决条件。话虽如此,由于某些概念在整个课程中的重要性,这里将提及这些概念。

💡 初学者提示:本模块的重要性

即使您已经了解 C 语言,本模块也值得仔细学习,因为它涵盖了:

  • Windows API 编程中常用的模式
  • 结构体的高级使用
  • 位运算操作(加密和混淆的基础)

这些是恶意软件开发的核心概念!

结构体 (Structures)#

结构体 (Struct) 是用户定义的数据类型,允许程序员将不同数据类型的相关数据项分组到一个单元中。结构体可用于存储与特定对象相关的数据。结构体有助于以易于访问和操作的方式组织大量相关数据。结构体中的每个项称为”成员 (member)“或”元素(element)“,这些术语在课程中互换使用。

在使用 Windows API 时,一个常见的情况是某些 API 需要填充好的结构体作为输入,而其他 API 将接受一个声明的结构体并填充它。下面是 THREADENTRY32 结构体的示例,此时无需理解这些成员的用途。

typedef struct tagTHREADENTRY32 {
  DWORD dwSize;              // 成员 1
  DWORD cntUsage;            // 成员 2
  DWORD th32ThreadID;
  DWORD th32OwnerProcessID;
  LONG  tpBasePri;
  LONG  tpDeltaPri;
  DWORD dwFlags;
} THREADENTRY32; 
c

📚 知识扩展:为什么 Windows API 大量使用结构体?

Windows API 是用 C 语言编写的,而 C 语言没有”类”的概念。结构体是 C 语言中组织复杂数据的最佳方式。

结构体的优势:

  1. 组织相关数据:把相关的信息放在一起
  2. 类型安全:编译器会检查类型错误
  3. 高效传参:通过指针传递结构体,避免复制大量数据

在恶意软件开发中的应用:

  • 进程信息 (PROCESS_INFORMATION)
  • 线程信息 (THREADENTRY32)
  • 内存信息 (MEMORY_BASIC_INFORMATION)
  • PE 文件头 (IMAGE_NT_HEADERS)

声明结构体#

本课程中使用的结构体通常使用 typedef 关键字声明,以给结构体一个别名。例如,下面的结构体使用名称 _STRUCTURE_NAME 创建,但 typedef 添加了两个其他名称,STRUCTURE_NAME*PSTRUCTURE_NAME

typedef struct _STRUCTURE_NAME {

  // 结构体元素

} STRUCTURE_NAME, *PSTRUCTURE_NAME;
c

STRUCTURE_NAME 别名引用结构体名称,而 PSTRUCTURE_NAME 表示指向该结构体的指针。Microsoft 通常使用 P 前缀来表示指针类型。

💡 初学者提示:理解 typedef 和命名约定

Windows 命名约定:

  • _STRUCTURE_NAME - 结构体的原始名称(带下划线)
  • STRUCTURE_NAME - 方便使用的别名
  • PSTRUCTURE_NAME - 指向结构体的指针类型(P = Pointer)

为什么有三个名字?

// 使用原始名称(较少使用)
struct _STRUCTURE_NAME myStruct;

// 使用别名(常用)
STRUCTURE_NAME myStruct;

// 使用指针别名(最常用)
PSTRUCTURE_NAME pMyStruct;
c

实际例子:

// Windows 定义
typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;
    HANDLE hThread;
    DWORD dwProcessId;
    DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION;

// 使用方式
PROCESS_INFORMATION pi;          // 结构体变量
PPROCESS_INFORMATION pPi = π  // 指向结构体的指针
c

初始化结构体#

初始化结构体将根据是初始化实际的结构体类型还是指向结构体的指针而有所不同。继续上一个示例,使用 _STRUCTURE_NAMESTRUCTURE_NAME 初始化结构体是相同的,如下所示。

STRUCTURE_NAME    struct1 = { 0 };  // '{ 0 }' 部分用于将 struct1 的所有元素初始化为零
// 或
_STRUCTURE_NAME   struct2 = { 0 };  // '{ 0 }' 部分用于将 struct2 的所有元素初始化为零
c

初始化结构体指针 PSTRUCTURE_NAME 时则不同。

PSTRUCTURE_NAME structpointer = NULL;
c

💡 初学者提示:为什么要初始化为 { 0 } 或 NULL?

未初始化的变量是危险的!

int x;  // x 包含垃圾值(内存中残留的随机数据)
c

正确的初始化:

// 结构体初始化为零
STRUCTURE_NAME s1 = { 0 };  // 所有成员都是 0

// 指针初始化为 NULL
PSTRUCTURE_NAME p1 = NULL;  // 指针指向空地址
c

为什么这很重要?

  1. 避免未定义行为:使用未初始化的数据可能导致程序崩溃
  2. 安全性:垃圾数据可能泄露敏感信息
  3. 调试友好:初始化的值更容易识别和调试

初始化和访问结构体成员#

结构体的成员可以直接通过结构体初始化,也可以间接通过指向结构体的指针初始化。在下面的示例中,结构体 struct1 有两个成员,IDAge,通过点运算符 (.) 直接初始化。

typedef struct _STRUCTURE_NAME {
  int ID;
  int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;

STRUCTURE_NAME struct1 = { 0 }; // 将 struct1 的所有元素初始化为零
struct1.ID   = 1470;   // 初始化 ID 元素
struct1.Age  = 34;     // 初始化 Age 元素
c

另一种初始化成员的方法是使用指定初始化器语法,可以指定要初始化结构体的哪些成员。

typedef struct _STRUCTURE_NAME {
  int ID;
  int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;

STRUCTURE_NAME struct1 = { .ID = 1470, .Age = 34 }; // 同时初始化 ID 和 Age 元素
c

另一方面,通过指针访问和初始化结构体是通过箭头运算符 (->) 完成的。

typedef struct _STRUCTURE_NAME {
  int ID;
  int Age;
} STRUCTURE_NAME, *PSTRUCTURE_NAME;

STRUCTURE_NAME struct1 = { .ID = 1470, .Age = 34 };

PSTRUCTURE_NAME structpointer = &struct1; // structpointer 是指向 'struct1' 结构体的指针

// 更新 ID 成员
structpointer->ID = 8765;
printf("结构体的 ID 成员现在是:%d \n", structpointer->ID);
c

箭头运算符可以转换为点格式。例如,structpointer->ID 等同于 (*structpointer).ID。也就是说,structurepointer 被解引用,然后直接访问。

💡 初学者提示:点 (.) 和箭头 (->) 运算符的区别

简单规则:

  • 使用变量时,用点 (.)
  • 使用指针时,用箭头 (->)

示例对比:

STRUCTURE_NAME s;      // 变量
PSTRUCTURE_NAME ps;    // 指针

s.ID = 100;            // 变量用点
ps->ID = 200;          // 指针用箭头

// 箭头运算符实际上是这个的简写:
(*ps).ID = 200;        // 先解引用,再用点
c

记忆技巧:

  • - 直接访问
  • 箭头 - 指针”指向”成员

为什么要使用指针? 在 Windows API 中,通常传递指针而不是整个结构体:

// 高效:只传递地址(8字节)
BOOL CreateProcess(..., PPROCESS_INFORMATION lpProcessInformation);

// 低效:复制整个结构体(可能几十或上百字节)
// Windows API 不这样设计
c

枚举 (Enumeration)#

枚举 (enum) 数据类型用于定义一组命名常量。要创建枚举,使用 enum 关键字,后跟枚举的名称和标识符列表,每个标识符代表一个命名常量。编译器自动为常量赋值,从 0 开始,每个后续常量增加 1。在本课程中,枚举可以表示特定数据的状态、错误代码或返回值。

枚举的一个示例是包含 7 个常量的”Weekdays”列表。在下面的示例中,Monday 的值为 0,Tuesday 的值为 1,依此类推。重要的是要注意,枚举列表不能使用点 (.) 运算符修改或访问。相反,每个元素都使用其命名常量值直接访问。

📚 知识扩展:枚举在恶意软件开发中的应用

枚举让代码更可读、更安全:

不使用枚举(不好的做法):

int injectionType = 1;  // 1 是什么意思?谁知道!

if (injectionType == 1) {  // 魔法数字,难以理解
    // DLL 注入
}
c

使用枚举(好的做法):

enum InjectionType {
    INJECTION_DLL = 0,
    INJECTION_SHELLCODE = 1,
    INJECTION_REFLECTIVE = 2,
    INJECTION_PROCESS_HOLLOWING = 3
};

enum InjectionType type = INJECTION_DLL;  // 清晰明了!

if (type == INJECTION_DLL) {
    // DLL 注入
}
c

Windows API 中的枚举示例:

// 内存保护标志
enum MemoryProtection {
    PAGE_NOACCESS = 0x01,
    PAGE_READONLY = 0x02,
    PAGE_READWRITE = 0x04,
    PAGE_EXECUTE = 0x10,
    PAGE_EXECUTE_READ = 0x20,
    PAGE_EXECUTE_READWRITE = 0x40
};
c

联合体 (Union)#

在 C 编程语言中,联合体 (Union) 是一种数据类型,允许在同一内存位置存储不同的数据类型。联合体提供了一种有效的方式来使用单个内存位置实现多种用途。联合体并不常用,但可以在 Windows 定义的结构体中看到。下面的代码说明了如何在 C 中定义联合体:

union ExampleUnion {
   int    IntegerVar;
   char   CharVar;
   float  FloatVar;
};
c

ExampleUnion 可以在同一内存位置存储 charintfloat 数据类型。要访问 C 中联合体的成员,可以使用点运算符,类似于用于结构体的运算符。

重要的是要注意,在联合体中,为任何成员赋新值都会改变所有其他成员的值,因为它们共享同一内存位置来存储数据。此外,为联合体分配的内存等于其最大成员的大小。

💡 初学者提示:联合体 vs 结构体

关键区别:

结构体 (Struct):

struct MyStruct {
    int a;     // 地址: 0x1000
    int b;     // 地址: 0x1004(不同的内存位置)
    int c;     // 地址: 0x1008
};
// 总大小: 12 字节 (3 个 int)
c

联合体 (Union):

union MyUnion {
    int a;     // 地址: 0x1000
    int b;     // 地址: 0x1000(相同的内存位置!)
    int c;     // 地址: 0x1000
};
// 总大小: 4 字节 (最大成员的大小)
c

实际例子:

union Data {
    int i;
    float f;
    char str[20];
};

union Data data;
data.i = 10;
printf("%d\n", data.i);  // 输出: 10

data.f = 220.5;          // 覆盖了 i 的值!
printf("%f\n", data.f);  // 输出: 220.5
printf("%d\n", data.i);  // 输出: 垃圾值(因为被 float 覆盖了)
c

联合体的用途:

  1. 节省内存:当多个变量不会同时使用时
  2. 类型转换:查看同一数据的不同解释
  3. Windows 结构体:某些 Windows 结构体使用联合体提供灵活性

位运算符 (Bitwise Operators)#

位运算符是操作二进制值的各个位的运算符,对每个对应的位位置执行操作。位运算符如下所示:

  • 右移 (>>)
  • 左移 (<<)
  • 位或 (|)
  • 位与 (&)
  • 位异或 (^)
  • 位非 (~)

💡 初学者提示:为什么位运算在恶意软件开发中重要?

位运算是许多高级技术的基础:

  1. 加密算法:XOR 加密
  2. 数据混淆:隐藏字符串和数据
  3. 标志操作:Windows API 参数
  4. 性能优化:位运算比算术运算快

Windows API 中的实际应用:

// 组合内存保护标志
DWORD protection = PAGE_EXECUTE_READ | PAGE_GUARD;

// 检查是否包含某个标志
if (protection & PAGE_EXECUTE) {
    printf("可执行的内存区域\n");
}
c

右移和左移#

右移 (>>) 和左移 (<<) 运算符分别用于将二进制数的位向右和向左移动指定数量的位置。

右移会丢弃最右边指定数量的位,并从左侧插入相同数量的零位。例如,下图显示 10100111 右移 2 位,变为 00101001

图片
图片

另一方面,左移会丢弃最左边的位,并从右侧插入相同数量的零位。例如,下图显示 10100111 左移 2 位,变为 10011100

图片
图片

📚 知识扩展:移位操作的实际应用

快速乘除法:

int x = 5;
int doubled = x << 1;   // 等同于 x * 2 = 10
int quadrupled = x << 2; // 等同于 x * 4 = 20

int halved = x >> 1;     // 等同于 x / 2 = 2
c

提取字节:

DWORD value = 0x12345678;
BYTE byte0 = value & 0xFF;           // 0x78
BYTE byte1 = (value >> 8) & 0xFF;    // 0x56
BYTE byte2 = (value >> 16) & 0xFF;   // 0x34
BYTE byte3 = (value >> 24) & 0xFF;   // 0x12
c

构建值:

WORD high = 0x1234;
WORD low = 0x5678;
DWORD combined = (high << 16) | low;  // 0x12345678
c

位或 (Bitwise OR)#

位或运算是一种在位级别涉及两个二进制值的逻辑运算。它将第一个操作数的每一位与第二个操作数的对应位进行评估,生成一个新的二进制值。新的二进制值在原始值的对应位中有一个或两个都是 1 的任何位位置包含 1。

下表表示所有可能输入位的位或输出。

图片
图片

💡 初学者提示:位或 (OR) 的作用

简单理解:只要有一个 1,结果就是 1

0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
c

实际应用 - 组合标志:

// Windows API 中常见的用法
DWORD flags = PAGE_EXECUTE | PAGE_READWRITE;
// 0x20 (PAGE_EXECUTE) | 0x04 (PAGE_READWRITE)
// 二进制: 00100000 | 00000100 = 00100100

// 添加更多标志
flags = flags | PAGE_GUARD;
// 或者简写:
flags |= PAGE_GUARD;
c

位与 (Bitwise AND)#

位与运算是一种在位级别涉及两个二进制值的逻辑运算。此操作仅在两个输入操作数的对应位都是 1 的情况下将新二进制值的位设置为 1。

下表表示所有可能输入位的位与输出。

图片
图片

💡 初学者提示:位与 (AND) 的作用

简单理解:两个都是 1,结果才是 1

0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
c

实际应用 - 检查标志:

DWORD protection = PAGE_EXECUTE_READWRITE;

// 检查是否可执行
if (protection & PAGE_EXECUTE) {
    printf("这个内存区域可以执行代码\n");
}

// 提取特定位
BYTE value = 0b10110010;
BYTE lowerNibble = value & 0x0F;  // 0b00000010 = 2
c

掩码 (Mask) 操作:

// 只保留某些位,清除其他位
DWORD addr = 0x12345678;
DWORD pageAligned = addr & 0xFFFFF000;  // 页面对齐(清除低12位)
c

位异或 (Bitwise XOR)#

位异或运算(也称为异或)是一种在位级别涉及两个二进制值的逻辑运算。如果只有一个位是 1,则每个位置的结果为 1。相反,如果两个位都是 0 或 1,则输出为 0。

下表表示所有可能输入位的位异或输出。

图片
图片

💡 初学者提示:位异或 (XOR) - 加密的基础

简单理解:两个位不同时结果为 1,相同为 0

0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
c

XOR 的神奇特性:

A ^ B ^ B = A  // XOR 两次相同的值,得到原始值
c

实际应用 - XOR 加密:

// 加密
unsigned char data = 'A';     // 0x41
unsigned char key = 0x55;
unsigned char encrypted = data ^ key;  // 0x14

// 解密(使用相同的密钥)
unsigned char decrypted = encrypted ^ key;  // 0x41 = 'A'

printf("原始: %c\n", data);        // A
printf("加密后: %02x\n", encrypted); // 14
printf("解密后: %c\n", decrypted);    // A
c

为什么 XOR 重要?

  1. 简单高效:只需一个操作就能加密和解密
  2. 可逆性:XOR 两次得到原值
  3. 广泛应用:几乎所有恶意软件都使用 XOR 来隐藏数据

提示: 后续模块会深入学习 XOR 加密!

位非 (Bitwise NOT)#

位非运算接受一个二进制数并翻转其所有位。换句话说,它将所有 0 更改为 1,将所有 1 更改为 0。下表表示所有可能输入位的位非输出。

图片
图片

💡 初学者提示:位非 (NOT) 的作用

简单理解:翻转所有位

~0 = 1
~1 = 0
c

实际例子:

unsigned char value = 0b10110010;  // 178
unsigned char inverted = ~value;    // 0b01001101 = 77

// 注意:对于有符号整数,结果可能不是你预期的
int x = 5;          // 0b00000101
int y = ~x;         // 0b11111010 = -6 (补码表示)
c

应用 - 创建掩码:

// 清除某个标志
DWORD flags = PAGE_EXECUTE_READWRITE | PAGE_GUARD;
flags = flags & ~PAGE_GUARD;  // 移除 PAGE_GUARD 标志
c

按值传递 (Passing By Value)#

按值传递是将参数传递给函数的一种方法,其中参数是对象值的副本。这意味着当参数按值传递时,对象的值被复制,函数只能修改对象值的本地副本,而不能修改原始对象本身。

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 和 y 按值传递

   return 0;
}
c

💡 初学者提示:按值传递的特点

简单理解:传递的是”复印件”

void modifyValue(int num) {
    num = 100;  // 只修改了副本
    printf("函数内: %d\n", num);  // 100
}

int main() {
    int x = 5;
    modifyValue(x);
    printf("函数外: %d\n", x);    // 仍然是 5!
    return 0;
}
c

优点:

  • ✅ 安全,不会意外修改原始数据
  • ✅ 简单易懂

缺点:

  • ❌ 复制大结构体时效率低
  • ❌ 无法修改原始值(有时这是需要的)

按引用传递 (Passing By Reference)#

按引用传递是将参数传递给函数的一种方法,其中参数是指向对象的指针,而不是对象值的副本。这意味着当参数按引用传递时,传递的是对象的内存地址而不是对象的值。然后函数可以直接访问和修改对象,而无需创建对象的本地副本。

💡 初学者提示:按引用传递(使用指针)

简单理解:传递的是”地址”,可以直接修改原始数据

符号说明:

  • & - 取地址符(Address-of operator)
  • * - 解引用符(Dereference operator)

详细例子:

void increment(int *num) {
    *num = *num + 1;  // 通过指针修改原始值
}

int main() {
    int x = 5;
    printf("之前: %d\n", x);  // 5
    
    increment(&x);  // 传递 x 的地址
    
    printf("之后: %d\n", x);  // 6(原始值被修改了!)
    return 0;
}
c

Windows API 中的应用:

何时使用按引用传递?

  1. 需要修改原始值
  2. 返回多个值
  3. 传递大结构体(避免复制)
  4. 与 Windows API 交互(大多数情况)

记忆口诀:

  • & - “我要告诉你地址”
  • * - “我要访问地址里的内容”

🎯 本模块学习要点

必须掌握的概念:

  1. ✅ 结构体的声明、初始化和访问
  2. ✅ 理解 .-> 运算符的使用
  3. ✅ 枚举的用途和优势
  4. ✅ 六种位运算符及其应用
  5. ✅ XOR 运算在加密中的应用
  6. ✅ 按值传递 vs 按引用传递
  7. ✅ 指针的基本使用(&*

实践建议:

  1. 编写使用结构体的小程序
  2. 练习位运算,特别是 XOR 加密
  3. 编写使用指针的函数
  4. 查看 Windows API 文档,注意参数类型

为什么这些概念重要?

  • 结构体:Windows API 的核心
  • 位运算:加密、混淆、标志操作
  • 指针:内存操作、API 调用的基础

下一步: 学习 Windows 架构和内存管理