CppGuide社区 CppGuide社区
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
首页
  • 最新谷歌C++风格指南(含C++17/20)
  • C++17详解
  • C++20完全指南
  • C++23快速入门
  • C++语言面试问题集锦
  • 🔥C/C++后端开发常见面试题解析 (opens new window)
  • 网络编程面试题 (opens new window)
  • 网络编程面试题 答案详解 (opens new window)
  • 聊聊WebServer作面试项目那些事儿 (opens new window)
  • 字节跳动面试官现身说 (opens new window)
  • 技术简历指南 (opens new window)
  • 🔥交易系统开发岗位求职与面试指南 (opens new window)
  • 第1章 高频C++11重难点知识解析
  • 第2章 Linux GDB高级调试指南
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 高性能网络通信协议设计精要
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 后端服务重要模块设计探索
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 源码分析系列

    • leveldb源码分析
    • libevent源码分析
    • Memcached源码分析
    • TeamTalk源码分析
    • 优质源码分享 (opens new window)
    • 🔥远程控制软件gh0st源码分析
  • 从零手写C++项目系列

    • C++游戏编程入门(零基础学C++)
    • 🔥使用C++17从零开发一个调试器 (opens new window)
    • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
    • 🔥使用C++从零写一个C语言编译器 (opens new window)
    • 从零用C语言写一个Redis
  • Windows 10系统编程
  • 🔥Windows Native API编程
  • 🔥Windows x64 ShellCode入门教程
  • 🔥Windows Shellcode实战
  • Go语言特性

    • Go开发实用指南
    • Go系统接口编程
    • 高效Go并发编程
    • Go性能调优
    • Go项目架构设计
  • Go项目实战

    • 使用Go从零开发一个数据库
    • 🔥使用Go从零开发一个编译器 (opens new window)
    • 🔥使用Go从零开发一个解释器 (opens new window)
    • 🔥用Go从零写一个编排器(类Kubernetes) (opens new window)
  • Rust编程

    • Rust编程指南
  • 数据库

    • SQL零基础指南
    • MySQL开发与调试指南
  • Linux内核

    • 心中的内核 —— 在阅读内核代码之前先理解内核
    • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
    • TCP源码实现超详细注释版.pdf (opens new window)
GitHub (opens new window)
  • Windows Shellcode实战 专栏说明
  • shellcodisation在病毒学中的应用
    • 1.1 背景定义
    • 1.2 恶意代码的选定目标
      • 1.2.1 避开基于特征码识别的反病毒软件检测
      • 1.2.2 在受感染主机上留下尽可能少的痕迹
      • 图3:恶意负载不写入硬盘驱动器的执行过程
      • 1.2.3 嵌入合法程序以创建特洛伊木马
      • 1.2.4 拦截使用受感染计算机的用户的私人数据
      • 1.2.4.1 代码注入
      • 1.2.4.2 API hooking
      • 1.2.5 加密自身以防止人工分析
      • 图6:加密恶意软件的执行过程
    • 1.3 基于可执行文件的实现
      • 1.3.1 实现多态性
      • 1.3.2 仅在内存中执行
      • 1.3.3 感染其他可执行文件
      • 1.3.4 执行代码注入
      • 1.3.5 实现真正的加密
      • 1.3.6 小结
    • 1.4 基于shellcode的实现
      • 1.4.1 实现多态性
      • 1.4.2 仅在内存中执行
      • 1.4.3 感染其他可执行文件
      • 1.4.4 执行代码注入
      • 1.4.5 实现真正的加密
      • 1.4.6 小结
  • 编写shellcode
  • WiShMaster:shellcodisation过程
  • 使用WiShMaster开发应用程序
  • 未来工作
  • 总结
目录

shellcodisation在病毒学中的应用

# 1 shellcodisation在病毒学中的应用

# 1.1 背景定义

通常情况下,恶意代码(malicious code)会尝试完成多项操作:避开反病毒软件(antivirus)的检测、传播到其他主机或可执行文件(executable)、执行特定操作(例如捕获用户私人数据、在系统中开启后门(backdoor)等);它们可能会采用多种技术来实现这些目标。

本部分将选取几个特定目标,解释其实现方式,并尝试说明在实现过程中可能遇到的困难。

本文中,我们仅考虑针对Windows系统的恶意程序(malicious program),尽管文中介绍的部分技术可能适用于其他操作系统(operating system)。

# 1.2 恶意代码的选定目标

