使用WiShMaster开发应用程序
# 4 使用WiShMaster开发应用程序
# 4.1 WiShMaster的目标
WiShMaster版本1仅能创建单体shellcode。而版本2的目标大幅拓展,具体包括:
- 支持开发模块化应用程序(modular application),即通过加载实现特定功能的模块(module),动态扩展自身功能的应用程序;
- 允许用户选择生成二进制文件(binary)的格式:可执行文件(executable)、动态链接库(dll)或shellcode;
- 支持代码复用(code reusability),即在多个项目中复用部分源代码,无需多次复制;
- 允许在功能强大的集成开发环境(IDE)Visual Studio中开发项目;
- 支持以源代码(source)或二进制格式分发项目。
# 4.2 WiShMaster版本2中应用程序的结构
# 4.2.1 应用程序结构概述
一个应用程序由一个或多个“模块”组成。每个模块实现一组可被其他模块调用的函数,因此每个模块都包含一个“导出”表(export table)(用于导出内部函数)和一个“导入”表(import table)(用于解析其他模块导出的函数),这与PE格式(PE format)类似。
模块可分为以下4种形式:
- 可执行文件;
- 动态链接库;
- shellcode;
- 内联(inlined)到另一个模块中。
当一个模块内联到另一个模块时,两个模块的导入表和导出表会合并。
图10总结了这一原理。该应用程序由三个模块组成,每个模块都导出一些函数(图中蓝色部分)并导入一些函数(图中紫色部分)。
模块1和模块2合并创建一个单独的shellcode,模块3创建一个可执行文件。shellcode和可执行文件的导入符号(imported symbol)在执行时解析。

图10:使用WiShMaster 2开发的应用程序结构
# 4.2.2 导出表和导入表
以可执行文件或动态链接库形式创建的模块,可以使用PE格式的导出表(exportation table)导出函数。但shellcode不遵循PE格式,因此无法使用这种机制。此外,我希望即使缺少某些必需的模块,一个模块也能加载,以便它可以等待这些模块(或尝试从互联网获取)。而如果启动一个导入了不存在的动态链接库的可执行文件,会弹出错误消息框(message box)。
最终,我决定创建自己的格式。导出表集成在GLOBAL_DATA结构体中,该结构体包含每个导出函数名称的校验和(checksum)。
导入过程与普通动态链接库的导入过程完全相同。从GLOBAL_DATA结构体的角度来看,从标准动态链接库导入的函数与从模块(可以是可执行文件、动态链接库或shellcode)导入的函数没有区别。
图11展示了GLOBAL_DATA中导入表和导出表的结构概述。

