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);
宏STR的定义如下:
宏STR的定义(Definition of the macro STR)
#define STR(x) x
# 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);
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”的修改差异:

图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
...
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的用户。

图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";
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"
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
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
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
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)
>>>
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']
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
...
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,
}
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;
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;
}
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
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)
...
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);
2
3
4
5
6