为了给出具体示例,我们假设要创建的恶意代码能够实现以下功能:

  • 避开基于特征码识别(signature identification)的反病毒软件检测;
  • 在受感染主机(infected host)上留下尽可能少的痕迹;
  • 嵌入合法程序(legitimate program)以创建特洛伊木马(Trojan);
  • 拦截使用受感染计算机的用户的私人数据(private data);
  • 加密自身以防止(或复杂化)人工分析(manual analysis)。

我们将选择一些具体技术来实现这些目标。需要注意的是,当然还有许多其他解决方案,但这些技术是实际恶意软件(malware)中常用技术的良好代表。

# 1.2.1 避开基于特征码识别的反病毒软件检测

为了避开检测,恶意代码可以实现多态性(polymorphism)。需要说明的是,多态恶意程序由两部分组成:

  • 真正的恶意负载(malicious payload),该部分经过加密;
  • 解密部分(decryption part),位于病毒(virus)开头,负责解密恶意负载并将执行权转移给它。

每次感染时,用于加密恶意负载的密钥(key)都会更改,因此同一病毒的两个副本会具有完全不同的负载。

图1展示了未实现多态性的恶意代码。反病毒软件可以建立其识别特征码(identification signature),一旦该特征码被添加到反病毒数据库(antivirus database)中,该恶意代码就会被检测到。

img

图1:基于特征码检测恶意负载

图2展示了实现多态性的同一恶意代码的两个副本。由于存储在解密部分中的加密密钥不同,加密后的负载也完全不同。这使得针对恶意负载建立识别特征码变得非常困难。

img

图2:实现多态性的同一病毒的两个副本

这一机制可以防止针对恶意负载建立特征码识别,但反病毒软件仍可能针对解密部分建立特征码。为了避免这种情况,可以对解密部分使用变形性(metamorphism)技术。需要注意的是,这仅在自动传播(automated propagation)场景下是必需的。如果该恶意程序是为定向攻击(targeted attack)专门开发的,只需手动重写解密循环(decryption loop)即可。

实现变形性难度较大,且超出了本文的讨论范围。我们仅关注在恶意负载上实现多态性的能力。

需要注意的是,由于解密密钥可能存在于解密部分中,这种多态性无法保护恶意负载免受人工分析。加密过程可以依赖于简单的32位密钥异或(XOR)操作。

# 1.2.2 在受感染主机上留下尽可能少的痕迹

为了在受感染系统(infected system)上留下尽可能少的痕迹,我们选择为恶意代码添加仅在内存(memory)中执行的功能。以下是一个场景示例:

  • 一段代码在目标系统(targeted system)上运行,例如在缓冲区溢出(buffer overflow)漏洞利用(exploitation)之后;
  • 该代码连接回服务器(server),并将恶意负载直接下载到当前进程(process)的内存中;
  • 该代码将执行权转移给恶意负载,恶意负载在未被复制到硬盘驱动器(hard drive)的情况下执行。

图3总结了这一原理。

img

# 图3:恶意负载不写入硬盘驱动器的执行过程

# 1.2.3 嵌入合法程序以创建特洛伊木马

其目的是将恶意负载嵌入合法程序中,使得当有人使用该程序时,恶意负载能够被执行。当然,程序的正常行为不能受到干扰,以免用户察觉感染。

这种嵌入可以通过多种方式实现。我们考虑将恶意负载添加到主可执行文件(main executable)中的情况。为了尽量减少对PE头(PE header)的修改,恶意负载将被添加到可执行文件的末尾,即最后一个节(section)之后。

执行流程(execution flow)的重定向可以通过以下方式实现:在PE头中,将可执行文件的入口点(entry point)替换为恶意负载的入口点;然后在恶意负载中添加一个跳转指令(jump),使其在执行完成后跳转到原始入口点(original entry point)。

然而,这种解决方案意味着恶意负载会首先执行。具有代码模拟(code emulation)功能的反病毒软件在扫描受感染的可执行文件时,将能够模拟并分析该恶意负载。更好的解决方案是修补(patch)一些在受感染程序使用过程中可能会执行的指令。例如,如果目标程序是文本编辑器(text editor),可以修补用于保存编辑文档(edited document)的函数(function)。这样一来,反病毒软件在扫描受感染的可执行文件时,既不会执行也不会分析该恶意代码。当然,这种解决方案需要对目标可执行文件进行人工分析,以找到合适的指令,因此无法用于自动感染(automatic infection)。

