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在病毒学中的应用
  • 编写shellcode
  • WiShMaster:shellcodisation过程
    • 3.1 工具介绍
    • 3.2 开发进展
      • 3.2.1 WiShMaster 版本1
      • 3.2.2 WiShMaster 版本2
    • 3.3 shellcodisation过程
      • 3.3.1 概述(Overview)
      • 3.3.2 “分析”步骤说明
      • 3.3.3 “获取全局变量大小”步骤说明
      • 3.3.4 “生成”步骤说明
      • 3.3.5 “自定义”步骤说明
      • 3.3.6 “集成”步骤说明
      • 3.3.7 WiShMaster中shellcodisation的实现
      • 3.3.7.1 自动模式
      • 3.3.7.2 交互模式
      • 3.3.7.3 脚本模式
      • 3.3.7.4 钩子函数(Hook functions)
      • 3.3.8 shellcode初始化
      • 3.3.8.1 查找GLOBAL_DATA的地址
      • 3.3.8.2 查找内部函数的地址
      • 3.3.8.3 查找导入函数的地址
      • 查找kernel32.dll的地址(Finding the address of kernel32.dll)
      • 查找LoadLibrary和GetProcAddress的地址(Finding the addresses of LoadLibrary and GetProcAddress)
      • 查找导入函数的地址(Finding the addresses of imported functions)
      • 3.3.8.4 shellcode初始化总结
  • 使用WiShMaster开发应用程序
  • 未来工作
  • 总结
目录

WiShMaster:shellcodisation过程

# 3 WiShMaster:shellcodisation过程

# 3.1 工具介绍

WiShMaster 是一款基于前文所述原理自动生成shellcode的工具。它接收一组“正常”编写的C源文件作为输入(这些源文件编译后通常会生成可执行文件),并输出生成的shellcode。如果将执行权转移到其第一个字节,该shellcode将执行与正常可执行文件完全相同的操作。这种转换后文称为“shellcodisation”。

# 3.2 开发进展

# 3.2.1 WiShMaster 版本1

WiShMaster 的第一个版本已开发完成,并在我的个人网站上发布了一年[5]。这是一个使用C#开发的图形化应用程序(graphical application)。

该版本相当稳定,但存在几个限制:

  • 作为图形化应用程序,WiShMaster 具有较好的用户友好性(user-friendly),但无法通过脚本(script)执行,也不能与脚本语言(script language)进行交互;
  • 它使用正则表达式(regular expression)解析源代码(source code),因此源代码必须遵循一些语法规则(syntax rule)才能被成功分析;
  • 代码中最多只能包含一个全局变量(global variable);
  • 该工具可以进行少量扩展,但附加模块(additional module)必须使用C#编写,而在安全社区(security community)中,C#的普及程度不如Python或Ruby等脚本语言。

# 3.2.2 WiShMaster 版本2

WiShMaster 版本2正在积极开发中。该版本修复了前文列举的问题:

  • WiShMaster 版本2是一款使用Python编写的控制台应用程序(console application),shellcodisation过程可以通过脚本执行;
  • 用户可以在shellcodisation过程的任何步骤进行干预,查看结果并纠正可能出现的错误;
  • 大幅减少了使用正则表达式解析源代码的操作,从而移除了大部分对C语言语法的限制;
  • 不再限制全局变量的数量。

本文仅聚焦于该版本。

# 3.3 shellcodisation过程

# 3.3.1 概述(Overview)

WiShMaster 完成的shellcodisation过程分为6个步骤:

  • 分析(Analysis):目的是识别内部函数(internal function)、导入函数(imported function)、全局变量和字符串(string);
  • 获取全局变量大小(Obtain the size of global variables):可选步骤,WiShMaster 自动获取可能存在的全局变量的大小(这些全局变量必须包含在GLOBAL_DATA中);
  • 创建环境(Create environment):WiShMaster 创建GLOBAL_DATA结构体(structure)和各种宏(macro)的定义,然后在临时目录(temporary directory)中创建所有源文件的副本,并对代码进行前文所述的小幅修改;
  • 生成(Generate):WiShMaster 编译修补后的源代码(patched source),从生成的可执行文件中提取二进制数据(binary data),最终生成shellcode。该shellcode可能包含特殊值,例如,如果shellcode需要连接服务器(server),则会包含IP地址(IP address)和端口(port),这些值目前填充为通用值(generic value);
  • 自定义(Customize):WiShMaster 对shellcode执行一些转换操作,例如将通用值替换为用户提供的值,或对shellcode进行加密(encrypt);
  • 集成(Integrate):WiShMaster 将shellcode“集成”到最终项目中,可能是将shellcode简单复制到特定目录,也可能将其转换为C数组(C array)并转储(dump)到C头文件(C header file)中。

