首页
论坛
课程
招聘

[原创]国庆PE总复习(1-7)合集

2010-10-1 17:44 136909

[原创]国庆PE总复习(1-7)合集

2010-10-1 17:44
136909
最新回复 (147)
熊猫正正 9 2010-10-5 23:47
51
0
昨天给大家把《加密与解密》第三版上的PE工具源代码给大家分析了一下,感觉用VC去写PE工具还是不能很好的休现PE的文件内部结构,可能是封装了一些函数的吧,上一篇文章主是为那些没有学过Win32汇编的朋友们准备的,下面我开始重注讲解用Win32汇编进行PE工具的编程,你绝对会对PE文件工具有一个全新的认识,因为用汇编去写PE工具更能体现PE文件的内部组成结构,这是我个人的观点,不代表所有人!!
如果对汇编不是很熟悉的朋友,建设去看老罗的《Win32汇编语言程序设计》写的还不错!
这里我主要以老罗书的章节为引导给大家讲解一下PE工具的编写,如果大家还有什么不清楚的,请直接参考老罗的《Win32汇编语言程序设计》,里面讲的非常详细!!谢谢!

要讲解PE工具,首先我们要弄清的是RVA与File Offset的转换,在VC中微软给我们在IMAGEHLP.H中给我们封装了一个函数ImageRvaToVa,这个函数就实现了上面的功能,但汇编中我们必须自己去实现它们两者的转换。如果大家有不清楚RVA和File Offset的朋友请参考前面的几篇文章,里面已经将这两个讲的很清楚的,我这里只讲编程,理论就不讲了!

当处理PE文件时,任何的RVA必须经过到文件偏移的换算,才能用来定位并访问文件中的数据,但换算却无法用一个简单的公式来完成,我们可以通过下面的一个算法来实现:
循环扫描节表并得到每个节在内存中的起始RVA(根据VirtualAddress字段),并根据节的大小(SizeOfRawData字段)算出节的结束RVA,最后比较判断目标RVA是否落在某个节之内。
如果目标RVA处于某个节之内,那么用目标RVA减去节的起始RVA,这样就得到了目标RVA相对于节起始地址的偏移量RVA。
在节表中获取节在文件中所处的偏移(PointerToRawData字段),将这个偏移值加上上一步得到的RVA值,这才是数据在文件中的真正偏移位置。
这里将两个函数封装在一个_RvaToFileOffset.asm文件中,以便于以后使用方便,在这个文件中有两个函数,其中_RVAToOffset函数是将RVA转换成文件偏移,输入的参数是已经读取到内存中的文件头的地址和RVA值;_GetRVASection函数用来获取RVA所在的节的名称。
_RVAToOffset将RVA转换成实际的数据位置,函数如下所示,这里我用图示的方法,这样看起来会更加清楚一点!!

_GetRVASection查找RVA所在节区,函数如下所示,这里我同样用图示的方法,注释的都很详解了,大家只要对照上面的算法,看下面的图,就会很清楚!!


这两个函数很重要,在后面的应用中会经常用到,这里作为重点讲解,请在家一定要弄明白!!

在老罗的书上讲的PE分析工具使用的方法,基本上都是利用映射文件的方法将PE文件映射到内存中以供处理,处理使用的代码就是根据具体情况去编写,这里我先把映射文件的代码给大家
在老罗的书上还使用的SEH异常处理,代码如下,如有什么不懂的请参考《Win32汇编语言程序设计》第十四章中介绍的SEH来设置一个异常处理回调函数
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 错误 Handler
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Handler        proc        C _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatcherContext

                pushad
                mov        esi,_lpExceptionRecord
                mov        edi,_lpContext
                assume        esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT
                mov        eax,_lpSEH
                push        [eax + 0ch]
                pop        [edi].regEbp
                push        [eax + 8]
                pop        [edi].regEip
                push        eax
                pop        [edi].regEsp
                assume        esi:nothing,edi:nothing
                popad
                mov        eax,ExceptionContinueExecution
                ret

_Handler        endp

文件映射到内存的函数如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_OpenFile        proc
                local        @stOF:OPENFILENAME
                local        @hFile,@dwFileSize,@hMapFile,@lpMemory

                invoke        RtlZeroMemory,addr @stOF,sizeof @stOF                  ;将OPENFILENAME结构填充为0
                mov        @stOF.lStructSize,sizeof @stOF
                push        hWinMain
                pop        @stOF.hwndOwner
                mov        @stOF.lpstrFilter,offset szExtPe
                mov        @stOF.lpstrFile,offset szFileName
                mov        @stOF.nMaxFile,MAX_PATH
                mov        @stOF.Flags,OFN_PATHMUSTEXIST or OFN_FILEMUSTEXIST     ;分别给OPENFILENAME结构赋值
                invoke        GetOpenFileName,addr @stOF                             ;调用通过对话框打开文件
                .if        ! eax
                        jmp        @F                                             ;如果打开失败则返回
                .endif
;********************************************************************
; 打开文件并建立文件 Mapping
;********************************************************************
                invoke        CreateFile,addr szFileName,GENERIC_READ,FILE_SHARE_READ or \
                        FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
                .if        eax !=        INVALID_HANDLE_VALUE                           ;将文件映射到内存中
                        mov        @hFile,eax
                        invoke        GetFileSize,eax,NULL                           ;得到文件大小
                        mov        @dwFileSize,eax
                        .if        eax
                                invoke        CreateFileMapping,@hFile,NULL,PAGE_READONLY,0,0,NULL  ;创建文件映射
                                .if        eax
                                        mov        @hMapFile,eax
                                        invoke        MapViewOfFile,eax,FILE_MAP_READ,0,0,0         ;映射文件
                                        .if        eax
                                                mov        @lpMemory,eax
;********************************************************************
; 创建用于错误处理的 SEH 结构
;********************************************************************
                                                assume        fs:nothing                            ;处理SEH异常
                                                push        ebp
                                                push        offset _ErrFormat
                                                push        offset _Handler
                                                push        fs:[0]
                                                mov        fs:[0],esp
;********************************************************************
; 检测 PE 文件是否有效
;********************************************************************
                                                mov        esi,@lpMemory   
                                                assume        esi:ptr IMAGE_DOS_HEADER            
                                                .if        [esi].e_magic != IMAGE_DOS_SIGNATURE       ;判断DOS头是否为MZ
                                                        jmp        _ErrFormat
                                                .endif
                                                add        esi,[esi].e_lfanew                         ;定位到PE头
                                                assume        esi:ptr IMAGE_NT_HEADERS
                                                .if        [esi].Signature != IMAGE_NT_SIGNATURE      ;判断PE头是否为PE
                                                        jmp        _ErrFormat                         ;如果不是,跳到相应错误处理
                                                .endif
                                                invoke        _ProcessPeFile,@lpMemory,esi,@dwFileSize   ;根据情况处理内存映射中的文件
                                                jmp        _ErrorExit                                 ;处理完结,处理相关结尾工作
_ErrFormat:
                                                invoke        MessageBox,hWinMain,addr szErrFormat,NULL,MB_OK
_ErrorExit:
                                                pop        fs:[0]
                                                add        esp,0ch
                                                invoke        UnmapViewOfFile,@lpMemory                 ;对应前面的映射
                                        .endif
                                        invoke        CloseHandle,@hMapFile                             ;对应前面打开的映射文件
                                .endif
                                invoke        CloseHandle,@hFile                                        ;关闭文件打开的句柄
                        .endif
                .endif
@@:
                ret

_OpenFile        endp

在老罗的书中的相关编程中都使用了上面的这个函数,唯一不同的就是调用_ProcessPeFile函数进行的操作不同!

上面简单对上面的这个函数时行分析,函数使用了SEH异常处理,一旦发生异常的话,则将程序转移到_ErrFormat标号处执行并认为文件的格式存在异常。由于PE文件的分析中涉及到很多指针操作,对任何一个指针都进行检测并判断它们是否已经越出了内存映射文件的范围是很麻烦的,使用SEH可以让这方面的工作开销的最少。
当一切准备结束之后,函数中简单的判断了一下打开的文件是否是一个PE文件,具体请参考上面的代码,算法其实很简单,ESI一开始被指向文件的头部,程序首先判断DOS文件头的标识符是否和"MZ"(也就是IMAGE_DOS_SIGNATURE)符合,如果符合的话,那么从003Ch处(也就是e_lfanew字段)取出PE文件头的偏移,并比较PE文件头的标识是否为IMAGE_NT_SIGNATURE,这两个步骤都通过的话,那么就可以认定这是一个合法的PE文件了,程序就真正开始分析工作了,调用_ProcessPeFile子程序进行分析。

得到区块信息
调用_ProcessPeFile得到区块的信息函数如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile        proc        _lpFile,_lpPeHead,_dwSize                      ;传入三个参数,文件,PE文件头,大小
                local        @szBuffer[1024]:byte,@szSectionName[16]:byte

                pushad
                mov        edi,_lpPeHead
                assume        edi:ptr IMAGE_NT_HEADERS                       ;定义PE文件头
;********************************************************************
; 显示 PE 文件头中的一些信息
;********************************************************************
                movzx        ecx,[edi].FileHeader.Machine                   ;得到文件运行平台
                movzx        edx,[edi].FileHeader.NumberOfSections          ;得到PE文件节区数量
                movzx        ebx,[edi].FileHeader.Characteristics           ;得到PE文件的文件标记
                invoke        wsprintf,addr @szBuffer,addr szMsg,\           ;格式化输出
                        addr szFileName,ecx,edx,ebx,\
                        [edi].OptionalHeader.ImageBase
                invoke        SetWindowText,hWinEdit,addr @szBuffer
;********************************************************************
; 循环显示每个节区的信息
;********************************************************************
                invoke        _AppendInfo,addr szMsgSection
                movzx        ecx,[edi].FileHeader.NumberOfSections         ;以区块数作为循环条件
                add        edi,sizeof IMAGE_NT_HEADERS                   ;PE文件头加上PE文件头的大小,定位到区块表
                assume        edi:ptr IMAGE_SECTION_HEADER
                .repeat
                        push        ecx
;********************************************************************
; 节区名称
;********************************************************************
                        invoke        RtlZeroMemory,addr @szSectionName,sizeof @szSectionName
                        push        esi                                             
                        push        edi
                        mov        ecx,8                               ;以8个字节为循环条件
                        mov        esi,edi
                        lea        edi,@szSectionName
                        cld                                         ;设置方向传递方向
                        @@:
                        lodsb                                       ;装载字符串
                        .if        ! al                                ;判断字符串是否为空
                                mov        al,' '                      ;如果为空则赋为空
                        .endif
                        stosb                                       ;字符串传递DS:ESI---->ES:EDI中
                        loop        @B                                  ;循环
                        pop        edi
                        pop        esi