图4展示了这些不同情况。左侧是原始可执行文件(original executable);中间是感染后的可执行文件,其中PE头中的可执行文件入口点已被替换为恶意负载的入口点;右侧是感染后的可执行文件,其中通过修补指令实现了执行流程的重定向。

img

图4:可执行文件的感染过程

# 1.2.4 拦截使用受感染计算机的用户的私人数据

进程会处理一些可能有价值的数据,而我们的恶意代码可以尝试拦截这些数据。然而,从操作系统的角度来看,我们的恶意代码只是一个普通进程。与其他进程一样,它有自己的内存空间(memory space),其与其他进程交互的能力受到操作系统安全模型(security model)的限制。如果想要拦截私人数据,就必须突破这些限制。

这可以通过多种方式实现:

  • 在内核层(kernel level)工作。Windows的安全模型设计如下:如果恶意代码能够将一些代码注入(inject)内核(kernel),它将能够分析所有输入输出(input and output),并捕获私人数据(如键盘敲击(keyboard stroke)、网络流量(network traffic)、文件系统访问(accesses to file system)等)。该解决方案的缺点是需要管理员权限(administrative privileges)。
  • 针对特定应用程序(specific application)。例如,如果想要捕获用户在网站上的凭据(credentials),可以为浏览器(browser)开发一个恶意插件(malicious plugin),该插件将分析所有网络流量并提取目标数据。然而,这种技术与特定应用程序绑定。
  • 使用代码注入(code injection)和应用程序编程接口挂钩(API hooking)。要拦截的数据存储在进程内存中的缓冲区(buffer)中,并作为函数调用(function call)的参数(argument)传递。例如,服务器有一个缓冲区,通过调用“recv”函数接收数据,还有一个缓冲区通过“send”函数发送数据。在这种解决方案中,恶意代码使用Windows应用程序编程接口(Windows API)的标准函数(standard function)将一些代码注入到其他进程中。注入的代码会在特定的导入函数(imported function)上安装挂钩(hook),以拦截对这些函数的每次调用。然后,它可以在调用前后分析参数(parameter),并查找有价值的数据。这种解决方案具有通用性,且不需要管理员权限。但需要注意的是,如果恶意代码在受限会话(limited session)中运行,它当然无法将代码注入到在另一个会话中运行的进程中。后续,我们将重点关注这种技术。

# 1.2.4.1 代码注入

代码注入可以通过多种方式实现:

  • 将要注入的代码构建到动态链接库(dll)中,然后将该动态链接库加载(load)到目标进程(targeted process)中并“执行”。动态链接库注入机制(dll injection mechanism)可以依赖多种技术(修改注册表(registry)、使用全局钩子(wide-system Hooks)、使用远程线程创建函数(CreateRemoteThread)等)[1]。
  • 通过使用Win32应用程序编程接口(Win32 API)的一些标准函数,将代码直接注入到远程进程(remote process)内存中:
    • 打开进程函数(OpenProcess):通过进程标识符(PID)指定进程,并打开一个具有特定权限的句柄(handle);
    • 远程内存分配函数(VirtualAllocEx):通过句柄指定进程,并在该进程中分配(allocate)内存;
    • 进程内存写入函数(WriteProcessMemory):将本地内存(local memory)中的数据复制到通过句柄指定的另一个进程的内存中;
    • 远程线程创建函数(CreateRemoteThread):通过句柄指定进程,并在该进程中创建一个线程(thread)。该函数特别需要传入所创建线程的执行地址(execution address)作为参数;
    • 句柄关闭函数(CloseHandle):关闭已打开的句柄。

图5总结了这些操作。

img

图5:使用VirtualAllocEx/WriteProcessMemory/CreateRemoteThread技术进行代码注入

这种技术允许在不将动态链接库写入硬盘驱动器的情况下执行代码。我们最终选择这种解决方案在恶意软件中实现代码注入。

需要注意的是,这种技术依赖于Windows内核提供的特定服务(specific services),而这些服务尤其受到个人防火墙(personal firewall)等防护程序(protection program)的监控。不过,有时可以通过一些小技巧绕过其中部分程序。这方面的内容超出了本文的讨论范围,后续将不再考虑。

# 1.2.4.2 API hooking