# 3.3.2 “分析”步骤说明

该步骤的目的是识别内部函数、导入函数、全局变量和字符串。实现这一目标的第一种方法是使用正则表达式解析所有源文件,查找这些元素。这是WiShMaster 版本1采用的解决方案,但该方案存在两个问题:源文件列表必须由用户手动选择;最重要的是,源代码必须遵循一些语法规则。

在版本2中,WiShMaster 依赖于微软编译器(Microsoft compiler)生成的一个特殊文件:浏览文件(browse file)。

浏览文件包含编译器找到的所有符号(symbol),以及这些符号在源文件中定义和使用的不同位置(文件名和行号)。

浏览文件的生成分为两个步骤:

  • 首先,必须使用“FR”选项调用编译器,然后编译器会为每个解析的源文件生成一个“.sbr”文件;
  • 工具“bscmake”根据所有“.sbr”文件生成最终的浏览文件(“.bsc”)。

可以使用“Microsoft Visual C++ Browser Toolkit”解析该浏览文件,该工具可从微软网站[6]下载。

通过解析该文件,我们将获得:

  • 项目中包含的文件列表;
  • 内部函数列表;
  • 导入函数列表;
  • 全局变量列表。

这看似可行,但实际上该分析并不完整。一些信息(如字符串列表或内部函数的返回类型(return type))缺失。

因此,WiShMaster 仍然需要使用正则表达式解析代码。如果代码的编写方式特殊,即使从C语言语法角度来看是正确的,WiShMaster 也可能无法找到某些信息。

最严格的约定是,所有字符串必须放在宏“STR”中:

宏STR的使用(Use of the macro STR)

PrintMsg(LOG_LEVEL_TRACE, STR("File successfully read: %s"), pData);
1

宏STR的定义如下:

宏STR的定义(Definition of the macro STR)

#define STR(x)         x
1

# 3.3.3 “获取全局变量大小”步骤说明

WiShMaster 必须知道全局变量的大小,才能在GLOBAL_DATA结构体中预留相应的空间。通过“分析”步骤,我们获得了全局变量列表及其定义位置(文件和行号)。我们可以尝试通过正则表达式分析该定义来计算全局变量的大小,但这并非理想方案,因为如果变量的类型(type)不是标准类型,我们将永远无法计算其大小。

选择的解决方案是让编译器完成这一计算:WiShMaster 在临时目录中创建所有源文件的副本,并在每个全局变量声明后添加一个全局变量定义,该变量初始化为对应全局变量的大小。

例如,对于变量“g_User”:

修补后的g_User变量定义(Patched definition of the variable g_User)

USER g_User = {"jmerchat", "password"};
int GLOBAL_VAR_SIZE_g_User = sizeof(g_User);
1
2

WiShMaster 编译修补后的源代码,并从生成的可执行文件中提取GLOBAL_VAR_SIZE变量的值。这种提取方式与“生成”步骤中提取shellcode的方式相同,将在该步骤的解释中详细描述。

该步骤的执行需要几秒钟时间,因为需要复制和编译所有文件。此外,如果全局变量的大小在之前的执行中已计算且未被修改,则无需执行该步骤。因此,WiShMaster 集成了缓存机制(mechanism of cache)来保存这些值,用户也可以在配置文件(configuration file)中手动设置这些值。

# 3.3.4 “生成”步骤说明

在该步骤中,WiShMaster 生成一个名为“global_data.h”的文件,其中包含宏和“GLOBAL_DATA”结构体的定义,然后在临时目录中创建所有文件的修补副本。

这些副本与原始文件的差异如下:

  • 每个内部函数的末尾添加后缀“TempDefinition”;
  • 宏“STR”替换为字符串STR_[NUM],其中NUM是唯一标识该字符串的数字;
  • 全局变量的引用添加后缀“Use”。

