首页
社区
课程
招聘
[旧帖] [原创]通过ntdll对PE文件格式的学习 0.00雪花
2012-3-20 22:03 2843

[旧帖] [原创]通过ntdll对PE文件格式的学习 0.00雪花

2012-3-20 22:03
2843

       入门级文章,大牛请飘过!

问题的出现    

问题源于一个驱动程序。其中一个关于PE部分的代码如下:

//  get the export table offset in file
pImageExportDir = RtlImageDirectoryEntryToData( FileBuffer, FALSE, IMAGE_DIRECTORY_ENTRY_EXPORT, &ExportSize);
  //  get the export table offset in memory
MappedExpBase = (ULONG)RtlImageDirectoryEntryToData( FileBuffer, TRUE, IMAGE_DIRECTORY_ENTRY_EXPORT, &ExportSize);

  //  find functions with special bytes
NumberOfNames = pImageExportDir->NumberOfNames;

AddressOfNameOrdinals=(PUSHORT)(pImageExportDir->AddressOfNameOrdinals+(ULONG)FileBuffer - MappedExpBase + (ULONG)pImageExportDir);

AddressOfNames = (PULONG)(pImageExportDir->AddressOfNames+(ULONG)FileBuffer - MappedExpBase + (ULONG)pImageExportDir);
  

AddressOfFunctions= (PULONG)(pImageExportDir->AddressOfFunctions+ (ULONG)FileBuffer - MappedExpBase + (ULONG)pImageExportDir);

