【论文存档】Windows可执行程序内存动态代码化的研究与实现

Windows可执行程序内存动态代码化的研究与实现

摘要

目前在缓冲区溢出攻击、软件加壳、软件代码保护等领域普遍使用到内存动态代码技术,具体是指编写一段二进制机器码,该代码无需经过任何预处理即可在进程内存空间的任意地址直接执行。内存动态代码一般直接使用汇编语言编写,存在开发难度较大、代码不易维护、隐藏软件风险等问题。

本课题提出一种方法用于将Windows可执行文件直接转化为内存动态代码,并给出动态代码设计、动态代码装配、命令行接口主程序、图形界面外壳四个方面的具体实现。课题解决了PE文件的读取与分析、代码自身基址重定位、数据执行保护机制的绕过、数据与代码压缩与解压、系统函数的导入等一系列的技术问题。

使用本课题提供的方法,开发者可以轻松将使用高级语言编译的可执行程序转化为动态代码,从而将精力放在软件功能的设计上,而无需关心动态代码在平台汇编级的具体实现。这样,动态代码技术相关应用的开发难度将大大降低。

关键词:动态代码、PE文件、代码自身基址重定位、数据执行保护、LZMA压缩算法

A Research and Implementation of the Conversion from Windows Executable Files to In-memory Dynamic Code

Abstract

In-memory dynamic code technology is widely used in buffer-overflow attacking, packer software, code protection, etc. This technology is to write a piece of code which can be executed from any addresses in the memory space of a process. Nowadays, dynamic code is usually written in assembly language directly. This often causes difficulties in programming and maintaining the code and increases the risk of software engineering.

In this paper, a method of converting Windows executable file to in-memory dynamic code was introduced. And the detailed implementations of the method were provided including the design and combination of the dynamic code, a command-line based converter and a GUI shell. Several technical problems such as reading and analysis of the Microsoft protable executable files, self base relocation of the code, by-passing of the data execution prevention, compression and decompression of the data, importation of the system APIs were discussed and solved in this paper.

Developers can convert the executable files which are compiled by high-level languges to dynamic code easily by using the method provided in this paper. They can pay their attention only on the software designing without caring about the implementations in assembly language and what happened in the platform level. As a result, the difficulty of developing applications using dynamic code technology can be obvious decreased.

Key words: dynamic code, portable executable file, self base relocation, data execution prevention, LZMA

目录

1 绪论– 1

1.1 研究背景– 1

1.2 目的与意义– 1

1.3 国内外研究现状– 2

1.4 论文主要内容与结构安排– 3

2 相关技术与工具平台介绍– 4

2.1 32位汇编与OllyDbg调试器– 4

2.2 Win32核心编程– 5

2.3 PE文件格式– 6

2.4 LZMA压缩算法– 7

2.5 相关开发平台与工具– 7

2.5.1 Visual Studio 2008- 7

2.5.2 Visual Basic 6.0- 9

2.5.3 WinHex- 10

2.5.4 LordPE- 10

2.6 本章小结– 11

3 可执行程序动态代码化的研究与设计– 12

3.1 项目总体架构– 12

3.2 DynMake主程序框架与外围程序– 13

3.2.1 命令行设定与命令行解析模块– 13

3.2.2 PE文件读取与信息输出模块– 15

3.2.3 预置模板类– 15

3.2.4 LZMA压缩模块– 16

3.3 PELoader动态代码预置模板– 17

3.3.1 动态代码的基本模块组成– 17

3.3.2 PELoader动态代码模板的结构设计– 17

3.3.3动态代码结尾Ret2Lib堆栈构造– 18

3.3.4 输入表的重新设计– 19

3.3.5 重定位表的重新设计– 20

3.3.6 PELoader预置模板类– 21

3.4 DynShell图形界面外壳– 23

3.5 本章小结– 23

4 可执行程序动态代码化的具体实现– 24

4.1 DynMake主程序框架与外围程序部分– 24

4.1.1 命令行解析– 24

4.1.2 PE文件的读入– 24

4.1.3 LZMA压缩算法包装– 25

4.2 PELoader模板动态代码的实现– 26

4.2.1 动态代码入口– 26

4.2.2 代码自身解压缩– 26

4.2.3 Kernel32.dll基地址的获取– 28

4.2.4 API函数指针的获取– 28

4.2.5 堆栈代码执行– 29

4.2.6 PE映像的解压缩– 31

4.2.7 映像重定位– 31

4.2.8 输入表函数导入– 32

4.2.9 调用入口函数– 34

4.3 PELoader预置模板的实现– 35

4.3.1 PE映像的转储与判断– 35

4.3.2 输入表的处理– 36

4.3.3 重定位表的处理– 36

4.3.4 其他数据的处理与PE映像的压缩– 37

4.3.5 代码符号链接– 38

4.3.6 动态代码的装配– 39

4.4 GUI图形界面的功能实现– 40

4.4.1 主窗体功能的实现– 40

4.4.2 调用外部程序与控制台标准输出重定向– 43

4.5 本章小结– 45

5 总结与展望– 46

5.1 总结– 46

5.2 展望– 46

参考文献– 47

致谢– 49

译文及原文– 50

1 绪论

1.1 研究背景

微软公司的Windows操作系统是当今世界应用最广泛的操作系统之一,更是个人电脑用户的第一选择,Windows系统是千千万万开发面向广大群众的桌面软件的开发商必需支持的平台。由于用户与研究学习者之多,Windows平台下的各种病毒、破解技术层出不穷。对于软件开发商而言,软件被破解将会给公司带来巨大的损失,由此催生出软件代码安全、软件保护技术的领域。

Windows下的内存动态代码技术(Dynamic Code),是一种在Windows用户级(又称应用层,即Ring 3层)限度内相对底层的技术,目标是不依托于可执行文件模块,直接在内存中执行所需功能的机器码。一方面这种技术通常经常被黑客用来对操作系统或应用软件进行缓冲区溢出攻击,一般称之为shellcode,另一方面这种技术也可用于安全友好的软件代码保护,如市面上常见的加壳加密软件。应用此类保护技术得当的软件,如同用户畏惧病毒与黑客一般,会让软件的破解者同样感到无从下手,使软件的安全性得到更高的保障。

事实上,技术并不分“黑”与“白”,而在于由何人来使用。通常黑客界聚集着IT领域的精英,黑客们的行动也通常推动着计算机科技的进步。为此,软件开发者就有必要学习和了解一部分此方面的技术,在开发中规避一些漏洞,防止自己的软件被很容易地破解;同时,一些技术也能巧妙地运用在正常的软件开发中,以更好更方便地实现某些功能。本课题即围绕内存动态代码技术展开。

1.2 目的与意义

Windows可执行程序内存动态代码化,亦即本课题的研究目的是设计一种通用的方法将任意的Windows可执行程序转化为内存动态代码。

目前市面上软件在使用动态代码技术保护软件中的关键代码时,通常是采用汇编语言书写动态代码,因为动态代码涉及到系统API导入、代码自身数据重定位等技术问题,不适用使用C语言等已经具有操作系统依赖性的高级语言实现。这种方式增加了开发实现的难度,要求开发者必须具有良好的汇编语言功底与Windows操作系统的知识,一般开发者不易胜任。由于汇编语言自身的缺陷,当要求动态代码具有比较复杂的功能时,就可能会因为开发中遗留的缺陷造成整个软件系统的不稳定或崩溃。

而本课题将探究一种方法使任意的Windows可执行程序(EXE或DLL文件)转化为内存动态代码,这样可以大大简化动态代码的开发。开发者可以直接使用高级语言如VB、C/C++、Delphi等编写代码,将精力放在需要实现的具体功能上,而无需接触与操作系统有关的汇编级的实现,同时也避免了上述提到的遗留BUG造成的风险。从用户,即有意于使用动态代码技术保护其关键代码的开发者的角度来讲,本课题的研究将解放软件开发的生产力,使此方面的技术能够为更多人所用,对软件代码安全领域有着深远的意义。

1.3 国内外研究现状

目前在计算机安全领域,动态代码技术有一黑一白两个应用方面,一个方面为用于Shell Code的编写,而Shell Code通常运用于病毒与黑客的溢出攻击。有关溢出攻击的研究应用不在本课题的应用范围之内,读者可参考信息安全相关的技术资料与病毒代码分析。

另一方面动态代码技术主要用于代码保护方面,其中一个主要的分支为加壳软件,又称代码保护软件。目前市面上的主流的加壳加密软件均会涉及到PE文件的处理、在内存中的解压与重构这些工作。初级的如UPX、ASPack这些简单的压缩壳,稍复杂的有ASProtect等一些加密壳,非常复杂的如运用了虚拟机技术的VMProtect、ExeCryptor等保护工具。

通常用于攻击的Shell Code亦可用于正常软件的使用,即将某块内存地址看作是一个函数指针而对其进行调用,笔者在研究一款网络游戏时发现了这种应用,其封包的加密算法并非编译到游戏的可执行文件中,而是每次连接服务器时动态地下载,而密钥被硬编码到这段动态代码中,同时添加了很多花指令的干扰,使破解的难度急剧上升。在这种非黑客攻击而是对软件关键代码的保护应用中,普遍将Shell Code称之为Dynamic Code以避免产生一些贬义的误解。受此启发,笔者也曾在自己的软件中使用这种方法,使关键的代码从服务器在线取得,以起到保护的作用。

从具体的实现机制来看,上一节笔者已经提到,这些代码普遍使用汇编语言编写,有着编码复杂、工作量大、代码艰深难懂等一系列问题,并且存在着一些软件开发上的风险。本课题将在这些前人研究的基础上设计并实现一种新的动态代码解决方案,以改善这一现状。

1.4 论文主要内容与结构安排

本课题研究并实现了一种将Windows可执行程序即PE文件转化为内存动态代码的方法,并设计开发出进行这种变换的面向开发人员的命令行接口工具及面向一般用户的图形化界面外壳程序。本文将按照从整体介绍到具体实现的步骤向读者介绍整个课题的研究内容,全文共分为五个章节。

第一章为绪论,这一章主要介绍课题的研究背景、研究目的与意义、国内外相关技术的发展以及文章的结构安排。

第二章介绍课题研究需要具备的前置基础知识以及使用到的工具与平台,以供读者对自身已具备知识是否能够进行本文的阅读及后续研究有个初步的了解,并按需准备需要的开发实验工具。

第三章对课题的研究内容进行了具体的模块划分并对每个模块进行基本的设计规划,读者可从中了解本课题“具体要做什么”以及“为什么要这样做”。

第四章为课题研究的具体实现,讲述每一项工作的代码级的具体实现方法,读者可仿照其中的示例代码进行自己独立的实验。

第五章为全文的总结,并对本课题目前的不足之处进行分析,以及对今后的工作进行一些展望。

2 相关技术与工具平台介绍

2.1 32位汇编与OllyDbg调试器

广义的角度来讲,32位汇编指部署在Intel x86构架CPU之上,32位保护模式下的汇编语言程序。在本课题中,这里的32位汇编仅限于Win32用户态汇编,即运行在32位Windows操作系统下的用户模式而非内核模式的汇编语言程序。