;********************************************************************
                        invoke        wsprintf,addr @szBuffer,addr szFmtSection,\     ;格式化输出
                                addr @szSectionName,[edi].Misc.VirtualSize,\
                                [edi].VirtualAddress,[edi].SizeOfRawData,\
                                [edi].PointerToRawData,[edi].Characteristics
                        invoke        _AppendInfo,addr @szBuffer                      ;输出格式化信息
                        add        edi,sizeof IMAGE_SECTION_HEADER                 ;定位到下一个区块
;********************************************************************
                        pop        ecx
                .untilcxz
                assume        edi:nothing
                popad
                ret

_ProcessPeFile        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

这里我就不过多的解释了,相信我上面的注释的很详细了,上面的注释我只是根据自己的理解来标注的,如果有什么失误的地方,请各位大侠们指出,谢谢!

导出PE文件中的输入表
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile        proc        _lpFile,_lpPeHead,_dwSize                       ;传入三个参数,文件,PE文件头,大小
                local        @szBuffer[1024]:byte,@szSectionName[16]:byte

                pushad
                mov        edi,_lpPeHead
                assume        edi:ptr IMAGE_NT_HEADERS                       ;定义PE文件头
;********************************************************************
                mov        eax,[edi].OptionalHeader.DataDirectory[8].VirtualAddress       ;从PE文件头中定位到导入表的位置
                .if        ! eax
                        invoke        MessageBox,hWinMain,addr szErrNoImport,NULL,MB_OK
                        jmp        _Ret
                .endif
                invoke        _RVAToOffset,_lpFile,eax                      ;将导入表的RVA转换为File Offset地址
                add        eax,_lpFile
                mov        edi,eax
                assume        edi:ptr IMAGE_IMPORT_DESCRIPTOR               ;定义导入表
;********************************************************************
; 显示 PE 文件名
;********************************************************************
                invoke        _GetRVASection,_lpFile,[edi].OriginalFirstThunk     ;调用前面的函数得到导入表所处的节的名称
                invoke        wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax
                invoke        SetWindowText,hWinEdit,addr @szBuffer
;********************************************************************
; 循环处理 IMAGE_IMPORT_DESCRIPTOR 直到遇到全零的则结束
;********************************************************************
                .while        [edi].OriginalFirstThunk || [edi].TimeDateStamp || \
                        [edi].ForwarderChain || [edi].Name1 || [edi].FirstThunk
                        invoke        _RVAToOffset,_lpFile,[edi].Name1        ;将导入库的Name字段RVA转换为File Offset地址
                        add        eax,_lpFile                             ;得到导入库的名字
                        invoke         wsprintf,addr @szBuffer,addr szMsgImport,eax,\   ;格式化输出
                                [edi].OriginalFirstThunk,[edi].TimeDateStamp,\
                                [edi].ForwarderChain,[edi].FirstThunk
                        invoke        _AppendInfo,addr @szBuffer
;********************************************************************
; 获取 IMAGE_THUNK_DATA 列表地址 ---> ebx
;********************************************************************
                        .if        [edi].OriginalFirstThunk               ;判断OriginalFirstThunk是否为0
                                mov        eax,[edi].OriginalFirstThunk   ;如果不为0,则以OriginalFirstThunk定位
                        .else
                                mov        eax,[edi].FirstThunk           ;如果为0,则以FirsThunk定位
                        .endif
                        invoke        _RVAToOffset,_lpFile,eax
                        add        eax,_lpFile                          
                        mov        ebx,eax
;********************************************************************
; 循环处理所有的 IMAGE_THUNK_DATA
;********************************************************************
                        .while        dword ptr [ebx]
;********************************************************************
; 按序号导入
;********************************************************************
                                .if        dword ptr [ebx] & IMAGE_ORDINAL_FLAG32    ;判断是按序号导入还是按名字导入
                                        mov        eax,dword ptr [ebx]
                                        and        eax,0FFFFh                        ;取出双字的低位就是函数的序号
                                        invoke        wsprintf,addr @szBuffer,addr szMsgOrdinal,eax
                                .else
;********************************************************************
; 按函数名导入
;********************************************************************
                                        invoke        _RVAToOffset,_lpFile,dword ptr [ebx]  
                                        add        eax,_lpFile
                                        assume        eax:ptr IMAGE_IMPORT_BY_NAME      ;按名字导入
                                        movzx        ecx,[eax].Hint                    ;函数的序号
                                        invoke        wsprintf,addr @szBuffer,\         ;格式化输出
                                                addr szMsgName,ecx,addr [eax].Name1
                                        assume        eax:nothing
                                .endif
                                invoke        _AppendInfo,addr @szBuffer
                                add        ebx,4
                        .endw
                        add        edi,sizeof IMAGE_IMPORT_DESCRIPTOR                ;指向下一个导入表
                .endw
_Ret:
                assume        edi:nothing
                popad
                ret

_ProcessPeFile        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
上面的注释,已经讲的很详细,我就不多此一举了!!!请大家对着前面的理论慢慢研究,其实很简单的!!

导出PE文件中的输出表
输出表的处理函数中会用到两个算法,在这里给大家说明一下:
从序号查找入口地址
Windows装载器查找导出函数入口地址的过程,如果已知函数的导出序号,如何得到入口地址呢?
步骤如下所示:
定位到PE文件头。
从PE文件头中的IMAGE_OPTIONAL_HEADER32结构中取出数据目录表,并从第一个数据目录中得到导出表的地址。
从导出表的nBase字段得到起始序号。
将需要查找的导出序号减去起始序号,得到函数在入口地址表中的索引。
检测索引值是否大于导出表的NumberOfFunctions字段的值,如果大于后者的话,说明输入的序号是无效的。
用这个索引值在AddressOfFunctions字段指向的导出函数入口地址表中取出相应的项目,这就是函数的入口地址RVA值,当函数被装入内存的时候,这个RVA值加上模块实际装入的基址,就得到了函数真正的入口地址。

从函数名称查找入口地址
最初的步骤是一样的,就是首先得到导出表的地址。
从导出表的NumberOfNames字段得到已命名函数的总数,并以这个数字作为循环的次数来构造一个循环。
从AddressOfNames字段指向的函数名称地址表的第一项开始,在循环中将第一项定义的函数名与要查找的函数名相比较,如果没有任何一个函数名是符合的,表示文件中没有指定名称的函数。
如果某一项定义的函数名与要查找的函数名符合,那么记下这个函数名在字符串地址表中的索引值,然后在AddressOfNameOrdinals指向的数组中以同样的索引值取出数组项的值,暂且假定这个值为X。
最后,以X值作为索引的值,在AddressOfFunctions字段指向的函数入口地址表中获取的RVA就是函数的入口地址。同样当函数被装入内存的时候,这个RVA值加上模块实际装入的基址,就得到了函数的真正的入口地址。
从函数名称查找入口地址的代码在病毒中经常见到,因为病毒是作为一段额外的代码被附加到可执行文件中,如果病毒代码中用到了某些的API的话,这些API的地址不可能在宿住文件导入表中为病毒代码准备,只能通过在内存中动态查找的办法来实现。

_ProcessPeFile函数的具体实现如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile        proc        _lpFile,_lpPeHead,_dwSize                           ;传入三个参数,文件,PE头,大小
                local        @szBuffer[1024]:byte,@szSectionName[16]:byte
                local        @dwIndex,@lpAddressOfNames,@lpAddressOfNameOrdinals

                pushad
                mov        esi,_lpPeHead
                assume        esi:ptr IMAGE_NT_HEADERS                            ;定义PE头
;********************************************************************
; 从数据目录中获取导出表的位置
;********************************************************************
                mov        eax,[esi].OptionalHeader.DataDirectory.VirtualAddress   ;从PE头的结构中定位到导出表的位置
                .if        ! eax
                        invoke        MessageBox,hWinMain,addr szErrNoExport,NULL,MB_OK
                        jmp        _Ret
                .endif
                invoke        _RVAToOffset,_lpFile,eax                           ;调用前面两个函数,将文件RVA地址转换为File Offset地址
                add        eax,_lpFile
                mov        edi,eax
;********************************************************************
; 显示一些常用的信息
;********************************************************************
                assume        edi:ptr IMAGE_EXPORT_DIRECTORY
                invoke        _RVAToOffset,_lpFile,[edi].nName                  ; 将导出输表的RVA转换为File Offset
                add        eax,_lpFile                     
                mov        ecx,eax                                           ;原始文件名
                invoke        _GetRVASection,_lpFile,[edi].nName                ;得到导出表所在的区块段名字
                invoke        wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax,ecx,[edi].nBase,\   ;格式化输出
                        [edi].NumberOfFunctions,[edi].NumberOfNames,[edi].AddressOfFunctions,\
                        [edi].AddressOfNames,[edi].AddressOfNameOrdinals
                invoke        SetWindowText,hWinEdit,addr @szBuffer
;********************************************************************
                invoke        _RVAToOffset,_lpFile,[edi].AddressOfNames         ;将函数名地址表的RVA转换为File Offset
                add        eax,_lpFile
                mov        @lpAddressOfNames,eax
                invoke        _RVAToOffset,_lpFile,[edi].AddressOfNameOrdinals  ;将函数名序号表的RVA转换为File Offset
                add        eax,_lpFile
                mov        @lpAddressOfNameOrdinals,eax
                invoke        _RVAToOffset,_lpFile,[edi].AddressOfFunctions     ;将导出函数地址表的RVA转换为File Offset
                add        eax,_lpFile
                mov        esi,eax                                                      ;ESI中存放函数地址表
;********************************************************************
; 循环显示导出函数的信息
;********************************************************************
                mov        ecx,[edi].NumberOfFunctions                       ;以导出函数的总数为循环
                mov        @dwIndex,0                                        ;索引
                @@:
                        pushad
