0 0 0

科锐学习笔记-第三阶段-PE 08 导出表

admin
225 0

本文共计4423个字,预计阅读时长17.7分钟。

目录

通过cff , depends灯等软件可以看到dll,导出函数的信息,因为dll中本身就存了这些信息,存了dll中有哪些导出函数,导出函数的序号是什么,名字是什么,以及他们的地址是什么,这些东西都存在导出表里面

导出表发展历程

最开始的时候只有序号导出,没有名称导出,使用的时候都是通过序号
序号 地址
1 1005
2 2005
3 3005
4 4005
5 5005
6 6005
7 7005
8 8005

但是上面的时间复杂度比较高是线性阶,但是对数阶占的体积比较大,所以只能折中,用常量阶.把地址作为数组,序号作为索引,只存地址,这样不仅速度更加快了,而且体积更小了

但是所以是从0开始的,因此可以数组首项加一个空

img

这样可以通过序号,直接去找对应数组对应的索引的值

序号不从1开始

但是上面有一个缺陷,序号不一定从0开始,例如 从 1001开始,这样数组不能前面加1000项0,这样不仅浪费空间,而且还用不上,解决办法是加一个 基址 (最小序号), 后面序号就根据基址来取偏移

img

例如上面,要拿到 序号1006 的函数地址, 可以 获取 1006对 1001 的偏移 5 ,再去数组取 索引为5的地址

序号不连续

解决了上面基本不从0开始的情况,还有一个问题,就是 序号不连续 ,例如 1001 , 1002 , 1020,1021,1050

这种情况下,数组中间对应的序号就需要填充0,因此会使体积变大,微软并没有解决这个问题,因为这是写dll的人自己的问题

例如

1. 不指定序号(这样序号连续)

img

img

可以看出大小是38kb

1. 部分指定序号使序号不连续

img

img

可以看到此时dll大小变成了 346KB

名称导出

img

序号跟成名2个表是拆开的,并不在一起,因此 基数数 ,导出地址表 ,导出名称表 ,导出序号表 构成了导出表

导出表结构

导出表结构

名称和序号是一一对应的,因此导出名称个数 和导出序号个数 只需要保存一个就可以了

IMAGE_EXPORT_DIRECTORY