Win32汇编与大学本科阶段通常教授的DOS环境下的16位汇编有很大的区别,主要有目标操作系统、指令集、平台调用规范等多个方面。事实上,在Win32汇编中,汇编语言仅仅作为一种指令的描述存在,开发者关注的重点不在于语言本身,而和C/C++、Pascal等语言一样,在于Windows操作系统的内存管理、API调用等规范,所以学习Win32汇编通常需要C/C++语言基础。另一方面,从某种意义上来讲,随着Windows 98操作系统的淘汰,DOS下的16位汇编在当前时代已经完全过时。

OllyDbg(简称OD)是德国Oleh Yuschuk同志开发的一款可视化用户模式调试器,运行于Windows操作系统上。OllyDbg结合了动态调试与静态分析功能,同时具有很强大的反汇编引擎,可以智能分析函数过程、跳转与循环等语句[1]。这些特性使OllyDbg成为汇编级调试Win32 Ring3级程序的首选工具,其可扩展设计使用户可以设计自己的插件与脚本,进而更进一步地扩充其强大的功能。OllyDbg的软件界面如图2-1所示。

clip_image002

图2-1 使用OllyDbg调试器调试分析某网络游戏

在本课题中,Win32汇编主要用于动态代码的设计。使用MASM 9.0(已包含在Visual Studio 2008中)编译动态代码,OllyDbg调试与测试。

2.2 Win32核心编程

在Windows操作系统用户态的底层,有三个最基本最重要的DLL文件,它们各司其职,一般各种程序都会直接或间接地调用它们。以32位系统为例它们分别是负责进程线程调度、文件IO、内存管理、动态链接库管理的KERNEL32.DLL,负责窗口创建与管理、鼠标键盘消息的派遣与处理的USER32.DLL,和负责图形的绘制、文字的打印的GDI32.DLL[2]

Win32核心编程(32 bit Windows Kernel Programming)指在32位Windows操作系统的用户模式(Ring 3)下,调用KERNEL32.DLL导出的系统API进行进程线程调度、文件IO、内存管理、动态连接库等操作的编程。与重视USER32.DLL与GDI32.DLL的图形界面编程不同,重视KERNEL32.DLL的Windows核心编程更多地涉及了与操作系统底层机制有关的知识[3]

由于研究的目标平台与对象是32位Windows操作系统及其使用的PE格式可执行文件,故Win32核心编程是本课题研究的基础。在本课题的研究中,此部分内容主要涉及到动态链接库的加载机制、内存分页管理与访问权限等。

2.3 PE文件格式

PE(Portable Executable,便携可执行)文件格式是微软公司在Windows NT系统中开始引进的可执行文件格式,衍生于早期的COFF(Common Object File Format,通用目标文件格式)。PE文件中的便携一义指其能通用于所有的Windows平台(如Win9X、WinNT、WinCE等)与所有的CPU架构(x86、IA64、ARM等)。PE文件又被称为广义的动态链接库,包括EXE、DLL、OCX、SYS等扩展名。其中一般将EXE文件称为“可执行文件”,将DLL文件称为“动态链接库”(狭义的)。图2-2所示为一个PE文件的基本框架结构[1]

clip_image003

图2-2 磁盘上的PE文件格式

可执行程序内存动态代码化即对PE文件的内存动态代码化,故PE文件以及其加载到内存中后被称为的PE映像(PE Image)是本课题研究的主要对象。PE文件格式包含具体内容较多,相对比较繁琐,笔者将在后文详细论述。

2.4 LZMA压缩算法

LZMA(Lempel-Ziv-Markov chain-Algorithm,亚伯拉罕·兰伯与雅各比·谢夫算法的基于马尔可夫链的改进算法)压缩算法是2001年以来得到很快发展的一个无损数据压缩算法,基于Deflate和LZ77算法改良与优化后实现。LZMA算法具有高压缩比、解压程序较小(不到3KB)、解压内存消耗小、解压速度快、支持多线程多处理器等特点[4]

LZMA算法为7-Zip压缩软件的默认算法,因为其开放性,LZMA被广泛运用在各种需要无损数据压缩功能软件产品中,如NSIS(Nullsoft Scriptable Install System,Nullsoft脚本安装系统)安装包制作、UPX加壳软件等。表2-1使用301Mb大小的文件测试比较了ZIP、WinRAR、7-Zip三种压缩算法的时间与压缩率[5]

表2-1 几种压缩软件的比较

软件 大小(字节) 压缩率 压缩时间(秒) 解压时间(秒)
未压缩 316,355,757 100.00% 0 0
WinZIP 111,503,290 35.24% 47.9 6.5
WinRAR 90,361,866 28.56% 36.2 4.7
7-Zip 88,580,416 28.00% 49.2 7.1

由表中数据可以看出,7-Zip能用不多的压缩时间获得较大的压缩率,并且解压速度很快,压缩率远远领先于WinZIP,和WinRAR持平并稍占上风。由于WinRAR为商业软件,压缩算法并不公开,无论从实际压缩效果来看,还是从发布协议的角度考虑,选择7-Zip的LZMA算法均是最优的选择。

在本课题中,笔者通过移植LZMA C SDK实现了动态代码的压缩与解压缩,在对执行效率影响不大的条件下大大缩小了生成动态代码的体积。

2.5 相关开发平台与工具

2.5.1 Visual Studio 2008

Visual C++ 2008为微软公司于2008年推出的C++集成开发环境,隶属于Visual Studio 2008产品,CRT(C Runtime)版本号为9.0,.NET Framework版本号为3.5。其开发环境如图2-3所示。

clip_image005

图2-3 Visual C++ 2008

VC2008主要用于部署在Windows操作系统下的Native C++(本机代码C++)程序的开发,主要包括使用MFC、ATL、COM、DirectX等技术开发的Windows应用软件、游戏等。在本机代码开发之外,VC2008也能进行建立在.NET Framework框架之上的托管代码应用程序开发,或是进行本机与托管代码的混合编程开发。

与经典的VC6.0相比,除了拥有更为高效快捷的开发与调试环境之外,VC2008与VC6的主要差别在编译器上。VC6不支持最新的ISO C语言标准,而VC2008即可很好的支持。选用较旧的VC6编译器将可能导致新的代码书写规范不能编译通过。另外一方面,VC6提供的Windows SDK头文件为1998年版本,对于Windows 2000以后新添加的Windows API无法提供支持。笔者同时安装有这两个版本的编译器与开发环境,以笔者以往的开发经验来看,通常选用VC2005以上版本开发,只有当项目有特殊需求,如要求部署文件精简等才考虑使用VC6编译应用程序。

在本课题中,VC2008用于开发动态代码生成的主程序。

2.5.2 Visual Basic 6.0

Visual Basic是一种快速可视化开发语言与开发环境,用于“所见即所得”的Windows应用程序开发,使用BASIC(Beginner’s All-purpose Symbolic Instruction Code,初学者通用符号指令代码)语言作为语言基础,非常方便于初学者的学习。Visual Basic 6.0由微软公司于1998年推出,隶属于Visual Studio 6产品套件中。其开发环境如图2-4所示。

clip_image007

图2-4 Visual Basic 6.0

与Visual C++不同,VB6并未被更新的版本如Visual Basic 2005等取代,其原因在于从版本7开始(Visual Basic .NET 2003),微软考虑到其.NET战略的实施,将Visual Basic与新产品Visual C#统一列入.NET框架,仅能开发基于.NET Framework的托管代码应用程序,而停止基于Win32与COM平台的本机代码应用程序的支持。这使得新版本开发的应用程序无法在没有安装.NET Framework 2.0环境的Windows XP及之前系统运行。考虑到软件的兼容性,在本课题中笔者选用VB6开发动态代码工具的图形界面外壳程序。

2.5.3 WinHex

二进制编辑软件指可以以二进制或十六进制形式查看、编辑文件的软件。目前市面上此类的软件有UltraEdit、WinHex、HexWorkshop等。在这些软件中,WinHex显得尤为突出,不仅可以编辑文件,还可以直接编辑内存、磁盘扇区等,使用得当甚至可以作为文件恢复、磁盘引导区备份与恢复的工具。在分析未知文件方面,其数据解释器功能可以很直观地显示该地址位置的数据在各种数据类型情况下的数值,便于使用者确定正确的数据类型。图2-5为该软件的主界面。

clip_image009

图2-5 WinHex十六进制编辑器

在本课题中,笔者使用WinHex检查分析使用MASM编译的OBJ目标文件,并将其代码部分复制到主程序中以备后续组装二进制机器码时使用。另一方面,借助其内存查看与编辑功能,笔者可以很方便地将动态代码写入到其它进程的内存地址空间,然后配合OllyDbg进行调试。

2.5.4 LordPE

LordPE是一款可视化的PE文件读取分析与编辑修改工具。借助此软件用户可以很直观地看到PE文件中各种结构的数据,而无需使用WinHex等软件在茫茫十六进制数据中来回定位,或花费时间与精力编写专用的PE格式读取与分析工具。使用LordPE,用户可以清楚地看到输入表、输出表、资源表、重定位表、IAT等一系列数据目录中的具体内容,还可以很方便地增加PE文件的区段或是增加导入函数来为已有的EXE或DLL文件添加新的功能,或改变其原来的执行过程(一般用于软件破解)。图2-6为笔者使用此软件分析本课题使用到的一个输入文件的输入表数据。

clip_image011

图2-6 使用LordPE分析PE文件输入表

2.6 本章小结

本章介绍了Windows可执行文件动态代码化研究需要用到的基本技术、开发平台与相关工具,包括32位汇编、Windows核心编程与PE文件格式、LZMA压缩算法等。这些内容是进行本课题研究之前必备的知识与技术。

3 可执行程序动态代码化的研究与设计

3.1 项目总体架构

从最终生成部署的软件来看,Windows可执行程序动态代码化的软件成果分为两个子程序,笔者将其分别命名为为DynMake(Dynamic Code Maker,动态代码生成工具)和DynShell(Dynamic Code Shell,动态代码界面外壳)。DynMake为生成动态代码的CUI(Console User Interface,控制台用户界面)程序,DynShell为DynMake的GUI(Graphic User Interface,图形用户界面)图形界面包装。从研究的内容来看,DynMake项目又可分为外围公共程序与负责具体动态代码实现的核心代码。综合考虑,Windows可执行程序动态代码化的研究课题可分为三个部分。

(1) DynMake主程序框架与外围程序

此部分为DynMake命令行程序的框架,主要编写与具体的动态代码设计与实现不相关的外围公共代码,包括命令行的解析、PE文件的读取、LZMA压缩算法的移植等。该部分需要学习C++ STL、PE文件格式等内容,使用Visual C++ 2008编写。

(2) 动态代码预置模板

此部分为动态代码的具体实现,为整个课题的核心部分,主要分为两个部分。