以下截图展示了我们的程序“simpletest”的修改差异:

img

图8:原始源文件与修补后源文件的差异(Figure 8: Difference between original and patched sources files)

编译修补后的源代码后,WiShMaster 必须从生成的可执行文件中查找并提取内部函数和全局变量。

WiShMaster 使用微软链接器(Microsoft linker)生成的一个特殊文件:映射文件(map file)。该文件是一个文本文件,描述了通过启动可执行文件创建的进程的内存映射(memory mapping)。

以下是为我们的项目“simpletest”生成的映射文件的片段:

映射文件概述(Overview of a map file)

Address                    Publics by Value            Rva+Base         Lib:Object
0000:00000000               __except_list              00000000         <absolute>
...
0002:00001f60              ?DisplayMessage@@YAXPAD@Z   00412f60  f       display.obj
0002:00001f90              ?DisplayFile@@YAHPAD@Z      00412f90  f       display.obj
0002:000020d0              ?DisplayData@@YAHXZ         004130d0  f       display.obj
...
1
2
3
4
5
6
7

在这个例子中,WiShMaster 会认为函数DisplayMessage的起始地址为0x00412f60,结束地址为0x00412f90。

WiShMaster 使用特殊选项(标志“CREATE_SUSPENDED”)启动可执行文件,这意味着进程在内存中创建但不执行;然后使用打开进程函数(OpenProcess)和进程内存读取函数(ReadProcessMemory)读取进程内存,提取所需的二进制数据。

最后,只需将函数拼接起来,并在末尾添加GLOBAL_DATA结构体,即可创建shellcode。

# 3.3.5 “自定义”步骤说明

自定义步骤由一系列函数组成,这些函数将对shellcode执行一些修改,并将修改后的shellcode传递给下一个函数。

该函数链的内容完全由用户通过配置文件定义。每个函数存储在一个Python模块(Python module)中。WiShMaster 附带了一些标准模块,但用户可以编写自己的模块。

为了理解该步骤的用途,我们举一个小例子。假设我们要编写一个连接服务器的shellcode,在源文件中会有两个变量表示服务器的IP地址和端口。如果直接在这些变量中填入实际值,生成的shellcode将包含这些特定值,从而与该服务器绑定,这会带来两个问题:

  • 如果要连接另一台服务器,必须修改源代码中的值并重新生成shellcode,这需要花费一定时间;
  • 无法以二进制形式(binary form)分发shellcode。

解决这些问题的方法是,使用一些在二进制代码中通常不会出现的特殊值初始化IP地址和端口,例如IP地址使用0xaaaaaaaa,端口使用0xbbbbbbbb。生成的shellcode将包含这些特殊值。

然后,我们编写一个Python“自定义”模块,从配置文件中读取服务器的IP和端口,并将特殊值0xaaaaaaaa和0xbbbbbbbb替换为这些值。现在,我们可以通过提供生成的shellcode(含特殊值)和“自定义”模块,以二进制形式分发shellcode。

如果其他用户想要使用该shellcode,只需打开生成的shellcode并应用自定义模块即可。

当然,“自定义”模块除了修补少量值外,还可以执行其他操作,例如对shellcode进行加密。WiShMaster 附带了两个可对shellcode进行加密的“自定义”模块:一个使用32位密钥执行异或(XOR)加密,另一个使用256位密钥执行高级加密标准-密码分组链接模式(AES-CBC)加密。

图9展示了加密shellcode的创建原理。上半部分代表开发者,下半部分代表自定义和加密shellcode的用户。

img

图9:shellcode开发者与使用者的分离原理

# 3.3.6 “集成”步骤说明

该步骤将自定义后的shellcode集成到最终项目中。这种集成可以通过将shellcode复制到指定目录来实现,也可以通过将其转换为C数组并转储到C头文件来实现:

转储后的shellcode概述(Overview of a dumped shellcode)

/*
Shellcode generated by WiShMaster.
Size=1022 bytes
*/
UCHAR bShellcode[] =
"\x55\x8B\xEC\x83\xEC\x40\x53\x56\x57\x8B\x45\x0C\x03\x45\x10\x5F"
...
"\x20\x2B\x20\x62\x20\x3D\x20\x25\x64\x00\x4C\x01\x00\x00";
1
2
3
4
5
6
7
8