注入到目标进程中的代码随后可以修改内存,以拦截函数调用。这可以通过两种方式实现:

  • 代码可以修补导入地址表(Import Address Table)。导入地址表是一个包含所有导入函数地址的表,由Windows加载器(Windows loader)在进程创建时填充。对导入函数(即导入表(import table)中的函数)的每次调用都是通过该表中的地址进行的。因此,通过修补其中一些值,我们可以拦截所需的函数调用。
  • 第一种解决方案实施起来相当容易。但问题在于,Windows提供了另一种导入函数的方式。我们可以声明一个函数指针(function pointer),并在执行过程中通过调用加载库函数(LoadLibrary)和获取进程地址函数(GetProcAddress)来解析函数地址,而不是在代码中直接使用该函数(这会在导入表中创建相应的条目)。当然,在这种情况下,该函数在导入表或导入地址表中都没有对应的条目。

为了解决这个问题,我们可以直接用跳转到我们代码的指令修补函数的头部(header),而不是修补导入地址表。这种解决方案会带来许多问题:必须更改节的内存权限(memory rights)、必须计算指令对齐(instruction alignment)以保存被覆盖的指令(overwritten instructions)、必须重建栈(stack)等,但它的优点是无论解析机制(resolution mechanism)如何,都能始终正常工作。该解决方案的完整描述超出了本文的讨论范围。

# 1.2.5 加密自身以防止人工分析

其原理与多态性中使用的原理类似,但目标不同:多态性用于防范反病毒软件等执行的自动分析(automated analysis),而此处我们希望保护恶意负载免受人工分析。这带来了一些差异:

  • 必须使用真正的加密算法(real encryption algorithm)。简单的32位密钥异或操作远远不够。例如,我们可以使用256位密钥的高级加密标准(AES)。
  • 密钥不应与加密后的恶意代码存储在同一个文件中。例如,可以使用另一个可执行文件来解密恶意代码,或者从服务器获取密钥。

图6展示了这一场景。

img

# 图6:加密恶意软件的执行过程

当然,如果从解码器(decoder)中提取密钥,解密恶意代码会很容易。然而,我们需要同时获取这两部分才能解密恶意代码,如果它们通过不同的方式被引入目标系统,这可能会变得困难。如果仅捕获了解码器,它不包含任何有价值的信息;如果仅捕获了恶意代码,则无法对其进行解密。