第一部分为研究设计一段部署在内存任意位置的动态代码。该动态代码执行时会进行自身的解密和解压缩、释放出PE可执行程序本身或其变形并对其进行基址重定位与输入表函数导入、执行可执行程序等工作,并在必要的用户需求与机器环境条件下进行代码的变形与隐蔽、系统保护机制的绕过等操作。该部分需要学习与研究Intel x86 32位汇编指令集、PE文件格式、Windows核心编程、DEP保护机制与Ret2Lib技术等,使用32位汇编语言编写。由于这段代码还没有与PE文件结合起来,所以不具有真正的使用价值,仅仅是一个类似于“模板”的东西。

第二部分为将上面设计的动态代码模板与输入的PE文件结合起来以生成段实际可用的动态代码,这个过程叫做动态代码的装配。主要工作包括读入PE映像、提取以及加密变形重定位表与输入表等有用数据、对数据的压缩、与动态代码模板的按需整合组装等工作。该部分除了上文提到的PE文件格式、x86指令集等知识外,还需要学习了解汇编器与链接器的基本原理。此部分使用Visual C++ 2008编写,以模块的形式整合到DynMake主程序中。

由于动态代码的设计方法与应用需求多种多样,而毕业设计的时间与论文的篇幅有限,对于本部分笔者仅给出一种动态代码的预置模板,即PELoader可执行程序自加载器的动态代码实现。本章的第3节以及本文第4章的4.2、4.3节均围绕PELoader模板展开讨论。其它设计方案请读者根据笔者设计的代码自行探讨。

(3) DynShell图形界面外壳

DynMake以CUI接口提供给用户,主要适用于开发人员在其他项目的开发过程中以Post-Build的方式调用。为方便使用,笔者为DynMake主程序设计了一个图形界面的包装外壳,用户可在图形界面上选择输入输出文件、设置转换选项,该外壳会自动调用DynMake主程序。图形界面开发的工具与类库多种多样,笔者选用Visual Basic 6.0作为开发环境,需要研究的内容有VB6语言、针对VB6的外部程序调用与标准输入输出重定向等。

3.2 DynMake主程序框架与外围程序

DynMake动态代码生成工具为本课题的主程序,以CUI命令行的形式呈现给用户。该主程序大体可分为以下几个模块:

1) Main函数、命令行解析、帮助。

2) PE文件的读入与基本信息分析。

3) 预置模板类基类与继承的各种动态代码预置模板的实现。其中具体的实现不包含在本节论述的范围内。

4) 全局公用的一些功能,如LZMA压缩调用。

3.2.1 命令行设定与命令行解析模块

在开始此模块的设计之前,需首先规划工具的命令行书写规范。笔者参考了NeroAAC音频压缩工具的命令行接口,设计了类似的命令行书写规范,并在此规则上设计了DynMake程序的基本使用方法,如清单3-1所示。这种书写规则的优点是可以使用一个字符串字典保存所有的参数,便于后续的调用。

清单3-1 DynMake命令行书写基本规则与使用方法

基本规则:

程序名.exe -参数1 值1 -参数2 值2 … -参数n 值n

使用方法:

DynMake.exe –if 输入文件 –of 输出文件 –profile 预置模板名 [附加参数]

DynMake.exe –if 输入文件 –show

DynMake.exe –help

DynMake.exe –help –profile 预置模板名

命令中-if参数指定输入文件,即需要转换为动态代码的EXE或DLL文件,-of参数指定输出文件,即转换后的二进制动态代码,一般扩展名为BIN,-profile参数指定需要使用的预置模板,有关预置模板将在后文论述。第二行命令为显示输入文件的PE格式分析信息,类似于Dumpbin工具,但仅是简要输出,功能不如Dumpbin强大。第三行命令为输出程序帮助。第四行命令为输出预置模板的帮助信息。

附加参数由预置模板使用,指定动态代码生成中的一些细节,每一种预置模板可能有相同的或不同的参数。以PELoader预置模板为例,其附加参数及说明如清单3-2所示。

清单3-2 PELoader动态代码预置模板附加参数

-strip-reloc 分离重定位

-strip-import 分离输入表

-no-res 去除资源表

-dep 与DEP保护机制兼容

-crypt X 对代码进行X层加密

-no-bound 去除绑定输入信息

命令行解析模块在ParseCommand.cpp中实现,包括三个全局函数,其函数声明如清单3-3所示。

清单3-3 ParseCommand.h

#pragma once

extern int ParseCommand(int argc, char** argv);

extern bool IsParamDefined(const char *param);

extern const char *GetParameter(const char *param);

ParseCommand函数在main函数中被调用,用于解析由main函数传入的参数列表,并生成参数字典的键值对。IsParamDefined函数用于判断一个参数开关是否被传入。GetParameter函数用于取得带有值的参数的值,如果参数不含值数据,此函数将返回空字符串。

3.2.2 PE文件读取与信息输出模块

PE文件在转换为动态代码前需要被加载到内存中,这部分工作无论使用何种动态代码预置模板都是相同的,故将其设计为主程序的一个独立的模块,在PEFile.cpp中实现。此部分包括两个函数,分别为LoadPEFile与ShowFileInfo。LoadPEFile用于将PE文件以类似于Windows加载器的方法按照PE文件的RVA读入内存,如果指定文件不存在或不是有效的PE文件则输出相应错误信息。ShowFileInfo用于输出PE文件的一些简要的信息,如区段表、数据目录表、输入表等数据,使用户可简要看出此PE文件是否已被进行过加壳一类的处理,当主程序指定-show参数开关时被调用。

3.2.3 预置模板类

因为动态代码的设计有多种,为了满足不同的需求,主程序引入“预置模板”的概念,一个模板即为一种动态代码输出的固定格式,PE文件转换为动态代码二进制文件的具体实现由预置模板完成。在CUI程序中用户通过指定-profile参数来选择需要用到的预置模板。读者可在本课题代码的基础上设计新的预置模板添加至程序中。

在代码层面上,笔者设计ProfileBase虚基类,各预置模板均由此类继承。在C++语言中,虚基类实际上等同于COM、.NET等语言中接口(Interface)的作用[6]。ProfileBase类的声明如清单3-4所示。

清单3-4 ProfileBase类声明

class ProfileBase

{

public:

virtual const char* GetProfileName() = 0;

virtual const char* GetProfileHelp() = 0;

virtual int ProcessMemoryMap(unsigned char* pMemoryMap) = 0;

virtual const void* GetResult() = 0;

virtual unsigned int GetResultLength() = 0;

};

GetProfileName函数返回预置模板的名称;GetProfileHelp函数返回预置模板的帮助文本,主要包括对此预置模板的介绍以及参数的使用说明等;ProcessMemoryMap函数定义生成动态代码的过程,参数pMemoryMap传入输入PE映像的地址;GetResult函数返回生成的动态代码的内存地址,GetResultLength返回生成的动态代码的长度。

主程序通过一个名为CreateProfile的函数根据用户指定的预置模板名称或序号动态创建相应预置模板的实例对象,并利用C++的多态性调用其接口函数。预置模板必须继承ProfileBase基类并重写这些函数,重点是ProcessMemoryMap函数,该函数用以实现具体的PE映像转二进制动态代码的过程。

3.2.4 LZMA压缩模块

LZMA压缩模块用于对PE映像和动态代码的压缩。移植LZMA压缩算法需要LZMA SDK中的LzmaEnc.c与LzFind.c两个源文件以及它们依赖的头文件[7]。主程序对压缩的需求是对一段内存空间的数据进行压缩,所以可以使用SDK提供的LzmaEnc_MemEncode函数。但此函数的使用比较复杂,不适合在需要调用的现场中直接使用,故有必要设计一个包装程序对此函数进行包装。笔者设计的包装函数在LzmaWrapper.cpp中实现,其函数声明如清单3-5所示。

清单3-5 LzmaWrapper声明

int LzmaEncodeWrap(

const unsigned char *inStream,

unsigned int inSize,

unsigned char *outStream,

unsigned int *outSize,

void(*progress)(unsigned int, unsigned int, void*),

void *cookie

);

参数inStream传入源数据的地址;inSize指定源数据的长度;outStream传入压缩后数据写入的地址;outSize是一个指针,初始时存放目标缓冲区的长度,函数调用结束后会被填入实际压缩后的大小;progress为压缩进度回调函数,三个参数分别为inSize表示已压缩的大小、outSize表示已压缩压缩后的大小、cookie表示回调状态;cookie传入回调状态,一般用于在类成员函数中调用时传入this指针。在不需要处理压缩进度时,progress与cookie可设置为空指针。返回值为0时表示成功压缩,其它数值为出错,具体数值意义可参考LZMA头文件中指定的状态码。

3.3 PELoader动态代码预置模板

3.3.1 动态代码的基本模块组成

动态代码按照执行步骤来说大体可分为以下几个模块:

1) 动态代码头。用于获取动态代码自身地址、保护调用现场等。

2) 解压。用于动态代码自身的解压缩。

3) 获取API。获取系统API函数指针。

4) 解压数据。用于将原可执行文件还原到内存中。

5) 重定位。对原PE文件的基址重定位。

6) 输入表函数导入。对原PE文件导入输入函数。

7) 动态代码尾。恢复调用现场、跳转到原程序入口。

根据一些特殊的用户与系统环境需求,还需要设计一些可选的模块如:

1) 代码解密。用于动态代码被加密的情况。

2) 代码转移执行。用于当运行内存空间与预定解压目标空间覆盖的情况。

3) DEP保护绕过。用于转移到栈中执行时系统开启了DEP保护机制的情况。

动态代码模板并不是单一的,而是根据具体的需要与系统环境的需求按需设计,可以有很多个模板。下文将围绕PELoader预置模板展开讨论。

3.3.2 PELoader动态代码模板的结构设计

PELoader模板的作用是在动态代码的自身地址上实现PE文件的自加载并最终调用PE文件的入口函数。由于DLL的入口函数为三个参数的DllMain,EXE的入口函数为无参数的ModuleEntryPoint,故对于两种PE文件要分别处理,但主要是在调用栈的构造上,在代码布局上二者没有大的区别。PELoader动态代码的代码布局设计如图3-1所示。

clip_image013

图3-1 PELoader加载器模板设计图

3.3.3动态代码结尾Ret2Lib堆栈构造

Ret2Lib(Return To Library,返回到库函数)技术用于绕过DEP保护机制。在开启了DEP保护机制的系统上,动态代码跳转到栈中执行前必须先调用VirtualProtect函数将栈空间设为可执行,同时,动态代码在结束栈代码执行后也有义务将栈空间恢复为不可执行,但在栈内直接调用VirtualProtect会使返回地址也在栈内,函数返回时便会造成访问违规,需要找到一种方法使在恢复栈地址空间为不可执行后还能使代码跳转到PE映像入口地址,Ret2Lib技术即可解决这个问题。以加载DLL文件为例,可构造如图3-2所示的堆栈框架。

clip_image015

图3-2 PELoader动态代码尾部Ret2Lib堆栈框架构造

3.3.4 输入表的重新设计

