阅读:3904回复:10
古老的一篇关于病毒的文章
这是一篇我在学习脱壳时收集的关于编写病毒的资料,今天整理资料是再次看到,依然感到还是一篇很好的文章。这里先给大家.[cyliu ]
-------------------------------------------------------------------------------------------------------------- Billy Belceb病毒编写教程---Win32篇 翻译:onlyu 【译者声明】 ~~~~~~~~~~~ 这是一篇关于病毒基础知识的教程,作者Billy Belceb,西班牙人,在16岁时写的这篇教程,曾 创建了病毒组织DDT。翻译这篇教程的目的是想揭开病毒的神秘面纱,从编写病毒的角度来学习病毒,希望对大家有用。由于原文为西班牙人写的英文,译者翻译 教程也不多,英语只是凑合,错误之处还请大家原谅,如果大家发现翻译有什么不当之处,欢迎改正,大家也可对照原文学习。(原文在29A#4中)。大家都知 道,我们脱一个壳经常见到某某壳用了某某病毒技术,到底病毒技术是那些呢?比较经典而全面的Win32病毒教程就是Billy Belceb写 的本教程,可惜一直没有人翻译成中文,我作为一个大傻鸟,就决定翻译了。谨以此翻译献给所有的Cracker和所有对Win32汇编感兴趣的人。下面为原 文译文,祝你好运! 【声明】 ~~~~~~~ 作者对因对此文档使用不当 而造成的任何损失概不负责。这篇教程的目的是教会人们编写病毒和防护一些破坏力大的病毒的破坏。这篇教程仅作为教学目的。所以,如果有人利用这篇文章编写 了破坏力很大的病毒,我可不负责任。如果通过这篇文章你看到我鼓励人们破坏数据的字眼,先去买副眼镜再说。 【介绍】 ~~~~~~~ 亲爱的同志们,大家好,你还记得Billy Belceb的病毒编写教程吗?那是一篇关于过时的MS- DOS病毒的教程。在那篇教程中,我一步一步地介绍了很多有名的DOS病毒技术的知识,而且它是为初学者写的,使他们尽快地入门。现在,我又写了一篇很酷 (我希望是)的教程,但是这一次我将介绍现在计算机的新威胁,Win32病毒,毫无疑问,所有的东西都是和那个有关了。我发现现在一个完整的教程很缺,所 以我曾问自己...为什么我不写一篇关于这个的教程?所以我又写出来了:)真正的在Win32病毒的先驱是VLAD组织,而用这种方式来写教程的作者是 Lord Julus。但是我不会忘记那些写了很多有趣教程的人,和在Lord Julus的教程之前的所有相关东西,当然我在说 JHB啦。有趣的技术是由Murkry研究的,后来Jacky Qwerty...我希望我没有忘记在Win32病毒编写(很短)史上的重要的 人。注意我从来没有忘本。象在我的病毒编写教程系列里一样,我要谢谢一些音乐组织,如Blind Guardian,HammerFall, Stratovarius,Rhapsody,Marilyn Manson,Iron Maiden,Metallica,Iced Earth,RAMMS+EIN,Mago De Oz,Avalanch,Fear Factory,Korn,Hamlet 和Def Con Dos。所有这些东西营造了写一篇巨大的教程和代码的完美的氛围。 嗨,我的教程的结构已经有了很大的改变,现在我给出一个索引,几乎所有给出的代码都是我编写的,或者基于其他人的但是被我改编了的,或者有一点删改的;) 但是,嗨,我已经努力的解决在我的现在已经绝种了的MS-DOS(RIP)版VWG中遇到的所有问题。 我必须向Super/29A问好,是他帮助了这篇教程的一些方面的东西,他是我的beta测试人之一,而且他已经对这篇教程贡献了一些东西。 说 明:英语不是我的母语(西班牙语才是)【译者注:所以这篇西班牙式的病毒教程很难翻译,不当之处还请原谅】,所以原谅我的许多拼写错误,请告知我,我会修 正的。我已经引用了已经在一些独立的病毒杂志里发表了的文章,但是它们仍然值得一读因为我已经修改了,进行了语法检查,并加入了一些额外的信息。记住:这 篇文章并不完美,所以原谅在这篇教程中的错误。 ----------跟我联系 -E-mail billy_belcebu@hotmail.com billy_belcebu@cryogen.com -ICQ # 22290500 个人主页 http://members.xoom.com/billy_bel&n...sp; http://www.cryogen.com/billy_belcebu 组织主页 http://sourceofkaos.com/homes/ddt IRC [Billy_Bel] Undernet #vir, Irc-Hispano #virus 祝玩得快乐! Billy Belceb 美梦从这里开始... (c) 1999 Billy Belcebu/iKX 【索引】 ~~~~~~~ 有人(Hi Qozah!)已经告诉我,当他读这篇教程的beta版本时,它有一点混乱,因为容易迷失在各章之间。无论如何,我已经对这个重新组织了,我仍然很混乱,而且我的教程也是:) 01.声明 02.介绍 03.索引 04.病毒编写中的有用的东西 05.简单介绍 06.PE 文件头 07.Ring-3,用户级编码 08.Ring-0,系统级编码 09.Per-Process residency 10.Win32优化 11.Win32反调试 12.Win32多态 13.高级Win32技术 14.附录一:病毒发作 15.附录二:关于作者 16.结束语 【病毒编写中的有用的东西】 ~~~~~~~~~~~~~~~~~~~~~~~~~ 在开始编写病毒之前,你需要一些东西。下面是我给你推荐的程序(如果你没有足够的金钱来买它们...下载!) :) Windows 95 或 Window NT 或 Windows 98 或 Windows 3.x + Win32s :) TASM 5.0 包(包括TASM32 和 TLINK 32) SoftICE 3.23+(或更好) for Win9X,和 for WinNT。 API 列表(Win32.HLP) Windows95 DDK,Windows98 DDK,Windows2000 DDK...即所有的微软DDK和SDK。 强烈推荐Matt Pietrek关于PE文件头的文章。 Jacky Qwerty的PEWRSEC工具(在你在'.code'里添加代码时用)。 一些hash...哦,shit!它是我想要的! :) 一些电子杂志如29A(#2,#3),Xine(#2,#3,#4),VLAD(#6),DDT(#1)... 一些Windows病毒,如Win32.Cabanas,Win95.Padania,Win32.Legacy... 一些Windows病毒查杀工具(强烈推荐NODICE32)->www.eset.sk Neuromancer,by William Gibson,它是一本好书。 毫无疑问,这篇教程! 我希望没有忘掉任何重要的东西。 【简要介绍】 ~~~~~~~~~~~ 好了,开 始把你的大脑中的16位MS-DOS编码概念,迷人的16位偏移地址,中断,驻留内存的方法...都清除掉。所有这些我们已经使用了很多年的东西,现在已 经再也不用了。是的,它们确实现在用不到了。在这篇教程里面,当我说Win32,我的意思是Windows 95(normal,OSR1, OSR2),Windows 98,Windows NT或Windows 3.x+Win32s。最最明显的变化,至少 在我看来是由中断变成了API,在这之前是由16位寄存器和偏移地址变到了32位的。Windows给我们开了使用其它语言代替ASM(和C)的方便之 门,但是我仍然对ASM情有独钟:利用它能更好的理解一些东西和更容易的优化(hi Super!)。正如我在上面所说的,你必须使用一种新东 西叫做API。你必须知道这些参数必须在堆栈中,而且调用这些API是使用的CALL。 注:在上面我把上面所有提到的平台叫做Win32,我把Win95(它的所有版本)和Win98叫做Win9x,把Windows 2000叫做Win2k。请注意这一点。 %由16位到32位编程的改变% ~~~~~~~~~~~~~~~~~~~~~~~~ 我们现在将会使用双字(DWORD)而不是单字(WORD)了,而这个改变将会给我们一个全新的世界。 在已知的CS,DS,ES和SS:FS,GS之外,我们又多了两个段。而且我们有32位寄存器如EAX,EBX,ECX,EDX,ESI,EDI,EBP 和ESP。让我们来看看对这些寄存器怎么操作:假如我们要使用EAX的less significant word(简称LSW), 我们该怎么做呢?这个部分可以使用AX来访问,即处理LSW。假如EAX=0000000,我们想要在它的LSW放置1234h。我们必须简单地使用一个 "mov ax,1234h"就可以了。但是如果我们想要访问EAX的MSW(Most Significant Word),该怎么做呢?为了达到这个目的我们不能使用一个寄存器了:我们必须使用ROL。问题并不是在这里,它是把MSW值移到了LSW。 当我们得到一个新语言的时候,我们总是要试的一个经典的例子:"Hello world!" :) %Win32中的Hello World% ~~~~~~~~~~~~~~~~~~~~~~ 它很简单,我们必须使用"MessageBoxA"这个API,所以我们用大家已经知道的"extrn"命令来定义 它,把参数压栈然后调用这个API。注意这个字符串必须为ASCIIZ(ASCII,0),记住参数必须以相反的顺序压栈。 ;-------从这里开始剪切---------------------------------------------------- .386 ; Processor (386+) .model flat ; Uses 32 bit registers extrn ExitProcess:proc ; The API it uses extrn MessageBoxA:proc ;- ;利用"extrn"我们把在程序中要用到的所有API列出来。ExitProcess是我们用来把 ;控制权交给操作系统的API,而MessageBoxA用来显示一个经典的Windows消息框。 ;- .data szMessage db "Hello World!",0 ; Message for MsgBox szTitle db "Win32 rocks!",0 ; Title of that MsgBox ;------------------------------------------------------------------------------------------------------------------------------ ;这里我们不能把真正病毒的数据放这里了,因为这是一个例子,我们不能 ;使用它,而且又因为如果我们不在这里放置一些数据,TASM将会拒绝汇编。 ;无论如何...在第一次产生你的病毒主体的时候用它放置数据。 ;- .code ; Here we go! HelloWorld: push 00000000h ; Sytle of MessageBox push offset szTitle ; Title of MessageBox push offset szMessage ; The message itself push 00000000h ; Handle of owner call MessageBoxA ; The API call itself ;------------------------------------------- ; int MessageBox( ; HWND hWnd, // handle of owner window ; LPCTSTR lpText, // address of text in message box ; LPCTSTR lpCaption, // address of title of message box ; UINT uType // style of message box ; ); ; ;在调用这个API之前,我们把参数压栈,如果你还记得,堆栈使用那个迷人的 ;东西叫做LIFO(后进先出Last In First Out),所以我们要按相反的顺序来 ;压参数。让我们看看这个函数的每个参数的简要描述: ; ; hWnd:标志将要被创建的消息框的宿主窗口(owner window)。 ; 如果这个参数是NULL,这个消息框没有宿主窗口。 ; lpText:指向以空字符结尾的包含将要显示消息的字符串的指针。 ; lpCaption:指向一个以空字符结尾的字符串的指针,这个字符串是这个 ; 对话框的标题。如果这个参数是一个NULL,缺省的标题Error被使用。 ; uType:以一些位标志来确定对话框的样式和行为。这个参数可为一些标志的组合。 ;- push 00000000h call ExitProcess ;-------------------------------------------------------------------------------------------------------------------- ; VOID ExitProcess( ; UINT uExitCode // exit code for all threads ; ); ; 这个函数在Win32环境下相当于著名的Int 20h,和Int 21h的00,4C功能等等。 ; 它是关闭当前进程的简单方式,即结束程序执行。下面给出唯一的一个参数: ; ; uExitCode:标志进程退出的代码,并作为所有线程终止时的代码。使用 ; GetExitCodeProcess函数来刷新这个进程的退出值。使用GetExitCodeThread ; 函数来刷新一个线程的退出值。 ;- end HelloWorld ;-----到这里为止剪切------------------------------------------------------- 正如你看到的,编写代码很简单。可能没有16位环境下那么简单,但是如果你考虑到32位所带给我们的优点确实很简单 了。现在,既然你已经知道怎么来编写"Hello World",你就有能力来感染整个世界了;) %Rings% ~~~~~~~ 我知道你对下面的东西很害怕,但是,正如我所演示的,它看起来没有那么难。让我们记住你必须清楚的东西:处理器有4 个特权级别:Ring-0,Ring-1,Ring-2和Ring-3,越往后就有越多的限制,而病毒要是用第一个特权级别,几乎编码时没有任何限制。只 要记住在迷人的DOS下面,我们总是处于Ring-0...现在想到在Win32平台下你还可以做相同的事情...好了,停止幻想了,让我们开始工作。 Ring-3还表示"用户"级,在这个级别下,我们有很多的限制,那确实不能我们的需要。Microsoft程序员 在他们发行Win95的时候犯了个错误,声称它是"无法感染"的,正如在这个操作系统卖出去之前所表明的,利用可怕的Bizatch(后来改名为 Boza,但那是另外一段历史了)。他们认为这些API不能被一个病毒访问和使用,但是他们没有想到病毒编写者们的超级智慧,所以...我们可以在用户级 下编写病毒,毫无疑问,你只要看看大量近期发布的新的Win32运行期病毒,它们都是Ring-3级下的...它们不差,不要误解了我,Ring-3病毒 是现在有可能感染所有Win32环境下文件的病毒。它们是未来...主要是因为即将发布的Windows NT 5.0(或者 Windows 2000)。我们不得不寻找能使我们的病毒(由Bizatch生成的病毒传播很差,因为它对API地址"harcoded", 并且它们可能会因Windows版本的改变而改变)存活的API,而且我们可以用其它不同的方法来实现,正如我后面解释到的。 Ring-0是另外一段历史了,和Ring-3有着很大的区别。在这个级别下我们拥有内核编码的级别,"内核(kernel)级 别"。是不是很迷人啊?我们可以访问端口,放置我们还没有梦想过的代码...和原先的汇编最接近的东西。我们不使用一些已知的花招是不能直接访问一些东西 的,如IDT修改,SoPinKy/29A在29A#3里发表的"Call Gate"技术,或者VMM插入,在Padania或者 Fuck Harry病毒里见到的技术。当我们直接利用VxD的服务时,我们不需要API,而且它们的地址在所有Win9x系统中是被假设为相 同的,所以我们"hardcode"它们。我将在fully dedicated to Ring-0这一章里面深入讨 论。 %重要的东西% ~~~~~~~~~~~~ 我想无论如何我应该在这篇教程的开头放上这些,然而我知道知道总比不知道好啊:)好了,让我们来讨论Win32操作系统内部的东西。 首先,你必须清楚一些概念。让我们从selector开始。什么是一个selector呢?相当简单,它是一个非常 大的段,而且它组成了Win32的内存,也叫做平坦内存。我们可以用4G内存(4,294,967,295字节),仅仅通过使用32位地址。那所有这些内 存是怎么组织的呢?看看下面的示意图: __________________________ | |<-------OFFSET=00000000h <-> 3FFFFFFFh | 应用程序代码和数据 | |__________________________| | |<-------OFFSET=40000000h <-> 7FFFFFFFh | 共享内存 | |__________________________| | |<-------OFFSET=80000000h <-> BFFFFFFFh | 内核 | |__________________________| | |<-------OFFSET=C0000000h <-> FFFFFFFFh | 设备驱动 | |__________________________| 结果:我们拥有4G可用内存。是不是很迷人啊? 注意一件事情:WinNT的后两段是分开的。现在我将给出你必须知道的一些定义,其它的一些本文之外的一些概念,我假设你已经知道了。 VA: VA表示Virtual Address,即某些程序的地址,但是在内存中(记住在Windows中在内存中和在磁盘上是不一样的)。 RVA: RVA表示Relative Virtual Address。清楚这个概念很重要,RVA是文件在内存映射(由你或由系统)时的偏移地址。 RAW Data: RAW Data是我们用来表示数据物理的存储的,也就是说,在磁盘(磁盘上的数据!=内存中的数据)上的存储。 Virtual Data: Virtual Data是指那些已经被系统载入内存的数据。 File Mapping: 一种技术,在所有的Win32环境下都有,由一种快速的(并使用更少内存)文件操作方法和比DOS更容易理解的方法 组成。所有我们在内存中改变的东西,在磁盘上也会改变。文件映射还是所有Win32环境(甚至NT)下内存之间交换信息的唯一方法。 %怎么来编译东西% ~~~~~~~~~~~~~~~~ 该死,我几乎忘记了这个:)编译一个Win32 ASM程序的通常参数是,至少在这篇教程的所有例子中,按如下(当ASM文件的名字为'program',但是没有任何扩展名): tasm32 /m3 /ml program,,; tlink32 /Tpe /aa program,program,,import32.lib pewrsec program.exe 我希望足够清晰了。你还可以使用makefiles,或者建立一个bat文件来使它自动完成(就象我做的!)。 【PE文件头】 ~~~~~~~~~~ 这是这篇文件的最重要的一章。仔细读! %介绍% ~~~~~~ 对PE头的结构很清晰在写我们的Windows病毒很重要。下面我将给出我认为重要的东西,但是并不是关于PE文件的所有的信息,想要知道更多的东西,看看我在上面关于PE文件推荐的资料,在"有用的东西..."这一章。 _______________________________ | |<-----OFFSET=00000000h | DOS stub | |_______________________________| | |<-----OFFSET=[DOS Stub+3Ch] | PE stuff | |_______________________________| 让我们对这两大部分进行深入的分析,让我们看看Micheal J. O'Leary的示意图: __________________________________ | |<----Base of Image Header | DOS compatible EXE header |--| |__________________________________| | | | | | Unused | | |__________________________________| | | | | | OEM identifier | | |__________________________________| | | | | | OEM info | |-->Uninteresting(DOS Compatibility) |__________________________________| | | | | | Offset to PE Header |----->Very interesting |__________________________________| | | | | | DOS Stub program and reloc table | | |__________________________________| | | | | | Unused |__| |__________________________________| | | | PE header(IMAGE_FILE_HEADER) |--| |__________________________________| | | | | | PE header(IMAGE_OPTIONAL_HEADER) | | |__________________________________| |-->Very very interesting :) | | | | Section Table | | |__________________________________| | | | | | Sections |__| |__________________________________| 现在你已经对PE文件头已经有了一个大体的了解,确实很新奇(但也很复杂),我们的新目标。Ok,ok,你对那些东 西有了一个"大体"的了解,但是,你仍然需要知道PE文件头中IMAGE_FILE_HEADER本身的内部结构。勒紧你的裤腰带! IMAGE_FILE_HEADER ^^^^^^^^^^^^^^^^^ ________________________________ | "PE\0\0" |<----+00000000h |________________________________| Size:1 DWORD | Machine |<----+00000004h |________________________________| Size:1 WORD | Number Of Section |<----+00000006h |________________________________| Size:1 WORD | Time Date Stamp |<----+00000008h |________________________________| Size:1 DWORD | pointer To Symbol Table |<----+0000000Ch |________________________________| Size:1 DWORD | Number Of Symbols |<----+00000010h |________________________________| Size:1 DWORD | Size Of Optional Header |<----+000000014h |________________________________| Size:1 WORD | Characteristics |<----+000000016h |________________________________| Size:1 WORD Total Size:18h BYTES 我将继续对IMAGE_FILE_HEADER的各个域给出简要的描述。 PE\0\0: 它是每个PE文件都有的标志,只要在编写你的感染程序的时候检查它是否存在。如果它在那儿,它就不是一个PE文件,ok? Machine: 因为我们所使用的计算机的理想可以是一个非PC兼容的(NT对这些东西有一个开放等级,你知道的),又因为PE文件是普遍的,在这个域中是这个应用程序所编写的代码的机器类型,可以为下面的值: IMAGE_FILE_MACHINE_I386 equ 14Ch ; Intel 386. IMAGE_FILE_MACHINE_R3000 equ 162h ; MIPS little-endian,160h big-endian IMAGE_FILE_MACHINE_R4000 equ 166h ; MIPS little-endian IMAGE_FILE_MACHINE_R10000 equ 168h ; MIPS little-endian IMAGE_FILE_MACHINE_ALPHA equ 184h ; Alpha_AXP IMAGE_FILE_MACHINE_POWERPC equ 1F0h ; IBM PowerPC Little-Endian Number Of Sections: 我们的感染程序的非常重要的域,它告诉我们这个文件的节(section)的个数。 Time Date Stamp: 保存了从1969.10.31 4:00到文件连结时所过的秒数。 Pointer To Symbol Table: 没意思,因为它仅仅被OBJ文件使用。 Number Of Symbols: 没意思,因为它仅仅被OBJ文件使用。 Size Of Optional header: 保存了IMAGE_OPTIONAL_HEADER域的字节数(看下面IMAGE_OPTIONAL_HEADER的描述)。 Characteristics: 这些标志给了我们关于这个文件的更多信息,对于我们所有人都没意思。 IMAGE_OPTIONAL_HEADER ^^^^^^^^^^^^^^^^^^^^^ ________________________________ | Magic |<----+00000018h |________________________________| Size:1 WORD | Major Linker Version |<----+0000001Ah |________________________________| Size:1 BYTE | Minor Linker Version |<----+0000001Bh |________________________________| Size:1 BYTE | Size Of Code |<----+0000001Ch |________________________________| Size:1 DWORD | Size Of Initialized Data |<----+00000020h |________________________________| Size:1 DWORD | Size of UnInitialized Data |<----+00000024h |________________________________| Size:1 DWORD | Address Of Entry Point |<----+00000028h |________________________________| Size:1 DWORD | Base Of Code |<----+0000002Ch |________________________________| Size:1 DWORD | Base Of Data |<----+00000030h |________________________________| Size:1 DWORD | Image Base |<----+00000034h |________________________________| Size:1 DWORD | Section ALignment |<----+00000038h |________________________________| Size:1 DWORD | File Alignment |<----+0000003Ch |________________________________| Size:1 DWORD | Major Operating System Version |<----+00000040h |________________________________| Size:1 WORD | Minor Operating System Version |<----+00000042h |________________________________| Size:1 WORD | Major Image Version |<----+00000044h |________________________________| Size:1 WORD | Minor Image Version |<----+00000046h |________________________________| Size:1 WORD | Major Subsystem Version |<----+00000048h |________________________________| Size:1 WORD | Minor Subsystem Version |<----+0000004Ah |________________________________| Size:1 WORD | Reserved1 |<----+0000004Ch |________________________________| Size:1 DWORD | Size Of Headers |<----+00000050h |________________________________| Size:1 DWORD | CheckSum |<----+00000054h |________________________________| Size:1 DWORD | SubSystem |<----+00000058h |________________________________| Size:1 DWORD | Dll Characteristics |<----+0000005Eh |________________________________| Size:1 WORD | Size Of Stack Reserve |<----+00000060h |________________________________| Size:1 DWORD | Size Of Stack Commit |<----+00000064h |________________________________| Size:1 DWORD | Size OF Heap Reserve |<----+00000068h |________________________________| Size:1 DWORD | Size Of Heap Commit |<----+0000006Ch |________________________________| Size:1 DWORD | Loader Flags |<----+00000070h |________________________________| Size:1 DWORD | Number Of Rva And Sizes |<----+00000074h |________________________________| Size:1 DWORD Total Size:78h BYTES (加上 IMAGE_FILE_HEADER ^^^^^^^^^) Magic: 看起来总为010Bh,实际上会使我们认为它是一种签名,没有意思。 Major Linker Version and Minor Linker Version: 产生这个文件的连结器的版本,没有意思。 Size of Code: 它是所有包含可执行代码的段的总字节数。 Size of Initialized Data: 它是所有包含初始数据的段的总大小。 Size of Uninitialized data 未初始数据不占磁盘空间,但是当系统装载这个文件的时候,它会分配一些内存(实际上是虚拟内存)。 Address of EntryPoint: 是装载器开始执行代码的地方。它是一个RVA,当系统装载这个文件的时候和image base相关。非常有意思。 Base Of Code: 是文件的code段开始的RVA。code段在内存中通常在data段之前,在PE文件头之后。这个RVA在用 Microsoft连结器产生的EXE文件中通常为0x1000。Borland的TLINK32看起来把image base加到了第一个 code段的RVA处,并把结果存储在这个域中。 Base Of Data: 是文件的data段开始的RVA,data段通常在内存中处于最后,在PE文件头和code段之后。 Image Base(基址): 当连结器创造一个可执行文件的时候,它会假定将会内存映射到内存的某个地址当中。这个地址被保存在这个域中,假定的一个装载地址 来允许连结器进行优化。如果这个文件确实被装载器内存映射到那个地址,在它可以运行之前代码就不需要任何补丁了。在为Windows NT产生 的可执行文件中,缺省的Image Base为0x10000。对DLL来说,缺省的为0x400000。在Win9X中,地址0x10000 不能被用来装载EXE文件因为它在被所有进程的共享地址中。因为这个,Microsoft就把Win32的缺省Image Base改为 0x400000。老的以基址0x10000进行连结而成的可执行文件在Win9x下装载将会花费更长的时间,因为装载器需要进行基址重定位。 Section Alignment: 当映射到内存中的时候,每一节要保证是这个值的一个倍数的虚拟地址作为开始地址。对于按页的时候,缺省的节对齐方式是0x1000。 File Alingnment: 在PE文件中,构成每一节的原始数据要保证从这个值的倍数开始。缺省的值为0x200字节,可能是为了保证各节总是以磁盘节 (disk sector,它的长度也为0x200)的开始作为开始。这个域在NE文件中等价于segment/resource alignment。和NE文件不同的是,PE文件通常不会有成百个节,所以由于对齐文件的节而浪费的空间几乎很少。 Major Operating System Version and Minor Operating System Version: 使用这种类型的可执行文件的操作系统的最低版本号。既然subsystem fields目的看起来和它相类似,这个域有点摸棱两可。这个域在所有的Win32 EXE文件中缺省为1.0。 Major Image Version and Minor Image Version: 是一个用户可定义的域,它允许你可以有不同版本的EXE或DLL。你可以通过连结器的/VERSION开关来设置这个域。如:"LINK /VERSION:2.0 muobj.obj"。 Major Subsystem Version and Minor Subsystem Version: 包含了运行这个可执行文件所需要的最小子系统版本。这个域的一个经典值为3.10(意思为Windows NT 3.1)。 Reserved1: 看起来总为0(最为感染标志太完美了)。 Size Of Headers: PE文件头的大小和节(对象)表。这些节的原始数据就从这些所有文件头组件之后开始。 Checksum: 为这个文件的CRC校验值。正如在其它的Microsoft可执行文件格式中,这个域是忽略的并总设为0,这个规则的例外是这些EXE文件必须有合法的校验值。 SubSystem: 这些可执行文件的子系统的类型被它用来用户界面。WINNT.h定义了下面的值: NATIVE 1 Doesn't require a subsystem (such as a device driver) WINDOWS_GUI 2 Runs in the Windows GUI subsystem WINDOWS_CUI 3 Runs in the Windows character subsystem (console app) OS2_CUI 5 Runs in the OS/2 character subsystem (OS/2 1.x only) POSIX_CUI 7 Runs in the Posix character subsystem 一个标志集表明了在什么环境下一个DLL的初始函数(如DLLMain)将会调用。这个值看起来总是设置为0,然而操作系统仍然对所有四个事件调用DLL初始函数。下面是定义的值: 1 当DLL第一次装载到一个进程的地址空间中时调用 2 当一个线程终止时调用 3 当一个线程开始时调用 4 当DLL已经存在时调用 Size Of Stack Reserve: 为初始线程的堆栈而保留的虚拟内存数量,然而并不是所有的内存都可以做(看下一个域)。这个域的缺省值为0x100000(1MB)。如果你用CreateThread把0作为堆栈的大小,那么创建出来的堆栈就会有相同的大小。 Size Of Stack Commit: 保证初始线程的堆栈时的内存数量。对于Microsoft的连结器这个域的初始值为0x1000字节(1页)而TLINK32为2页。 Size Of Heap Reserve: 用来保留给初始进程堆时的虚拟内存,这个堆的句柄可以通过调用GetProcessHeap函数来获得。并不能保证所有内存(看下一个域)。 Size Of Heap Commit: 在进程堆中初始时的内存数量。缺省值为1页。 Loader Flags: 从WINNT.h来看,这个域和调试支持相关。我还没有看到任何一个这些位都有效的可执行文件,也没有看到这些位都清空的。怎么用连结器设置它们呢,下面是定义的值: 1 在开始进程前唤醒一个断点指令 2 当进程已经载入后唤醒一个调试器 Number Of Rva and Sizes: DataDirectory 数组(下面)的入口个数,这个值用当前的工具总是设置为16。 IMAGE_SECTION_HEADER ^^^^^^^^^^^^^^^^^^^^ _____________________________ | Section Name |<-----Begin of section header |_____________________________| Size:8 BYTES | Virtual Size |<-----+00000008h |_____________________________| Size:1 DWORD | Virtual Address |<-----+0000000Ch |_____________________________| Size:1 DWORD | Size Of Raw Data |<-----+00000010h |_____________________________| Size:1 DWORD | Pointer To Raw Data |<-----+00000014h |_____________________________| Size:1 DWORD | Pointer To Relocations |<-----+00000018h |_____________________________| Size:1 DWORD | Pointer To Line Numbers |<-----+0000001Ch |_____________________________| Size:1 DWORD | Number Of Relocations |<-----+00000020h |_____________________________| Size:1 WORD | Number Of Line Numbers |<-----+00000022h |_____________________________| Size:1 WORD | Characteristics |<-----+00000024h |_____________________________| Size:1 DWORD Total Size: 28h BYTES Section Name: 命名节用的是一个8-byte的ANSI名字(非UNICODE),大多数的节的名字以一个.(如".text") 作为开始,但是这不是必须的,你可以在一些关于PE的文章里验证这一点。你可以直接用汇编语言来命名你的节,或者在Microsoft C/C ++编译器下用"#pragma data_seg"和"pragma code_seg"。注意节名是否占了满满8个字节很重要, 没有NULL终止符。如果你是一个printf的热爱者,你可以使用%.8s来避免把名字字符串拷贝到另外一个你可以用NULL来终止的缓冲区里面。 Virtual Size: 这个域在EXE或者OBJ中有不同的意思。在一个EXE中,它存储代码或者数据的实际大小。这个大小是在把文件凑整 到文件对齐大小的倍数之前的大小。后面的SizeOfRawData域(看起来有点用词不当)存储的是凑整之后的值。Borland的连接器把这两个域的 意思颠倒过来了,看起来是正确的。对于OBJ文件,这个域表示节的物理地址。第一个节是从地址0开始的。为了寻找在一个OBJ文件中的下一个节的物理地 址,把当前节的物理地址加上SizeOfRawData值就可以了。 Virtual Address: 在EXE中,这个域指装载器应该对节进行映射的RVA。为了计算一个给定的节在内存中的真正起始地址,把映象的基址加上存储在这 个域中的VirtualAddress就可以了。利用Microsoft的工具,第一个节的缺省的RVA为0x1000。在OBJ文件中,这个域是没有意 义的并设置成0。 Size Of Raw Data: 在EXE中,这个域包含了节在按文件对齐大小凑整之后的大小。例如,假设一个文件的对齐大小为0x200,如果上述的 VirtualSize域的节的长度为0x35A,这个域就会以0x400作为节长。在OBJ文件中,这个域包含了由编译器或汇编程序所设置的精确大小。 也就是说,对于OBJ文件来说,它等于EXE中的VirtualSize域的值。 Pointer To Raw Data: 这是节基于文件的偏移量,原始数据是由编译器或汇编器设置的。如果你的程序内存映射了一个PE文件或者COFF文件 本身(而不是由操作系统来装载它),这个域比VirtualAddress域重要。在这种情况下,你将会拥有完全的线形文件映射,所以你将会发现在这个偏 移地址出的节的数据,而不是在VirtualAddress处的特定RVA。 Pointer To Relocations 在OBJ文件中这个是节基于文件的偏移量的重定位信息,对于每一个节的重定位信息直接跟在那个节的原始数据后面。在 EXE文件中这个域(和子域)是没有意义的并设置成0。当连接器产生EXE文件的时候,它解决了大多数的修正问题,只剩基址重定位和输入函数。关于基址重 定位和输入函数的信息是保存在它们自己的节中,所以没有必要使一个EXE文件的每一个节的重定位数据在原始节数据后面。 Pointer To Line Numbers: 这是基于文件的行号表的偏移量,一个行号表使源文件的行号和一个给定的行所产生的代码地址 相关联。在现代的调试格式如CodeView格式中,行号信息是作为调试信息的一部分存储的。在COFF调试格式中,然而,行号信息是和符号名/符号类型 分开存储的。通常,只有code节(如.text)有行号。在EXE文件中,行号是在节的raw data(原始数据)之后向文件尾累加的。在 OBJ文件中,一个节的行号表是在原始节数据和这个节的重定位表之后开始的。 Number Of Relocations: 在节的行号表中的行号的数值(上面的PointerToLinenumbers域)。 Characteristics: 大多数程序员叫做标志(flag),在COFF/PE格式中叫做特征(characterstic)。这个域是一些 表面节属性(如代码/数据,可读,或可写)的标志。要看所有可能的节属性的列表,看看定义在WINNT.H中的IMAGE_SCN_XXX_XXX。下面 给出一些比较重要的标志: 0x00000020 这个节包含代码。通常和可执行标志(0x80000000)联合设置。 0x00000040 这个节包含了初始化了的数据(initialized data)。除了可执行和.bss节之外几乎所有的节都有这个标志。 0x00000080 这个节包含了未初始化的数据(uninitialized data),如.bss节。 0x00000200 这个节包含了一些注释或者一些其它类型的信息。这个节的一个典型利用是由编译器所设置的.drectve节,这个节包含了连接器的命令。 0x00000800 这个节的内容是不应该放在最终的EXE文件中的。这些节被编译器/汇编器用来传递信息给连接器。 0x02000000 在它被装载之后,进程就不再需要它了,这个节就可以被丢弃。最普通的可丢弃的节是基址重定位节(.reloc)。 0x10000000 这个节是可共享的。当使用一个DLL时,这个节中的数据将会通过DLL来给所有的进程共享。数据节的默认是不共享的。用更专业的术语,一个共享节告诉内存 管理器设置这个节的页映射使得所有使用这个DLL的进程指向内存中的同一个物理页。要使一个节可共享的,在连接的时候使用共享(SHARED)属性。如: LINK /SECTION:MYDATA,RWS... 就告诉了连接器一个叫做MYDATA的节是可读的,可写的,而且是共享的。 0x20000000 这个节是可执行的。这个标志通常在"包含代码"的标志(0x00000020)被设置后设置。 0x40000000 这个节是可读的。这个标志几乎在EXE文件的所有节中都被设置。 0x80000000 这个节是可写的。如果这个标志在一个EXE文件的节中没有被设置,装载器就会标志内存映射页为只读的或只能执行的。有这个属性的典型的节是.data和.bss。有趣的是,.idata节也设置了这个属性。 %要改变的东西% ~~~~~~~~~~~~~~ 下面,我将介绍在编写一个普通的PE病毒时的一些改变。假设你要编写一个会增加PE文件的最后一个节内容的病毒,这 个在我们看来更容易成功的技术,然而添加一个节更容易。让我们看看一个病毒是怎么来改变一个可执行文件的头。我使用了Lord Julus [SLAM]的INFO-PE程序。 -------- DOS INFORMATION --------------------------------------------------- Analyzed File: GOAT002.EXE DOS Reports: ?File Size - 2000H (08192d) ?File Time - 17:19:46 (hh:mm:ss) ?File Date - 11/06/1999 (dd/mm/yy) ?Attributes : Archive [...] -------- PE Header ---------------------------------------------------------- --------------- ‖O_DOS |O_PE ‖(Offset from Dos Header / PE Header ‖------|------‖ |0100H |0000H | PE Header Signature - PE/0/0 |0104H |0004H | The machine for this EXE is Intel 386 (value = 014CH) |0106H |0006H | Number of sections in the file - 0004H |0108H |0008H | File was linked at : 23/03/2049 |010CH |000CH | Pointer to Symbol Table : 00000000H |0110H |0010H | Number of Symbols : 00000000H |0114H |0014H | Size of the Optional Header : 00E0H | | | |0116H |0016H | File Characteristics - 818EH : | | | ?File is executable | | | ?Line numbers stripped from file | | | ?Local symbols stripped from file | | | ?Bytes of machine word are reversed | | | ?32 bit word machine | | | ?Bytes of machine word are reversed ‖_______|_____‖ -------- PE Optional Header ----------------------------------- --------------- ‖O_DOS |O_PE ‖(Offset from Dos Header / PE Header ‖------|------‖ |0118H |0018H | Magic Value : 010BH (`Θ`) |011AH |001AH | Major Linker Version : 2 |011BH |001BH | Minor Linker Version : 25 | | | Linker Version : 2.25 |011CH |001CH | Size of Code : 00001200H |0120H |0020H | Size of Initialized Data : 00000600H |0124H |0024H | Size of Uninitialized Data : 00000000H |0128H |0028H | Address of Entry Point : 00001000H |012CH |002CH | Base of Code (.text ofs.) : 00001000H |0130H |0030H | Base of Data (.bss ofs.) : 00003000H |0134H |0034H | Image Base : 00400000H |0138H |0038H | Section Alignment : 00001000H |013CH |003CH | File Alignment : 00000200H |0140H |0040H | Major Operating System Version : 1 |0142H |0042H | Minor Operating System Version : 0 |0144H |0044H | Major Image Version : 0 |0146H |0046H | Minor Image Version : 0 |0148H |0048H | Major SubSystem Version : 3 |014AH |004AH | Minor SubSystem Version : 10 |014CH |004CH | Reserved Long : 00000000H |0150H |0050H | Size of Image : 00006000H |0154H |0054H | Size of Headers : 00000400H |0158H |0058H | File Checksum : 00000000H |015CH |005CH | SubSystem : 2 | | | Image runs in the Windows GUI subsystem |015EH |005EH | DLL Characteristics : 0000H |0160H |0060H | Size of Stack Reserve : 00100000H |0164H |0064H | Size of Stack Commit : 00002000H |0168H |0068H | Size of Heap Reserve : 00100000H |016CH |006CH | Size of Heap Commit : 00001000H |0170H |0070H | Loader Flags : 00000000H |0174H |0074H | Number Directories : 00000010H [...] ------- PE Section Headers --------------------------------- --------------------------- ‖O_DOS |O_PE ‖(Offset from Dos Header / PE Header ‖------|------‖[...] |0270H |0170H | Section name : .reloc |0278H |0178H | Physical Address : 00001000H |027CH |017CH | Virtual Address : 00005000H |0280H |0180H | Size of RAW data : 00000200H |0284H |0184H | Pointer to RAW data : 00001C00H |0288H |0188H | Pointer to relocations : 00000000H |028CH |018CH | Pointer to line numbers : 00000000H |0290H |0190H | Number of Relocations : 0000H |0292H |0192H | Number of line numbers : 0000H |0294H |0194H | Characteristics : 50000040H | | | ?Section contains initialized data. | | | ?Section is shareable. | | | ?Section is readable. | | | ‖______|______‖ 这是一个正常文件,没有被感染。下面是同一个文件,但是被我的Aztec病毒(一个Ring-3病毒例子,看下面的)感染了。 ------------ DOS INFORMATION ----------------------------------------------------------------- Analyzed File: GOAT002.EXE DOS Reports: ?File Size - 2600H (09728d) ?File Time - 23:20:58 (hh:mm:ss) ?File Date - 22/06/1999 (dd/mm/yy) ?Attributes : Archive [...] -------------- PE Header ----------------------------------------------------------------- --------------- ‖O_DOS |O_PE ‖(Offset from Dos Header / PE Header ‖------|------‖[...] |0100H |0000H | PE Header Signature - PE/0/0 |0104H |0004H | The machine for this EXE is Intel 386 (value = 014CH) |0106H |0006H | Number of sections in the file - 0004H |0108H |0008H | File was linked at : 23/03/2049 |010CH |000CH | Pointer to Symbol Table : 00000000H |0110H |0010H | Number of Symbols : 00000000H |0114H |0014H | Size of the Optional Header : 00E0H | | | |0116H |0016H | File Characteristics - 818EH : | | | ?File is executable | | | ?Line numbers stripped from file | | | ?Local symbols stripped from file | | | ?Bytes of machine word are reversed | | | ?32 bit word machine | | | ?Bytes of machine word are reversed ‖_____|_______‖ --------- PE Optional Header ------------------------------------------------------ --------------- ‖O_DOS |O_PE ‖(Offset from Dos Header / PE Header ‖------|------‖ |0118H |0018H | Magic Value : 010BH | | | |011AH |001AH | Major Linker Version : 2 |011BH |001BH | Minor Linker Version : 25 | | | Linker Version : 2.25 |011CH |001CH | Size of Code : 00001200H |0120H |0020H | Size of Initialized Data : 00000600H |0124H |0024H | Size of Uninitialized Data : 00000000H |0128H |0028H | Address of Entry Point : 00005200H |012CH |002CH | Base of Code (.text ofs.) : 00001000H |0130H |0030H | Base of Data (.bss ofs.) : 00003000H |0134H |0034H | Image Base : 00400000H |0138H |0038H | Section Alignment : 00001000H |013CH |003CH | File Alignment : 00000200H |0140H |0040H | Major Operating System Version : 1 |0142H |0042H | Minor Operating System Version : 0 |0144H |0044H | Major Image Version : 0 |0146H |0046H | Minor Image Version : 0 |0148H |0048H | Major SubSystem Version : 3 |014AH |004AH | Minor SubSystem Version : 10 |014CH |004CH | Reserved Long : 43545A41H |0150H |0050H | Size of Image : 00006600H |0154H |0054H | Size of Headers : 00000400H |0158H |0058H | File Checksum : 00000000H |015CH |005CH | SubSystem : 2 | | | -Image runs in the Windows GUI subsystem |15EH |005E | DLL Characteristics : 0000H |160H |0060H | Size of Stack Reserve : 00100000H |0164H |0064H | Size of Stack Commit : 00002000H |0168H |0068H | Size of Heap Reserve : 00100000H |016CH |006CH | Size of Heap Commit : 00001000H |0170H |0070H | Loader Flags : 00000000H |0174H |0074H | Number Directories : 00000010H ‖___________‖ [...] ---------PE Section Headers------------------------------------------ ---------------- ‖O_DOS |O_PE ‖(Offset from Dos Header / PE Header ‖------|------‖[...] |0270H |0170H | Section name : .reloc |0278H |0178H | Physical Address : 00001600H |027CH |017CH | Virtual Address : 00005000H |0280H |0180H | Size of RAW data : 00001600H |0284H |0184H | Pointer to RAW data : 00001C00H |0288H |0188H | Pointer to relocations : 00000000H |028CH |018CH | Pointer to line numbers : 00000000H |0290H |0190H | Number of Relocations : 0000H |0292H |0192H | Number of line numbers : 0000H |0294H |0194H | Characteristics : F0000060H | | | -Section contains code. | | | -Section contains initialized data. | | | -Section is shareable. | | | -Section is executable. | | | -Section is readable. | | | -Section is writeable. | | | |_______|______| 那一个正常的文件,没有被感染。下面给出的是同一个文件,但是被我的Aztec(Ring-3例子病毒,看下文)感染了。 --------------------------------------------------------------------------------------------------- -------DOS INFORMATION ------------------------------------------------- Analyzed File: GOAT002.EXE DOS Reports: ?File Size - 2600H (09728d) ?File Time - 23:20:58 (hh:mm:ss) ?File Date - 22/06/1999 (dd/mm/yy) ?Attributes : Archive [...] -------PE Header------------------------------------------------------- ---------------- ‖O_DOS |O_PE ‖(Offset from Dos Header / PE Header ‖------|-------‖ |0100H |0000H | PE Header Signature - PE/0/0 |0104H |0004H | The machine for this EXE is Intel 386 (value = 014CH) |0106H |0006H | Number of sections in the file - 0004H |0108H |0008H | File was linked at : 23/03/2049 |010CH |000CH | Pointer to Symbol Table : 00000000H |0110H |0010H | Number of Symbols : 00000000H |0114H |0014H | Size of the Optional Header : 00E0H | | | |0116H |0016H | File Characteristics - 818EH : | | | -File is executable | | | -Line numbers stripped from file | | | -Local symbols stripped from file | | | -Bytes of machine word are reversed | | | -32 bit word machine | | | -Bytes of machine word are reversed |______|_______| ---------PE Optional Header--------------------------------------- ----------------- ‖O_DOS |O_PE ‖(Offset from Dos Header / PE Header ‖-------|-------‖ |0118H |0018H | Magic Value : 010BH |011AH |001AH | Major Linker Version : 2 |011BH |001BH | Minor Linker Version : 25 | | | Linker Version : 2.25 |011CH |001CH | Size of Code : 00001200H |0120H |0020H | Size of Initialized Data : 00000600H |0124H |0024H | Size of Uninitialized Data : 00000000H |0128H |0028H | Address of Entry Point : 00005200H |012CH |002CH | Base of Code (.text ofs.) : 00001000H |0130H |0030H | Base of Data (.bss ofs.) : 00003000H |0134H |0034H | Image Base : 00400000H |0138H |0038H | Section Alignment : 00001000H |013CH |003CH | File Alignment : 00000200H |0140H |0040H | Major Operating System Version : 1 |0142H |0042H | Minor Operating System Version : 0 |0144H |0044H | Major Image Version : 0 |0146H |0046H | Minor Image Version : 0 |0148H |0048H | Major SubSystem Version : 3 |014AH |004AH | Minor SubSystem Version : 10 |014CH |004CH | Reserved Long : 43545A41H |0150H |0050H | Size of Image : 00006600H |0154H |0054H | Size of Headers : 00000400H |0158H |0058H | File Checksum : 00000000H |015CH |005CH | SubSystem : 2 | | | -Image runs in the Windows GUI subsystem |015EH |005EH | DLL Characteristics : 0000H |0160H |0060H | Size of Stack Reserve : 00100000H |0164H |0064H | Size of Stack Commit : 00002000H |0168H |0068H | Size of Heap Reserve : 00100000H |016CH |006CH | Size of Heap Commit : 00001000H |0170H |0070H | Loader Flags : 00000000H |0174H |0074H | Number Directories : 00000010H |_______|_______| [...] ----------PE Section Headers--------------------------------------- ----------------- ‖O_DOS | O_PE ‖(Offset from Dos Header / PE Header ‖-------|-------‖[...] |0270H | 0170H | Section name : .reloc |0278H | 0178H | Physical Address : 00001600H |027CH | 017CH | Virtual Address : 00005000H |0280H | 0180H | Size of RAW data : 00001600H |0284H | 0184H | Pointer to RAW data : 00001C00H |0288H | 0188H | Pointer to relocations : 00000000H |028CH | 018CH | Pointer to line numbers : 00000000H |0290H | 0190H | Number of Relocations : 0000H |0292H | 0192H | Number of line numbers : 0000H |0294H | 0194H | Characteristics : F0000060H | | | -Section contains code. | | | -Section contains initialized data. | | | -Section is shareable. | | | -Section is executable. | | | -Section is readable. | | | -Section is writeable. | | | |_______|_______| -------------------------------------------------------------------------------- 好了,我希望这已经帮助你更理解在通过增加它的最后一节来感染PE文件的时候,做了些什么。为了避免你在比较这些每一个表时花更多的精力,我给出了一个列表: ============================================================== | Values to change |Before |After |Location | ============================================================== |Address Of Entrypoint |00001000h |00005200h |Image File Hdr | -------------------------------------------------------------- |Reserved1 (inf. mark) |00000000h |43545A41h |Image File Hdr | -------------------------------------------------------------- |Virtual Size |00001000h |00001600h |Section header | -------------------------------------------------------------- |Size Of Raw Data |00000200h |00001600h |Section header | -------------------------------------------------------------- |Characteristics |50000040h |F0000060h |Section header | -------------------------------------------------------------- 实现这个的代码非常简单。对于那些没有看到代码还没有理解的人,可以看看Win32.Aztec,在下一章将详细描述。 【Ring-3,在用户级下编程】 ~~~~~~~~~~~~~~~~~~~~~~~~~~ 嗯,用户级给了我们所有人很多令人压抑和不方便的限制,这是正确的,这妨碍了我们所崇拜的自由,这种我们在编写DOS病毒时所感 受到的自由。但是,伙计,这就是生活,这就是我们的悲哀,这就是Micro$oft。Btw,这是唯一的(当今)能够完全Win32兼容的病毒的方法,而 且这个环境是未来,正如你必须知道的。首先,让我们看看怎么用一种非常简单的方法来获得KERNEL32的基址(为了Win32兼容性): %获得KERNEL32基址的一个简单方法% ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 正 如你所知道的,当我们在执行一个应用程序的时候,代码是从KERNEL32 "call"一部分代码的(也就像KERNEL调用我们的代码一 样)。而且,如果你还记得的话,当一个call调用之后,返回的地址是在堆栈里(即,在由ESP所指定的内存地址里的)的。让我们看看关于这个的一个实际 例子: ;---------------从这里开始剪切--------------------------------------------- .586p ; Bah... simply for phun. .model flat ; Hehehe i love 32 bit stuph ;) .data ; Some data (needed by TASM32/TLINK32) db ? .code start: mov eax,[esp] ; Now EAX would be BFF8XXXXh (if w9X) ; ie, somewhere inside the API ; CreateProcess :) ret ; Return to it ;) end start ;------------到这里为止剪切-------------------------------------------------- 相当简单。我们在EAX中得到一个值大约为BFF8XXXX(XXXX是一个不重要的值,这里这么写是 因为不需要精确地知道它,再也不要拿那些无聊的东西来烦我了:))。因为Win32平台通常会对齐到一个页,我们可以搜索任何一个页的开头,而且因为 KERNEL32头就在一个页的开头,我们能够很轻松地检查它。而且当我们找到我现在正在讨论的PE头的时候,我们就知道了KERNEL32的基址。嗯, 作为限制,我们可以以50h页为限。呵呵,不要担心,下面是一些代码:) ;--------从这里开始剪切------------------------------------------------ .586p .model flat extrn ExitProcess:PROC .data limit equ 5 db 0 ;-------------------------------------- ; 没有用而且没有意义的数据 :) ; ;-------------------------------------- .code test: call delta delta: pop ebp sub ebp,offset delta mov esi,[esp] and esi,0FFFF0000h call GetK32 push 00000000h call ExitProcess ;------------------------------------- ; 呃,我认为你至少是一个普通ASM程序员, 所以我假定你知道指令的第一块是为了获得 ; 地址偏移变化量(特别在这个例子里面不需要,然而,我喜欢使得它就像我们的病毒代码)。 ; 第二块是我们所感兴趣的东西。我们把我们的程序开始调用的地址放在ESI中,即由ESP ; 所显示的地址(当然是如果我们在程序装载完后没有碰堆栈的情况下)。第二个指令,那个 ; AND,是为了获得我们的代码正在调用的页的开头。我们调用我们的例程,在这之后,我 ; 们结束处理:) ;------------------------------------- GetK32: __1: cmp byte ptr [ebp+K32_Limit],00h jz WeFailed cmp word ptr [esi],"ZM" jz CheckPE __2: sub esi,10000h dec byte ptr [ebp+K32_Limit] jmp __1 ;------------------------------------- ; 首先我们检查我们是否已经达到了我们的极限(50页)。在这之后,我们检查是否在页的开 ; 头(它应该是)是否为MZ标志,而且如果找到了,我们继续检查PE头。如果没有,我们减 ; 去10页(10000h字节),我们增加限制变量,再次搜索 ;------------------------------------- CheckPE: mov edi,[esi+3Ch] add edi,esi cmp dword ptr [edi],"EP" jz WeGotK32 jmp __2 WeFailed: mov esi,0BFF70000h WeGotK32: xchg eax,esi ret K32_Limit dw limit ;-------------------------------------- ; 我们在MZ头开始后的偏移地址3CH处得到值(存着从哪儿开始PE头的RVA),我们把这个 ; 值和页的地址规范化,而且如果从这个偏移地址处的内存地址标志是PE标志,我们就假 ; 设已经找到了...而且我们确实是找到了! ;-------------------------------------- end test ;--------到这里为止剪切----------------------------------------------------- 一个建议:我测试了它,而且在Win98下和WinNT4 SP3下面没有给我们任何类型的 问题,然而,我不知道在其它任何地方会发生什么,我建议你使用SEH来避免可能的页错误(和它们相关的蓝屏)。SEH将会在后面介绍。嗨, Lord Julus在他的教程里面所使用的方法(在感染文件里面搜索GetModuleHandleA函数)并不能很好地满足我的需要,无论 如何,我将给出那个我自己版本的代码,在那里我将解释怎么来玩输入函数。例如,它在per-process驻留病毒里面要用到,在这个例程里面有一小点改 变:) %获取那些令人疯狂的API函数!!!% ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 正如我在介绍那一章所介绍的,Ring-3是用户级的,所以我们只能访问它的有限的权限。例如,我们不能使用端口, 读或写某些的内存区域,等等。当开发Win95(那些再也没有人说的"Win32平台是不可感染"的系统)的时候,微软如果压制住过去所编写的病毒,微软 就确信能够击败我们。在他们的美梦中,他们认为我们不能使用他们的API函数,而且,他们更没想到我们能跳转到Ring-0,但是,这是另外一段历史了。 正如你以前所说的,我们以API函数名作为外部函数,所以import32.lib给了我们函数的地址,而且它已经 汇编了,但是我们在写病毒的时候有一个问题。如果我们hardcode(也就是说我们调用一个API函数的时候给的是一个固定的偏移地址),最可能发生的 事情是在下一个版本的Win32版本中,那个地址再也不起作用了。你可以看看Bizatch中的一个例子。我们该怎么做呢?好了,我们有一个函数叫做 GetProcAddress,它返回给我们的是我们所需要的API的地址。聪明的你可能已经注意到了GetProcAddress也是一个API,所以 如果我们没有得到那个API还谈什么利用它来搜索其它API呢。正如在生活中我们所遇到的事情一样,我们有许多可能性的东西去做,而且我将提及我认为最好 的两种方法: 1.在输入表中搜索GetProcAddress API函数。 2.当我们感染一个文件的时候,在它的输入函数里寻找GetProcAddress。 因为最早的方法是第一个,猜猜现在我将会解释哪一个呢?:)OK,让我们以理论学习开始,在这之后,一些代码。 如果你看看PE头的格式,我们在偏移地址78h(是PE头,不是文件!)得到输入表。好了,我们需要利用内核的输出 地址。在Window 95/98下,内核通常在偏移地址0BFF70000h处,而Window NT的内核看起来是在 077F00000h处。在Win2K中我们在偏移地址077E00000h处得到它。所以,首先,我们把它的地址保存到寄存器中,我们将用来作为指针。 我强烈建议使用ESI,主要是因为我们可以通过使用LODSD来优化一些东西。好了,我们检查在这个地址处是不是"MZ"(恩反过来为"ZM",该死的 intel处理器架构),因为内核是一个库(.DLL),而库有一个PE头,正如我们以前看PE头的时候,是DOS-兼容的一部分的时候所看到的。在那个 比较之后,让我们检查它是不是PE,所以我们到头的偏移image_base+[3Ch] (=内核的偏移地址+内核的PE头的3Ch偏移), 搜索比较"PE\0\0",PE文件的签名。 如果所有都正确,那么让我们继续。我们需要输出表 的RVA,正如你所能看到的,它在PE头的偏移地址78h处。所以我们得到了它。但是,正如你所知道的,RVA(Relative Virtual Address),正如它的名字所表明的,是和一个OFFSET的相对值,在这种image base为 kernel的情况下,正如我以前所说的,那就是它的地址。就这么简单:仅仅把kernel的偏移加上在输出表(Export Table)中 的RVA即可。好了,我们现在已经在输出表中了:) 让我们看看它的格式: ---------------------------------- <----+00000000h | Export Flags | Size : 1 DWORD |----------------------------------|<----+00000004h | Time/Date stamp | Size : 1 WORD |----------------------------------|<----+00000006h | Major version | Size : 1 WORD |----------------------------------|<----+00000008h | Minor version | Size : 1 DWORD |----------------------------------|<----+0000000Ch | Name RVA | Size : 1 DWORD |----------------------------------|<----+00000010h | Number Of Exported Functions | Size : 1 DWORD |----------------------------------|<----+00000014h | Number Of Exported Names | Size : 1 DWORD |----------------------------------|<----+00000018h | Export Address Table RVA | Size : 1 DWORD |----------------------------------|<----+0000001Ch | Export Name Pointers Table RVA | Size : 1 DWORD |----------------------------------|<----+00000020h | Export Ordinals RVA | Size : 1 DWORD |__________________________________| Total Size : 24h BYTES 对我们来说是最后6个域。在地址表RVA的值中,正如你能想象的是,Name Pointers RVA 和 Ordinals RVA都是和KERNEL32的基址相关的。所以,获得 API地址的第一步是知道这个API的位置,而知道它的最简单的方法是到Name Pointers所指示的偏移地址处去寻找,把它和我们想要 找的API做比较,如果它们完全相同,我们就要计算API的偏移地址了。好了,我们已经到了这一步了,而且我们在计数器中有一个值,因为我们没检查一次 API的名字就加一次。这个计数器,正如你能想象的,将会保存我们已经找到的API名字的个数,而且它们不相等。这个计数器可以是一个字或一个双字,但是 最好不要是一个字节,因为我们需要超过255个API函数:) 说明:我假设你把地址的VA(RVA+kernel image base),Name 和 (序数表)Ordinal tables已经保存到相关的变量中了。 OK,假设我们已经获得了我们想要得到的API的名字,所以,我们得到了它在名字指针表中的计数。接下来可能对你来 说是最复杂的,开始Win32编码。嗯,让我们继续下去。我们得到了计数,而且我们现在要在Ordinal Table(一个dword数组) 中搜索我们想要得到的API的序数。当我们得到了API在数组(在计数器)中的数字,我们仅仅把它乘以2(记住,序数数组是由字组成的,所以,我们必须对 字进行计算...),而且当然了,把它加上序数表的开始偏移地址。为了继续我已经解释的东西,我们需要由下面公式指向的字: API's Ordinal location: ( counter * 2 ) + Ordinal Table VA 很简单,是不是啊?下一步(而且是最后一步)是从地址表中获得API的确定地址。我们已经得到了API的序号,对 吗?利用它,我们的生活变得非常容易。我们只有把序号乘以4(因为地址数组是双字形式的而不是字,而一个双字的大小是4),而且把它加上先前得到的地址表 开始的偏移地址。呵呵,现在,我们得到了API地址的RVA啦。所以我们要把它规范化,加上Kernel的偏移地址,那样就好了。我们得到了它!!!让我 们看看这个的数学公式: API's Address: ( API's Ordinal * 4 ) + Address Table VA + KERNEL32 imagebase --------------------------------------------------------------------- So, as we retrieve the position | EntryPoint | Ordinal | Name | that occupies the string in the |--------------------|---------------|-------------------------------| Names table, we can know its | 00005090 | 0001 | AddAtomA | ordinal (each name has an ordi- |--------------------|---------------|-------------------------------| nal that is in the same position | 00005100 | 0002 | AddAtomW | than the API name), and knowing |--------------------|---------------|-------------------------------| the ordinal, we can know its | 00025540 | 0003 | AddConsoleAliasA | Address, that is, its entrypoint |--------------------|---------------|-------------------------------| RVA. We normalize it, and voila, | 00025500 | 0004 | AddConsoleAliasW | you have what you need, the \/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\ required API address. [...]这些表还有更多的入口,但是有那些就足够了... 我希望你已经理解了我解释的东西。我试图尽可能的使它表述简单,如果你不能理解它,不要往下看了,一步 一步地重读它。要有耐心。我肯定你会懂地。嗯,现在你可能需要一些代码了。下面给出我例程,作为一个示例,在我的Iced Earth病毒中用 到了。 ;----从这儿开始剪切----------------------------------------------------------- ; ; GetAPI & GetAPIs procedures ; =========================== ; ; 这是我的寻找所有需要的API的函数... 它们被分成了两部分。 ; GetAPI函数仅仅获得了我们需要的一个函数, 而GetAPIs函数 ; 则搜索病毒所需要的所有API函数。 ; GetAPI proc ;-------------------------------------------------------------------------- ; 让我们来看看,这个函数需要和返回的参数如下: ; ; ; 输入: ESI : 指向API名字的指针 (区分大小写) ; 输出: EAX : API 地址 ;-------------------------------------------------------------------------- mov edx,esi ; Save ptr to name @_1: cmp byte ptr [esi],0 ; Null-terminated char? jz @_2 ; Yeah, we got it. inc esi ; Nopes, continue searching jmp @_1 ; bloooopz... @_2: inc esi ; heh, don't forget this ;) sub esi,edx ; ESI = API Name size mov ecx,esi ; ECX = ESI :) ;-------------------------------------------------------------------------- ; 好了,我亲爱的朋友们,这很容易理解。我们在ESI中是指向API名字开始 ; 的指针,让我们想象一下,我们想要寻找"FindFirstFileA": ; ; FFFA db "FindFirstFileA",0 ; ↑ 指针指向这儿 ; ; 而且我们需要保存这个指针,并知道了API名的大小,所以 ; 我们把指向API名字的初始指针保存到一个我们不用的寄存器中如EDX ; 然后增加在ESI中的指针的值,直到[ESI]=0 ; ; FFFA db "FindFirstFileA",0 ; ↑ 现在指针指 向这儿了 ; ; 也就是说,以NULL结尾:)然后,通过把新指针减去旧指针,我们得 ; 到了API名字的大小,搜索引擎需要它。然后我把它保存到ECX中, ; 也是一个我们不会使用的寄存器。 ;--------------------------------------------------------------------------- xor eax,eax ; EAX = 0 mov word ptr [ebp+Counter],ax ; Counter set to 0 mov esi,[ebp+kernel] ; Get kernel's PE head. offset add esi,3Ch lodsw ; in AX add eax,[ebp+kernel] ; Normalize it mov esi,[eax+78h] ; Get Export Table RVA add esi,[ebp+kernel] ; Ptr to Address Table RVA add esi,1Ch ;--------------------------------------------------------------------------- ; 首先,我们清除EAX,然后为了避免无法预料的错误,使得计数变量为0。 ; 如果你还记得PE文件头偏移地址3CH(从映象基址MZ标志开始计数)的作用, ; 你会理解这个的。我们正在请求得到KERNEL32 PE头偏移的开始。因为 ; 它是一个RVA,我们把它规范化,那就是我们得到了它的PE头偏移地址。 ; 现在我们所要做的是获得输出表(Export Table)的地址(在PE头+78h处), ; 然后,我们避开这个结构的不想要的数据,直接获得地址表(Address Table) ; 的RVA。 ;--------------------------------------------------------------------------- lodsd ; EAX = Address Table RVA add eax,[ebp+kernel] ; Normalize mov dword ptr [ebp+AddressTableVA],eax ; Store it in VA form lodsd ; EAX = Name Ptrz Table RVA add eax,[ebp+kernel] ; Normalize push eax ; mov [ebp+NameTableVA],eax lodsd ; EAX = Ordinal Table RVA add eax,[ebp+kernel] ; Normalize mov dword ptr [ebp+OrdinalTableVA],eax ; Store in VA form pop esi ; ESI = Name Ptrz Table VA ;--------------------------------------------------------------------------- ; 如果你还记得,在ESI中是指向地址表RVA(Address Table RVA)的指针,所以, ; 我们为了得到那个地址,用了一个LODSD,它把由ESI所指定的双字(DWORD)保 ; 存到EAX中。因为它是一个RVA,我们需要对它规范化。 ; ; 让我们看看Matt Pietrek关于这个第一个域的描述: ; ; “这个域是一个RVA而且指向一个函数地址数组。这个函数地址是这个模块中 ; 每一个输出地址的入口点(RVA)。” ; ; ; 毫无疑问了,我们把它保存到它的变量中了。然后,接下来我们找到的 ; 是名字指针表(Name Pointers Table),Matt Pietrek的描述如下: ; ; “这个域是一个RVA,而且指向一个字符串指针数组。这些字符串是模块 ; 的输出函数的名字。” ; ; 但是我没有把它保存到一个变量中,我把它压栈,仅仅是因为我很快就要用到 ; 它。最终,我们找到了,下面是Matt Pietrek关于它的描述: ; ; “这个域是一个RVA,而且指向一个字(WORD)数组。这些字是这个模块 ; 的所有输出函数的序号”。 ; ; 好了,那就是我们所做的事情。 ;--------------------------------------------------------------------------- @_3: push esi ; Save ESI for l8r restore lodsd ; Get value ptr ESI in EAX add eax,[ebp+kernel] ; Normalize mov esi,eax ; ESI = VA of API name mov edi,edx ; EDI = ptr to wanted API push ecx ; ECX = API size cld ; Clear direction flag rep cmpsb ; Compare both API names pop ecx ; Restore ECX jz @_4 ; Jump if APIs are 100% equal pop esi ; Restore ESI add esi,4 ; And get next value of array inc word ptr [ebp+Counter] ; Increase counter jmp @_3 ; Loop again ;--------------------------------------------------------------------------- ; 嗨,是不是我放了太多的代码而没有注释?因为我刚做好,但是懂得了这一块代码 ; 不能因为解释它而分离开来。我们首先所做的是把ESI(在CMPSB指令执行中将改变) ; 压栈,以备后用。然后,我们获得由ESI(Name Pointerz Table)指向的双字保存到 ; 累加器(EAX)中,所有这些通过LODSD指令实现。我们通过加上kernel的基址来规范 ; 化它。好了,现在我们在EAX中是指向某一个API名字的指针,但是我们不知道(仍然) ; 是什么API。例如,EAX可以指向诸如"CreateProcessA",而这个API对我们的病毒来 ; 说不感兴趣...为了把那个字符串和我们想要的字符串(现在由EDX指向),我们有CMPSB。 ; 所以,我们准备它的参数:在ESI中,我们使得指针指向现在在Name Pointerz Table中 ; 的API的开始,在EDI中,我们使之指向需要的API)。在ECX中我们保存它的大小, ; 然后我们按字节比较。如果所有的字符相等,就设置0标志,然后跳转到获取那个API ; 地址的例程,但是如果它失败了,我们恢复ESI,并把它加上DWORD的大小,为了获取 ; 在Name Pointerz Table数组中的下一个值。我们增加计数器的值(非常重要),然后 ; 继续搜索。 ;--------------------------------------------------------------------------- @_4: pop esi ; Avoid shit in stack movzx eax,word ptr [ebp+Counter] ; Get in AX the counter shl eax,1 ; EAX = AX * 2 add eax,dword ptr [ebp+OrdinalTableVA] ; Normalize xor esi,esi ; Clear ESI xchg eax,esi ; EAX = 0, ESI = ptr to Ord lodsw ; Get Ordinal in AX shl eax,2 ; EAX = AX * 4 add eax,dword ptr [ebp+AddressTableVA] ; Normalize mov esi,eax ; ESI = ptr to Address RVA lodsd ; EAX = Address RVA add eax,[ebp+kernel] ; Normalize and all is done. ret ;--------------------------------------------------------------------------- ; Pfff, 又一个巨大的代码块,而且看起来很难理解,对吗?呵呵,不要害怕,我将要 ; 注释它:) ; |
|
|
沙发#
发布于:2007-03-26 20:15
都是汇编啊,看着很费劲
|
|
板凳#
发布于:2007-03-29 16:43
很有价值
|
|
|
地板#
发布于:2007-04-01 11:03
很好
|
|
地下室#
发布于:2007-08-16 02:27
好东西..
顶.. |
|
5楼#
发布于:2007-08-16 02:32
天啊..
巨长.. 复制都用了我N多时间.. |
|
6楼#
发布于:2007-08-16 23:19
收藏!
|
|
|
7楼#
发布于:2007-08-17 00:59
太长了。
|
|
|
8楼#
发布于:2007-09-30 01:00
太太太长了
|
|
9楼#
发布于:2008-04-15 11:37
cyliu,什么时候把你的资料打个包发上来吧
|
|
10楼#
发布于:2008-05-19 22:49
学习中,谢谢
|
|