;********************************************************************
; 在按名称导出的索引表中
;********************************************************************
                        mov        eax,@dwIndex
                        push        edi
                        mov        ecx,[edi].NumberOfNames                   ;以名称导出的函数总数为循环条件
                        cld                                               ;设置方向位
                        mov        edi,@lpAddressOfNameOrdinals
                        repnz        scasw                                     ;字符串查找,看有没有符合的函数名
                        .if        ZERO?        ;找到函数名称
                                sub        edi,@lpAddressOfNameOrdinals      ;由于AddressOfNameOrdinals指定的数组是WORD类型的,所以查找
                                sub        edi,2              ;指令用的是SCASW而不是SCASB,当查找结束后,如果标志位为0则表示查找成功,这时
                                shl        edi,1              ;EDI的值指向找到的项目后面一个WORD位置,将EDI去数组的基址并减去2(一个WORD的长度),
                                                           ;得到的就是找到的项目的位置偏移。由于这个数组是WORD类型的,而AddressOfNames指向
                                add        edi,@lpAddressOfNames   ;的数组是DWORD类型的,所以还要将偏移乘以2来修正,用修正后的偏移在AddressOfNames
                                invoke        _RVAToOffset,_lpFile,dword ptr [edi]   ;表中就可以得到指向函数名称字符串的RVA了。
                                add        eax,_lpFile
                        .else
                                mov        eax,offset szExportByOrd
                        .endif
                        pop        edi
;********************************************************************
; 序号 --> ecx
;********************************************************************
                        mov        ecx,@dwIndex
                        add        ecx,[edi].nBase           ;用函数在入口表的索引加上nBase字段的起始序号,就得到要查找导出序号
                        invoke        wsprintf,addr @szBuffer,addr szMsgName,\  ;格式化输出
                                ecx,dword ptr [esi],eax
                        invoke        _AppendInfo,addr @szBuffer
                        popad
                        add        esi,4
                        inc        @dwIndex              
                loop        @B
_Ret:
                assume        esi:nothing
                assume        edi:nothing
                popad
                ret

_ProcessPeFile        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
上面的注释已经讲解的很详细了,如果还有什么不懂,请参考相关资料!
得到PE文件的资源
在书中,笔者用了一个单独的函数来处理资源,并用_ProcessPeFile来调用
先来看看_ProcessPeFile这个函数:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile        proc        _lpFile,_lpPeHead,_dwSize
                local        @szBuffer[1024]:byte,@szSectionName[16]:byte

                pushad
                mov        esi,_lpPeHead
                assume        esi:ptr IMAGE_NT_HEADERS
;********************************************************************
; 检测是否存在资源
;********************************************************************
                mov        eax,[esi].OptionalHeader.DataDirectory[8*2].VirtualAddress    ;根据PE文件头结构定位到资源
                .if        ! eax
                        invoke        MessageBox,hWinMain,addr szErrNoRes,NULL,MB_OK
                        jmp        _Ret
                .endif
                push        eax
                invoke        _RVAToOffset,_lpFile,eax
                add        eax,_lpFile
                mov        esi,eax
                pop        eax
                invoke        _GetRVASection,_lpFile,eax             ;得到资源所处的区块名称
                invoke        wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax
                invoke        SetWindowText,hWinEdit,addr @szBuffer
                invoke        _ProcessRes,_lpFile,esi,esi,1          ;调用处理资源的函数
_Ret:
                assume        esi:nothing
                popad
                ret

_ProcessPeFile        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
这个函数很简音,没什么好说的,只是在函数中调用了一个处理资源的函数_ProcessRes,我们下面来讲解一下这个函数。
_ProcessRes函数如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessRes        proc        _lpFile,_lpRes,_lpResDir,_dwLevel   ;传入四个参数,文件,资源,资源目录,目录的层次
                local        @dwNextLevel,@szBuffer[1024]:byte
                local        @szResName[256]:byte

                pushad
                mov        eax,_dwLevel      ;传一个数值过来,为1,表示前当所处的目录的层次为1
                inc        eax               ;将值自加1
                mov        @dwNextLevel,eax  ;将自加1的值存储在局部变量@dwNextLevel中,作为递归调用时的_dwLevel参数使用

;********************************************************************
; 检查资源目录表,得到资源目录项的数量
;********************************************************************
                mov        esi,_lpResDir
                assume        esi:ptr IMAGE_RESOURCE_DIRECTORY
                mov        cx,[esi].NumberOfNamedEntries        ;得到以名称命名的入口数量
                add        cx,[esi].NumberOfIdEntries           ;以ID命名的入口数量加上以名称命名的入口数量,得到本目录的目录项总和
                movzx        ecx,cx
                add        esi,sizeof IMAGE_RESOURCE_DIRECTORY  ;定义资源结构
                assume        esi:ptr IMAGE_RESOURCE_DIRECTORY_ENTRY
;********************************************************************
; 循环处理每个资源目录项
;********************************************************************
                .while        ecx >        0                           ;以目录项总和作为循环条件
                        push        ecx
                        mov        ebx,[esi].OffsetToData      ;得到目录项指针
                        .if        ebx & 80000000h             ;OffsetToData字段的位31是否为1
                                and        ebx,7fffffffh       ;取出低位,低位数据指向下一层目录块的起始地址
                                add        ebx,_lpRes          ;指向下一层资源,作为递归函数的参数
                                .if        _dwLevel == 1
;********************************************************************
; 第一层:资源类型
;********************************************************************
                                        mov        eax,[esi].Name1      ;取得资源目录的名称或者ID
                                        .if        eax & 80000000h      ;判断Name1的最高位是为1,还是为0
                                                and        eax,7fffffffh ;如果最高位为1,则低位数据当指针使用,指向下面的结构
                                                add        eax,_lpRes
                                                movzx        ecx,word ptr [eax]        ;IMAGE_RESOURCE_DIR_STRING_U结构
                                                add        eax,2
                                                mov        edx,eax
                                                invoke        WideCharToMultiByte,CP_ACP,WC_COMPOSITECHECK,\  ;字符串转换
                                                        edx,ecx,addr @szResName,sizeof @szResName,\
                                                        NULL,NULL
                                                lea        eax,@szResName
                                        .else
                                                .if        eax <=        10h    ;如果最高位为0,则表示字段的值作为ID使用
                                                        dec        eax    ;如果ID在1到16之间,表示是系统预定义的类型
                                                        mov        ecx,sizeof szType
                                                        mul        ecx
                                                        add        eax,offset szType   ;得到资源的类型
                                                .else
                                                        invoke        wsprintf,addr @szResName,addr szLevel1byID,eax
                                                        lea        eax,@szResName
                                                .endif
                                        .endif
                                        invoke        wsprintf,addr @szBuffer,addr szLevel1,eax
;********************************************************************
; 第二层:资源ID(或名称)
;********************************************************************
                                .elseif        _dwLevel == 2              ;当资源在第二层时
                                        mov        edx,[esi].Name1
                                        .if        edx & 80000000h
;********************************************************************
; 资源以字符串方式命名
;********************************************************************
                                                and        edx,7fffffffh
                                                add        edx,_lpRes        ;IMAGE_RESOURCE_DIR_STRING_U结构
                                                movzx        ecx,word ptr [edx]
                                                add        edx,2
                                                invoke        WideCharToMultiByte,CP_ACP,WC_COMPOSITECHECK,\
                                                        edx,ecx,addr @szResName,sizeof @szResName,\
                                                        NULL,NULL
                                                invoke        wsprintf,addr @szBuffer,\
                                                        addr szLevel2byName,addr @szResName
                                        .else
;********************************************************************
; 资源以 ID 命名
;********************************************************************
                                                invoke        wsprintf,addr @szBuffer,\
                                                        addr szLevel2byID,edx
                                        .endif
                                .else
                                        .break
                                .endif
                                invoke        _AppendInfo,addr @szBuffer
                                invoke        _ProcessRes,_lpFile,_lpRes,ebx,@dwNextLevel     ;递归处理资源
;********************************************************************
; 不是资源目录则显示资源详细信息
;********************************************************************
                        .else
                                add        ebx,_lpRes
                                mov        ecx,[esi].Name1                ;代码页
                                assume        ebx:ptr IMAGE_RESOURCE_DATA_ENTRY
                                mov        eax,[ebx].OffsetToData               ;得到资源的RVA
                                invoke        _RVAToOffset,_lpFile,eax
                                invoke        wsprintf,addr @szBuffer,addr szResData,\
                                        eax,ecx,[ebx].Size1                  ;得到资源的大小
                                invoke        _AppendInfo,addr @szBuffer
                        .endif
                        add        esi,sizeof IMAGE_RESOURCE_DIRECTORY_ENTRY    ;指向下一层资源项
                        pop        ecx
                        dec        ecx                                          ;目录下减一
                .endw
_Ret:
                assume        esi:nothing
                assume        ebx:nothing
                popad
                ret

_ProcessRes        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

上面的算法,可以表示为如下所示:
if OffsetToData字段的位31=1

       (表明OffsetToData字段指向的是下一层的目录块)

        .if 当前是第1层

              (表明Name1字段代表的是资源类型)

               .if Name1字段的位31=1

                    Name1指向的是一个UNICODE字符串

               .else

                     Name1中包含的是资源类型ID

                .endif

          .elseif 当前是第2层

                (表明Name1字段代表的是资源名称)

                 .if Name1字段的位31=1

                      Name1指向的是一个UNICODE字符串

                  .else

                      Name1中包含的是资源名称ID

                  .endif

          .endif

          将层次加1继续递归处理OffsetToData所指的下一层目录块

.else

  (表明OffsetToData字段指向的是IMAGE_RESOURCE_DATA_ENTRY结构)

  (表明Name1字段代表的是资源的代码页)

  IMAGE_RESOURCE_DATA_ENTRY结构地址=OffsetToData字段

  资源RVA=IMAGE_RESOURCE_DATA_ENTRY.OffsetToData

  资源大小=IMAGE_RESOURCE_DATA_ENTRY.Size1

.endif
上面的这个算法,很清楚的明达了函数所进行的操作,不得不说算法是程序的灵魂!!

这里说明一点,代码在每次处理一个目录项或者资源数据的时候,都将它们的名称或ID等信息显示出来。如果例子中的代码被移植到了其他地方用来寻找资源的话,这些显示信息的语句就可以全部去掉了,因为这时程序的最终目的就是最后两句获取资源RVA和大小的指令。

得到PE文件的重定位表
重定位所使用的一个_ProcessPeFile来处理,我们来看看这个函数是怎么样实现的:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile        proc        _lpFile,_lpPeHead,_dwSize                  ;传入三个参数,文件,PE文件头,大小
                local        @szBuffer[1024]:byte,@szSectionName[16]:byte

                pushad
                mov        esi,_lpPeHead
                assume        esi:ptr IMAGE_NT_HEADERS                   ;定义PE文件头