输入表用于PE文件的依赖动态链接库导出函数的导入。许多加壳软件中,为了确保代码保护的安全性,破坏或重新设计原程序的输入表几乎成为了必经的步骤。正常PE文件的输入表如图3-3所示[1]

clip_image017

图3-3 PE文件输入表结构示意图

Windows加载器在加载PE文件时,会通过IMAGE_IMPORT_DESCRIPTOR结构中的OriginalFirstThunk字段找到INT(Import Name Table,输入名称表),通过INT表中的DLL名称或序号信息查找到相应导入函数的指针并填写到IAT(Import Address Table,输入地址表)中,在动态代码的设计中,必须手工完成这一步骤,即保证动态代码加载器执行完成后IAT表中已经存放了正确的函数指针。为了防止破解者从动态代码释放的PE映像中找到输入表信息,笔者设计了一种新的输入表结构,此表用一字节流来存储输入表的所有信息,其结构描述伪代码如清单3-6所示。

清单3-6 输入表的重新设计

foreach 导入的DLL

DLL文件名(ANSI字符串,以\0结尾)

IAT的RVA(4字节)

foreach 导入函数

函数名称(ANSI字符串,以\0结尾)

or

函数序号(4字节大端字节序,最高位为1)

结束标记(\0)

结束标记(\0)

因导入函数既可以以名称导入也可以以函数序号导入,笔者设计的上述结构即可很好地区分它们,4字节大端字节序下最高位即为第一个字节,与ANSI字符串的第一个字节重复,可通过判断ANSI字符串的首字节的符号来确定该函数是通过函数名称导入还是通过函数序号导入。

3.3.5 重定位表的重新设计

DLL文件由于在进程空间中加载的位置并不固定,所以每次加载后需要进行基址重定位操作。正常PE文件的基址重定位表由多个IMAGE_BASE_RELOCATION结构组成,每个表项存放0x1000也就是一个内存页面的重定位信息。使用表项的VirtualAddress字段加上TypeOffset数据的低12位即可得到需要重定位的地址。

为减小动态代码的体积、简化代码的复杂度并增强代码的安全性,与输入表类似,笔者亦重新设计了重定位表。此表为一个数组,第一个数据为第一个需要重定位的RVA地址,而后的数据为后续重定位地址与前一个地址的差值,因为并未指定数据的总个数,故最后一个数据结束后写入0作为结束标记。

由于重定位地址通常距离较小,大部分不超过256字节,但也有偶然超过的,故笔者考虑设计一种可变长度的整数数据流,其格式为每次读取一个字节,最高位为结束标记,低7位为数据。若最高位为1说明此数值未结束,则将当前数据左移7位,然后继续读取下个字节,直到最高位为0为止。表3-1所示为一个重定位表的重新构造举例,最后生成的新重定位数据为字节流A04206AD380485AF5400。

表3-1 重定位表的重新设计

原表RVA 原表TypeOffset 重定位地址 新表数值 新表数据
00001000 3042 00001042 1042 A0 42
3048 00001048 6 06
00002000 3700 00002700 16B8 AD 38
3704 00002704 4 04
00017000 3ED8 00017ED8 157D4 85 AF 54
3.3.6 PELoader预置模板类

LoaderProfile类是PELoader的预置模板的实现,继承自ProfileBase基类,用于实现动态代码的装配。LoaderProfile类的声明如清单3-7所示。

清单3-7 LoaderProfile类声明

class LoaderProfile : public ProfileBase

{

public:

LoaderProfile();

virtual ~LoaderProfile(void);

virtual const char* GetProfileName();

virtual const char* GetProfileHelp();

virtual int ProcessMemoryMap(unsigned char* pMemoryMap);

virtual const void* GetResult();

virtual unsigned int GetResultLength();

private:

unsigned char *m_pDynCode;

unsigned int m_DynCodeSize;

int LoadPEImage(unsigned char* pMemoryMap);

void *m_pImage;

unsigned int m_imageSize;

unsigned int m_rawImageSize;

PIMAGE_NT_HEADERS m_nt_header;

bool IsImageDll();

int SaveImportData();

int ClearImportData();

unsigned char *m_importData;

unsigned int m_importDataSize;

int SaveRelocData();

int ClearRelocData();

unsigned char *m_relocData;

unsigned int m_relocDataSize;

int ClearBoundAndIAT();

int ClearResData();

int CompressPEImage();

void CompressPEImage_Callback(unsigned int inSize, unsigned int outSize);

static void CompressPEImage_Callback_Static(unsigned int inSize, unsigned int outSize, void *cookie);

unsigned char *m_pImageComp;

unsigned int m_imageCompSize;

map<string, int> m_linkSymbol;

map<int, pair<string, char>> m_linkReloc;

void SetLinkSymbol(int position, const char *name);

void SetLinkReloc(int position, const char *name, bool isRelative = false);

int GetLinkSymbol(const char *name);

void LinkSymbol(int begin, int end);

};

LoaderProfile函数与~LoaderProfile函数为类的构造函数与析构函数,构造函数用以初始化类各成员变量,析构函数用于释放未释放的内存空间。GetProfileName、GetProfileHelp、ProcessMemoryMap、GetResult、GetResultLength函数继承自ProfileBase类,分别用于返回预置模板的名称、返回参数帮助、转换PE文件为动态代码、取得转换后动态代码的指针、取得转换后动态代码的长度。成员变量m_pDynCode、m_DynCodeSize用以保存转换后动态代码的指针和长度。LoadPEImage函数用以转储输入的PE映像,m_pImage为转储的指针,m_imageSize为映像的大小,m_rawImageSize为映像在文件中的大小,m_nt_header为便于访问的映像NT头指针,IsImageDll函数用以判断PE映像是否为DLL映像。SaveImportData、ClearImportData、m_importData、m_importDataSize用于以新结构转储输入表及删除原有输入表。SaveRelocData、ClearRelocData、m_relocData、m_relocDataSize用于以新结构转储重定位表及删除原有重定位表。ClearBoundAndIAT、ClearResData用于删除输入绑定与资源信息。CompressPEImage、CompressPEImage_Callback、CompressPEImage_Callback_Static、m_pImageComp、m_imageCompSize用于对PE映像的压缩。m_linkSymbol、m_linkReloc、SetLinkSymbol、SetLinkReloc、GetLinkSymbol、LinkSymbol用以动态代码装配时符号链接。

3.4 DynShell图形界面外壳

考虑到软件的易用性,在提供CUI命令行用户界面之外,工具提供GUI图形界面包装接口供用户使用。GUI程序提供给用户以直观的选项,并根据用户的选择生成需要的命令行参数在后台调用CUI主程序。图3-4为笔者设计的主窗口界面。由于不是课题研究重点,此部分无需经过很详细的设计过程,在界面设计完成后即可进入实现阶段,笔者将在第四章具体论述。

clip_image019

图3-4 DynShell GUI图形界面

3.5 本章小结

本章探讨了Windows可执行程序动态代码化课题的模块划分并阐述了对每个模块的基本设计,包括DynMake主程序框架、动态代码的结构及其中一种PELoader预置模板的设计、图形界面的设计等。在第4章内笔者将对这些内容进行有关具体实现上的论述。

4 可执行程序动态代码化的具体实现

4.1 DynMake主程序框架与外围程序部分

4.1.1 命令行解析

前文中提到,DynMake主程序的命令行解析模块由ParseCommand、IsParamDefined和GetParameter三个函数实现。笔者使用C++ STL中的map容器定义g_Params变量存放命令行参数键值对[8]。其中IsParamDefined与GetParameter函数简单地使用map容器的find函数从g_Params中查找指定参数是否存在或查找取得参数的值,ParseCommand用于解析命令行参数并向g_Params变量添加键值对,其实现代码如清单4-1所示。

清单4-1 ParseCommand命令行解析

for (int i = 1; i < argc; ++i)

{

if (strlen(argv[i]) > 1 && argv[i][0] == ‘-‘)

{

if (i + 1 < argc && argv[i + 1][0] != ‘-‘)

{

g_Params.insert(pair<string, string>(argv[i], argv[i + 1]));

++i;

}

else

{

g_Params.insert(pair<string, string>(argv[i], “”));

}

}

}

4.1.2 PE文件的读入

将PE文件读入内存可以有两种方法,其中一种是直接将整个文件读入内存,这样的好处是读取简单,但在以后的数据操作上会有很大的弊端,因为PE内部数据的定位全部由VA或RVA完成,使用这种方法必须每次都将VA或RVA转换为文件偏移,显得非常麻烦并且很容易出错。更好的方法是模仿Windows加载器将PE文件按区段加载到内存,使用数据只需用RVA加上基地址即可[1]。此部分代码在PEFile.cpp中LoadPEFile函数中实现,主要代码如清单4-2所示。

清单4-2 读取PE文件

FILE *fp = fopen(fileName, “rb”);

IMAGE_DOS_HEADER t_dos_header;

PIMAGE_DOS_HEADER dos_header = &t_dos_header;

fread(dos_header, sizeof(IMAGE_DOS_HEADER), 1, fp);

IMAGE_NT_HEADERS t_nt_header;

PIMAGE_NT_HEADERS nt_header = &t_nt_header;

fseek(fp, dos_header->e_lfanew, SEEK_SET);

fread(nt_header, sizeof(IMAGE_NT_HEADERS), 1, fp);

void *codeBase = malloc(nt_header->OptionalHeader.SizeOfImage);

fseek(fp, 0, SEEK_SET);

fread(codeBase, 1, nt_header->OptionalHeader.SizeOfHeaders, fp);

dos_header = (PIMAGE_DOS_HEADER)codeBase;

nt_header = (PIMAGE_NT_HEADERS)CALCULATE_ADDRESS(codeBase, dos_header->e_lfanew);

PIMAGE_SECTION_HEADER section;

section = IMAGE_FIRST_SECTION(nt_header);

for (int i = 0; i < nt_header->FileHeader.NumberOfSections; ++i)

{

if (section->SizeOfRawData > 0)

{

fseek(fp, section->PointerToRawData, SEEK_SET);

fread((LPVOID)CALCULATE_ADDRESS(codeBase, section->VirtualAddress), 1, section->SizeOfRawData, fp);

}

++section;

}

return codeBase;

PE文件的基本信息输出由ShowFileInfo函数实现,因与本课题重点研究的动态代码关系不大,限于篇幅不再展示,读者可自行参阅工程代码。

4.1.3 LZMA压缩算法包装

LZMA压缩算法包装函数LzmaEncodeWrap定义在LzmaWrapper.cpp中,其主要目的是调用包装对LzmaEnc_MemEncode函数的调用,并实现压缩进度的回调过程。主要代码如清单4-3所示。

清单4-3 LzmaEncodeWrap主要代码

CLzmaEncHandle p = LzmaEnc_Create(&alloc);

SRes res;

CLzmaEncProps props;

LzmaEncProps_Init(&props);

LzmaEncProps_Normalize(&props);

res = LzmaEnc_SetProps(p, &props);

if (res == SZ_OK)

{

m_cookie = cookie;

m_progress = progress;

res = LzmaEnc_MemEncode(p, outStream, outSize, inStream, inSize, 0, &lzmaprogress, &alloc, &alloc);

}