for( i = 0; i < NumberOfNames; i++)
{
  Index = *(AddressOfNameOrdinals + i);
  Address = (PUCHAR)*(AddressOfFunctions + Index);
Address = Address + (ULONG)FileBuffer - MappedExpBase +(ULONG)pImageExportDir;
...

一开始看到这个位置怎么都想不明白,主要就是下面这个函数
RtlImageDirectoryEntryToData

上网找资料只有:

反汇编的结果如下:
DWORD RtlImageDirectoryEntryToData 
(DWORD imagebase,
BOOL   bVA,
DWORD  dwDataDirectoryIndex, 
DWORD  *size);

其中,imagebase 为模块基址, bVA为真表示函数返回一个VA, 否则函数返回一个PointerToRawData(减去基址就是在文件中的位置),

dwDataDirectoryIndex为索引值, 从0到f,如0表示导出表。size为接受这项数据目录大小的指针。

如果该索引不存在, 那么函数返回0, 并且不改变size指针的值。

看了也还是不明白,文件偏移,虚拟地址,相对虚拟地址啊...什么的,我倒是了解一点。不过那是在看《加密与解密》里面讲的,

用里面讲方法找到输出表的文件偏移却和上面这个函数得到的结果pImageExportDir完全不一样,这一步不明白,后面计算输出表

里面的偏移地址就更看不明白了。于是开始重新看PE文件格式。

重温PE结构



 从虚拟机中拷贝出一份xp,sp3的ntdll。载入WinHex



PE头的最后80H个字节为数据目录表,紧随其后的是区块表。数据目录的第一个IMAGE_DATA_DIRECTORY就是输出表。



.text节表



由此可知,.text的RVA为00001000H  Size为00079FB6H .因为data段紧跟text之后,段首地址又要以1000H对齐。

所以data的RVA: 1000H + 79FB6H = 7AFB6H向1000H对齐则为7B000H。即可知输出表3400H在ntdll的text段中。

那么可以计算输出表文件偏移为(上上一图)3400H–1000H + 400H = 2800H 
 


模块名RAV:00006786H,
函数及有名函数个数:都为00000523H,
函数地址数组RAV:00003428H,
函数名数组RAV:000048B4H,
函数序号数组RAV:00005D40H.

1.  模块名RAV=00006786H,文件偏移=00005B86H

 

2.  函数总数和有名函数总数都为523H=1315个函数

 

同PE Explorer查看的结果一样。

3.  函数序号数组RAV=00005D40H,文件偏移=00005140H
  
  
  

序号一直到0522H因为是从0开始的,但起始序号并不是0而是07H后来才知道0~6这七个都是打乱的。

之前也不知道,序号太多又不好找,后来就通过自写的一个程序将序号都按顺序列了出来才知道。

   

4.  函数名数组RAV=000048B4H文件偏移为00003CB4H

   
 
举个例子第一个函数名的RAV=00006790H,其文件偏移=00005B90H其后全是导出函数的名称

  

5.  函数地址数组RAV=00003428H,文件偏移=00002828H
 
   

还发现一个问题是,在ntdll中Nt*函数和Zw*函数的地址完全相同。

以NtClose和ZwClose函数为例

  
 
NtClose序号为111-1=110。计算其函数地址所在内存位置的文件偏移为

函数地址数组+110*4。2828H + 110*4 = 29E0H
 
  

可以看到NtClose的RAV为0000CFD0H。再看ZwClose

  
 
ZwClose序号为921-1=920。计算其函数地址算在内存位置的文件偏移,方法同上,其值为3688H

  
 
可以看到也是CFD0H。所以说在ntdll里面这两个是同一个函数只是有两个名字而已,一个是别名。

函数RAV为CFD0H,加上可选头里面指定的基址就等于7C92CFD0H如PE Explorer显示的一样。

   
 

回到问题的原点

AddressOfNameOrdinals=
(PUSHORT)(pImageExportDir->AddressOfNameOrdinals+ (ULONG)FileBuffer - MappedExpBase + (ULONG)pImageExportDir);

现在我在明白为什么要这样计算AddressOfNameOrdinals了。

1.  首先看FileBuffer是什么
Status= LoadFile( L"\\??\\c:\\windows\\system32\\ntdll.dll", &FileBuffer );

LoadFile函数为自写函数,实际上是先ZwCreateFile创建文件句柄,然后通过句柄将文件ZwReadFile读入内存。

FileBuffer很明显是读入内存的文件的首地址。

但是现在有个问题在我脑中——这个读入的文件是以什么样的方式组织的?也就是说它到底是磁盘上的二进制文件

直接原封不动的拷贝到内存中,还是像加载dll文件一样需要什么区段对齐啊什么的呢?动手看看就知道了!

   
 
对驱动进行调试,如上图击中断点

   
 

pImageExportDir – FileBuffer = 2800H 正好是输出表的文件偏移

MappedExpBase – FileBuffer = 3400H 正好是输出表的RAV

所以这个函数RtlImageDirectoryEntryToData获得的文件偏移和内存偏移都是参照FileBuffer的,也就是加载ntdll到内存的首地址。

(以前我们在LoadPE里面计算得到的文件偏移,其参照的首地址是0。在这个例子中却是FileBuffer)

   
 
于是RtlImageDirectoryEntryToData这个函数也基本明白。

接着看

AddressOfNameOrdinals=
(PUSHORT)(pImageExportDir->AddressOfNameOrdinals+ (ULONG)FileBuffer - MappedExpBase + (ULONG)pImageExportDir);

pImageExportDir->AddressOfNameOrdinals函数序号数组。它是数组的首地址,又涉及到地址。

由于在文件中存放的地址都是RAV,所以上面的代码可能是在计算文件偏移。

暂时认为AddressOfNameOrdinals是函数名数组参照FileBuffer的文件偏移。

可以这样理解:
  AddressOfNameOrdinals - (ULONG)FileBuffer为函数序号数组的文件偏移,称为ArrayOffest;

  (ULONG)pImageExportDir - (ULONG)FileBuffer为输出表的文件偏移,称为ExpOffest

  pImageExportDir->AddressOfNameOrdinals为函数序号数组的RAV称为ArrayRAV;

  MappedExpBase - (ULONG)FileBuffer为输出表的RAV称为ExpRAV;

  又因为输出表和函数序号数组都在text段中.

综上: 
ArrayOffest – ExpOffest = ArrayRAV – ExpRAV

得出语句正好和上面的一样。

再看看AddressOfNameOrdinals是不是序号数组的文件偏移

  
 
AddressOfNameOrdinals – FileBuffer = 00005140H正好是序号数组的文件偏移。

由此可以看出在读入内存的时候确实是按照磁盘文件的形式进行读入的。

到这儿之前一直不明白的位置也弄清楚了,主要就是这个函数RtlImageDirectoryEntryToData和它后面计算文件偏移的代码。

不过现在回头想想其实之前网上找的关于这个函数的说明已经说得很明白了,也不知道为什么当时没领悟到:文件偏移和RAV都是参照其首地址的,

而首地址并不总是像我们在WinHex里面看到的为00000000H的。当要访问其中某一个位置的时候,是文件形式的话就是首地址

(如上面的FileBuffer)加上Offest;是PE映像的话(加载进内存中)就是模块基地址(ImageBase)加上RAV。

导出表的问题

  for( i = 0; i < NumberOfNames; i++)
  {
    Index = *(AddressOfNameOrdinals + i);
    Address = (PUCHAR)*(AddressOfFunctions + Index);
    Address = Address + (ULONG)FileBuffer - MappedExpBase + (ULONG)pImageExportDir; 

    //  b8 xx xx xx xx   mov    eax, XXXX
    //  ba 00 03 fe 7f   mov    edx, 7FFE0300h
    //  ff 12            call   dword ptr [edx]
    if(*Address = 0xB8 &&
       *(Address + 0x05) == 0xBA &&
       *(PULONG)(Address + 0x06) == 0x7FFE0300 &&
       *(PUSHORT)(Address + 0x0A) == 0x12FF)
    {
      Temp = *(PULONG)(Address + 1);
      Address = (PUCHAR)*(AddressOfNames + Index);
      Address = Address + (ULONG)FileBuffer - MappedExpBase + (ULONG)pImageExportDir;

      //  safer than strcpy
      RtlCopyMemory(FuncName + Temp * 0x32, Address, 0x32);

      *(FuncName + Temp * 0x32 + 0x31) = 0x00;
    }
  }

总觉得

Index = *(AddressOfNameOrdinals + i);
Address = (PUCHAR)*(AddressOfFunctions + Index);
Address = (PUCHAR)*(AddressOfNames + Index);

这三行有点问题,大家都知道序号数组和函数名数组是一一对应的(函数名和对应函数序号在各自的数组中的排列顺序相同),

也就是说序号是*(AddressOfNameOrdinals + i)的话那么对应函数名也应该是*(AddressOfNames + i)。

而上面的代码却不是这样,它认为函数名和函数地址一一对应。
 
   

事实是函数名和序号一一对应,函数名通过函数序号与函数地址对应。

那么这样说的话上面的代码确实有错,Address = (PUCHAR)*(AddressOfNames + Index);这一句中的Index应该改为i。

但是程序的运行结果却是对的,就是获得的函数名和函数地址确实可以对应起来。

这确实让我百思不得其解。后来才知道原因是函数序号从864开始往下就是依次增长排列的,

  

 也就是说函数序号864及其后的序号,在序号数组中的偏移大小和序号值相同(i等于Index)。

而程序想要获取的函数名和函数地址正好是导出序号在864之后的函数,所以程序的结果是正确的。

但怎么说上面那个代码都应该用i代替Index,这样每一次循环所获取的函数名和函数地址才是正确对应的。

总结

     在弄清楚这些之后,我才发现自己花那么大功夫去弄清楚的问题,其实一点技术含量都没有,都是很基础的东西,但是因为自己基础不是很牢固,

导致总是在小问题上大费周章,感觉确实有点小题大做。但是我始终相信一点,基础性的知识书本上都有,我们都会去看,但是看的终究是别人的,

理解了才是自己的,但是无论书上讲得多么透彻,当时貌似看懂了,到要用的时候却又忘了,所以每次只有在我一步一步自己动手弄清楚相同的问题后

我才会很清楚的理解它,并且可以一直都记在心里。

     这篇文章可能会让很多人见笑了,但是我坚信追求技术的路上没有什么问题是小问题,不懂的就是大问题。我就是要在很多人不屑的时候

坚持的纠结那一两行代码,不为别的就因为我看出了问题,而我正好不懂,就这么简单。面对问题我做不到熟视无睹,我欺骗不了自己。

     唉~说多了。以前没写过这些东西,写的很烂,会有很多错误,希望朋友们本着交流的心态,指出错误大家互相研究互相进步。

    初来乍道真心希望能在这里认识更多要好技术的朋友

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

上传的附件:
收藏
点赞5
打赏
分享
最新回复 (3)
雪    币: 601
活跃值: (256)
能力值: ( LV11,RANK:190 )
在线值:
发帖
回帖
粉丝
RootSuLe 4 2012-3-20 22:06
2
0
谢谢分享,学习了
雪    币: 206
活跃值: (86)
能力值: ( LV7,RANK:110 )
在线值:
发帖
回帖
粉丝
酒色财气 2 2012-3-20 22:38
3
0
支持!!!好多图!!!
讲的很清楚!!!
雪    币: 249
活跃值: (71)
能力值: ( LV7,RANK:100 )
在线值:
发帖
回帖
粉丝
cxthl 2 2012-3-20 22:49
4
0
楼主探索的精神值得小白学习!!!内容讲的也非常详细!强烈支持!!!
游客
登录 | 注册 方可回帖
返回