;********************************************************************
; 根据 IMAGE_DIRECTORY_ENTRY_BASERELOC 目录表找到重定位表位置
;********************************************************************
                mov        eax,[esi].OptionalHeader.DataDirectory[8*5].VirtualAddress   ;根据PE文件头定位到重定位表
                .if        ! eax
                        invoke        MessageBox,hWinMain,addr szErrNoReloc,NULL,MB_OK
                        jmp        _Ret
                .endif
                push        eax
                invoke        _RVAToOffset,_lpFile,eax                           ;将重定位表的RVA转换为File Offset
                add        eax,_lpFile
                mov        esi,eax
                pop        eax
                invoke        _GetRVASection,_lpFile,eax                      ;得到重定位表所处的区块名
                invoke        wsprintf,addr @szBuffer,addr szMsg,addr szFileName,eax
                invoke        SetWindowText,hWinEdit,addr @szBuffer
                assume        esi:ptr IMAGE_BASE_RELOCATION
;********************************************************************
; 循环处理每个重定位块
;********************************************************************
                .while        [esi].VirtualAddress                            ;以重定位表在内存中的起始RVA为循环条件
                        cld                                             ;设置方向标志DF
                        lodsd                        ;eax = [esi].VirtualAddress   重定位内存页的起始RVA
                        mov        ebx,eax
                        lodsd                        ;eax = [esi].SizeOfBlock   重定位块的长度
                        sub        eax,sizeof IMAGE_BASE_RELOCATION   
                        shr        eax,1           ;重定位项的数量n就等于(SizeOfBlock-sizeof IMAGE_BASE_RELOCATION)/2
                        push        eax                ;eax = 重定位项数量
                        invoke        wsprintf,addr @szBuffer,addr szMsgRelocBlk,ebx,eax
                        invoke        _AppendInfo,addr @szBuffer
                        pop        ecx
                        xor        edi,edi
                        .repeat
                                push        ecx
                                lodsw
                                mov        cx,ax
                                and        cx,0f000h         ;取重定位项的高四位得到重定位项的类型
;********************************************************************
; 仅处理 IMAGE_REL_BASED_HIGHLOW 类型的重定位项
;********************************************************************
                                .if        cx ==        03000h
                                        and        ax,0fffh  ;取重定位项的低十二位得到重定位项的地址
                                        movzx        eax,ax
                                        add        eax,ebx   ;低十二位加上前面得到的重定位内存页的RVA(虚拟地址)
                                .else
                                        mov        eax,-1
                                .endif
                                invoke        wsprintf,addr @szBuffer,addr szMsgReloc,eax    ;格式化输出
                                inc        edi             ;EDI自增1
                                .if        edi ==        4        ;每显示4个项目换行
                                        invoke        lstrcat,addr @szBuffer,addr szCrLf
                                        xor        edi,edi
                                .endif
                                invoke        _AppendInfo,addr @szBuffer
                                pop        ecx
                        .untilcxz
                        .if        edi
                                invoke        _AppendInfo,addr szCrLf
                        .endif
                .endw
_Ret:
                assume        esi:nothing
                popad
                ret

_ProcessPeFile        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
参考资料《Win32汇编语言程序设计》老罗的第二版!
上面的注释已经讲的很清楚了,我就不重复了,如果大家真的把前面的几篇文章弄懂了,我想看上面的代码并不困难,就是将前面几章的内容进行编程实现,然后按规定的格式显示出来了,PE文件的复习就基本上就完了,在接下来的两天里,我会讲解几个PE文件的编程的实际应用的例子与代码,希望对大家理解PE文件有所帮助!!
有点累了,先睡了,鼻子很不舒服,可能是感冒了,不断流鼻涕,不过还是写完了!
上传的附件:
熊猫正正 9 2010-10-5 23:53
52
0
我倒,本来我上面的图片很清楚的,不知道为什么传上去了,这么模糊!!算了,还是在下面帖出这两个函数的源代码吧!!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 将 RVA 转换成实际的数据位置
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_RVAToOffset        proc        _lpFileHead,_dwRVA                  ;这个函数带两个参数,一个是映射头,还有一个就是RVA的值
                local        @dwReturn

                pushad
                mov        esi,_lpFileHead
                assume        esi:ptr IMAGE_DOS_HEADER         
                add        esi,[esi].e_lfanew                  
                assume        esi:ptr IMAGE_NT_HEADERS
                mov        edi,_dwRVA
                mov        edx,esi
                add        edx,sizeof IMAGE_NT_HEADERS              ;PE文件头加上PE文件头的大小,得到区块的首地址
                assume        edx:ptr IMAGE_SECTION_HEADER             ;定义EDX为区块
                movzx        ecx,[esi].FileHeader.NumberOfSections    ;以PE文件的区块个数作为循环条件,进行循环
;********************************************************************
; 扫描每个节区并判断 RVA 是否位于这个节区内
;********************************************************************
                .repeat                              
                        mov        eax,[edx].VirtualAddress         ;VirtualAddress是区块的RVA地址,即表示这个区块的开头位置
                        add        eax,[edx].SizeOfRawData                 ;SizeofRawData是在文件对齐后的尺寸,即表示这个区块的结尾位置
                        .if        (edi >= [edx].VirtualAddress) && (edi < eax)
                                mov        eax,[edx].VirtualAddress ;VirtualAddress是区块的RVA地址,即表示这个区块的开头位置
                                sub        edi,eax                         ;EDI中存放的是这个RVA在内存中相对于起始RVA的偏移量
                                mov        eax,[edx].PointerToRawData ;PointerToRawData表示文件中的偏移起始地址
                                add        eax,edi                         ;EAX存放的是RVA转换成的File Offset的值
                                jmp        @F                       ;结束循环
                        .endif
                        add        edx,sizeof IMAGE_SECTION_HEADER  ;EDX指向下一个区块
                .untilcxz                                
                assume        edx:nothing                              
                assume        esi:nothing
                mov        eax,-1                                   ;如果不在区块之类,则返回-1
@@:
                mov        @dwReturn,eax                            ;将返回值传到@dwReturn中保存
                popad
                mov        eax,@dwReturn
                ret

_RVAToOffset        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 查找 RVA 所在的节区
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetRVASection        proc        _lpFileHead,_dwRVA          ;这个函数带两个参数,一个是映射文件头,一个是RVA的值
                local        @dwReturn

                pushad
                mov        esi,_lpFileHead               
                assume        esi:ptr IMAGE_DOS_HEADER
                add        esi,[esi].e_lfanew
                assume        esi:ptr IMAGE_NT_HEADERS
                mov        edi,_dwRVA
                mov        edx,esi
                add        edx,sizeof IMAGE_NT_HEADERS              ;PE文件头加上PE文件头的大小,得到区块的首地址
                assume        edx:ptr IMAGE_SECTION_HEADER             ;定义EDX为区块
                movzx        ecx,[esi].FileHeader.NumberOfSections    ;以PE文件的区块个数作为循环条件,进行循环
;********************************************************************
; 扫描每个节区并判断 RVA 是否位于这个节区内
;********************************************************************
                .repeat
                        mov        eax,[edx].VirtualAddress         ;VirtualAddress是区块的RVA地址,即表示这个区块的开头位置
                        add        eax,[edx].SizeOfRawData                 ;SizeofRawData是在文件对齐后的尺寸,即表示这个区块的结尾位置
                        .if        (edi >= [edx].VirtualAddress) && (edi < eax)
                                mov        eax,edx                         ;EAX存储的区块的名字
                                jmp        @F
                        .endif
                        add        edx,sizeof IMAGE_SECTION_HEADER  ;指向下一个区块
                .untilcxz
                assume        edx:nothing
                assume        esi:nothing
                mov        eax,offset szNotFound
@@:
                mov        @dwReturn,eax                            ;将区块的名字传给@dwReturn
                popad
                mov        eax,@dwReturn
                ret

_GetRVASection        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
kagayaki 2010-10-6 07:06
53
0
不错.............
bestshow 4 2010-10-6 08:49
54
0
顶~~学习了~~
xygwf 2010-10-6 09:46
55
0
我一上来就想找关于加壳软件是如何使还原后的程序的导入/导出表作用的, 动态更改目录表还是?

您辛苦了, 感谢!!
赤目狂人 2010-10-6 10:26
56
0
建议楼主将pe总复习的一系列的帖子的链接都放到这个帖子里来,以后谁看完了这个还想看的话就可以直接点击链接就可以了!
salwtp 2010-10-6 10:27
57
0
非常感谢楼主哦 很实用的讲解材料 顶一下~~
bjjgq 2010-10-6 14:54
58
0
图片怎么都看不见呢
wuoo 2010-10-6 15:03
59
0
不错,不错,收藏了
要学会编 2010-10-6 15:57
60
0
学习。期待最后两篇
熊猫正正 9 2010-10-6 20:26
61
0
前面有一个朋友给我留言说要搞个链接,好的,等我把第七篇文章写完之后,会在最后一篇文章的最后把前面的六篇文章的地址都链接上去,也样第一方便了大家,也同时方便了自己阅读学习,给人方便,就是给自己方便!!
参考文献《Win32汇编语言程序设计》第二版

前面的五篇文章,所讲的都是对PE文件的结构进行了分析,以及对PE工具的编写进行了介绍,但在实际的应用中还未有涉及到很多方面的内容,其实学好PE在很多方面都有很重要的应用,比如文件的加密,压缩,编写病毒等都涉及到修改及重组PE文件,另外,像API HOOK,PE文件的内存映像DUMP等应用则涉及分析内存中的PE映像。在接下来的两篇文章中我将用几个例子来进一步说明这方面的应用,可以说这是编写病毒的基础吧!!

首先讲在病毒中用到的一种很常用的技术,就是如何从内存中动态获取某个API的地址。

动态获取API的入口地址
在Win32环境下编程,不便用API几乎是不可能的事情,一般情况下,在代码中使用API不外乎两种办法:第一是编译链接的时候使用导入库,那么生成的PE文件中就会包含导入表,这样程序执行时会由Windows装载器根据导入表中的信息来修正API调用语句中的地址;第二种方法是使用LoadLibrary函数动态装入某个DLL模块,并使用GetProcAddress函数从被装载入的模块中获取API函数的地址。