LzmaEnc_Destroy(p, &alloc, &alloc);

return res;

4.2 PELoader模板动态代码的实现

4.2.1 动态代码入口

动态代码入口通常需要完成三项工作,一是取得当前动态代码自身的基地址,二是申请栈局部存储空间,三是保存调用寄存器现场。

对于第一个问题,通常在动态代码的第一句使用“call $+5”,即机器码“E800000000”这样的汇编语句,一般称为Dummy Call,与一般调用子函数的call不同,这种call的作用就是将下一句代码的地址压入堆栈,然后将这个地址减掉5即得到动态代码的基地址。

第二个问题为申请堆栈局部存储空间。这里需要注意一个问题,即Windows堆栈申请需要逐页进行,如果跨页访问则会引发访问违规,即申请0x1000大小以下的栈内存可以直接使用“sub esp,xxx”这样的语句进行,而大于这个大小则必须每减掉0x1000进行一次内存的访问。笔者设计的方法为“mov ecx,xxx push 0 loop $-2”,ecx存放需要申请空间的大小除以4并向上4字节对齐。

第三个问题可以采用简单的“pushad”语句,亦可根据Windows 平台下的C/C++函数调用约定即被调用函数只能修改EAX、ECX、EDX三个寄存器的规范按需保存即将覆盖的寄存器。

4.2.2 代码自身解压缩

与压缩相对应,本节需要使用LZMA解压缩算法。与通常的C语言实现相比,在动态代码中由于操作上的限制,在解压缩算法的实现上有更为严格的要求,其中一个重要的方面就是无法使用堆内存,而只能使用固定大小的栈空间,且由于栈本身大小的限制(默认为1MB),解压不能使用大规模的压缩字典。笔者通过对LZMA解压缩算法代码的研究与修改,最终成功移植编译出一个精简代码并且适用于栈空间的LZMA解压函数,函数体大小2296字节,栈空间消耗为16KB[9]。其函数声明如清单4-4所示。

清单4-4 LzmaDecode解压函数声明

int __stdcall LzmaDecode(

const unsigned char *inStream, unsigned int inSize,

unsigned char *outStream, unsigned int outSize);

参数inStream指定压缩数据的内存地址,inSize指定压缩流的大小,outStream传入解压目标的位置,outSize为解压缓冲区的大小。

由上文设计的动态代码布局可知,本例中动态代码有两段压缩数据,第一段是对后续代码与转储的输入表、重定位表的压缩,第二段是对PE映像的压缩,解压算法需调用两次,故以带有RET指令的函数形式写入到动态代码中。另外一点,由于动态代码生成的文件中压缩数据是紧接着未压缩数据存放的,在解压之前必须将压缩数据转移至内存的其他地址才能在原先的地址解压输出。笔者将两段压缩数据一并搬移到PE映像空间的末尾处,而后对第一段压缩数据进行解压。主要代码如清单4-5所示。

清单4-5 动态代码压缩数据转移与自身解压缩

lea esi,[ebx+CompDataSrc]

lea edi,[ebx+(imageSize-1)]

mov ecx,CompDataSize

std

rep movsb ;内存移动

cld

push CompStubSize ;outSize

lea esi,[ebx+CompStubStart]

push esi ;outStream

push CompStubCompSize ;inSize

inc edi

push edi ;inStream

push esi ;返回到CompStubStart

LzmaDecode:

…… ;LzmaDecode代码

ret 10

CompStubStart:

…… ;解压后的代码

4.2.3 Kernel32.dll基地址的获取

动态代码在执行的过程中需要调用诸如LoadLibrary、GetProcAddress一类的API以完成PE文件的输入表填充,而动态代码本身并不是由Windows加载器加载,如何“凭空”获取API函数的指针便成为亟需解决的问题。目前普遍的做法是先设法获取kernel32.dll的基地址,而后通过查找DLL的输出表来计算API函数指针。

获取kernel32.dll基地址的方法有多种,如堆栈返回值暴力搜索、SEH链搜索等。笔者将使用一种比较常用的方法,即访问进程PEB(Process Environment Block,进程环境块)结构中的加载模块信息来取得。

PEB结构的指针保存在TEB(Thread Environment Block,线程环境块)结构偏移30h位置处,可通过访问FS段寄存器FS:[30h]位置获取。PEB加载模块信息PEB_LDR_DATA结构位于PEB结构的0ch偏移处。

PEB_LDR_DATA结构有三个模块链表,分别按照加载顺序、内存地址顺序和初始化顺序排列,我们使用其中的InInitializationOrderModuleList链表。在Windows 2000/XP系统下,进程模块的初始化顺序首先为ntdll.dll,其次为kernel32.dll,故可直接枚举到第二个模块即为kernel32.dll;更新到Windows 7之后,模块的初始化顺序为ntdll.dll到KernelBase.dll再到kernel32.dll,故不宜仅枚举到第二个模块,而需要比较DLL文件名字符串中的特征来确定kernel32.dll的加载地址[10]。笔者设计的代码如清单4-6所示。

清单4-6 通过PEB获取KERNEL32.DLL基地址

0000 33c0 xor eax,eax

0002 64 8b70 30 mov esi,fs:[eax+30]

0006 8b76 0c mov esi,[esi+0c]

0009 8b76 1c mov esi,[esi+1c]

000c 8b4e 08 mov ecx,[esi+8]

000f 8b7e 20 mov edi,[esi+20]

0012 8b36 mov esi,[esi]

0014 66 3947 18 cmp word ptr [edi+18],ax

0018 75 f2 jnz 000c

该段代码执行后,ECX寄存器保存的即为kernel32.dll的起始地址。

4.2.4 API函数指针的获取

获得kernel32.dll地址之后,需要通过手工查找DLL输出表取得API的函数指针。通常的方法是将需要查找的DLL导出函数名与输出表中的函数名逐个进行字符串比较。笔者设计了一种方法,不直接传入函数名称,而是传入经过一定类似Hash算法的变换后的名称特征值,以减小生成动态代码的体积,并有一定程度的代码保密效果。代码使用C语言编写,函数声明与调用示例如清单4-7所示。

清单4-7 HashImport函数声明与调用示例

//函数声明:

void __fastcall HashImport(DWORD codeBase, DWORD* hashTable);

;调用示例:

…… ;此处为获取Kernel32.dll基地址的代码,执行后ECX为基地址

push 0

push 26d6d27ch ;GetProcAddress的特征值

push 14202374h ;LoadLibraryA的特征值

push 42ab5949h ;VirtualProtect的特征值

lea edx,[esp]

…… ;FASTCALL HashImport的代码

函数第一个参数codeBase传入DLL的基地址,第二个参数hashTable传入需查找的函数信息的数组,数组以0结尾。数组项的最高位为0则低31位表示导出函数名的特征值,最高位为1则表示该函数使用函数序号导入,此时低31位表示函数序号。查找到的函数指针写回到hashTable数组的相应位置。__fastcall表示函数遵守Fastcall调用约定,该调用约定规定函数的第一个参数和第二个参数分别使用ECX和EDX寄存器传送。这段代码紧接着上节获取kernel32.dll基地址的代码执行,ECX寄存器存放的即为kernel32.dll的加载起始地址。

此代码执行完成后,ESP指针处地址由低至高即分别存放VirtualProtect、LoadLibraryA、GetProcAddress三个kernel32.dll导出函数的入口指针。

4.2.5 堆栈代码执行

因为即将对PE映像进行解压缩,这要求正在执行的代码不能处于将要在解压缩时被覆盖的内存区域,解决方案可以有两种,一种是使用Windows API的VirtualAlloc函数或HeapAlloc函数申请一块新的内存空间,将正在执行的代码拷贝到新的内存空间中然后跳转过去继续执行,另外一种方法是直接在栈中开辟一块空间,填充代码后跳转执行。笔者选用第二种方法,即在堆栈中执行代码。堆栈代码有着申请空间高效(直接操作ESP指针)、操作方便、无需主动释放等优点。另外由于栈空间在代码执行完毕后自动释放,其中的代码即永远消失,具有非常高的安全保密性,而无需专门调用系统函数申请空间,也使破解者无法通过对API下断进行逆向跟踪。复制代码到堆栈空间并跳转执行的代码如清单4-8所示。

清单4-8 堆栈代码的复制与执行

lea esi,[ebx] ;动态代码地址

lea edi,[ebp-CodeSize] ;堆栈中的目标地址

mov ecx,CodeSize ;动态代码代码部分的长度

rep movsb ;复制内存

lea edx,[ebp-CodeSize] ;堆栈中的目标地址

add edx,(StackCode–start) ;StackCode的位置

jmp edx

StackCode:

…… ;从这里开始代码在栈中执行

以上代码在Windows 2000/XP可以正常运行,但在Windows 2003/Vista/7系统上无法正常运行。原因是为了防止缓冲区溢出攻击,从Windows 2003开始系统默认开启了DEP(Data Execution Protection,数据执行保护)机制,DEP开启后系统将为虚拟内存的每一页将设置NE(Not Executable)标志,表示此页面不可执行,此时若贸然进入则会造成访问违规。堆栈空间在通常情况下是不允许被执行的,若确有必要执行堆栈中的代码,需调用核心API中的VirtualProtect函数修改指定内存的访问权限为可执行。VirtualProtect函数有4个参数,参数lpAddress指定要修改权限的内存段的起始地址,dwSize为内存段长度,flNewProtect为新的权限,如设置为可读可写可执行则设为PAGE_EXECUTE_READWRITE,函数执行成功后lpflOldProtect中存放之前的权限值,供以后恢复原来的访问权限时使用[3]

栈代码执行退出时需要配合Ret2Lib技术使程序能够正确地跳转到PE映像入口函数处,有关动态代码结尾的Ret2Lib堆栈构造笔者已在上一章说明,在此不再赘述。清单4-9为利用Ret2Lib技术进入栈空间执行的清单4-8的DEP兼容版本。

清单4-9 使用Ret2Lib技术跳转到栈内存空间

lea esi,[ebx] ;动态代码地址

lea edi,[ebp-CodeSize] ;堆栈中的目标地址

mov ecx,CodeSize ;动态代码代码部分的长度

rep movsb ;复制内存

lea eax,[ebp+14]

push eax ;lpflOldProtect

push 40 ;flNewProtect

mov ecx,CodeSize

push ecx ;dwSize

lea edx,[ebp-CodeSize]

push edx ;lpAddress

add edx,(StackCode-start)

push edx ;返回地址到StackCode

jmp [ebp+4] ;跳转到VirtualProtect

StackCode:

…… ;从这里开始代码在栈中执行

4.2.6 PE映像的解压缩

动态代码进行的第二次解压缩调用是对PE映像的解压缩,此时动态代码已运行在栈空间中,LZMA解压算法亦在栈中有一份拷贝。由于LZMA为字节流式压缩算法,无论是压缩还是解压缩每次都只会顺序读取、写入一个字节,不会产生寻址或回溯的操作,所以笔者将PE映像的压缩数据放置在解压缩空间的尾部,由于LZMA算法顺序读取的特性,即使解压到最后压缩数据的前端被解压后的数据覆盖,也不会产生任何异常情况。解压完成后,压缩数据即完全被覆盖。这样可以使内存中产生一个干干净净的PE映像,并且在栈被释放后不留下任何痕迹。解压PE映像的主要代码如清单4-10所示。