图11:全局数据(GLOBAL_DATA)中导入表和导出表的结构概述
# 4.2.3 入口点(Entry point)
每个模块都可以有一个入口点,即初始化完成后必须调用的内部函数。GLOBAL_DATA中存储着该函数的指针(pointer)。
# 4.3 标准模块
有几个标准模块,它们提供了其他模块经常使用的一些函数。本节简要描述这些模块。
# 4.3.1 “日志”模块
该模块提供函数“PrintMsg”,用于打印格式化消息(formatted message)。消息可以发送到标准输出(standard output)或附加调试器输出(attached debugger output)。如果没有调试器(debugger)附加到进程,可以使用工具DebugView[10]显示发送到调试器的输出。这对于调试在图形化应用程序中运行的shellcode特别有用。
# 4.3.2 “初始化shellcode”模块
该模块提供初始化shellcode所需的所有函数:
- 函数“InitialiseShellcode”:shellcode的入口点,用于初始化GLOBAL_DATA结构体;
- 函数“GetProcAddressByCksumInDll”:查找动态链接库导出的函数地址(如前文所述);
- 函数“ResolveImportedFunctions”:接收GLOBAL_DATA结构体作为参数,解析所有导入函数;
- 函数“GetProcAddressByCksumInModule”:查找模块导出的函数地址。
# 4.3.3 “加载器”模块
该模块用于管理一组模块,仅提供一个函数“AddModuleToLoad”,用于将模块加载到内存中。
函数AddModuleToLoad的原型(Prototype of the function AddModuleToLoad)
BOOL AddModuleToLoad(CHAR * szFilePathModule, MODULE_TYPE moduleType);
- “szFilePathModule”表示要加载的模块的完整路径;
- “moduleType”表示模块类型:可执行文件、动态链接库或shellcode。
“加载器”负责模块的所有加载和初始化工作:
- 将模块加载到内存中;
- 如果模块是加密的shellcode,则对其进行解密;
- 解析所有导入符号(来自标准库或其他模块);
- 检查是否有之前加载的模块在等待该新模块导出的函数;
- 调用所有已初始化和解析完成的模块的入口点。
当然,“加载器”依赖于“初始化shellcode”模块。实际上,“初始化shellcode”模块内联在“加载器”模块中。
# 4.4 shellcode加密
WiShMaster集成了两个可对shellcode进行加密的自定义模块(customization module):一个使用32位密钥执行异或(XOR)加密,另一个使用256位密钥执行高级加密标准-密码分组链接模式(AES-CBC)加密。
# 4.4.1 32位密钥的异或(XOR)加密
这种加密仅用于多态性(polymorphism),通常用于创建一个单独的可执行文件,该文件包含解密循环(带有加密密钥)和加密后的shellcode。
# 4.4.2 256位密钥的高级加密标准-密码分组链接模式(AES-CBC)加密(The AES-CBC encryption with a 256-bits key)
# 4.4.2.1 工作原理
该模块使用256位密钥执行AES-CBC加密,依赖于免费的加密库(cryptographic library)PolarSSL[11](前身为XySSL),用于保护shellcode免受人工分析(manual analysis)。解密部分(和加密密钥)与shellcode存储在两个不同的文件中。“加载器”模块实现了所有解密加密shellcode的机制。
该攻击分为两个步骤:
- 目标计算机感染自定义的“加载器”;
- 真正的恶意负载(加密的shellcode)传输到目标计算机。
这两个元素通过不同的方式、在不同的时间引入目标系统。关键在于,单独一个元素无法提供任何关于攻击的信息:
- “加载器”是一个通用框架(generic framework),不包含任何代表真正攻击的代码;
- 没有“加载器”中的密钥,无法解密恶意负载。
以下两个小节给出了基于此原理的两个攻击示例。
# 4.4.2.2 示例1:从浏览器缓存获取加密的shellcode
向目标用户发送一封包含网页链接(web link)的电子邮件。用户点击链接,访问一个伪造的网站(fake web site),该网站会安装自定义版本的“加载器”。如果此攻击失败,对“加载器”的分析只会显示它在某些目录中递归搜索(recursive searches)jpg文件,显然没有真正的危险。
之后(可能几天后),用户收到另一封包含链接的邮件并点击,访问一个表面上无害的网站。
实际上,真正的恶意负载已转换为加密的shellcode,并隐藏在一个jpg文件中。该图片放置在第二个网站的页面中。当用户的浏览器显示该图片时,会将文件存储在其缓存目录(cache directory)中。
“加载器”定期扫描该目录,找到图片后,在内存中解密模块并跳转到其入口点。
该攻击已在实验室环境中成功执行。恶意负载是一个后门(backdoor),能够通过“基本”身份验证(basic authentication)绕过代理服务器(proxy)。
# 4.4.2.3 示例2:从USB密钥获取加密的shellcode
目标是从目标计算机复制一些文件到USB密钥。攻击者只能插入USB密钥(自动运行(autorun)已禁用)。由于这是安全区域,USB密钥可能事先经过人工分析。
假设目标计算机上运行着“加载器”的扩展版本。如果安全团队捕获到“加载器”,他们只会发现它在等待USB事件(USB events),并在映射驱动器(mapped drive)上查找文件。
攻击者将加密的shellcode放置在USB密钥上。如果安全团队分析该密钥,只会发现一个包含“随机”数据的文件。
攻击者将USB密钥插入目标计算机,“加载器”检测到事件,找到并加载加密的shellcode,仅在内存中解密它并跳转到其入口点。例如,shellcode可以查找有价值的文件,并在加密后将其复制到USB密钥上。
该攻击已在实验室环境中成功执行。恶意负载是一个简单的反向shell(reverse shell)。
# 4.4.3 使用共享密钥(Using shared secrets)
如果恶意应用程序由多个模块组成,当然可以使用相同的密钥加密每个shellcode。另一种解决方案是使用密钥共享算法(secret sharing algorithm),并将密钥的一部分存储在每个模块中。由于应用程序需要所有模块才能正常工作,我决定不实现带有阈值方案(threshold scheme)的密钥共享算法(如沙米尔秘密共享算法(Shamir’s Secret Sharing algorithm))。
密钥被分成N部分,需要这N部分才能计算出完整密钥。首先,可以将密钥分成N段,每段作为一部分。因此,如果密钥长度为L位,每段长度为L/N位。
这是一个糟糕的解决方案,因为每获取一段,就会得到关于密钥的真实信息。例如,如果获取了K(0≤K≤N)段,只需暴力破解(bruteforce)L×(N-K)/N位即可找到密钥。
我更倾向于使用以下算法:
- 每个模块都有一个256位的私钥(private key);
- 密钥是所有私钥按字节求和的结果;
- 所有模块都使用最终的共享密钥(secret key)加密;
- 所有模块都包含自己的私钥(明文形式)。
# 4.5 开发反向shell
# 4.5.1 程序结构概述
为了给出实际示例,假设我们要开发一个简单的反向shell,即一个在“cmd”进程和远程服务器(remote server)之间建立连接的后门。
该后门(后文称为“rvshell”)由两层组成:
- 网络层(network layer):建立与服务器的通信;
- 应用层(application layer):创建“cmd”进程,并使用网络层提供的服务。
图12展示了rvshell的工作原理。