这一原理可以进一步扩展:将密钥分成多个部分,并存储在多个位置。可以使用沙米尔秘密共享(Shamir's secret sharing)算法[2]来实现这一点。

# 1.3 基于可执行文件的实现

在本部分中,我们假设恶意代码是一个通过正常编写的一组C源文件编译生成的可执行文件“malware.exe”。本节仅简要概述在实现前面列出的功能时可能遇到的问题。

# 1.3.1 实现多态性

其目的是加密“malware.exe”中所有表征恶意负载的二进制数据(binary data):所有函数、已初始化数据(initialized data)和字符串(string)。主要问题是这些数据分散在整个可执行文件中,解密部分必须找到并解密每个数据块。由于这可能会变得非常复杂,因此很难在解密部分实现变形性。此外,PE文件的元数据(metadata)不能被加密,否则Windows将无法加载该可执行文件。

更好的解决方案是使用一个能够加密整个可执行文件的工具,例如加壳工具(packer)。使用知名的UPX[3]等公共加壳工具不一定是一个好的解决方案,因为生成的可执行文件具有一些非常特定的属性,可能会被反病毒软件识别。我们必须开发自己的加壳工具,以实现真正的多态性,这是一项相当艰巨的工作。

# 1.3.2 仅在内存中执行

在远程服务器上运行的代码能够将“malware.exe”复制到其地址空间(address space)中。但此时我们在内存中得到的是PE文件的副本,而不是可直接执行的映射可执行文件(mapped executable)。在跳转到“malware.exe”的入口点之前,我们必须完成Windows加载器通常执行的所有初始化工作:

  • 将节映射(map)到正确的地址,因为“malware.exe”可能包含硬编码地址(hardcoded address)。这可以通过使用内存分配函数(VirtualAlloc)来实现,该函数允许在参数中指定的地址分配内存。当然,如果该内存已经被分配,该函数将执行失败;
  • 解析导入函数(resolved imported functions):加载所需的库(required libraries)、查找所需的函数并更新导入地址表。需要注意的是,通过使用动态地址解析(dynamic address resolution)可以避免这一步骤。

这并不复杂,但需要一定的工作量。此外,获取“malware.exe”的代码段大小会显著增加,因此它可能无法作为缓冲区溢出漏洞利用中的shellcode使用。

# 1.3.3 感染其他可执行文件

其目的是将从“malware.exe”中提取的恶意负载添加到目标可执行文件(以下称为“target.exe”)中。

要执行感染,可以简单地尝试将“malware.exe”的节添加到“target.exe”的最后一个节之后。然而,该操作并不简单:

  • 必须修改受感染可执行文件的PE头的多个部分:节的数量、节表(section table)、镜像大小(size of image)等;

  • 由于“malware.exe”的代码可能包含硬编码地址,必须精心选择其首选加载地址(preferred load address),以便表征恶意负载的节在“malware.exe”进程和受感染的“target.exe”进程中都加载到相同的地址;

  • 添加代码中的导入表(importation table)不会被Windows加载器解析。因此,所需的库不会被加载,导入地址表也不会被填充。与上一节一样,解决方案可能是在“malware.exe”的编程过程中仅使用动态地址解析,但这很快会变得繁琐。

# 1.3.4 执行代码注入

我们总会遇到相同的问题:“malware.exe”必须映射到正确的地址,并且导入的函数必须被解析。

# 1.3.5 实现真正的加密

与多态性一样,最好的解决方案可能是使用特定的加壳工具,该工具将从服务器等位置获取密钥并解密恶意代码。同样,这是一项相当艰巨的工作。

# 1.3.6 小结

总而言之,当然可以为可执行文件添加这些功能,但这需要大量的工作。

这些困难源于可执行文件的几个特性:

  • 代码和数据分散在可执行文件中;
  • 在开始执行之前,进程需要一些初始化工作(通常由Windows加载器完成):将节映射到内存中的正确位置、加载所需的库、填充导入地址表等;
  • 代码包含硬编码地址,因此节必须映射到正确的地址。否则,必须使用重定位表(relocation table)来修补每个硬编码地址。

因此,如果代码仅由一个代码块组成、能够初始化地址空间且不包含硬编码地址(即代码是shellcode),那么所有这些功能都可以更容易地实现。

# 1.4 基于shellcode的实现

现在,让我们假设我们的恶意代码是一个shellcode,即一个完全自主的代码块。如果我们将执行权转移到其第一个字节,该shellcode将执行与正常可执行文件完全相同的操作。

前面列出的功能的实现将变得非常简单。

# 1.4.1 实现多态性

解密部分将成为一个对shellcode执行解密操作的循环。如果加密算法是简单的32位密钥异或操作,那么解密循环将非常简单:

// 多态负载的解密循环(Decryption loop of the polymorphic payload)
UINT uiXorKey = 0xaabbccdd;
UINT i = 0;
UINT j = 0;

/* Decrypt shellcode */
for (i = 0, j = 0; i < sizeof(bShellcode) - 1; i++, j = (j + 1) & 0x3) {
    bShellcode[i] = bShellcode[i] ^ ((CHAR *)&uiXorKey)[j];
}

/* Jump on shellcode */
((VOID (*)(VOID))&bShellcode)();
1
2
3
4
5
6
7
8
9
10
11
12

# 1.4.2 仅在内存中执行

根据定义,shellcode能够在任何进程的任何地址执行。在服务器上运行的外部代码只需将shellcode从远程位置获取到已分配的缓冲区中,并跳转到该缓冲区的开头即可。无需进行任何初始化,因为所有工作都由shellcode自身处理。此外,不需要将shellcode映射到特定地址。

# 1.4.3 感染其他可执行文件

执行简单的感染变得非常容易。例如,可以将shellcode复制到最后一个节中(该节的大小会相应增加)。

# 1.4.4 执行代码注入

在另一个进程中执行shellcode变得非常容易:只需在其他进程的地址空间中分配一些内存、复制shellcode,并创建一个新线程,将起始地址设置为已分配内存的第一个字节。

# 1.4.5 实现真正的加密

真正加密的实现基于与多态性相同的原理。唯一的区别是,加密将不再是简单的32位密钥异或操作,而是高级加密标准(AES)等“真正的”加密算法。

# 1.4.6 小结

我们可以看到,如果恶意代码是shellcode而不是可执行文件,这些功能的实现会大大简化。现在的问题是找到一种从一组C源文件生成shellcode的方法。

Windows Shellcode实战 专栏说明
编写shellcode

← Windows Shellcode实战 专栏说明 编写shellcode→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2026 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式