先讲解一下原理吧!
在DOS环境下,一个可执行文件既可以用INT 21h/4ch来结束程序,也可以用一个Ret指令来结束程序,实际上,在Win32下也可以用这种方法来结束程序,虽然大部分的Win32程序都使用ExitProcess函数来终止执行,但是使用Ret指令确实也是有效的。

如下图所示,当父进程要创建一个子进程的时候,它会调用Kernel32.dll中的CreateProcess函数,CreateProcess函数在完成装载应用程序后,会将一个返回地址压入堆栈并转而执行应用程序,如果应用程序用ExitProcess函数来终止,那么这个返回地址没有什么用途,但如果应用程序使用Ret指令的话,程序就会返回CreateProcess函数设定的地址。也就是说,应用程序的主程序可以看作是被Windows调用的一个子程序。

图中表示Win32可执行文件退出的示意图

那么Ret指令返回到的地址上究竟有什么指令呢?用Soft-ICE看看就会发现,它包含一句push eax指令和一句call ExitThread,也就是说,假如用Ret指令返回的话,Windows会替程序去调用ExitThread函数,如果这是进程的最后一个线程的话,ExitThread函数又会自动去调用ExitProcess,这样程序就会被终止执行。

从这个过程可以得到一个很重要的数据,那就是堆栈中的返回地址,这个地址只要在程序入口的地方用[esp]就可以将它读出,说它重要是因为它位于Kernel32.dll模块中,而LoadLibrary和GetProcAddress函数正是处于Kernel32.dll模块中,换句话说就是,我们得到的地址和这两个函数近在咫尺,完全可以从这个地址经过某种算法来找到这两个函数的入口地址,得到这两个函数的入口地址以后,什么问题都解决了。

结合本章前面内容中提到过的两个事实,可以确定这种想法是可行的。

首先,PE文件被装入内存后(包括Kernel32.dll文件),除了一些可丢弃的节如重定位节以外,其他的内容都会被装入内存,这样获取导出函数地址所需的PE文件头、导出表等数据都存在于内存中;第二,PE文件被装入内存时是按内存页对齐的,只要从Ret指令返回的地址按照页对齐的边界一页页地向低地址搜寻,就必然可以找到Kernel32.dll文件的文件头位置。
下面请看具体的源代码,我会在其中作出注释,以方便大家理解!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; NoImport.asm
; 以从内存中动态获取的办法使用 API
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                .386
                .model flat,stdcall
                option casemap:none
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include                windows.inc

_ProtoGetProcAddress        typedef        proto        :dword,:dword
_ProtoLoadLibrary        typedef        proto        :dword
_ProtoMessageBox        typedef        proto        :dword,:dword,:dword,:dword
_ApiGetProcAddress        typedef        ptr        _ProtoGetProcAddress
_ApiLoadLibrary                typedef        ptr        _ProtoLoadLibrary
_ApiMessageBox                typedef        ptr        _ProtoMessageBox
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 数据段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                .data?
hDllKernel32        dd        ?
hDllUser32        dd        ?
_GetProcAddress        _ApiGetProcAddress        ?
_LoadLibrary        _ApiLoadLibrary                ?
_MessageBox        _ApiMessageBox                ?

                .const
szLoadLibrary        db        'LoadLibraryA',0
szGetProcAddress db        'GetProcAddress',0
szUser32        db        'user32',0
szMessageBox        db        'MessageBoxA',0

szCaption        db        'A MessageBox !',0
szText                db        'Hello, World !',0
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 代码段
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                .code
include                _GetKernel.asm                                             ;包含获取Kernel32.dll的基址的源代码
start:
;********************************************************************
; 从堆栈中的 Ret 地址转换 Kernel32.dll 的基址,并在 Kernel32.dll
; 的导出表中查找 GetProcAddress 函数的入口地址
;********************************************************************
                invoke        _GetKernelBase,[esp]                               ;获取Kernel32.dll的基址
                .if        eax
                        mov        hDllKernel32,eax                           ;保存Kernel32.dll的基址
                        invoke        _GetApi,hDllKernel32,addr szGetProcAddress ;在Kernel32.dll的导出表中查找GetProcAddress函数的入口地址
                        mov        _GetProcAddress,eax                        ;保存GetProcAddress函数的入口地址
                .endif
;********************************************************************
; 用得到的 GetProcAddress 函数得到 LoadLibrary 函数地址并装入其他 Dll
;********************************************************************
                .if        _GetProcAddress                                            ;如果LoadLibrary函数地址不为NULL   
                        invoke        _GetProcAddress,hDllKernel32,addr szLoadLibrary    ;通过GetProcAddress得到LoadLibrary函数的地址
                        mov        _LoadLibrary,eax                                   ;保存LoadLibrary函数的地址
                        .if        eax
                                invoke        _LoadLibrary,addr szUser32                 ;使用LoadLibrary函数装载User32.dll
                                mov        hDllUser32,eax                             ;保存User32.dll
                                invoke        _GetProcAddress,hDllUser32,addr szMessageBox   ;获取User32.dll中MessageBox函数的地址
                                mov        _MessageBox,eax                                ;保存MessageBox函数的地址
                        .endif
                .endif
;********************************************************************
                .if        _MessageBox                                                ;如果MessageBox函数的地址不为NULL
                        invoke        _MessageBox,NULL,offset szText,offset szCaption,MB_OK
                .endif
                ret
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                end        start
上面这个函数很简单也没什么好说的,我已经注释的很详细了,这里就不多说了,我们来看看它所包含的一个源文件_GetKernel.asm是怎么样获取Kernel32.dll基址的
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 公用模块:_GetKernel.asm
; 根据程序被调用的时候堆栈中有个用于 Ret 的地址指向 Kernel32.dll
; 而从内存中扫描并获取 Kernel32.dll 的基址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 错误 Handler
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_SEHHandler        proc        C _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatcherContext    ;这里我不讲了前面已经说过SEH异常处理

                pushad
                mov        esi,_lpExceptionRecord
                mov        edi,_lpContext
                assume        esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT
                mov        eax,_lpSEH
                push        [eax + 0ch]
                pop        [edi].regEbp
                push        [eax + 8]
                pop        [edi].regEip
                push        eax
                pop        [edi].regEsp
                assume        esi:nothing,edi:nothing
                popad
                mov        eax,ExceptionContinueExecution
                ret

_SEHHandler        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 在内存中扫描 Kernel32.dll 的基址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetKernelBase        proc        _dwKernelRet                                  ;传入一个参数         
                local        @dwReturn

                pushad
                mov        @dwReturn,0
;********************************************************************
; 重定位
;********************************************************************
                call        @F                                           ;这里我就不说了,在病毒中经常用到,很多病毒开始就这经典的四句
                @@:                                                  ;主要就于重定位
                pop        ebx
                sub        ebx,offset @B
;********************************************************************
; 创建用于错误处理的 SEH 结构
;********************************************************************
                assume        fs:nothing
                push        ebp
                lea        eax,[ebx + offset _PageError]
                push        eax
                lea        eax,[ebx + offset _SEHHandler]
                push        eax
                push        fs:[0]
                mov        fs:[0],esp
;********************************************************************
; 查找 Kernel32.dll 的基地址
;********************************************************************
                mov        edi,_dwKernelRet                             ;将参数中传递过来的目标地址按64K对齐
                and        edi,0ffff0000h                               ;与0ffff0000h进行AND操作
                .while        TRUE
                        .if        word ptr [edi] == IMAGE_DOS_SIGNATURE     ;在内存中寻找DOS MZ文件头标识和PE文件头标识
                                mov        esi,edi
                                add        esi,[esi+003ch]
                                .if word ptr [esi] == IMAGE_NT_SIGNATURE
                                        mov        @dwReturn,edi
                                        .break
                                .endif
                        .endif
                        _PageError:                                 ;页面异常处理
                        sub        edi,010000h                         ;以一个页面作为间隔
                        .break        .if edi < 070000000h
                .endw
                pop        fs:[0]
                add        esp,0ch
                popad
                mov        eax,@dwReturn
                ret

_GetKernelBase        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 从内存中模块的导出表中获取某个 API 的入口地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_GetApi                proc        _hModule,_lpszApi                           ;传入两个参数,模块名和函数名,用于获取传入的函数的入口地址
                local        @dwReturn,@dwStringLength

                pushad
                mov        @dwReturn,0
;********************************************************************
; 重定位
;********************************************************************
                call        @F
                @@:
                pop        ebx
                sub        ebx,offset @B
;********************************************************************
; 创建用于错误处理的 SEH 结构
;********************************************************************
                assume        fs:nothing
                push        ebp
                lea        eax,[ebx + offset _Error]
                push        eax
                lea        eax,[ebx + offset _SEHHandler]
                push        eax
                push        fs:[0]
                mov        fs:[0],esp
;********************************************************************
; 计算 API 字符串的长度(带尾部的0)
;********************************************************************
                mov        edi,_lpszApi                                 
                mov        ecx,-1
                xor        al,al
                cld                                                 ;设置方向位,使EDI增1
                repnz        scasb                                       ;循环比较EDI中存放的函数名
                mov        ecx,edi
                sub        ecx,_lpszApi                                ;EDI的值减去函数名首地址的值为函数名的长度
                mov        @dwStringLength,ecx                         ;保存函数的长度
;********************************************************************
; 从 PE 文件头的数据目录获取导出表地址
;********************************************************************
                mov        esi,_hModule
                add        esi,[esi + 3ch]
                assume        esi:ptr IMAGE_NT_HEADERS
                mov        esi,[esi].OptionalHeader.DataDirectory.VirtualAddress
                add        esi,_hModule
                assume        esi:ptr IMAGE_EXPORT_DIRECTORY
;********************************************************************
; 查找符合名称的导出函数名
;********************************************************************
                mov        ebx,[esi].AddressOfNames                   ;从AddressOfNames字段指向的函数名称地址表的第一项开始         
                add        ebx,_hModule
                xor        edx,edx
                .repeat                                            
                        push        esi
                        mov        edi,[ebx]
                        add        edi,_hModule
                        mov        esi,_lpszApi
                        mov        ecx,@dwStringLength                ;以函数的长度作为循环,查找相符合的函数名
                        repz        cmpsb                              ;字符串比较,比较EDI与ESI中的字符串
                        .if        ZERO?
                                pop        esi
                                jmp        @F
                        .endif
                        pop        esi
                        add        ebx,4
                        inc        edx
                .until        edx >=        [esi].NumberOfNames
                jmp        _Error