图12:反向shell(rvshell)的工作原理
我们开发两个模块:
- “ntstacksmpl”:实现网络层,导出两个函数:
“ntstacksmpl”导出的函数原型(Prototypes of the functions exported by ntstacksmpl)
BOOL OpenConnection(IN UINT uiServerAddressNt, IN USHORT usServerPortNt, OUT SOCKET * pSock);
BOOL CloseConnection(IN SOCKET sock);
2
第一个函数在服务器上建立传输控制协议(TCP)连接,第二个函数关闭TCP连接。
- “rvshell”:实现应用层,不导出任何函数,但有一个入口点——函数“ExecuteShell”,该函数创建“cmd”进程,并使用“OpenConnection”在服务器上建立TCP连接。
需要注意的是,如果要通过代理服务器(proxy)使用“CONNECT”方法建立连接,只需开发一个新模块,提供相同的“OpenConnection”和“CloseConnection”函数。该模块的“OpenConnection”函数不执行简单的TCP连接,而是在服务器上建立CONNECT隧道(CONNECT tunnel)。
# 4.5.2 生成反向shell
# 4.5.2.1 生成可执行文件
要生成可执行文件,只需向WiShMaster提供以下配置文件(configuration file):
用于生成可执行文件形式的rvshell的配置文件(Configuration file used to generate rvshell as an executable)
<solution>
<module name="rvshell" config="rvshell/rvshell.cfg" input_type="code"
specific_config="" output_type="exe"/>
<module name="ntstacksmpl" config="ntstacksmpl/ntstacksmpl.cfg" specific_config=""
input_type="code" output_type="inline" inline_destination="rvshell"/>
<module name="log" config="log/log.cfg" specific_config="" input_type="code"
output_type="inline" inline_destination="rvshell"/>
</solution>
2
3
4
5
6
7
8
“log”模块和“ntstacksmpl”模块内联到“rvshell”模块中,因此“rvshell”获得了显示格式化消息和建立TCP连接的功能。
图13展示了这一原理。

图13:生成可执行文件形式的反向shell的结果
# 4.5.2.2 生成shellcode
要生成shellcode,向WiShMaster提供以下配置文件:
用于生成shellcode形式的rvshell的配置文件(Configuration file used to generate rvshell as a shellcode)
<solution>
<module name="rvshell" config="rvshell/rvshell.cfg" specific_config=""
input_type="code" output_type="shellcode"/>
<module name="ntstacksmpl" config="ntstacksmpl/ntstacksmpl.cfg" specific_config=""
input_type="code" output_type="inline" inline_destination="rvshell"/>
<module name="initsh" config="initsh/initsh.cfg" specific_config=""
output_type="inline" inline_destination="rvshell"/>
<module name="log" config="log/log.cfg" specific_config="" input_type="code"
output_type="inline" inline_destination="rvshell" />
</solution>
2
3
4
5
6
7
8
9
10
我们只需添加“initsh”模块,该模块包含执行shellcode初始化所需的所有函数。
然后,WiShMaster将生成一个shellcode,该shellcode可以经过异或加密后包含在可执行文件中:我们只需实现一种多态反向连接shell(polymorphic reverse-connect shell)。

图14:生成shellcode形式的反向shell的结果
# 4.5.2.3 将“加载器”与shellcode和动态链接库配合使用
在这种情况下,“rvshell”构建为加密的shellcode,“ntstacksmpl”构建为动态链接库。这两个模块将由“加载器”加载。因此,无需在每个模块中包含“PrintMsg”或“InitialiseShellcode”等共享函数。“log”模块和“initsh”模块内联在“加载器”中,并被“rvshell”和“ntstacksmpl”视为导入模块(imported module)。
shellcode“rvshell”使用存储在“加载器”中的私钥加密。图15总结了这一原理。

图15:使用加载器执行反向shell;通过密钥加密的一个shellcode
# 4.5.2.4 将“加载器”与两个shellcode和共享加密密钥配合使用(Using “loader”with a two shellcodes and a shared encryption key)
在最后这种情况下,我们将“rvshell”和“ntstacksmpl”生成为shellcode。这些shellcode使用共享密钥(图16中红色部分)加密,该共享密钥由三个密钥计算得出:一个存储在“加载器”中(绿色部分),一个存储在“rvshell”中(黄色部分),最后一个存储在“ntstacksmpl”中(蓝色部分)。