清单4-10 PE映像的解压缩

lea esi,[ebx+(ImageSize-ImageCompSize)]

lea edi,[ebx]

mov ecx,ImageCompSize

push ImageSize ;outSize

push edi ;outStream

push ecx ;inSize

push esi ;inStream

call LzmaDecode ;调用LZMA解压函数

4.2.7 映像重定位

由于动态代码加载的起始地址不固定,PE映像解压之后需要进行基址重定位操作以确保程序中对数据的引用正确。对于标准的PE文件,笔者使用C语言实现该过程,其函数声明与调用方法如清单4-11所示。参数codeBase为PE文件加载的起始地址,依据Fastcall调用约定,此参数通过ECX寄存器传送。

清单4-11 标准PE重定位函数声明与调用示例

//函数声明:

DWORD __fastcall PEReloc(LPVOID codeBase);

;调用过程:

lea ecx,[ebx] ;codeBase

…… ;FASTCALL PEReloc的代码

前文中笔者提到了可以通过将重定位表分离起到减小生成文件体积并提高代码安全性的作用,并设计了一种新的重定位信息存放格式,使用可变长度的整数记录两个相邻的重定位数据之间的距离。之所以设计为这种可变长度整数是因为考虑到这种数据格式读取时指令非常简洁。清单4-12是根据设计的可变整数重定位数据对PE映像进行基址重定位的代码[11]

清单4-12 可变整数基址重定位算法

0000 33c0 xor eax,eax

0002 c1e0 07 shl eax,7

0005 ac lodsb

0006 d0e0 shl al,1

0008 72 f8 jb 0002

000a d1e8 shr eax,1

000c 74 06 je 0014

000e 03f8 add edi,eax

0010 0117 add [edi],edx

0012 eb ec jmp 0000

0014 ……

在清单所示的可变整数重定位代码中,寄存器EDX传入当前PE映像当前加载地址与预设加载地址ImageBase的差值,ESI传入重定位数据,EDI传入PE映像基地址。调用示例如清单4-13所示。

清单4-13 可变整数基址重定位算法的调用示例

lea edx,[ebx-ImageBase] ;重定位差值

lea esi,[ebp-StubCodeSize] ;栈代码起始地址

add esi,VarIntRelocData ;可变整数重定位数据

mov edi,ebx ;PE基地址

…… ;VarIntReloc的代码

4.2.8 输入表函数导入

与重定位表类似,PE文件还需要对输入表中的函数进行导入。使用C语言实现的函数声明与调用示例如清单4-14所示。其中codeBase参数为PE映像的起始地址,pFN为结构CLibFuncs的指针,指向函数导入需要用到的两个API函数:LoadLibraryA与GetProcAddress,这两个函数的指针已在前文论述有关获取KERNEL32.DLL中API函数指针的一节中得到,在此不再赘述。函数返回值为PE文件的入口函数地址,供最后一步跳转到入口函数使用。

清单4-14 标准PE输入表函数导入函数声明及调用示例

//结构声明:

typedef struct _CLibFuncs

{

HMODULE(WINAPI* pLoadLibraryA)(LPCSTR);

FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);

} CLibFuncs;

//函数声明:

DWORD __fastcall PEImport(LPVOID codeBase, CLibFuncs* pFN);

;调用示例:

lea edx,[esp] ;pFN

lea ecx,[ebx] ;codeBase

…… ;FASTCALL PEImport的代码

同样地,前文笔者也设计了一种转储输入表的方案,以进一步提高代码的安全性。清单4-15是针对这种新的输入表存放格式设计的导入算法[11]

清单4-15 新输入表函数导入算法

loc_10:

cmp byte ptr[esi],0

je loc_end

push esi ;DllName

call dword ptr[esp+4] ;LoadLibraryA

push eax

or ecx,-1

xchg esi,edi

repne scasb

xchg esi,edi

lodsd ;FirstThunk RVA

lea edi,[ebx+eax] ;FirstThunk

loc_20:

cmp byte ptr[esi],0

jne loc_30

inc esi

pop eax

jmp loc_10

loc_30:

jl loc_40

push esi

xor al,al

or ecx,-1

xchg esi,edi

repne scasb

xchg esi,edi

jmp loc_50

loc_40:

lodsd

and al,7fh

bswap eax ;ord

push eax

loc_50:

push dword ptr[esp+4] ;hModule

call dword ptr[esp+10h] ;GetProcAddress

stosd

jmp loc_20

loc_end:

另外,当重定位表和输入表均使用标准PE格式而没有转储时,为了进一步节省空间,笔者将它们合并成一个C语言函数,其声明与调用示例如清单4-16所示,其调用接口与前文单独使用标准输入表一致,返回值亦为可执行程序的入口函数地址。

清单4-16 PE映像重定位与函数输入整合的函数声明与调用示例

//结构声明

typedef struct _CLibFuncs

{

HMODULE(WINAPI* pLoadLibraryA)(LPCSTR);

FARPROC(WINAPI* pGetProcAddress)(HMODULE, LPCSTR);

} CLibFuncs;

//函数声明

DWORD __fastcall PERelocAndImport(LPVOID codeBase, CLibFuncs* pFN)

;调用示例:

lea edx,[esp] ;pFN

lea ecx,[ebx] ;codeBase

…… ;FASTCALL PERelocAndImport的代码

4.2.9 调用入口函数

在完成了PE映像的解压、重定位与函数输入之后,动态代码的使命也基本完成,最后一步即调用PE映像的入口函数,将程序的控制权转交给原先的可执行程序本身。

当动态代码无需考虑DEP保护机制时,可直接跳转或使用堆栈返回到PE映像的入口点处,而当DEP机制开启时,在将控制权转移到PE映像之前,动态代码有义务将堆栈的页面访问权限恢复为不可执行,以避免被恶意软件利用来进行溢出攻击而造成的系统安全问题。清单4-17为笔者设计的有DEP保护机制的收尾代码。

清单4-17 DEP兼容的动态代码尾部

mov [ebp+8],eax ;Ret2Lib到入口点

lea eax,[ebp+18]

mov [ebp+18],eax ;lpflOldProtect

pop eax

pop eax

pop eax ;平衡堆栈

pop edi

pop esi

pop ebx ;恢复寄存器

leave

ret ;Ret2Lib到VirtualProtect

4.3 PELoader预置模板的实现

4.3.1 PE映像的转储与判断

前文在主程序公共模块部分已经实现了PE文件的读取,并传递给预置模板类的ProcessMemoryMap接口函数。由于可能需要PE映像进行重定位、输入表、资源表的删除,需修改PE映像的数据,而ProcessMemoryMap的pMemoryMap参数声明为const,故需将此PE映像复制一份,以后对复制的映像进行操作。此过程由LoaderProfile::LoadPEImage函数实现,函数首先判断DOS头部的e_magic与NT头部的Signature以判断映像是否为有效的PE文件,而后根据NT头的OptionalHeader.SizeOfImage字段确定PE映像的大小,并写入m_imageSize成员变量中。然后为m_pImage申请该大小的空间,并使用memcpy将原PE映像完整地复制到新内存空间中[12]。此外,该函数还取得了PE映像的文件中大小,记录在m_rawImageSize变量中,供以后计算最终生成文件的压缩率使用,取得映像在文件中的大小的方法为找到区段表中最后一个区段的结构,将其PointerToRawData与SizeOfRawData值相加。

另一方面,由于EXE与DLL文件入口函数不同,在动态代码的栈构造上亦会存在不同,故这里还需要对PE文件的类型进行判断,IsImageDll函数用以判断当前的PE映像是否为DLL,具体做法为检查NT头中FileHeader.Characteristics字段中是否含有IMAGE_FILE_DLL位标记。

4.3.2 输入表的处理

前文笔者设计了新的输入表结构,在用户选择“分离输入表”功能时使用。此功能的实现包括以新的结构转储PE映像的输入表和删除原PE映像的输入表两个部分,分别由SaveImportData函数与ClearImportData函数实现,由于篇幅所限,这里不贴出具体的代码清单,仅以文字描述其实现过程[13]

在SaveImportData函数中,首先为m_importData即新输入表字节流申请内存空间,因为输入表的信息不会太大,笔者申请了0x10000即64Kb的空间供转储后的输入表信息使用。接下来通过NT头中的数据目录表输入表数据目录入口,判断其Size的大小。如果大于零,则根据RVA定位到输入表,对输入表的每一个DLL文件进行枚举,将其DLL文件名与FirstThunk的RVA依次写入字节流中。而后根据OriginalFirstThunk或FirstThunk枚举每个导入函数的信息,如果函数通过名称导入,则将函数名写入m_importData字节流中,如果通过序号导入,则将序号最高位设为1,然后将字节序转换为大端字节序写入到字节流中。在一个DLL所有函数枚举完毕后向字节流写入一个0字节,输入表所有的DLL枚举完毕后再向字节流写入一个0字节。

在ClearImportData函数中,与转储输入表类似,首先也是找到NT文件头中数据目录表中输入表的入口,然后枚举每个DLL文件。对于每个DLL文件,首先将DLL文件名字符串清零,然后枚举OriginalFirstThunk中每个函数,如果函数通过函数名导入,则将函数名的字符串以及其Hint清零,其后将thunk的数据清零。在一个DLL的函数枚举完毕后,将该DLL的IMAGE_IMPORT_DESCRIPTOR结构清零。最后将输入表数据目录入口的VirtualAddress与Size清零。

4.3.3 重定位表的处理

与输入表类似,笔者在前文亦设计了新的重定位信息的数据结构,并在用户选择“分离重定位”功能时使用。分离重定位包括以新的结构转储重定位信息与清除原映像中重定位表两部分,分别由SaveRelocData函数与ClearRelocData函数实现[13]

在SaveRelocData函数中,首先找到NT头数据目录表中重定位表的入口,判断其Size的大小,如果大于零则说明存在重定位项。因为转储后的重定位信息以可变整数存储两个重定位地址的差值,大部分的数据均为一个字节,故新的重定位信息总大小不会超过以两个字节表示一个数据项的旧重定位表的大小,为m_relocData申请旧重定位表大小也就是目录入口中Size字段所示的内存空间即可。转储时首先设lastAddr局部变量为零,表示上一个重定位项的地址,而后枚举每一个重定位内存页面块中的每一个表项,计算其地址与lastAddr的差值,以可变整数的形式写入m_relocData字节流中。在构造可变整数时,需要用上后进先出的栈的数据结构,笔者使用STL中vector容器实现,其主要代码如清单4-18所示[8]。所有重定位数据枚举完毕后,在m_relocData字节流尾部写入一个0字节。

清单4-18 使用vector容器构造可变整数重定位信息

vector<unsigned char> varint;