@@:
;********************************************************************
; API名称索引 --> 序号索引 --> 地址索引
;********************************************************************
                sub        ebx,[esi].AddressOfNames                   ;记下这个函数名在字符串地址表中的索引值
                sub        ebx,_hModule
                shr        ebx,1  
                add        ebx,[esi].AddressOfNameOrdinals            ;然后在AddressOfNameOrdinals指向的数组中以同样的索引值取出数组项的值
                add        ebx,_hModule
                movzx        eax,word ptr [ebx]
                shl        eax,2
                add        eax,[esi].AddressOfFunctions               ;以上面得到了值在AddressOfFunctions字段指向的函数入口地址表中获取函数RVA
                add        eax,_hModule
;********************************************************************
; 从地址表得到导出函数地址
;********************************************************************
                mov        eax,[eax]
                add        eax,_hModule
                mov        @dwReturn,eax
_Error:
                pop        fs:[0]
                add        esp,0ch
                assume        esi:nothing
                popad
                mov        eax,@dwReturn
                ret

_GetApi                endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

上面的代码中GetApi有前面讲的得到导出表的代码差不多,大家可以对照的看,我这里也讲了一些简单的注释,如果大家对前面的已经掌握,在来看这段代码,应该不会有什么问题,好了,今天就先讲到了这!!明天是国庆的最后一天,希望大家在这个国庆长假中都能玩好,吃好,学好!
上传的附件:
ddsoft 2010-10-6 20:59
62
0
来份C++的代码吧。。汇编看的不习惯。虽然也基本可以看明白。
暗徒 2010-10-7 01:55
63
0
惨了,好多不懂,看来要复习window编程的书了...
随风而去 2010-10-7 09:35
64
0
LZ辛苦了!!!
随风而去 2010-10-7 09:38
65
0
LZ辛苦了!
随风而去 2010-10-7 09:41
66
0
好东西,我们要支持顶起来哦,呵呵
随风而去 2010-10-7 09:43
67
0
LZ辛苦了!!支持一下!!!
熊猫正正 9 2010-10-7 15:56
68
0
今天是国庆长假的最后一天,对看雪的几位朋友的承诺顿就在今天对现了,这是我做人原格问题,说实话要想对现一个承诺,真要要做很我,付出才会有回报,是这我一直相信的道理,虽然前几天感冒了,但为了承诺还是一边用手巾擦鼻涕,一边写着文章,总算是写完了,不管是好是坏,总算是带着一些朋友把PE的相关知识又重新温习了一遍,技术含量肯定没有玩命大哥的高,不过自己也一直在向牛人们学习,希望有一天,也能向他们那样吧!!
我相信每一个牛人都是这样一步一步走上去的,从什么都不会,到技术达人!!依惜记得高中毕业时我还不知道QQ是什么,毕业那会有很多同学说给我QQ号,我没要,因为我根本不知道那个有什么用,无语!!那时也不会上网,只是听别人说上网像冲浪,其实也没玩过冲浪,所以也不知道冲浪是啥感觉,但几年过去了,不说自己技术有多强,但至少有很多东西,我基本上都掌握了,什么事情都是这样的,只要你想去做,就一定要坚持做下去,不管结局是好是坏,总会有一些意外的收获!相信只要大家努力,总有一天能够和看雪的几位大牛们一样的!!有梦想就一直要大胆去追,这样才活的有意义!!

如果这七天,你都认认真真的所这七篇文章看完了,如果能给你一点小小的收获的话,我倍感兴慰~~~

最后一篇文章,我就作个结尾,讲一个比较简单的应用,相信写过病毒的朋友们一般都会参考过这段代码!!如果还没写过病毒的朋友,一定会觉得很有意思,然后会爱上这个,我以前就是这样的,觉得好好玩的!!!如果不学PE的话,这些东西都很少提到,一般的编程书上也很少讲这方面的知识!!

参考文献《Win32汇编语言程序设计》第二版
在PE文件上添加可执行代码!!!!
本章所涉及到的实例将演示在PE文件上添加一段可执行代码,并且让这段代码在原来的代码之前被执行,经过修改的目标PE文件被运行后,将首先弹出一个带"YES"和"NO"的消息框并提示“一定要运行这个程序吗?”,如果用户选择"YES"的话,原文件被运行,否则程序直接退出。

先讲解一下原理吧!
根据前面对PE文件各个部分进行的分析,可以得到在PE文件中添加代码需要以下几个步骤:
将添加的代码写到目标PE文件中,这段代码既可以插入原代码所处的节的空隙中(由于每个节保存在文件中时是按照FileAlignment的值对齐的,所以节的最后必然会有一些空余的空间),也可以通过添加一个新的节对附在原文件的尾部。
PE文件原来的入口指针必须被保存在添加的代码中,这样,这段代码执行完以后可以转移到原始文件处执行。
PE文件原来的入口指针需要被修改,指向新添加代码的入口地址。
PE文件头中的一些值需要根据情况做相应的修正,以符合修改后PE文件的情况。
另外有一些操作是应该避免的,因为它们是无法实现的,或者实现它们的复杂性远远超过它们带来的好处,这些操作是:
如果节的空隙不足以插入代码的话,应该在文件尾新建一个节而不是去扩大原来的代码节并将它后面的其他节后移,因为程序无法得知整个PE文件中有多少个RVA值会指向这些被移动位置的节,修正所有这些RVA值几乎是不可能的。
如果附加的代码中要用到API函数的话,不要尝试在原始目标文件的导入表添加导入函数名称,因为这样将涉及在目标PE文件的导入表中插入新的模块名和函数名,其结果同样是造成导入表的一些项目被移动位置,修正指向这些项目的RVA同样是很难实现的。
_ProcessPeFile这个源代码中包含了一个_AddCode.asm的源文件,先来看看_AddCode.asm为我们做了些什么?
代码如下:
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 要被添加到目标文件后面的执行代码
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;
;
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 一些函数的原形定义
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProtoGetProcAddress        typedef        proto        :dword,:dword
_ProtoLoadLibrary        typedef        proto        :dword
_ProtoMessageBox        typedef        proto        :dword,:dword,:dword,:dword
_ApiGetProcAddress        typedef        ptr        _ProtoGetProcAddress
_ApiLoadLibrary                typedef        ptr        _ProtoLoadLibrary
_ApiMessageBox                typedef        ptr        _ProtoMessageBox
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
;
;
APPEND_CODE        equ        this byte                            ;定义这段代码的开头位置
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 被添加到目标文件中的代码从这里开始
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
include                _GetKernel.asm                               ;这个源程序我在前面已经讲过,这时太不重提了,不清楚的看以前的讲解吧!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
hDllKernel32        dd        ?
hDllUser32        dd        ?
_GetProcAddress        _ApiGetProcAddress        ?
_LoadLibrary        _ApiLoadLibrary                ?
_MessageBox        _ApiMessageBox                ?
szLoadLibrary        db        'LoadLibraryA',0
szGetProcAddress db        'GetProcAddress',0
szUser32        db        'user32',0
szMessageBox        db        'MessageBoxA',0
szCaption        db        '问题提示',0
szText                db        '你一定要运行这个程序吗?',0
;********************************************************************
; 新的入口地址
;********************************************************************
_NewEntry:
;********************************************************************
; 重定位并获取一些 API 的入口地址
;********************************************************************
                call        @F
                @@:
                pop        ebx
                sub        ebx,offset @B
;********************************************************************
                invoke        _GetKernelBase,[esp]        ;获取Kernel32.dll基址
                .if        ! eax
                        jmp        _ToOldEntry
                .endif
                mov        [ebx+hDllKernel32],eax        ;获取GetProcAddress入口
                lea        eax,[ebx+szGetProcAddress]
                invoke        _GetApi,[ebx+hDllKernel32],eax
                .if        ! eax
                        jmp        _ToOldEntry
                .endif
                mov        [ebx+_GetProcAddress],eax
;********************************************************************
                lea        eax,[ebx+szLoadLibrary]        ;获取LoadLibrary入口
                invoke        [ebx+_GetProcAddress],[ebx+hDllKernel32],eax
                mov        [ebx+_LoadLibrary],eax
                lea        eax,[ebx+szUser32]        ;获取User32.dll基址
                invoke        [ebx+_LoadLibrary],eax
                mov        [ebx+hDllUser32],eax
                lea        eax,[ebx+szMessageBox]        ;获取MessageBox入口
                invoke        [ebx+_GetProcAddress],[ebx+hDllUser32],eax
                mov        [ebx+_MessageBox],eax
;********************************************************************
                lea        ecx,[ebx+szText]
                lea        eax,[ebx+szCaption]
                invoke        [ebx+_MessageBox],NULL,ecx,eax,MB_YESNO or MB_ICONQUESTION     
                .if        eax !=        IDYES                                 ;如果选择的不是YES,则直接返回,不会返回到原入口点处
                        ret
                .endif
;********************************************************************
; 执行原来的文件                                                      ;这里主要是为了从当前入口点返回到原入口点
;********************************************************************
_ToOldEntry:
                db        0e9h        ;0e9h是jmp xxxxxxxx的机器码
_dwOldEntry:
                dd        ?        ;用来填入原来的入口地址               ;这里用来填入原程序的入口地址
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
APPEND_CODE_END        equ        this byte                             ;定义这段代码的结束

_AddCode.asm中包含要被添加到其他可执行文件中的代码。这段代码是按照能够自身重定位的方式写的,而且必须按照这种格式书写,因为当它被添加到目标PE文件后,对于不同的PE文件所处的位置肯定是不同的,不进行重定位处理必然无法正常运行。
这段附加代码实现的功能和前一篇文章中NoImport例子大致相同,也是首先使用_GetKernel..asm中提供的两个函数来获取Kernel32.dll模块的基址和GetProcAddress函数的入口地址,并由此最后得到MessageBox函数的入口地址以便显示消息框。
在这段程序的最后_ToOldEntry标号处的数据是0e9h是JMP XXXXXXXX的机器码的第一个字节,它与下面的_dwOldEntry标号处的双字一起组成整个JMP指令,这条JMP指令将在主程序中根据具体情况修正。

现在我们来重点不看看_ProcessPeFile这个源程序的代码吧!!
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; Sample code for < Win32ASM Programming 2nd Edition>
; by 罗云彬
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; AddCode 例子的功能模块
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
                .const

szErrCreate        db        '创建文件错误!',0dh,0ah,0
szErrNoRoom        db        '程序中没有多余的空间可供加入代码!',0dh,0ah,0
szMySection        db        '.adata',0
szExt                db        '_new.exe',0
szSuccess        db        '在文件后附加代码成功,新文件:',0dh,0ah
                db        '%s',0dh,0ah,0

                .code