# 图16:使用加载器执行反向shell;通过共享密钥加密的两个shellcode
# 4.6 实现USB攻击以执行反向shell
# 4.6.1 攻击准备
本节给出了前文描述的、依赖于插入USB密钥的攻击的实现要点。
已开发了三个附加模块:
- “searchmodindir”:在目录中递归搜索模块,提供一个函数“AddDirectoryToAnalyse”,用于在指定目录中启动新的模块搜索。找到模块后,“searchmodindir”调用“加载器”的“AddModuleToLoad”函数,并传入对应文件的完整路径。
- “detectusbkey”:等待USB密钥插入,使用Windows应用程序编程接口(Windows API)的标准函数通知系统,希望在USB密钥插入时得到提醒。然后,将对应的驱动器号(drive letter)传入“searchmodindir”的“AddDirectoryToAnalyse”函数。
- “injecter”:将shellcode注入另一个进程。该进程可以是通过名称指定的正在运行的进程、要启动的新进程,或默认浏览器的新实例(隐藏启动)。
# 4.6.1.1 生成密钥(Generating secret key)
生成三个私钥,将这些密钥相加创建共享密钥“shared.key”。
# 4.6.1.2 创建反向shell(Creating the reverse shell)
使用WiShMaster将“rvshell”和“ntstacksmpl”转换为两个shellcode。
这些shellcode使用“shared.key”加密,并为每个模块添加一个私钥。
生成的两个文件放置在USB密钥的两个不同目录中。
# 4.6.1.3 创建加载器(Creating the loader)
“加载器”创建为shellcode,内联以下模块:initsh、log、searchmodindir和detectusbkey。“加载器”还包含最后一个私钥。
# 4.6.1.4 创建特洛伊木马(Creating the Trojan)
现在,我们将创建一个包含“加载器”的特洛伊木马(Trojan)。被感染的应用程序是一个简单的测试应用程序,名为“MyEditor”,当点击工具栏中的“帮助”(Help)按钮时,会显示一个消息框。

图17:测试应用程序“MyEditor”
当用户尝试显示该消息框时,“加载器”将启动。为此,我们将修补跳转到显示消息框的函数(后文称为“About()”)的“call”指令。
当然,我们可以直接跳转到“加载器”,但这有两个缺点:
- 首先,无法从“加载器”返回,因此消息框不会显示,窗口会冻结;
- 其次,“加载器”的生命周期与“MyEditor”进程绑定,这不是一个好主意,因为用户可能会关闭该应用程序。
相反,我们将使用“injecter”模块将“加载器”注入默认浏览器的隐藏实例中。使用WiShMaster将“injecter”转换为shellcode。最终的shellcode通过拼接“injecter”shellcode和“加载器”shellcode创建,并使用异或算法加密。
然后,使用外部工具(称为“infector”)感染MyEditor.exe:
- 扩大最后一个节,添加:
- 解密循环(包含解密“injecter/加载器”shellcode的32位密钥);
- 加密后的shellcode。
- 更新PE头以反映此修改;
- 将跳转到“About()”函数的“call”指令修补为调用解密循环;
- 在解密循环的末尾添加跳转到“About()”函数的指令。
图18总结了这一原理。

# 图18:“MyEditor”的感染原理
# 4.6.2 攻击流程(Sequence of the attack)
目标用户将“MyEditor.exe”下载到自己的计算机上。本地反病毒软件通过特征码(signature)进行验证,模拟执行该可执行文件,未发现任何威胁。
用户启动MyEditor,随后点击“About()”按钮,解密循环被调用。解密循环解密“injector/加载器”shellcode,并跳转到“injector”。“injector”从注册表(registry)中提取默认浏览器的完整路径,启动该应用程序的隐藏实例,并将“加载器”注入其中。然后,从“injector”返回解密循环,跳转到“About()”函数:消息框显示。
现在,用户可以关闭MyEditor:“加载器”在隐藏进程中运行,不受影响。
之后,有人来拜访目标用户,他带着一个USB密钥。快速扫描显示,该密钥包含一些带有随机数据的文件(可能是加密卷(encrypted volume)),但没有可执行文件或潜在危险文件。
此人与目标用户会面,询问是否可以将其USB密钥插入目标用户的计算机(当然是在目标用户的监督下)。由于密钥已被检查,目标用户同意。当USB密钥插入时,Windows向“加载器”发送消息。“加载器”扫描USB密钥,找到两个模块,将它们加载到内存中,计算共享密钥并解密,然后跳转到“rvshell”的入口点:反向shell开始运行。