while (var != 0)

{

varint.push_back(var & 0x7f);

var >>= 7;

}

while (varint.size() > 1)

{

m_relocData[m_relocDataSize++] = varint.back() | 0x80;

varint.pop_back();

}

m_relocData[m_relocDataSize++] = varint.back();

varint.pop_back();

ClearRelocData函数用以清除原PE映像的重定位表。与输入表只存储DLL信息而以指针的方式存储函数信息与字符串不同,重定位表的所有数据均在数据目录入口处VirtualAddress与Size字段指定的地址区间中,故此函数只需使用memset将这段区间清零,而后将数据目录入口的这两个字段填零即可。

4.3.4 其他数据的处理与PE映像的压缩

为精简生成的动态代码体积,用户可以指定-no-res、-no-bound等参数清除PE映像中的资源表与绑定输入信息,清除资源与清除绑定输入信息分别由ClearResData函数与ClearBoundAndIAT函数实现。

与重定位表类似,资源表、绑定输入、输入地址表的数据均由数据目录表中的VirtualAddress与Size字段指定而没有间接的指针,故清除这些信息与清除重定位表的方法相同,根据数据目录表中指定的地址与长度将相应内存空间清零,其后将数据目录表中的信息填写零即可。

在PE映像所有相关数据转储分离完毕后,即可对PE映像进行压缩。对PE映像的压缩通过CompressPEImage、CompressPEImage_Callback与CompressPEImage_Callback_Static三个函数实现。CompressPEImage函数中首先为m_pImageComp申请大小为m_imageSize的内存空间,用以存放压缩后的数据,而后调用LzmaEncodeWrap压缩函数。CompressPEImage_Callback为压缩过程的回调函数,用以事实输出压缩进度、已压缩数据大小、压缩率等信息。CompressPEImage_Callback_Static为回调函数的静态函数版,由于LZMA压缩算法的进度回调函数必须传入C函数而不能是类成员函数,此函数被用于实际的传入,同时根据cookie保存的this指针对实际的CompressPEImage_Callback进行调用。

4.3.5 代码符号链接

动态代码的编写过程相当于汇编器进行一句一句汇编的过程,有可能会在前面的代码中引用到后面的代码或数据。这时后面数据的地址还没有被确定也不能被预测,故在此句的编写中不能写入具体的数值而要使用符号标签代替,而后在所有代码编写完成后再对这些符号进行统一的链接。有关符号链接的基本知识可以参考链接器的设计思想。

在PELoader动态代码预置模板类中,笔者使用两个STL中的map容器保存链接符号。m_linkSymbol用以保存被引用的链接符号表,m_linkReloc用以保存代码编写中需要引用符号进行重定位的地址表[8]。另外使用四个成员函数进行符号链接相关的工作,分别是SetLinkSymbol、GetLinkSymbol、SetLinkReloc和LinkSymbol函数。SetLinkSymbol函数用于向m_linkSymbol表中添加符号。GetLinkSymbol函数用于从m_linkSymbol表中根据符号的名称取得符号的地址。SetLinkReloc函数用于向m_linkReloc表中添加需要引用符号的重定位地址,其中符号重定位的方式有直接与相对两种,分别在表中用“I”(immediate)与“R”(relative)表示。LinkSymbol函数用于对代码进行链接,begin与end参数指定链接的起始与终止地址,其代码如清单4-19所示。

清单4-19 LinkSymbol符号链接

for (map<int, pair<string, char>>::iterator iter = m_linkReloc.begin(); iter != m_linkReloc.end();)

{

if (iter->first >= begin && iter->first < end)

{

if (iter->second.second == ‘I’)

{

*(int*)&m_pDynCode[iter->first] = GetLinkSymbol(iter->second.first.c_str());

m_linkReloc.erase(iter++);

}

else if (iter->second.second == ‘R’)

{

*(int*)&m_pDynCode[iter->first] = GetLinkSymbol(iter->second.first.c_str()) – (iter->first + 4);

m_linkReloc.erase(iter++);

}

}

else

{

++iter;

}

}

4.3.6 动态代码的装配

动态代码的装配即按照动态代码模板的设计将每一步的模块在机器码的层次上组装起来,由预置模板类的ProcessMemoryMap函数实现。在这里将调用到上文几节设计的相关函数。由于要对各种用户指定参数进行处理而生成不同的代码,装配的过程十分繁琐,清单4-20仅写出能够显示此函数基本执行流程的语句,完整的代码请读者自行查看工程代码中的LoaderProfile.cpp文件。

清单4-20 动态代码装配基本执行流程

LoadPEImage(pMemoryMap);

if (IsParamDefined(“-strip-reloc”)) //重定位分离

{

printf(“分离重定位…”);

SaveRelocData();

ClearRelocData();

printf(“Done!\n”);

}

if (IsParamDefined(“-strip-import”)) //输入表分离

{

printf(“分离输入表…”);

SaveImportData();

ClearImportData();

printf(“Done!\n”);

}

if (IsParamDefined(“-no-res”)) //删除资源

{

ClearResData();

}

if (IsParamDefined(“-no-bound”)) //删除IAT绑定

{

ClearBoundAndIAT();

}

CompressPEImage(); //压缩PE映像

printf(“动态代码装配第一阶段…”);

……

LinkSymbol(compStubStart, w.Tell()); //第一次Link

printf(“Done!\n”);

printf(“压缩动态代码…”); //StubCode压缩

LzmaEncodeWrap(m_pDynCode + compStubStart, compStubSize, m_pDynCode + compStubStart, &compStubCompSize, NULL, NULL);

printf(“Done!\n”);

printf(“动态代码装配第二阶段…”);

……

LinkSymbol(0, w.Tell()); //第二次Link

printf(“Done!\n”);

printf(“All Done!\n”);

printf(“生成动态代码大小: %d => %d bytes 压缩率:%2.1f%%\n”, m_rawImageSize, m_DynCodeSize, m_DynCodeSize * 100.0 / m_rawImageSize);

4.4 GUI图形界面的功能实现

4.4.1 主窗体功能的实现

主窗体的功能实现包括主窗体上所有按钮控件事件的编写以及与之相关的不必另外划分为独立模块的代码[14]。本节仅讨论其中几个相对有价值一些的功能,对于完整的事件处理代码请读者参阅工程代码。

(1) 打开文件与保存文件窗口

打开文件与保存文件窗口通过调用Windows提供的通用对话框API(由COMDLG32.DLL导出)实现,具体做法为先填充OPENFILENAME结构,然后调用API函数GetOpenFileName用以显示打开文件对话框,或调用API函数GetSaveFileName用以显示保存文件对话框[15]。打开文件对话框的代码如清单4-21所示,保存文件的代码与之类似,在此不再赘述。

清单4-21 OpenFileDialog

Dim file As OPENFILENAME

file.lStructSize = Len(file)

file.hInstance = App.hInstance

file.lpstrFile = String(255, 0)

file.nMaxFile = 255

file.lpstrFilter = “可执行文件(*.exe,*.dll)” & Chr(0) & “*.exe;*.dll” & Chr(0) & “所有文件(*.*)” & Chr(0) & “*.*” & Chr(0)

file.flags = OFN_HIDEREADONLY + OFN_PATHMUSTEXIST + OFN_FILEMUSTEXIST

If GetOpenFileName(file) > 0 Then

OpenFileDialog = Left(file.lpstrFile, InStr(file.lpstrFile, Chr(0)) – 1)

End If

(2) 命令行的构建

图形界面程序需根据用户的选择生成正确的命令行参数用以调用DynMake主程序。该步骤由ParseCmdLine函数实现。其简要代码如清单4-22所示。

清单4-22 ParseCmdLine

Dim tok() As String

ReDim tok(0)

tok(0) = “””” & App.Path & “\DynMake.exe” & “”””

If txtInputFile.Text <> “” Then ReDim Preserve tok(UBound(tok) + 1): tok(UBound(tok)) = “-if “”” & txtInputFile.Text & “”””

If txtOutputFile.Text <> “” Then ReDim Preserve tok(UBound(tok) + 1): tok(UBound(tok)) = “-of “”” & txtOutputFile.Text & “”””

ReDim Preserve tok(UBound(tok) + 1): tok(UBound(tok)) = “-profile ” & cboBuildProfile.ItemData(cboBuildProfile.ListIndex)

If chkUseStripReloc.Value = Checked Then ReDim Preserve tok(UBound(tok) + 1): tok(UBound(tok)) = “-strip-reloc”

……

ParseCmdLine = Join(tok, ” “)

(3) 时钟循环与压缩进度条显示

当图形界面调用ExecuteShell开始执行DynMake主程序时,时钟循环也同时启动,用以每间隔100毫秒时间从控制台读取输出的内容,一方面将内容传递到frmInfo窗口中输出,另一方面解析出其中压缩的进度、压缩率等数据显示在主窗体的进度条中。解析输出信息中压缩进度与压缩率可使用正则表达式技术轻松实现,相关代码如清单4-23所示。

清单4-23 使用正则表达式解析进度与压缩率

Dim re

Set re = CreateObject(“VBScript.RegExp”)

re.Global = True

re.Pattern = “\((\d+\.?\d*)%\)”

For Each mh In re.execute(s)

progress = mh.submatches(0)

UpdateStatus picProgressBar, progress / 100

Next

re.Pattern = “(\d+) => (\d+) bytes 压缩率:(\d+\.?\d*)%”

For Each mh In re.execute(s)

ratio = mh.submatches(2)

UpdateStatus picRatioBar, ratio / 100

Next

笔者参考VB打包部署安装向导的源代码设计了一种进度条的实现。进度条使用了在PictureBox控件上手工绘制进度文字及进度色块的方法,并使用NOT XOR笔刷使进度条覆盖的地方产生进度文字反色的效果[16]。更新进度条状态由UpdateStatus函数实现,主要代码如清单4-24所示。

清单4-24 UpdateStatus更新进度条状态

Dim intX As Integer

Dim intY As Integer

Dim intWidth As Integer

Dim intHeight As Integer

Const colBackground = &HF0F0F0 ‘&HFFFFFF ‘ white

Const colForeground = &H984E00 ‘&H800000 ‘ dark blue

pic.ForeColor = colForeground

pic.BackColor = colBackground

strPercent = Format(sngPercent * 100, “0.0”) & “%”

intWidth = pic.TextWidth(strPercent)

intHeight = pic.TextHeight(strPercent)

intX = pic.ScaleWidth / 2 – intWidth / 2

intY = pic.ScaleHeight / 2 – intHeight / 2

pic.CurrentX = intX

pic.CurrentY = intY

pic.Print strPercent

pic.DrawMode = 10 ‘ Not XOR Pen

If sngPercent > 0 Then

pic.Line (0, 0)-(pic.ScaleWidth * sngPercent, pic.ScaleHeight), pic.ForeColor, BF

End If

pic.Refresh

4.4.2 调用外部程序与控制台标准输出重定向

图形界面外壳程序通过调用DynMake主程序并重定向其控制台标准输入输出以实现动态代码的生成以及实时显示压缩进度与压缩率的功能。对于标准输入输出重定向可通过Windows的管道机制实现[17]