include                _AddCode.asm

;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
; 计算按照指定值对齐后的数值
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_Align                proc        _dwSize,_dwAlign

                push        edx
                mov        eax,_dwSize
                xor        edx,edx
                div        _dwAlign
                .if        edx
                        inc        eax
                .endif
                mul        _dwAlign
                pop        edx
                ret

_Align                endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
_ProcessPeFile        proc        _lpFile,_lpPeHead,_dwSize
                local        @szNewFile[MAX_PATH]:byte
                local        @hFile,@dwTemp,@dwEntry,@lpMemory
                local        @dwAddCodeBase,@dwAddCodeFile
                local        @szBuffer[256]:byte

                pushad
;********************************************************************
; (Part 1)准备工作:1-建立新文件,2-打开文件
;********************************************************************
                invoke        lstrcpy,addr @szNewFile,addr szFileName
                invoke        lstrlen,addr @szNewFile
                lea        ecx,@szNewFile
                mov        byte ptr [ecx+eax-4],0
                invoke        lstrcat,addr @szNewFile,addr szExt
                invoke        CopyFile,addr szFileName,addr @szNewFile,FALSE     ;从原始PE文件拷贝一个名为“原始文件名_new.exe”的文件
                                                                           ;这个文件将被添加上可执行代码,原来的文件不会被改动
                invoke        CreateFile,addr @szNewFile,GENERIC_READ or GENERIC_WRITE,FILE_SHARE_READ or \
                        FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_ARCHIVE,NULL
                .if        eax ==        INVALID_HANDLE_VALUE                       ;文件拷贝成功后,打开拷贝生成的新文件,以便进行修改
                        invoke        SetWindowText,hWinEdit,addr szErrCreate
                        jmp        _Ret
                .endif
                mov        @hFile,eax
;********************************************************************
;(Part 2)进行一些准备工作和检测工作
; esi --> 原PeHead,edi --> 新的PeHead
; edx --> 最后一个节表,ebx --> 新加的节表
;********************************************************************
                mov        esi,_lpPeHead
                assume        esi:ptr IMAGE_NT_HEADERS,edi:ptr IMAGE_NT_HEADERS
                invoke        GlobalAlloc,GPTR,[esi].OptionalHeader.SizeOfHeaders ;分配一个等于目标PE文件的文件头大小的内存块
                mov        @lpMemory,eax
                mov        edi,eax
                invoke        RtlMoveMemory,edi,_lpFile,[esi].OptionalHeader.SizeOfHeaders   ;并使用RltMoveMemory将PE文件头拷贝到这个内存中
                add        edi,esi
                sub        edi,_lpFile
                movzx        eax,[esi].FileHeader.NumberOfSections           ;得到节表的数量
                dec        eax
                mov        ecx,sizeof IMAGE_SECTION_HEADER                 ;节表的长度
                mul        ecx                                             ;节表的数量*节表的长度

                mov        edx,edi
                add        edx,eax
                add        edx,sizeof IMAGE_NT_HEADERS                    ;节表尾部的指针
                mov        ebx,edx
                add        ebx,sizeof IMAGE_SECTION_HEADER                ;节表的最后一项的指针
                assume        ebx:ptr IMAGE_SECTION_HEADER,edx:ptr IMAGE_SECTION_HEADER
;********************************************************************
; (Part 2.1)检查是否有空闲的位置可供插入节表
;********************************************************************
                pushad
                mov        edi,ebx
                xor        eax,eax                                        ;将EAX值设为0
                mov        ecx,IMAGE_SECTION_HEADER
                repz        scasb                                          ;看节表中是否存在一个全零的位置,进行扫描
                popad
                .if        ! ZERO?