该文件随后可用于其他项目。

# 3.3.7 WiShMaster中shellcodisation的实现

WiShMaster 版本2的开发遵循以下目标:

  • 允许用户逐步或一次性执行shellcodisation过程;
  • 允许用户查看每个步骤的结果,并在发现错误时进行修改;
  • 允许用户编写自己的shellcodisation脚本,使用WiShMaster 提供的各种函数。

WiShMaster 可以三种模式启动:自动模式(automatic)、交互模式(interactive)和脚本模式(script)。

# 3.3.7.1 自动模式

在该模式下,WiShMaster 自动执行shellcodisation过程:

自动模式下的WiShMaster(WiShMaster in automatic mode)

$ ./wishmaster.py -s ./Projects/CODE/simpletest.xml -a
[INFO       ] Configuration of module "simpletest" successfully loaded
...
[INFO       ] Final shellcode is in file "...\simpletest\shellcodes\simpletest.bin"
[INFO       ] Binary "...\simpletest\shellcodes\simpletest.bin" successfully
copied to "...\integration\simpletest.bin"
1
2
3
4
5
6

# 3.3.7.2 交互模式

在该模式下,WiShMaster 启动Python解释器(Python interpreter),让用户驱动shellcodisation过程。shellcodisation分为6个步骤,每个步骤又细分为多个子步骤,每个子步骤由一个可从解释器调用的函数表示。例如,第一个步骤是“分析”,第一个子步骤是生成浏览文件,由函数“GenerateBrowse”处理:

交互模式下的WiShMaster:执行GenerateBrowse(WiShMaster in interactive mode: execution of GenerateBrowse)

$ ./wishmaster.py -s ./Projects/CODE/simpletest.xml -i
[INFO       ] Configuration of module "simpletest" successfully loaded
=== WiShMaster interactive shell ===
(simpletest:manual mode)
>>> GenerateBrowse()
[INFO       ] Browse file successfully generated
True
1
2
3
4
5
6
7

下一个子步骤是分析生成的浏览文件,由函数“AnalyseBrowse”处理:

交互模式下的WiShMaster:执行AnalyseBrowse(WiShMaster in interactive mode: execution of AnalyseBrowse)

(simpletest:manual mode)
>>> AnalyseBrowse()
[INFO       ] Browse file successfully analysed
True
1
2
3
4

所有结果都存储在对象中,只需对这些对象调用“print”函数即可显示。例如,在执行这两个子步骤后,如果我们显示表示当前项目的对象:

交互模式下的WiShMaster:显示结果(WiShMaster in interactive mode: display of the results)

(simpletest:manual mode)
>>> print Solution.dicProjects['simpletest']

================================================================================

Project name            : simpletest
Input type             : code
Output type           : shellcode
Inlined projects  : initsh,log
Fast mode                   : True

List of files

-   [H] ...\simpletest\headers\stdafx.h
-   [H] ...\simpletest\headers\global_data.h
-   [S] ...\simpletest\sources\global_data.cpp
-   [S] ...\simpletest\sources\main.cpp
...

List of internal functions

DisplayFile   [decorated name="DisplayFile(char *)" parameters="char *" return="BOOL"]
DisplayMessage   [decorated name="DisplayMessage(char *)" parameters="char *" return="VOID"]
...

List of imported functions