// IMAGE_EXPORT_DIRECTORY 导出表结构体,40B 
typedef struct _IMAGE_EXPORT_DIRECTORY { 
   DWORD   Characteristics;  // 无用 
   DWORD   TimeDateStamp;   // 时间戳 
   WORD    MajorVersion;   // 无用 
   WORD    MinorVersion;   // 无用 
   DWORD   Name;     // 描述性字段:'模块的名字' 
   //上面20字节没用,说明性的 

   DWORD   Base;     // * 序号base基数,序号导出函数的索引值值从Base开始递增 
   DWORD   NumberOfFunctions;  // * 导出地址的个数 
   DWORD   NumberOfNames;   // * 导出名称的个数 

   DWORD   AddressOfFunctions;  // * 导出地址表的地址RVA 
   DWORD   AddressOfNames;   // * 导出名称表的地址RVA,按照ASCII码固定排序 
   DWORD   AddressOfNameOrdinals; // * 导出序号表的地址RVA 
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

定位导出表: 

​        数据目录第一项指针指向导出表。(第二项是导入表),该项只能说明导出表所在地址,导出表有多大并不是该Size来决定的(**但是它是导出函数是否转发的判断条件之一**)。 

img 

img 

img 

导出地址表 

img 

img 

导出名称表 

img 

img 

img 

可以看出跟我们导出的顺序是不一样的,他进行了 函数名  ascii 码排序 

导出序号表 

img 

解析 

正常情况 

img 

在通过cff去看 

img 

1.  像上面如果我们找到处序号为4的函数信息 

  •   拿到下标索引    序号 - base  =   4 - 1 = 3 
  •   根据索引取导出地址表拿到地址    11195 
  •   用OD验证 

img 

img 

1.  拿到函数 foo 的地址 

  •   在导出名称表遍历,根据该函数名在导出名称表的索引    2   
  •   在根据上面索引在导出序号表拿到函数导出序号    2 
  •   在根据序号在 导出地址表拿到导出函数地址    110cd 

img 

导出表序号不从0开始的情况 

img 

img 

img 

此时情况是 

imgimg 

导出表序号不连续的情况 

img 

img 

img 

img 

img 

1.  例如要找序号为110的函数地址 

  •   获取在导出地址指表的索引   110 -100  =  10 
  •   根据索引在导出地址指表数组取值  110cd 
  •   OD验证 

img 

img 

1.  寻找test的函数地址 

  •   先到导出名称表找到 test  在该数组索引   5   
  •   再到 导出序号表找到对应索引的导出地址的索引 
  •   再根据导出地址表的索引取导出地址表 获取 地址 1132f 

img 

1.  找序号108 的 函数地址 

  •   获取序号108对应的导出地址的索引  108-100 = 8 
  •   再根据导出地址表的索引取导出地址表 获取 地址 0000 ,说明没有这个序号的导出函数 
导出函数没有名称只有序号导出 

img 

img 

img 

img 

img 

这种情况OD可以看到 函数名是通过pdb文件知道的,删掉之后就不知道了 

函数转发 

img 

img 

img 

img 

img 

img 

img 

img 

可以看出转发的话,导出地址表存的是一个字符串的地址,而系统需要解析这个字符串拿出dll名和函数名,继续去查 

而且该地址和其他地址不一样,  该地址只需要位于  导出表地址   和  导出表地址+导出表大小  中间 那么他就是一个转出函数,如果没有位于这中间,那么他就不是一个转发函数  

如何判断转发函数,即地址指向应为字符串而不是实现? 
  •   如果是指向的字符串(表示为转发函数),是与导出函数名放在一起的,所以应该先借助正常遍历获取到地址,然后借助导出表的地址和大小判断地址的范围是否在导出表的范围之内: 

  •   -   不是则为正常函数,直接返回地址节课;     -   是则为转发函数,进行转发函数的处理。 

遇到转发函数应该如何处理? 
  •   1.字符串为dll.函数名,所以要先分割解析,分别拿到dll名和函数名; 
  •   2.调用LoadLibrary; 
  •   3.再GetProcAddress递归遍历拿到地址。 

模拟GetProcAddress  


.586 
.model flat,stdcall 
option casemap:none 

  include windows.inc 
  include user32.inc 
  include kernel32.inc 
  include msvcrt.inc 

  includelib user32.lib 
  includelib kernel32.lib 
  includelib msvcrt.lib 

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD 

.data 
  g_szDll db "user32.dll",0 
  g_szFunc  db "MessageBoxA",0 

.code 

;参数:   句柄   导出函数名 
MyGetProcAddress proc hMod:HMODULE, lpProcName:LPCSTR    

   LOCAL @pDosHdr:ptr IMAGE_DOS_HEADER          ;dos头 
   LOCAL @pNTHdr:ptr IMAGE_NT_HEADERS           ;Nt头 
   LOCAL @pExpDir:ptr IMAGE_EXPORT_DIRECTORY    ;导出表 

   LOCAL @pAddrTbl:DWORD     ;导出地址表地址 
   LOCAL @pNameTbl:DWORD     ;导出名称表地址 
   LOCAL @pOrdTbl:DWORD      ;导出序号表地址 

   ;解析 
   ;dos 头 
   mov eax, hMod 
   mov @pDosHdr, eax 

   ;nt头 
   mov esi, @pDosHdr 
   assume esi:ptr IMAGE_DOS_HEADER 
   mov eax, hMod 
   add eax, [esi].e_lfanew 
   mov @pNTHdr, eax 

   mov esi, @pNTHdr 
   assume esi:ptr IMAGE_NT_HEADERS 

   ;获取导出表 
   mov esi, @pNTHdr 
   assume esi:ptr IMAGE_NT_HEADERS 

   mov eax, [esi].OptionalHeader.DataDirectory[0].VirtualAddress 
   add eax, hMod 
   mov @pExpDir, eax 

   mov esi, @pExpDir 
   assume esi:ptr IMAGE_EXPORT_DIRECTORY 

   ;导出函数地址表 
   mov eax, [esi].AddressOfFunctions 
   add eax, hMod 
   mov @pAddrTbl, eax 

   ;导出函数名称表 
   mov eax, [esi].AddressOfNames 
   add eax, hMod 
   mov @pNameTbl, eax 

   ;导入序号表 
   mov eax, [esi].AddressOfNameOrdinals 
   add eax, hMod 
   mov @pOrdTbl, eax 

   ;判断是序号还是名称  (序号是一个 word,对于 dword来说高位都是0) 
   .if lpProcName & ffff0000h    
       ;名称 
       mov ebx, @pNameTbl 
       xor ecx, ecx 
       .while ecx <[esi].NumberOfNames 
           ;获取名称地址 
           mov eax, [ebx+ecx*4] 
           add eax, hMod 

           ;字符串比较 
           push ecx 
           invoke crt_strcmp, lpProcName, eax 
           pop ecx 
           .if eax == 0 
               ;找到了, 从导出序号表取出函数地址下标 
               mov edi, @pOrdTbl 
               movzx eax, word ptr [edi+ecx*2] 

               ;从导入地址表,下标寻址,获取导出函数地址 
               mov ebx, @pAddrTbl 
               mov eax, [ebx+eax*4] 

               ;判断转发 。。。。解析函数名,递归判断 

               ;返回地址 
               .if eax != NULL 
                   add eax, hMod 
                   ret 
               .endif 

           .endif 

           inc ecx 
       .endw 

   .else  
       ;序号 
       mov eax, lpProcName 
       sub eax, [esi].nBase ;获取索引值 

       ;从导入地址表,下标寻址,获取导出函数地址 
       mov ebx, @pAddrTbl 
       mov eax, [ebx+eax*4] 

       .if eax != NULL 
           add eax, hMod 
           ret 
       .endif 

   .endif 

   xor eax, eax 
   ret 

MyGetProcAddress endp 

start: 
   invoke LoadLibrary, offset g_szDll 
   invoke MyGetProcAddress,eax, offset g_szFunc 

   push MB_OK 
   push offset g_szDll 
   push offset g_szFunc 
   push NULL 
   call eax 

invoke ExitProcess,0 
end start

最新回复 ( 0 )
全部楼主