;********************************************************************
; (Part 3.1)如果没有新的节表空间的话,则查看现存代码节的最后
; 是否存在足够的全零空间,如果存在则在此处加入代码
;********************************************************************
                        xor        eax,eax
                        mov        ebx,edi
                        add        ebx,sizeof IMAGE_NT_HEADERS
                        .while        ax <=        [esi].FileHeader.NumberOfSections  ;扫描现存的节
                                mov        ecx,[ebx].SizeOfRawData            ;节在磁盘文件中对齐后的大小
                                .if        ecx && ([ebx].Characteristics & IMAGE_SCN_MEM_EXECUTE)  ;节的属性
                                        sub        ecx,[ebx].Misc.VirtualSize ;节在磁盘中对齐后的大小减去节的实际大上,得到节空隙大小
                                        .if        ecx > offset APPEND_CODE_END-offset APPEND_CODE   ;看现存的节空隙的大小是否大于加入的代码长度
                                                or        [ebx].Characteristics,IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
                                                add        [ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
                                                jmp        @F                 ;如果存在一个节的空隙大于加入到的代码长度,则跳到加入代码处
                                        .endif
                                .endif
                                add        ebx,IMAGE_SECTION_HEADER           ;指向下一个节
                                inc        ax
                        .endw
                        invoke        CloseHandle,@hFile
                        invoke        DeleteFile,addr @szNewFile                    
                        invoke        SetWindowText,hWinEdit,addr szErrNoRoom
                        jmp        _Ret
                        @@:
;********************************************************************
; 将新增代码加入代码节的空隙中
;********************************************************************
                        mov        eax,[ebx].VirtualAddress                  ;节装载到内存中的偏移地址
                        add        eax,[ebx].Misc.VirtualSize                ;加上节的实际大小
                        mov        @dwAddCodeBase,eax                        ;新添代码在内存中的位置
                        mov        eax,[ebx].PointerToRawData                ;在文件中的偏移
                        add        eax,[ebx].Misc.VirtualSize                ;加上节的实际大小
                        mov        @dwAddCodeFile,eax                        ;新添加代码在文件中的位置            
                        invoke        SetFilePointer,@hFile,@dwAddCodeFile,NULL,FILE_BEGIN        ;定位到加入代码地址处
                        mov        ecx,offset APPEND_CODE_END-offset APPEND_CODE
                        invoke        WriteFile,@hFile,offset APPEND_CODE,ecx,addr @dwTemp,NULL   ;将需加入的代码写入空隙处
                .else
;********************************************************************
; (Part 3.2)如果有新的节表空间的话,加入一个新的节
;********************************************************************
                        inc        [edi].FileHeader.NumberOfSections                  ;增加一个新的节,节的数量加1
                        mov        eax,[edx].PointerToRawData                  
                        add        eax,[edx].SizeOfRawData
                        mov        [ebx].PointerToRawData,eax
                        mov        ecx,offset APPEND_CODE_END-offset APPEND_CODE
                        invoke        _Align,ecx,[esi].OptionalHeader.FileAlignment      ;将新添加后的节按指定值对齐
                        mov        [ebx].SizeOfRawData,eax
                        invoke        _Align,ecx,[esi].OptionalHeader.SectionAlignment   ;将新添加后的节按指定的值对值
                        add        [edi].OptionalHeader.SizeOfCode,eax        ;修正SizeOfCode
                        add        [edi].OptionalHeader.SizeOfImage,eax        ;修正SizeOfImage
                        invoke        _Align,[edx].Misc.VirtualSize,[esi].OptionalHeader.SectionAlignment
                        add        eax,[edx].VirtualAddress
                        mov        [ebx].VirtualAddress,eax                             ;给新添加的节的各个字段赋值
                        mov        [ebx].Misc.VirtualSize,offset APPEND_CODE_END-offset APPEND_CODE
                        mov        [ebx].Characteristics,IMAGE_SCN_CNT_CODE\
                                or IMAGE_SCN_MEM_EXECUTE or IMAGE_SCN_MEM_READ or IMAGE_SCN_MEM_WRITE
                        invoke        lstrcpy,addr [ebx].Name1,addr szMySection         
;********************************************************************
; 将新增代码作为一个新的节写到文件尾部
;********************************************************************
                        invoke        SetFilePointer,@hFile,[ebx].PointerToRawData,NULL,FILE_BEGIN   ;定位到新添加的节的位置
                        invoke        WriteFile,@hFile,offset APPEND_CODE,[ebx].Misc.VirtualSize,\   ;将代码写入到新添加的节的位置
                                addr @dwTemp,NULL
                        mov        eax,[ebx].PointerToRawData
                        add        eax,[ebx].SizeOfRawData
                        invoke        SetFilePointer,@hFile,eax,NULL,FILE_BEGIN                      ;定位到文件的开始
                        invoke        SetEndOfFile,@hFile                                            ;然后得到文件的结尾位置
;********************************************************************
                        push        [ebx].VirtualAddress        ;eax = 新加代码的基地址               
                        pop        @dwAddCodeBase
                        push        [ebx].PointerToRawData
                        pop        @dwAddCodeFile
                .endif
;********************************************************************
; (Part 4)修正文件入口指针并写入新的文件头
;********************************************************************
                mov        eax,@dwAddCodeBase
                add        eax,(offset _NewEntry-offset APPEND_CODE)
                mov        [edi].OptionalHeader.AddressOfEntryPoint,eax                          ;将入口地址写入文件头
                invoke        SetFilePointer,@hFile,0,NULL,FILE_BEGIN                               ;定位到文件开始,写入新的文件头
                invoke        WriteFile,@hFile,@lpMemory,[esi].OptionalHeader.SizeOfHeaders,\
                        addr @dwTemp,NULL
;********************************************************************
; (Part 5)修正新加代码中的 Jmp oldEntry 指令
;********************************************************************
                push        [esi].OptionalHeader.AddressOfEntryPoint
                pop        @dwEntry
                mov        eax,@dwAddCodeBase
                add        eax,(offset _ToOldEntry-offset APPEND_CODE+5)                         ;定位到原来入口点代码处+5是因为
                sub        @dwEntry,eax                                                          ;JMP XXXXXXXX占用了五个字节
                mov        ecx,@dwAddCodeFile
                add        ecx,(offset _dwOldEntry-offset APPEND_CODE)
                invoke        SetFilePointer,@hFile,ecx,NULL,FILE_BEGIN
                invoke        WriteFile,@hFile,addr @dwEntry,4,addr @dwTemp,NULL                    ;写入跳转到原代码的代码
;********************************************************************
; (Part 6)关闭文件
;********************************************************************
                invoke        GlobalFree,@lpMemory                                                  ;释放内存,关闭文件句柄
                invoke        CloseHandle,@hFile
                invoke        wsprintf,addr @szBuffer,Addr szSuccess,addr @szNewFile
                invoke        SetWindowText,hWinEdit,addr @szBuffer
_Ret:
                assume        esi:nothing
                popad
                ret

_ProcessPeFile        endp
;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
上面的我作了简单的注释,这里我在具体分析一下这段代码,请大家仔细研究一下,很有意思的!!
Part 1从原始PE文件拷贝一个名为“原始文件名_new.exe”的文件,这个文件将被添加上可执行代码,原来的“原始文件名.exe”文件则不会被改动。当文件成功拷贝后,程序将打开拷贝生成的新文件以便进行修改。

Part 2分配一个等于目标PE文件的文件头大小的内存块,并将文件头拷贝到这个内存块中,所有对PE文件头的修改操作都是在这个内存块中完成的,这个内存块的内容最终将被写到“原始文件名_new.exe”文件中。完成拷贝工作以后,程序计算两个指针以备后用:指向节表最后一项指针和指向节表尾部的指针,这两个指针可以从节表的数量和节表的长度计算而来的,节表的数量是从PE文件头中的FileHeader.NumberOfSections字段获取的。
正如本节的开始所述,新增的代码既可以插入原代码所处的节的空隙中,也可以通过添加一个新的节来附在原文件的尾部,为了增加成功的机会,应该对这两种情况都给予考虑,于是在Part2中对节表的尾部进行全零数据的扫描,如果存在一段全零的位置可供放入一个新的节表,那么采取增加新节的办法(Part3.2),否则采用在代码节的空隙中插入的方法(Part3.1)

Part 3.1 中对所有节表进行循环扫描,以便于找到代码节并检测节的空隙是否可以容纳新增的代码,程序首先判断SizeOfRawData是否为0,这个数值为0,说明这个节是包含未初始化数据的节,不能用于插入代码,如果SizeOfRawData大于0的话,则检测Characteristics字段查看当前节是否为代码节(包含IMAGE_SCN_MEM_EXECUTE标志)。
通过检测后,程序计算空隙的大小(SizeOfRawData和Misc.VirtualSize之差)是否大于插入代码的长度,如果空隙足够大的话,则进行插入操作。在插入代码的同时,这个节的属性中必须被加上IMAGE_SCN_MEM_READ和IMAGE_SCN_MEM_WRITE标志,因为附加代码中使用了对被加入部分进行写操作的指令。另外,VirtualSize字段中的实际数据大小也需要被修正。上面的程序只考虑了在代码节中插入的情况,要是代码节中的空隙大小不够,那么程序就退出了。实际上,程序也可以在其他的节中插入代码,只要将节的属性同时也加上IMAGE_SCN_MEM_EXECUTE标志就可以了,要是进一步将程序完成的话,当单个节的空隙大小不够的时候,也可以将附加代码分块插入多个节中,不过这时附加代码中就必须考虑在执行前将代码重新拼装在一起这个步骤了。

Part 3.2在节表中加入一个新的节表项目,节表项中的VirtualSize,VirtualAddress,SizeOfRawData,PointerToRawData,Characteristics和Name1字段需要被设置。其中Name1中的名称被设置为“.adata”;Characteristics字段中的标志被设置为可执行和可读写,其他几个字段值的算法如下(下面的“上一节”指原始PE文件的最后一节):

●  PointerToRawData=(上一节的PointerToRawData)+(上一节的SizeOfRawData)

●  SizeOfRawData=附加代码的长度按FileAlignMent值对齐

●  VirtualAddress=(上一节的VirtualAddress)+(上一节的VirtualSize按SectionAlignMent的对齐值)

●  VirtualSize=附加代码的长度按SectionAlignMent值对齐

其中的对齐算法是用_Align子程序来完成的。在这一部分中,程序还修正了文件头中的SizeOfCode和SizeOfImage的值。如果SizeOfImage的值不被修正的话,Windows将无法装入修改后的PE文件,报的错误为“这不是一个有效的Win32可执行文件”。Part3.2的最后,程序将附加代码写到文件的最后,由于附加代码的长度还没有按FileAlignment的值对齐,所以程序再次使用SetFilePointer函数将文件指针移动到对齐后的位置并用SetFileEnd函数将文件长度扩展到这里。无论是Part3.1还是Part3.2的最后,程序将新增代码在文件中位置和在内存中的位置分别保存在@dwAddCodeFile和@dwAddCodeBase变量中以备后用。

Part4 修改PE文件头中的文件入口地址,并将修改后的整个PE文件头写入到新文件中。

Part 5将原始PE文件的入口地址取出,和附加代码的入口地址计算得出“jmp 原入口地址”这条指令中的二进制码值,并将这个值写到附加代码的对应位置中。JMP指令的编码方式是由一个0e9h字节加上指令执行后的EIP的修正值,也就是说,当JMP指令的下一句指令地址是addr1,而跳转的目标地址是addr2的放在,那么0e9h字节后的双字的值就是addr2-addr1,所以下面的几句就是将指令改成"JMP原入口址"的样子:
push [esi].OptioinalHeader.AddressOfEntryPoint
   pop @dwEntry
   mov eax,@dwAddCodeBase
add eax,(offset _ToOldEntry--offset APPEND_CODE+5)
sub @dwEntry,eax
在指令列执行前,ESI指向PE文件头,@dwAddCodeBase中保存有新增代码被装载到内存后的起始地址,所以由(1)标出的指令执行后,EAX的值是_ToOldEntry后面的5个字节的位置,或者说是JMP XXXXXXXX后一条指令的位置,也就是上面算式中的addr1。@dwEntry中的原始值是可执行文件原来的入口地址,也即addr2,指令(2)执行后,@dwEntry中的值就是addr2-adr1b ,这就是需要填入_dwOldEntry位置的数据。
接下来程序用SetFilePointer函数将文件指针移动到新增代码中的_dwOldEntry位置,并将上面计算出的结果写入文件中。

Part 6进行扫尾工作,如释放内存、关闭文件和显示成功信息等。至此,程序的所有功能就完成了。
写到这里,基本上也对现了承诺,在这七天的时间里陪大家重新温习了一下PE的相关知识,最近在学习内核和驱动方面的知识,希望想交流的朋友能加我的看雪ID号或QQ号,谢谢!
前面有朋友说作个链接,在这里作了一个小链接,以方便大家参考学习,温故而知新!!!
国庆PE总复习(一)(二)      http://bbs.pediy.com/showthread.php?t=121488
国庆PE总复习(三)           http://bbs.pediy.com/showthread.php?t=121595
国庆PE总复习(四)           http://bbs.pediy.com/showthread.php?t=121672
国庆PE总复习(五)           http://bbs.pediy.com/showthread.php?t=121695
国庆PE总复习(六)      http://bbs.pediy.com/showthread.php?t=121748

国庆长假结束了,希望大家在看雪都能找一个属于自己的位置,早日成为看雪上的技术牛人!!
pencil 5 2010-10-7 16:13
69
0
很好的复习资料,谢谢楼主了
我把七篇合集整理成了一个pdf,看EBook的朋友拿去~
PE格式复习.pdf
上传的附件:
ilovehuhu 2010-10-7 16:38
70
0
太详细了,保存学习了
小菜鸟一 2010-10-7 17:20
71
0
感谢总结,继续学习中
熊猫正正 9 2010-10-7 17:25
72
0
感谢pencil帮我整理了,谢谢!!
daidaihu 2010-10-7 17:32
73
0
学习了,多谢!!!
jguoguo 2010-10-7 17:49
74
0
这国庆过得很有充实嘛···
kwzlj 2010-10-7 21:41
75
0
感谢楼主的教程,同时感谢二楼汇总。
奘和 2010-10-7 22:20
76
0
感谢LZ的辛勤分享
感谢pencil的热心整理
BeMaverick 2010-10-7 23:32
77
0
真的很不错 我刚要整理已经有人先了
godfather杰 2010-10-8 09:58
78
0
嗯。谢谢楼主的劳动!
CYBER涛 1 2010-10-8 10:17
79
0
国庆在出差,刚刚上看雪就看到这么好的文章,谢谢作者了!可以作为研究PE的详细资料保存!
xhflovemy 2010-10-8 11:09
80
0
感谢楼主无私奉献,同时也感谢pencil整理
scdymy 2010-10-8 11:18
81
0
谢谢,一直搞不懂pe
沙加L 1 2010-10-8 12:04
82
0
楼主,你真的是一牛人
冷煞xiaosan 2010-10-8 14:11
83
0
谢谢LZ的不懈努力!支持一下~
koday 2010-10-8 17:39
84
0
谢谢各位朋友,我学汇编的有用了
魔族智者 2010-10-9 11:20
85
0
不错的帖子,谢谢楼主分享
superdayu 2010-10-9 15:03
86
0
虽然不明白,但是还是支持楼主
lxzl 2010-10-9 21:39
87
0
多谢,好东西,收藏了
bgcelite 2010-10-10 07:13
88
0
能够整理成word或者ppt更好
潜行修身 2010-10-10 11:42
89
0
潜心学习中,感谢楼主!
紫色幽默 2010-10-10 12:12
90
0
呵呵
菜鸟来学习了
snss 2010-10-10 13:14
91
0
谢谢LZ的辛勤分享
谢谢pencil整理成PDF.
xiaodengzi 2010-10-10 15:39
92
0
长知识了 感谢楼主~~~
lovecm 2010-10-10 19:16
93
0
感谢楼主,感谢pencil整理,谢谢!!
ycmint 5 2010-10-10 20:52
94
0
正正你是俺的偶像,也是俺的老乡,嘻嘻。。。讲的很好 受益了~~
一九零五 2010-10-10 20:59
95
0
好强大,慢慢读,希望能对PE有进一步的了解
iSpeak 2010-10-11 00:22
96
0
学习了,谢谢楼主分享.
iSpeak 2010-10-11 08:30
97
0
不错,非常感谢,看老罗的书,快看到PE章节了
flyingayi 2010-10-11 09:24
98
0
跟着楼主一起复习!
潜行修身 2010-10-11 10:21
99
0
看到第二部分突然有个疑问,虚拟地址(VRA)与文件偏移地址(FileOffset)的相互转换的规则是固定的吗?
不清楚各节的VRA是在加载的时候加载器填充到数据结构中的还是在生成PE文件时就已经确定。。求解
mrhts 2010-10-11 22:53
100
0
来学习了。。谢谢
游客
登录 | 注册 方可回帖
返回