(real name=fflush" return="", call="", parameters="")
(real name=CreateFileA" return="", call="", parameters="")
(real name=HeapAlloc" return="", call="", parameters="")
...

List of global variables

g_szMessage  type: CHAR; size: 0 bytes; is an array: True
g_User  type: USER; size: 0 bytes; is an array: False
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

然后,我们可以检查结果,在继续之前纠正任何错误。

该模式提供了很大的灵活性,但用户必须记住所有必须调用的函数名称,这并不是非常用户友好。WiShMaster 可以设置为另一种模式,自动构建执行流程(execution flow)。用户随后可以通过调用函数“stepi()”执行下一个子步骤,函数“step()”则命令WiShMaster 执行对应步骤中的所有子步骤。

这些关键字源自“gdb”解释器,因此WiShMaster 也识别以下函数:

  • “restart()”:重新初始化shellcodisation过程;
  • “cont()”:从当前步骤执行shellcodisation过程至结束;
  • “run()”:从开始执行shellcodisation过程。

执行流程由对象“ExecutionFlow”表示,可以通过“print”函数显示。以下是一个小示例:

交互模式下的WiShMaster:显示执行流程(WiShMaster in interactive mode: display of the execution flow)

(simpletest:analyse/generate browse)
>>> print ExecutionFlow
simpletest:analyse
        generate browse
        analyse browse
        analyse code
        generate exported functions database
        load exported funtions database
        update modules' imports
simpletest:get size of global variables
        fill patch
        apply patch
        build environment
        extract size of global variables
simpletest:create environment
        create sorted lists
        create global header
        fill patch
        apply patch
simpletest:generate
        Build created environment
        extract shellcode
        dump shellcode
        customize shellcode
simpletest:integrate
        copy binary

(simpletest:analyse/generate browse)
>>> stepi()
[INFO       ] Browse file successfully generated
True
(simpletest:analyse/analyse browse)
>>> stepi()
[INFO       ] Browse file successfully analysed
True
(simpletest:analyse/analyse code)
>>> print ExecutionFlow
simpletest:analyse
        generate browse
        analyse browse
        >       analyse code
        generate exported functions database
        load exported funtions database
        update modules' imports
simpletest:get size of global variables
        fill patch
        apply patch
        build environment
        extract size of global variables
simpletest:create environment
        create sorted lists
        create global header
        fill patch
        apply patch
simpletest:generate
        Build created environment
        extract shellcode
        dump shellcode
        customize shellcode
simpletest:integrate
        copy binary

(simpletest:analyse/analyse code)
>>> step()
[INFO       ] Code successfully analysed
[INFO       ] Exported functions database successfully generated
[INFO       ] Exported functions database successfully loaded
[INFO       ] Imports successfully updated
True

(simpletest:get size of global variables/fill patch)
>>>
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

注意小符号“>”表示下一个要执行的子步骤。

# 3.3.7.3 脚本模式

在该模式下,WiShMaster 执行用户提供的Python脚本。以下是一个脚本示例:

WiShMaster脚本示例(Example of WiShMaster script)

#!/usr/bin/python
# -*- coding: ISO-8859-15 -*-
# Standard importations
import sys
# Script entry point
print "Starting shellcodisation"
stepi()
stepi()
print Solution.dicProjects['simpletest']
1
2
3
4
5
6
7
8
9

执行该脚本会产生以下输出:

脚本模式下的WiShMaster(WiShMaster in script mode)

$ ./wishmaster.py -s ./Projects/CODE/simpletest.xml -e simpletest
[INFO       ] Configuration of module "simpletest" successfully loaded
Starting shellcodisation
[INFO       ] Browse file successfully generated
[INFO       ] Browse file successfully analysed
...
1
2
3
4
5
6

# 3.3.7.4 钩子函数(Hook functions)

WiShMaster 允许用户在子步骤上设置“钩子函数”(hook function)。钩子函数是一个Python脚本,将在对应子步骤执行后调用。如果WiShMaster 出现错误,钩子函数通常可用于自动执行shellcodisation过程中的修补操作,也可用于设置全局变量的大小,使WiShMaster 无需处理该操作。

例如,以下脚本在浏览文件分析后插入一个钩子。该钩子函数手动设置全局变量的大小,因此WiShMaster 在“获取全局变量大小”步骤中无需计算这些值。

设置钩子函数的脚本示例(Example of script to set a hook function)

#!/usr/bin/python
# -*- coding: ISO-8859-15 -*-

# Standard importations

# Personal importations
from output import Output
from shell import EnumSteps

def HookFunction_AnalyseBrowse(Project):
    Project.dicGlobalVariables['g_szMessage'].SetInformationManually(46, True)
    Project.dicGlobalVariables['g_User'].SetInformationManually(64, False)

    Output.debug('Hook point of step "analyse browse" successfully executed')
    return True

lstHookFunctions = {
    EnumSteps.ID_STEPI_ANALYSE_ANALYSE_BROWSE: HookFunction_AnalyseBrowse,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 3.3.8 shellcode初始化

前文描述的shellcodisation过程创建了一段可在任何地址运行的二进制代码。然而,要正常工作,shellcode必须初始化GLOBAL_DATA结构体,即填充内部函数和导入函数的指针(pointer)。

shellcode中包含了一些用于执行该初始化的函数。本节简要概述这些函数的工作原理。

# 3.3.8.1 查找GLOBAL_DATA的地址

首先,WiShMaster 通过简单的“call”后跟“pop”指令查找shellcode的加载地址(load address):

shellcode用于查找其加载地址的代码(Code used by the shellcode to find its load address)

UCHAR * pShellcode = NULL;

/* Get load address */
__asm
{
    push                eax
    call                GetLoadAddress
GetLoadAddress:
    pop                 eax
    mov                 pShellcode, eax
    pop                 eax
}

/* Find "push ebp"/"mov ebp, esp" instructions to get real load address */
while (((* (UINT *)(pShellcode - i) != 0x83EC8B55)) && (i++ < 512));
if (i == 512)
    return FALSE;
pShellcode -= i;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

找到加载地址后,shellcode只需添加shellcode的总大小减去GLOBAL_DATA的大小,即可获得GLOBAL_DATA结构体的地址。

# 3.3.8.2 查找内部函数的地址

在shellcodisation过程中,WiShMaster 将每个内部函数的大小集成到GLOBAL_DATA中(实际上是从生成的可执行文件中提取的二进制数据的大小)。然后,可以从shellcode的加载地址逐步快速计算出内部函数的地址:

shellcode用于重建内部函数指针的代码(Code used by the shellcode to rebuild pointers on internal functions)

/* Rebuild pointers on internal functions */
for (i = 0; i < pGlobalDataHeader->uiNbOfInternalFunctions; i++)
{
    pGlobalDataHeader->pInternalFunctionsTable[i].pFunctionPointer = p;
    p += pGlobalDataHeader->pInternalFunctionsTable[i].uiFunctionSize;
}
1
2
3
4
5
6

# 3.3.8.3 查找导入函数的地址

查找导入函数的地址意味着加载导出(expose)该函数的共享库(shared library),并在该库中找到该函数的地址。这可以通过Win32 API的加载库函数(LoadLibrary)和获取进程地址函数(GetProcAddress)快速完成,但这并没有解决查找这两个函数地址的问题……

我们知道这些函数由库“kernel32.dll”导出,但该库的加载地址取决于Windows的版本。

WiShMaster 使用Windows shellcode编写者熟知的技巧来解决这个问题:

  • 首先,通过分析内存获取kernel32.dll的地址;
  • 其次,遍历kernel32.dll的导出表(exportation table)以查找这些函数的地址。
# 查找kernel32.dll的地址(Finding the address of kernel32.dll)

该操作由名为“GetKernel32Address”的函数处理,该函数执行以下步骤:

  • 查找进程环境块(Process Environment Block,PEB)的地址。PEB是在每个进程的用户态地址空间(user-land address space)中分配的结构体,包含了该进程的大量信息。PEB的结构描述可在微软网站[7]上找到。PEB的地址存储在fs:[0x30]处;
  • 提取PEB中偏移量(offset)为0xC处的“PEB_LDR_DATA”结构体的地址。该结构体特别包含三个指向“LDR_DATA_TABLE_ENTRY”对象链表(linked list)的指针。每个对象代表一个已加载的模块(loaded module),包含一个表示库名称的UNICODE_STRING结构体和模块加载地址。这三个链表按不同顺序排序;
  • 遍历按加载顺序(load order)排序的第三个链表。通常,第一个对象始终代表“ntdll.dll”,第二个代表“kernel32.dll”。但为了确保准确性,该函数会将模块名称的前两个字母与“ke”进行比较;
  • 确认找到正确的结构体后,只需提取加载地址即可。
# 查找LoadLibrary和GetProcAddress的地址(Finding the addresses of LoadLibrary and GetProcAddress)

这些函数的地址通过解析“kernel32.dll”的导出表找到。该操作的描述并非必需(只需了解PE格式(PE format)),本文不再详细说明。

# 查找导入函数的地址(Finding the addresses of imported functions)

要查找导入函数的地址,可以使用“GetProcAddress”函数。但这意味着必须在GLOBAL_DATA结构体中存储所有导入函数的名称。现在,只要我们查看一下Windows函数的名称,就会发现它们相当长(例如“CreateRemoteThread”、“WriteProcessMemory”等)。

另一种解决方案是编写一个函数(后文称为“GetProcAddressByCksumInDll”),该函数能够通过解析已加载库的PE格式,根据函数名称的“校验和”(checksum)查找导出函数。此处的“校验和”指的是将任意长度的字符串转换为32位值的转换函数的结果。这种技术有两个优点:首先,在GLOBAL_DATA中,每个导入函数只需存储一个32位值,而不是冗长的函数名称;其次,由于这些校验和大小相同,更容易编写遍历所有校验和的循环。

必须精心选择校验和转换算法,以尽可能避免冲突(即两个名称不同的函数具有相同的哈希值(hash))。WiShMaster 使用与Metasploit[8]相同的算法,该算法的效果非常好。

以下是Python中的代码:

用于计算函数名称哈希值的函数(Function used to compute the hash of a function name)

def ComputeCksum(szString):
    """ Compute a 32-bits checksum for a string
    Arguments
        szString = the string on which the checksum must be computed
    Return
        int = checksum
    """
    uiCksum = 0
    for c in szString:
        uiCksum = ((uiCksum >> 0xd) | (uiCksum << (0x20 - 0xd))) & 0xffffffff
        uiCksum = (uiCksum + ord(c)) & 0xffffffff
    return uiCksum
1
2
3
4
5
6
7
8
9
10
11
12

需要注意的是,“GetProcAddressByCksumInDll”并不是一个全新的函数,因为我们已经需要这样一个函数来查找“LoadLibrary”和“GetProcAddress”的地址。实际上,查找“LoadLibrary”、“GetProcAddress”以及所有其他导入函数的地址都使用同一个函数。

“GetProcAddress”函数看似多余,但实际上在特殊情况下可能需要它:有时,库导出的函数只是转发到另一个库中另一个函数的转发器(forwarder)。在这种情况下,导出目录(export directory)不包含该函数的地址,而是包含一个指向字符串“[dll名称].[函数名称]”的指针,其中[dll名称]是导出函数[函数名称]的dll的名称。

例如,如果我们使用工具pedump[9]显示“kernel32.dll”导出的函数,会发现“HeapAlloc”是转发到“ntdll.dll”中“RtlHeapAlloc”的转发器(在我的系统上):

pedump对kernel32.dll库的分析片段(Extract of the analysis the library kernel32.dll by pedump)

C:\> pedump C:\WINDOWS\system32\kernel32.dll

...
000090DA|518|HeapAlloc   (forwarder -> NTDLL.RtlAllocateHeap)
00036136|519|HeapCompact
00012C46|520|HeapCreate
0005F7D1|521|HeapCreateTagsW
00010F88|522|HeapDestroy
0005F7A0|523|HeapExtend
000090F0|524|HeapFree   (forwarder -> NTDLL.RtlFreeHeap)
...
1
2
3
4
5
6
7
8
9
10
11

WiShMaster 中用于解析导入函数的函数能够自动处理转发器:如果检测到导出函数是转发器,只需使用LoadLibrary和GetProcAddress查找真正的函数即可。

# 3.3.8.4 shellcode初始化总结

shellcode初始化依赖于三个函数:

  • “GetKernel32Address”:返回“kernel32.dll”的加载地址;
  • “GetProcAddressByCksumInDll”:根据函数名称的校验和查找导出函数;
  • “InitialiseShellcode”:shellcode的入口点,初始化GLOBAL_DATA结构体。

用于初始化shellcode的函数原型(Prototypes of the functions used to initialise shellcode)

UINT GetKernel32Address(VOID);

LPVOID GetProcAddressByCksumInDll(UINT uiChecksum, HMODULE hLib,
    LPVOID pLoadLibrary, LPVOID pGetProcAddress);

BOOL _InitializeShellcode(VOID);
1
2
3
4
5
6
编写shellcode
使用WiShMaster开发应用程序

← 编写shellcode 使用WiShMaster开发应用程序→

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