执行外部程序由ExecuteShell函数实现,该函数负责创建用于重定向的管道、设置进程创建参数、创建进程以及保存进程句柄的工作,其主要代码如清单4-25所示。

清单4-25 ExecuteShell

CreatePipe hStdInputReadPipe, hStdInputWritePipe, ByVal 0, ByVal 0

CreatePipe hStdOutputReadPipe, hStdOutputWritePipe, ByVal 0, ByVal 0

CreatePipe hStdErrorReadPipe, hStdErrorWritePipe, ByVal 0, ByVal 0

Dim stProcessInfo As PROCESS_INFORMATION

Dim stStartInfo As STARTUPINFO

stStartInfo.cb = LenB(stStartInfo)

stStartInfo.dwFlags = STARTF_USESTDHANDLES

stStartInfo.hStdError = hStdErrorWritePipe

stStartInfo.hStdOutput = hStdOutputWritePipe

stStartInfo.hStdInput = hStdInputReadPipe

If False = CreateProcess(ByVal vbNullString, ByVal commandLine, ByVal 0, ByVal 0, ByVal True, ByVal DETACHED_PROCESS, ByVal 0, ByVal vbNullString, stStartInfo, stProcessInfo) Then

MsgBox “启动进程失败! ”

Exit Sub

Else

CloseHandle stProcessInfo.hThread

hProcess = stProcessInfo.hProcess

End If

ReadFromStdOutput函数用于从外部程序中取得控制台输出的数据,在主窗口的时钟循环中被调用,其代码如清单4-26所示。WriteToStdInput函数用以向外部程序的标准输入写入数据,使用API函数WriteFile将需要写入的数据写入到hStdInputWritePipe管道中即可,在本课题中此功能未被使用。IsProcessTerminated函数用于判断外部程序是否结束,使用API函数WaitForSingleObject对进程句柄hProcess进行超时为零的等待即可,如果返回句柄只有一个引用即说明进程已经结束,此时调用API函数CloseHandle关闭hProcess句柄并返回True。

清单4-26 ReadFromStdOutput

Dim nRead As Long

Dim strBuffer() As Byte

Dim nBufferLen As Long

nRead = -1

Do While nRead <> 0

nBufferLen = 4096

ReDim strBuffer(nBufferLen – 1)

ReadFile hStdOutputReadPipe, strBuffer(0), ByVal nBufferLen, nRead, ByVal 0

If nRead <> 0 Then

ReDim Preserve strBuffer(nRead – 1)

ReadFromStdOutput = ReadFromStdOutput + StrConv(strBuffer, vbUnicode)

End If

Loop

此外,frmInfo窗口用以实时显示控制台的输出信息,在主窗口的时钟循环中调用ReadFromStdOutput函数后会调用frmInfo窗口的PrintInfo函数将控制台输出显示到该窗口中。由于控制台在输出压缩进度时会输出回车符“\r”用以将光标移动到当前行的开始处,故在图形界面中使用TextBox亦须模拟这一行为,笔者选择在取得的输出数据中逐字符搜索“\r”与“\n”控制符来实现这一功能。PrintInfo函数的代码如清单4-27所示。

清单4-27 frmInfo窗体的PrintInfo函数

Dim s1 As String

s1 = txtInfo.Text

For i = 1 To Len(s)

Select Case Mid(s, i, 1)

Case vbCr

If Mid(s, i + 1, 1) <> vbLf Then

Dim posLf

posLf = InStrRev(s1, vbLf)

If posLf > 0 Then s1 = Left(s1, posLf)

End If

Case vbLf

s1 = s1 & vbCrLf

Case Else

s1 = s1 & Mid(s, i, 1)

End Select

Next i

txtInfo.Text = s1

txtInfo.SelStart = Len(s1)

4.5 本章小结

本章详细论述了Windows可执行程序动态代码化课题每个模块的详细实现,包括DynMake主程序的框架与外围程序、PELoader预置模板的汇编级实现与机器码的装配过程、DynShell图形界面包装外壳等内容。由于论文篇幅有限,本章内容在代码的举例上有所取舍,仅举出关键的代码步骤,完整的步骤读者可参考工程源代码实现。

5 总结与展望

5.1 总结

本课题通过对32位汇编语言、Windows操作系统、PE文件格式的学习与研究设计并实现了一种方法,将Windows可执行文件转换为内存动态代码,并在报告论文中给出了动态代码与转换工具的详细设计与实现步骤。读者在阅读本论文及相关文档与代码之后,既可以使用本课题已经研究出的软件成果对自己编写的程序进行动态代码化处理以起到代码加密保护的作用或具有远程进程注入的功能,也可以在本课题研究的基础上实现自己设计的动态代码以获得更有针对性的按需定制的效果。

5.2 展望

由于毕业设计时间有限与Windows操作系统本身的限制,本课题还存在着一些不足,如动态代码对于PE文件资源表的处理、Windows Side-by-Side机制等方面还存在着一些尚未解决的难题,这些问题虽然可以在现阶段认为是Windows系统本身的限制,但随着研究的进一步深入或许能够在更深一步的层次上得以实现(如研究比KERNEL32.DLL更底层的NTDLL.DLL)。

另一方面,在飞速发展的Windows代码安全领域中,笔者不得不承认动态代码技术仅仅是该领域的冰山一角,在对代码的加密保护上诸如代码混淆与变换、虚拟机技术这些新兴的技术笔者并未涉足。在未来的学习与工作中,笔者也会在这些方面有所涉猎,相信会学到更多的知识。

参考文献

[1] 段钢. 加密与解密 [M]. 3版. 北京:电子工业出版社, 2008.

[2] Charles Petzold. Programming Windows fifth Edition [M]. Redmond, Washington, U.S.A.: Microsoft Press, 1999.

[3] Jeffrey Richter, Christophe Nasarre. Windows via C/C++ [M]. 葛子昂,周靖,廖敏,译. 北京:清华大学出版社, 2008.

[4] Anon. LZMA [EB/OL]. (2012-05-02) [2012-05-12]. http://en.wikipedia.org/wiki/Lempel%E2%80%93Ziv%E2%80%93Markov_chain_algorithm

[5] Anon. Summary of the multiple file compression benchmark tests [EB/OL]. [2012-05-12]. http://www.maximumcompression.com/data/summary_mf.php.

[6] 谭浩强. C++程序设计 [M]. 北京:清华大学出版社,2004.

[7] iceboy_. 7z LZMA C SDK 移植备忘 [EB/OL]. (2009-12-04) [2012-05-12]. http://hi.baidu.com/iceboy_/blog/item/bb46888338ccaf9af703a64f.html.

[8] Nicolai M.Josuttis. The C++ Standard Library [M]. 侯捷,孟岩,译. 武汉:华中科技大学出版社,2002.

[9] 梁效斐. 打造世界最小LZMA解压DLL(最终话) [EB/OL]. (2012-03-23) [2012-05-12]. http://lxf.me/158.

[10] SkyLined. Shellcode: finding the base address of kernel32 in Windows 7 [EB/OL].(2009-07-22) [2012-05-12]. http://skypher.com/index.php/2009/07/22/shellcode-finding-kernel32-in-windows-7/.

[11] 钱晓捷. 新版汇编语言程序设计 [M]. 北京:电子工业出版社,2008.

[12] Matt Pietrek . Inside Windows: An In-Depth Look into the Win32 Portable Executable File Format [J/OL] . 2002(2)-. MSDN Magazine, 2002- [2012-05-12]. http://msdn.microsoft.com/en-us/magazine/cc301805.aspx

[13] Microsoft Corporation. Microsoft Portable Executable and Common Object File Format Specification [S/OL]. (2010-10-05) [2012-05-12]. http://www.microsoft.com/whdc/system/platform/firmware/PECOFF.mspx.

[14] 于鹏. Visual Basic 6.0案例教程 [M]. 北京:电子工业出版社,2005.

[15] hebeizjq. (原创)VB中利用API调用保存文件对话框和打开文件对话框 [EB/OL]. (2009-04-17) [2012-05-12]. http://hi.baidu.com/cive/blog/item/84837127c172840b908f9d5f.html.

[16] Microsoft Corporation. Visual Basic Package and Deployment Wizard. [CP/CD]. Redmond, Washington, U.S.A.: Microsoft Corporation, c1998.

[17] goodname008. (结论)如何对CMD窗口进行输入输出重定向 [EB/OL]. (2005-10-27) [2012-05-12]. http://topic.csdn.net/t/20051027/10/4353375.html.

致谢

在进行这次毕业设计的过程中,我获得了来自许多人的支持与帮助。

首先我要感谢我的父母与家人。是你们创造环境使我有机会认识计算机并深深地爱上了它,是你们支持我选择自己喜欢的东西作为大学的主专业。四年来,无论我做出什么样的选择,你们都在默默地支持、帮助着我。

然后我要感谢大学四年帮助、教育过我的老师们。正因为有你们的教诲,我才得以成为一名合格的毕业生,并学得很多有用的计算机知识与技术。尤其感谢我的毕业设计导师朱明老师对我自主选择课题的支持、对我编程设计上的建议以及对我论文上的悉心指导。

我还要感谢四年来陪伴我并给予我帮助的同学们。在这次毕业设计的过程中,我要尤其感谢毛飞同学对论文英文摘要的写作建议与修改,以及朱利军同学对这次毕业设计涉及的计算机安全领域的技术交流帮助。

最后,我要感谢曾经在网络游戏辅助产业与我进行技术交流与商业合作的朋友们。这次毕业设计的立题来源于当年研发时遗留的疑难问题,虽然自己现在不再从事那个行业,不过我会默默为你们加油。

恍惚间四年过去,随着考研升学,我也将暂时告别以计算机作为主专业的日子而去追随我的第二专业音乐。但是,四年来的苦与乐,四年来的日日夜夜我不会忘记。感谢各位陪伴我走过来的亲们,计算机与音乐是对我同等重要的两个梦想,有朝一日,我会回来。

译文及原文

5 Replies to “【论文存档】Windows可执行程序内存动态代码化的研究与实现”

  1. 很好奇,对于是否把一堆二进加载到内存中通过修复定位和输出表,就可以当正常使用DLL一样??

    1. 此文介绍的是将DLL直接扔内存里修复输入后使用。不过写payload是个乐趣,可以依自己喜好整出各种玩法,不过要习惯写汇编。

  2. 汇编目前还算可以,对于DLL的加密有种猜想不知道是否可以实现。
    现在的注入方式都是把目录下某个DLL注入到指定的EXE中,如果把DLL直接以资源形式放到注入器中。
    这样注入到其他进程运行,可以避免直接获取这个DLL。

  3. 有个疑问请教一下,最终提取的payload,仍到数组之后,有什么办法可以调用payload里的导出函数

    1. 我见过一种实现是将默认入口写成一个函数,传入枚举数字或者字符串,返回需要的函数指针

Leave a Reply to 幽幽 Cancel reply

Your email address will not be published. Required fields are marked *