首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
88 阅读
2
nvim番外之将配置的插件管理器更新为lazy
73 阅读
3
2018总结与2019规划
55 阅读
4
PDF标准详解(五)——图形状态
37 阅读
5
为 MariaDB 配置远程访问权限
33 阅读
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
VimScript
emacs
linux
文本编辑器
Java
elisp
反汇编
OLEDB
数据库编程
数据结构
内核编程
Masimaro
累计撰写
311
篇文章
累计收到
27
条评论
首页
栏目
软件与环境配置
博客搭建
从0开始配置vim
Vim 从嫌弃到依赖
archlinux
Emacs
MySQL
Git与Github
AndroidStudio
cmake
读书笔记
编程
PDF 标准
从0自制解释器
qt
C/C++语言
Windows 编程
Python
Java
算法与数据结构
PE结构
Thinking
FIRE 运动
菜谱
页面
归档
友情链接
关于
搜索到
190
篇与
的结果
2017-03-21
PE文件详解(六)
这篇文章转载自小甲鱼的PE文件详解系列原文传送门之前简单提了一下节表和数据目录表,那么他们有什么区别?其实这些东西都是人为规定的,一个数据在文件中或者在内存中的位置基本是固定的,通过数据目录表进行索引和通过节表进行索引都是可以找到的,也可以这么说,同一个数据在节表和数据目录表中都有一份索引值,那么这两个表有什么区别?一般将具有相同属性的值放到同一个节区中,这也就是说同一个节区的值只是保护属性相同,但是他们的用途不一定是一样的,但是在同一数据目录表中的数据的作用是相同的,比如输入函数表中只会保存输入函数的相关信息,输出函数表中只会保存输出函数的信息,而输入输出函数在PE文件中可能都位于.text这个节中。输入函数表输入函数:一般将那些在本程序中调用,但是它的代码不在本程序中的函数称为输入函数,输入函数一般都在另外一个独立的dll中。在之前谈到PE头的时候说到,在PE头中有一个结构是数据目录表,它的结构如下:IMAGE_DATA_DIRECTORY STRUCT VirtualAddress DWORD ? ; 数据的起始RVA isize DWORD ? ; 数据块的长度 IMAGE_DATA_DIRECTORY ENDS这个结构大小为8,相对于PE文件头的偏移为0x78。在PE文件中,通过一个数组来保存多个数据目录表的信息,而输入函数表则是这个数组的第二个元素。而输入表是以一个 IMAGE_IMPORT_DESCRIPTOR(简称IID) 的数组开始。每个被 PE文件链接进来的 DLL文件都分别对应一个 IID数组结构。在这个 IID数组中,并没有指出有多少个项(就是没有明确指明有多少个链接文件),但它最后是以一个全为NULL(0) 的 IID 作为结束的标志。IMAGE_IMPORT_DESCRIPTORIMAGE_IMPORT_DESCRIPTOR STRUCT union Characteristics DWORD ? OriginalFirstThunk DWORD ? ends TimeDateStamp DWORD ? ForwarderChain DWORD ? Name DWORD ? FirstThunk DWORD ? IMAGE_IMPORT_DESCRIPTOR ENDSOriginalFirstThunk它指向first thunk,IMAGE_THUNK_DATA,该 thunk 拥有 Hint 和 Function name 的地址。TimeDateStamp该字段可以忽略。如果那里有绑定的话它包含时间/数据戳(time/data stamp)。如果它是0,就没有绑定在被导入的DLL中发生。在最近,它被设置为0xFFFFFFFF以表示绑定发生。ForwarderChain一般情况下我们也可以忽略该字段。在老版的绑定中,它引用API的第一个forwarder chain(传递器链表)。它可被设置为0xFFFFFFFF以代表没有forwarder。Name它表示DLL 名称的相对虚地址(译注:相对一个用null作为结束符的ASCII字符串的一个RVA,该字符串是该导入DLL文件的名称。如:KERNEL32.DLL)。FirstThunk它包含由IMAGE_THUNK_DATA定义的 first thunk数组的虚地址,通过loader用函数虚地址初始化thunk。在Orignal First Thunk缺席下,它指向first thunk:Hints和The Function names的thunks。这个OriginalFirstThunk 和 FirstThunk明显是亲家,两家伙首先名字就差不多哈。那他们有什么不可告人的秘密呢?IMAGE_THUNK_DATA STRUC union u1 ForwarderString DWORD ? ; 指向一个转向者字符串的RVA Function DWORD ? ; 被输入的函数的内存地址 Ordinal DWORD ? ; 被输入的API 的序数值 AddressOfData DWORD ? ; 指向 IMAGE_IMPORT_BY_NAME ends IMAGE_THUNK_DATA ENDS我们可以看出由于是union结构,所以IMAGE_THUNK_DATA 事实上是4个字节大小。这个共用体是怎么使用的呢:当 IMAGE_THUNK_DATA 值的最高位为 1时,表示函数以序号方式输入,这时候低 31位被看作一个函数序号。当 IMAGE_THUNK_DATA 值的最高位为 0时,表示函数以字符串类型的函数名方式输入,这时双字的值是一个 RVA,指向一个 IMAGE_IMPORT_BY_NAME 结构。接下来说明IMAGE_IMPORT_BY_NAME 结构:IMAGE_IMPORT_BY_NAME STRUCT Hint WORD ? Name BYTE ? IMAGE_IMPORT_BY_NAME ENDS结构中的 Hint 字段也表示函数的序号,不过这个字段是可选的,有些编译器总是将它设置为 0。Name 字段定义了导入函数的名称字符串,这是一个以 0 为结尾的字符串。输入函数表的加载从上面的图上来看,OriginalFirstThunk与FirstThunk指向的是同一个数据结构,在PE文件中既可以通过OriginalFirstThunk来找到函数名,也可以通过FirstThunk来找到函数名,为什么会出现两个指针指向同一个数据结构的现象呢,其实这个与PE文件的加载有关第一个数组(由 OriginalFirstThunk 所指向)是单独的一项,而且不能被改写,我们前边称为 INT。第二个数组(由 FirstThunk 所指向)事实上是由 PE 装载器重写的。PE 装载器首先搜索 OriginalFirstThunk ,找到之后加载程序迭代搜索数组中的每个指针,找到每个 IMAGE_IMPORT_BY_NAME 结构所指向的输入函数的地址,然后加载器用函数真正入口地址来替代由 FirstThunk 数组中的一个入口,也就是说此时的FirstThunk 不在指向这个INAGE_IMPORT_BY_NAME结构,而是真实的函数的RVA。因此我们称为输入地址表(IAT)。所以,当我们的 PE 文件装载内存后准备执行时,刚刚的图就会转化为下图:实验操作我们来编译一个具体的程序,源代码如下:#include <windows.h> int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow ) { MessageBox( NULL, TEXT("Hello, welcome to Fishc.com!"), TEXT("Welcome!"), MB_OKCANCEL | MB_OK ); return 0; }这个程序就是弹出一个MessageBox,通过W32Dasm静态反汇编发现MessageBox函数所在地址应该在0x0042A2AC在数据目录表中根据OriginalFirstThunk 项获取函数名称用UE打开这个PE文件,发现输入函数表的RVA = 0x0002A000在节表中查询发现它是在.idata这个节中通过之前说的公式,可以得到,这个RVA在文件中的偏移地址为0x0002A000 - 0x0002A000 + 0x00028000 = 0x00028000读取在这个位置的信息发现,OriginalFirstThunk = 0x0002A15C,这个偏移,发现它仍然在这个节中,通过上述公式计算得出,他在文件中的偏移地址为:0x0002815C从这个位置得到的值来看,它的最高值为0,也就是IMAGE_THUNK_DATA保存的是函数名的字符串,字符串的RVA为0x0002A2DC,通过计算得到它在文件中的偏移为:0x000282DC.从图上可以看出这个地址所对应的值正好是函数的名称MessgeBoxA通过FirstThunk成员找到函数名称首先根据PE文件的内容,可以知道,输入函数表在PE文件的偏移为0x00028000,而根据这个结构来看,FirstThunk在x00028000 + 16 = 0x00028010的位置,在这位置,我们发现它里面的值为0x0002A2AC计算得到在磁盘中的偏移为0x000282AC,在PE文件中这个值为0x0002A2DC,它的最高位仍然为0,也就是说这个地址保存的内容为函数名称。另外我们发现这个值与之前用OriginalFirstThunk 寻址到的函数名称所在RVA一样,也就是说到此成功找到函数名称查找函数在内存的偏移地址根据上面所说的内容,只有当这个PE文件被加载到内存中,PE加载器才会将IMAGE_IMPORT_BY_NAME结构中的值替换为对应函数的地址,所以要查找函数的地址就需要先将PE文件加载到内存,然后再将内存中的数据抓取下来,最后再来分析得出这个函数的偏移地址。其实这个工作可以由lordPE工具来帮忙完成。首先是启动程序,然后打开lordPE,找到程序的进程,然后选择dump full抓取全部即可这样会生成一个dump文件,分析这个文件,就可以得出相应的内容:由于这个是内存镜像的拷贝,所以在这在内存中的RVA就是在文件中的偏移。首先得到导入表的偏移为0x0002a000,这个值里面存储的值为0x0002A15C,这个值是OriginalFirstThunk的值,通过这个值找到对应的IMAGE_THUNK_DATA地址:0x0002A2DC我们发现这个值得高地址为是0,那么它所指向的应该就是函数名称,我们寻址到这个地址,发现它正好是函数名称接下来,再来解析函数地址,在0x0002a010中找到对应的FirstThunk值,这个值为0x0002A2AC,它是指向一个IMAGE_THUNK_DATA结构,在这个地址处,发现它的值为0x77D507EA,这个值的最高位为1,所以它对应的是一个函数地址,它的低32位是一个函数编号,此时0x0002A2AC指向的不在是一个IMAGE_IMPORT_BY_NAME结构,而是函数地址的偏移,而这个程序是由VC6.0编译而成,VC6默认的加载地址为0x00400000,所以基址 + 偏移地址就是函数的正确地址,也就是0x0042A2AC,与之前用静态反汇编得到的值相同
2017年03月21日
5 阅读
0 评论
0 点赞
2017-03-20
PE文件详解(五)
在前面几节中经常提到相对虚拟地址RVA,在这篇博客中主要说明这个概念。本来是想接着转载小甲鱼的,但是我自己根据这篇文章和他的视频来学习的时候,发现在RVA与文件的相对偏移地址进行转化的时候,那块我看不懂,不知道为什么要这样转化,而且前面很多东西都反复讲了好多遍,比如对齐的问题,所以,这篇我就自己根据自己掌握的情况来写,还是在此处放上原文的连接:原文(上)传送门原文(下)传送门什么是RVA某个位置的RVA是该位置在内存中的地址相对于整个文件在内存中首地址的偏移值,举个例子,当PE文件中某个全局区中的一个数值所在内存的地址为0x0040871234,而PE文件被系统加载到内存时,它的的首地址为0x00400000(PS:评论区的老哥说的不错,这个地址只是一个参考值,加载器会优先选择以这个地址作为加载的首地址,如果该地址被占用,则另外选取,一般在加载DLL中出现占用情况,由于exe一般作为进程来加载,它占用4GB的独立地址空间,所以一般不会出现占用),那么这个数值的RVA就是0x0040871234 - 0x00400000 = 0x871234RVA到文件偏移的转化有了RVA之后,发现所有数据在内存中进行索引都十分的方便,只要有了基地址和RVA后,通过基地址 + RVA就可以找到对应的数据,但是在PE中并没有类似值来记录某个数据到文件头的偏移,而且由于PE文件在内存中和在磁盘中的对齐值不同,造成某个数据在内存中的偏移和在磁盘中的偏移也不同,这样在磁盘中找到某些值就比较困难虽然PE文件在内存中的对齐值与在磁盘中的不同,各个区块在内存中的地址与在文件中的不同,但是各个区块里面数据的摆放基本是一样的,也就是在区块中,某个值相对于区块首地址的偏移是一样的,现在定义这样几个变量:Rva:某个数值在内存中的偏移Voffset:节点首地址在内存中相当于基地址的偏移Roffset:节点首地址在磁盘中相当于文件头的偏移fRva:某个数值在磁盘中相对于文件头的偏移根据上面的等量关系,可以得到这样的方程:fRva - Roffset = Rva - Voffset==>fRva = Rva- Voffset + Roffset根据这个公式来计算时需要进行如下的步骤:根据某个Rva落在哪个区块中:取每个节区在内存中的首地址(IMAGE_SECTION_HEADER中的VirtualAddress值) + 区块真实大小(IMAGE_SECTION_HEADER中的VirtualSize),如果Rva小于这个值得话,说明是落在这个节中通过第一步,我们可以知道Rva处于哪个区块,这样就可以得到区块的首地址在磁盘中相对于文件头的偏移就也就是Voffset的值。在文件中根据IMAGE_SECTION_HEADER中的PointerToRawData值可以得到这个区块在磁盘中相当于文件头的首地址,这样通过上述公式可以计算出Rva在文件中的偏移最终得到的公式如下:fRva = Rva - VirtualAddress + PointerToRawData举个例子,用lordPE这个工具打开一个.exe文件:我们来在磁盘中找到ImportTable在磁盘中的入口位置,即0x0001B1C4这个RVA对应在磁盘中的位置一个个对比发现.textbss:这个区块的RVA从0x00001000~0x00011000,这个值不在这个区块内.text:这个区块的RVA从0x00011000~0x00016060,这个值不在这个区块内.rdata这个区块的RVA从0x00017000~0x0001903d,这个值不在这个区块内.data这个区块的RVA从0x0001A000~0x000A158C,这个值不在这个区块内.idata这个区块的RVA从0x0001B000~0x0001BAF2,这个值在这个区块内根据上面的信息可以得到fRva = Rva - VirualAddress + PointerToRawData = 0x0001B1C4 - 0x0001B000 + 0x00007A00 = 7BC4
2017年03月20日
6 阅读
0 评论
0 点赞
2017-03-18
PE文件详解(四)
本文转自小甲鱼的PE文件详解系列原文传送门到此为止,小甲鱼和大家已经学了许多关于 DOS header 和 PE header 的知识。接下来就该轮到SectionTable (区块表,也成节表)。越学越多的结构,大家可能觉得PE挺乱挺杂的哈,所以这里插播下一下必要知识的详细注释,大伙可以按需要看。PE文件中所有节的属性都被定义在节表中,节表由一系列的IMAGE_SECTION_HEADER结构排列而成,每个结构用来描述一个节,结构的排列顺序和它们描述的节在文件中的排列顺序是一致的。全部有效结构的最后以一个空的IMAGE_SECTION_HEADER结构作为结束,所以节表中总的IMAGE_SECTION_HEADER结构数量等于节的数量加一。节表总是被存放在紧接在PE文件头的地方。另外,节表中 IMAGE_SECTION_HEADER 结构的总数总是由PE文件头 IMAGE_NT_HEADERS 结构中的 FileHeader.NumberOfSections 字段来指定的。typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如“.text” //IMAGE_SIZEOF_SHORT_NAME=8 union { DWORD PhysicalAddress; // 物理地址 DWORD VirtualSize; // 真实长度,这两个值是一个联合结构,可以使用其中的任何一个,一般是取后一个 } Misc; DWORD VirtualAddress; // 节区的 RVA 地址 DWORD SizeOfRawData; // 在文件中对齐后的尺寸 DWORD PointerToRawData; // 在文件中的偏移量 DWORD PointerToRelocations; // 在OBJ文件中使用,重定位的偏移 DWORD PointerToLinenumbers; // 行号表的偏移(供调试使用地) WORD NumberOfRelocations; // 在OBJ文件中使用,重定位项数目 WORD NumberOfLinenumbers; // 行号表中行号的数目 DWORD Characteristics; // 节属性如可读,可写,可执行等 } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;Name:区块名。这是一个由8位的ASCII 码名,用来定义区块的名称。多数区块名都习惯性以一个“.”作为开头(例如:.text),这个“.” 实际上是不是必须的。值得我们注意的是,如果区块名超过 8 个字节,则没有最后的终止标志“NULL” 字节。并且前边带有一个“$” 的区块名字会从连接器那里得到特殊的待遇,前边带有“$” 的相同名字的区块在载入时候将会被合并,在合并之后的区块中,他们是按照“$” 后边的字符的字母顺序进行合并的。另外小甲鱼童鞋要跟大家啰嗦一下的是:每个区块的名称都是唯一的,不能有同名的两个区块。但事实上节的名称不代表任何含义,他的存在仅仅是为了正规统一编程的时候方便程序员查看方便而设置的一个标记而已。所以将包含代码的区块命名为“.Data” 或者说将包含数据的区块命名为“.Code” 都是合法的。因此,小甲鱼建议大家:当我们要从PE 文件中读取需要的区块时候,不能以区块的名称作为定位的标准和依据。正确的方法是按照 IMAGE_OPTIONAL_HEADER32 结构中的数据目录字段结合进行定位。Virtual Size:对应的区块的大小,这是区块的数据在没有进行对齐处理前的实际大小。Virtual Address:该区块装载到内存中的RVA 地址。这个地址是按照内存页来对齐的,因此它的数值总是 SectionAlignment 的值的整数倍。在Microsoft 工具中,第一个块的默认 RVA 总为1000h。在OBJ 中,该字段没有意义地,并被设为0。SizeOfRawData:该区块在磁盘中所占的大小。在可执行文件中,该字段是已经被FileAlignment 潜规则处理过的长度。PointerToRawData:该区块在磁盘中的偏移。这个数值是从文件头开始算起的偏移量哦。PointerToRelocations:这哥们在EXE文件中没有意义,在OBJ 文件中,表示本区块重定位信息的偏移值。(在OBJ 文件中如果不是零,它会指向一个IMAGE_RELOCATION 结构的数组)PointerToLinenumbers:行号表在文件中的偏移值,文件的调试信息,于我们没用,鸡肋。NumberOfRelocations:这哥们在EXE文件中也没有意义,在OBJ 文件中,是本区块在重定位表中的重定位数目来着。NumberOfLinenumbers:该区块在行号表中的行号数目,鸡肋。Characteristics:该区块的属性。该字段是按位来指出区块的属性(如代码/数据/可读/可写等)的标志。具体内容可以参考MSDN在线文档:传送门.aspx)下面通过一个例子来详细朔门这些内容:还是以上次那个为例根据以前的内容可以知道这个文件PE头在0xf0的位置,上一次是通过各个结构体大小来找到PE头中这个OptionalHeader结构的地址,但是当时我忘记了,在FileHeader 这个结构中有一个SizeOfOptionalHeader这个域专门用来记录OptionalHeader结构的大小,它在PE头的偏移为0x14也就是在0xf0 + 0x14 = 0x104的位置查看文件得知这个值为0xe0, OptionalHeader偏移0x18 + 大小0xe0 + pe头的偏移0xf0 = 0x1e8根据这个结构中的成员很容易计算出来,这个结构占0x28个字节,这样根据上一个的起始地址 + 0x28就可以得到下一个的地址,这样可以陆陆续续找到所有的节节表中的最后一个为全0,这样这个PE文件中总共有.textbss、.text、.radta、.data、.idata、.rsrc、.reloc这样几个节。接下来读取各个部分的内容,比如说在text节中,VirtualSize = 0x00014360PointerToRawData = 0x000400 VirtualAddress = 0x00011000SizeOfRawData = 0x00014400Characteristics = 0x60000020这些节区都是按照文件中的某个值对齐,然后在紧密排列的,所以根据它在文件中的偏移 + 对齐后的值可以得到下一个节在文件中的偏移地址,根据这点在text节中 PointerToRawData + SizeOfRawData = 0x000400 + 0x00014400 = 0x00014800,而下一个的文件偏移地址正好是这个,这个根据在PE中查找到的数据,发现下一个确实是这个值
2017年03月18日
5 阅读
0 评论
0 点赞
2017-03-16
PE文件详解(三)
本文转自小甲鱼的PE文件详解系列传送门PE文件到内存的映射在执行一个PE文件的时候,windows 并不在一开始就将整个文件读入内存的,二十采用与内存映射文件类似的机制。也就是说,windows 装载器在装载的时候仅仅建立好虚拟地址和PE文件之间的映射关系。当且仅当真正执行到某个内存页中的指令或者访问某一页中的数据时,这个页面才会被从磁盘提交到物理内存,这种机制使文件装入的速度和文件大小没有太大的关系。但是要注意的是,系统装载可执行文件的方法又不完全等同于内存映射文件。当使用内存映射文件的时候,系统对“原著”相当忠实,如果将磁盘文件和内存映像比较的话,可以发现不管是数据本身还是数据之间的相对位置它丫丫的都是完全相同的。而我们知道,在装载可执行文件的时候,有些数据在装入前会被预处理,如重定位等,正因此,装入以后,数据之间的相对位置可能发生微妙的变化。Windows 装载器在装载DOS部分、PE文件头部分和节表(区块表)部分是不进行任何特殊处理的,而在装载节(区块)的时候则会自动按节(区块)的属性做不同的处理。一般情况下,它会处理以下几个方面的内容:内存页的属性;节的偏移地址;节的尺寸;不进行映射的节。内存页的属性:对于磁盘映射文件来说,所有的页都是按照磁盘映射文件函数指定的属性设置的。但是在装载可执行文件时,与节对应的内存页属性要按照节的属性来设置。所以,在同属于一个模块的内存页中,从不同节映射过来的的内存页的属性是不同的。节的偏移地址:节的起始地址在磁盘文件中是按照 IMAGE_OPTIONAL_HEADER32 结构的 FileAlignment 字段的值进行对齐的。而当被加载到内存中时是按照同一结构中的 SectionAlignment 字段的值对其的,两者的值可能不同。所以一个节被装入内存后相对于文件头的偏移和在磁盘文件中的偏移可能是不同的。注意,节事实上就是相同属性数据的组合!当节被装入到内存中的时候,相同一个节所对应的内存页都将被赋予相同的页属性。事实上,Windows 系统对内存属性的设置是以页为单位进行的,所以节在内存中的对齐单位必须至少是一个页的大小。(小甲鱼温馨提示:对于32位操作系统来说,这个值一般是4KB==1000H; 对于64位操作系统这个值一般是8KB==2000H)在磁盘中就没有这个限制,因为在磁盘中排放是以什么为主?肯定是以空间为主导,在磁盘只是存放,不是使用,所以不用设置那么详细的属性。试想想看,如果在磁盘中都是以4KB为大小对齐的话,不够就用0来填充,那么一个只占20字节的数据就要消耗4KB的空间来存放,是不是浪费?有木有??节的尺寸:对节的尺寸的处理主要分为两个方面:第一个方面,正如刚刚我们所讲的,由于磁盘映像和内存映像中节对齐存储单位的不同而导致了长度扩展不同(填充的0数量不同嘛~);第二个方面,是对于包含未初始化数据的节的处理问题。既然是未初始化,那么没有必要为其在磁盘中浪费空间资源,但在内存中不同,因为程序一运行,之前未初始化的数据便有可能要被赋值初始化,那么就必须为他们留下空间。不进行映射的节:有些节并不需要被映射到内存中,例如.reloc节,重定位数据对于文件的执行代码来说是透明的,无作用的,它只是提供Windows 装载器使用,执行代码根本不会去访问到它们,所以没有必要将他们映射到物理内存中。
2017年03月16日
5 阅读
0 评论
0 点赞
2017-03-14
PE文件详解二
本文转自小甲鱼的PE文件相关教程,原文传送门咱接着往下讲解IMAGE_OPTIONAL_HEADER32 结构定义即各个属性的作用!接着我们来谈谈 IMAGE_OPTIONAL_HEADER 结构,正如名字的意思,这是一个可选映像头,是一个可选的结构。但是呢,实际上上节课我们讲解的 IMAGE_FILE_HEADER 结构远远不足以来定义 PE 文件的属性。因此,这些属性在 IMAGE_OPTIONAL_HEADER 结构中进行定义。因此这两个结构联合起来,才是一个完整的 “PE文件结构” 。{ // // Standard fields. // +18h WORD Magic; // 标志字, ROM 映像(0107h),普通可执行文件(010Bh) +1Ah BYTE MajorLinkerVersion; // 链接程序的主版本号 +1Bh BYTE MinorLinkerVersion; // 链接程序的次版本号 +1Ch DWORD SizeOfCode; // 所有含代码的节的总大小 +20h DWORD SizeOfInitializedData; // 所有含已初始化数据的节的总大小 +24h DWORD SizeOfUninitializedData; // 所有含未初始化数据的节的大小 +28h DWORD AddressOfEntryPoint; // 程序执行入口RVA +2Ch DWORD BaseOfCode; // 代码的区块的起始RVA +30h DWORD BaseOfData; // 数据的区块的起始RVA // // NT additional fields. 以下是属于NT结构增加的领域。 // +34h DWORD ImageBase; // 程序的首选装载地址 +38h DWORD SectionAlignment; // 内存中的区块的对齐大小 +3Ch DWORD FileAlignment; // 文件中的区块的对齐大小 +40h WORD MajorOperatingSystemVersion; // 要求操作系统最低版本号的主版本号 +42h WORD MinorOperatingSystemVersion; // 要求操作系统最低版本号的副版本号 +44h WORD MajorImageVersion; // 可运行于操作系统的主版本号 +46h WORD MinorImageVersion; // 可运行于操作系统的次版本号 +48h WORD MajorSubsystemVersion; // 要求最低子系统版本的主版本号 +4Ah WORD MinorSubsystemVersion; // 要求最低子系统版本的次版本号 +4Ch DWORD Win32VersionValue; // 莫须有字段,不被病毒利用的话一般为0 +50h DWORD SizeOfImage; // 映像装入内存后的总尺寸 +54h DWORD SizeOfHeaders; // 所有头 + 区块表的尺寸大小 +58h DWORD CheckSum; // 映像的校检和 +5Ch WORD Subsystem; // 可执行文件期望的子系统 +5Eh WORD DllCharacteristics; // DllMain()函数何时被调用,默认为 0 +60h DWORD SizeOfStackReserve; // 初始化时的栈大小 +64h DWORD SizeOfStackCommit; // 初始化时实际提交的栈大小 +68h DWORD SizeOfHeapReserve; // 初始化时保留的堆大小 +6Ch DWORD SizeOfHeapCommit; // 初始化时实际提交的堆大小 +70h DWORD LoaderFlags; // 与调试有关,默认为 0 +74h DWORD NumberOfRvaAndSizes; // 下边数据目录的项数,这个字段自Windows NT 发布以来一直是16 +78h IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 数据目录表 } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;上述代码中的偏移地址是相对于PE头的偏移地址不是针对IMAGE_OPTIONAL_HEADER32的偏移其中重要的几个字段如下:AddressOfEntryPoint字段:指出文件被执行时的入口地址,这是一个RVA地址(RVA的含义在下一节中详细介绍)。如果在一个可执行文件上附加了一段代码并想让这段代码首先被执行,那么只需要将这个入口地址指向附加的代码就可以了。ImageBase字段:指出文件的优先装入地址。也就是说当文件被执行时,如果可能的话,Windows优先将文件装入到由ImageBase字段指定的地址中。当这个地址被其他程序或者模块霸占时,系统会进行重定向,将它放置到其他地址处链接器产生可执行文件的时候对应这个地址来生成机器码,所以当文件被装入这个地址时不需要进行重定位操作,装入的速度最快。如果文件被装载到其他地址的话,将不得不进行重定位操作,这样就要慢一点。对于EXE文件来说,由于每个文件总是使用独立的虚拟地址空间,优先装入地址不可能被其他模块占据,所以EXE总是能够按照这个地址装入。这也意味着EXE文件不再需要重定位信息。对于DLL文件来说,由于多个DLL文件全部使用宿主EXE文件的地址空间,不能保证优先装入地址没有被其他的DLL使用,所以DLL文件中必须包含重定位信息以防万一。因此,在前面介绍的 IMAGE_FILE_HEADER 结构的 Characteristics 字段中,DLL 文件对应的 IMAGE_FILE_RELOCS_STRIPPED 位总是为0,而EXE文件的这个标志位总是为1。在链接的时候,可以通过对link.exe指定/base:address选项来自定义优先装入地址,如果不指定这个选项的话,一般EXE文件的默认优先装入地址被定为00400000h,而DLL文件的默认优先装入地址被定为10000000h。SectionAlignment 字段和 FileAlignment字段:SectionAlignment字段指定了节被装入内存后的对齐单位。也就是说,每个节被装入的地址必定是本字段指定数值的整数倍。而FileAlignment字段指定了节存储在磁盘文件中时的对齐单位。Subsystem字段:指定使用界面的子系统,这个字段决定了系统如何为程序建立初始的界面,链接时的/subsystem:**选项指定的就是这个字段的值,在前面章节的编程中我们早已知道:如果将子系统指定为Windows CUI,那么系统会自动为程序建立一个控制台窗口,而指定为Windows GUI的话,窗口必须由程序自己建立。DataDirectory字段:这个字段可以说是最重要的字段之一,它由16个相同的IMAGE_DATA_DIRECTORY结构组成。虽然PE文件中的数据是按照装入内存后的页属性归类而被放在不同的节中的,但是这些处于各个节中的数据按照用途可以被分为导出表、导入表、资源、重定位表等数据块,这16个IMAGE_DATA_DIRECTORY结构就是用来定义多种不同用途的数据块的IMAGE_DATA_DIRECTORY结构的定义很简单,它仅仅指出了某种数据块的位置和长度。IMAGE_DATA_DIRECTORY STRUCT VirtualAddress DWORD ? ; 数据的起始RVA isize DWORD ? ; 数据块的长度 IMAGE_DATA_DIRECTORY ENDS在PE文件中寻找特定的数据时就是从这些IMAGE_DATA_DIRECTORY结构开始的。比如要存取资源,那么必须从第3个IMAGE_DATA_DIRECTORY结构(索引为2)中得到资源数据块的大小和位置;同理,如果要查看PE文件导入了哪些DLL文件的哪些API函数,那就必须首先从第2个IMAGE_DATA_DIRECTORY结构得到导入表的位置和大小。最后再根据这些信息接着解析上节中的PE文件PE头所在位置的偏移为0xf8 + IMAGE_OPTIONAL_HEADER 结构在IMAGE_NT_HEADERS结构中的偏移0x18 = OptionalHeader成员的地址0x110被选中的这块就是结构IMAGE_NT_HEADERS中的内容:从图中可以找到上面所表述的各个部分偏移的地址和它对应的具体的内容:AddressOfEntryPoint所在地址为:偏移 0x28 + 0xf8 = 0x120,值为0x011285ImageBase所在地址为:偏移0x34 + 0xf8 = 0x12c,值为0x00400000SectionAlignment 所在地址为:偏移0x38 + 0xf8 = 130,值为0x00001000,也就是一页内存FileAlignment所在的地址为偏移0x3c + 0xf8 = 0x134,值为0x00000200,也就是512,是一簇的大小Subsystem所在地址为:偏移0x5c + 0xf8 = 0x154,值为0x0003,也就是控制台程序DataDirectory所在地址为偏移0x78 + 0xf8 = 170 ,也是就是从0x170开始往后每8个字节为一个元素,指定了一些数据表的地址
2017年03月14日
3 阅读
0 评论
0 点赞
2017-03-13
PE格式详解讲解1
这篇文章主要转载自小甲鱼的加密解密部分,然后补充加上我自己的少许内容,原文地址-->[传送门](http://blog.fishc.com/1551.html)下面的内容主要是围绕这个图来进行MS-DOS头部这个头部是为了兼容早期的DOS系统,PE文件的第一个字节起始于一个传统的MS-DOS头,被称为IMAGE_DOS_HEADER,这个结构体完整的定义如下:(注:最左边是文件头的偏移量。) IMAGE_DOS_HEADER STRUCT { +0h WORD e_magic // Magic DOS signature MZ(4Dh 5Ah) DOS可执行文件标记 +2h WORD e_cblp // Bytes on last page of file +4h WORD e_cp // Pages in file +6h WORD e_crlc // Relocations +8h WORD e_cparhdr // Size of header in paragraphs +0ah WORD e_minalloc // Minimun extra paragraphs needs +0ch WORD e_maxalloc // Maximun extra paragraphs needs +0eh WORD e_ss // intial(relative)SS value DOS代码的初始化堆栈SS +10h WORD e_sp // intial SP value DOS代码的初始化堆栈指针SP +12h WORD e_csum // Checksum +14h WORD e_ip // intial IP value DOS代码的初始化指令入口[指针IP] +16h WORD e_cs // intial(relative)CS value DOS代码的初始堆栈入口 +18h WORD e_lfarlc // File Address of relocation table +1ah WORD e_ovno // Overlay number +1ch WORD e_res[4] // Reserved words +24h WORD e_oemid // OEM identifier(for e_oeminfo) +26h WORD e_oeminfo // OEM information;e_oemid specific +29h WORD e_res2[10] // Reserved words +3ch DWORD e_lfanew // Offset to start of PE header 指向PE文件头 } IMAGE_DOS_HEADER ENDS这个头中只有两个需要重点关注:e_magic:DOS可执行文件的标记,一般是两个字节,标记值是固定的,只有当其值是4D5A的时候,这个文件才被识别为可执行文件,这个结构在文件头位置e_lfanew:指向PE文件头的指针,这个在偏移3c处利用UE来分析可以看到,这两个在文件中的位置如下:PE文件头PE Header 是PE相关结构NT映像头(IMAGE_NT_HEADER)的简称,里边包含着许多PE装载器用到的重要字段。装载到内存中时,PE状态器将从IMAGE_DOS_HEADER结构中的e_lfanew字段中岛PE Header的起始偏移量,加上基地址就得到PE文件的头指针PEHeader = ImageBase + DosHeader->e_lfnewIMAGE_NT_HEADERS STRUCT { +0h DWORDSignature +4h IMAGE_FILE_HEADER FileHeader +18h IMAGE_OPTIONAL_HEADER32OptionalHeader } IMAGE_NT_HEADERS ENDSSignature字段在一个有效的 PE 文件里,Signature 字段被设置为00004550h, ASCII 码字符是“PE00”。标志这 PE 文件头的开始。“PE00” 字符串是 PE 文件头的开始,DOS 头部的 e_lfanew 字段正是指向这里。一般如果需要验证一个文件是否为PE文件就要验证这个标志是否为这个值FileHeader字段这个字段是一个IMAGE_FILE_HEADER结构,他的定义如下:struct IMAGE_FILE_HEADER { WORD Machine; //运行平台 WORD NumberOfSections; //区块表的个数 DWORD TimeDataStamp;//文件创建时间,是从1970年至今的秒数 DWORD PointerToSymbolicTable;//指向符号表的指针 DWORD NumberOfSymbols;//符号表的数目 WORD SizeOfOptionalHeader;//IMAGE_NT_HEADERS结构中OptionHeader成员的大小,对于win32平台这个值通常是0x00e0 WORD Characteristics;//文件的属性值 }(1)Machine:可执行文件的目标CPU类型,记载可执行文件的目标CPU类型,各个值表示的平台如下:ValueMeaningIMAGE_FILE_MACHINE_I386 0x014cx86IMAGE_FILE_MACHINE_IA64 0x0200Intel ItaniumIMAGE_FILE_MACHINE_AMD64 0x8664x64(2)NumberOfSection: 区块的数目。(注:区块表是紧跟在 IMAGE_NT_HEADERS 后边的)(3)TimeDataStamp: 表明文件是何时被创建的。这个值是自1970年1月1日以来用格林威治时间(GMT)计算的秒数,这个值是比文件系统(FILESYSTEM)的日期时间更加精确的指示器。(4)PointerToSymbolTable: COFF 符号表的文件偏移位置,现在基本没用了。(5)NumberOfSymbols: 如果有COFF 符号表,它代表其中的符号数目,COFF符号是一个大小固定的结构,如果想找到COFF 符号表的结束位置,则需要这个变量。(6)SizeOfOptionalHeader: 紧跟着IMAGE_FILE_HEADER 后边的数据结构(IMAGE_OPTIONAL_HEADER)的大小。(对于32位PE文件,这个值通常是00E0h;对于64位PE32+文件,这个值是00F0h )。(7)Characteristics: 文件属性,有选择的通过几个值可以运算得到。( 这些标志的有效值是定义于 winnt.h 内的 IMAGE_FILE_** 的值,具体含义见下表。普通的EXE文件这个字段的值一般是 0100h,DLL文件这个字段的值一般是 210Eh。)小甲鱼温馨提示:多种属性可以通过 “或运算” 使得同时拥有! 下面分析上述程序中的IMAGE_NT_HEADERS 结构:PE文件头的标记为0x00004550 后面所有方框都是IMAGE_FILE_HEADER的内容: 运行平台的值为0x014c,查上面的表得知,它是运行在intel x86平台下 区块表的个数为0x0007 文件创建时间为0x5848fc56 指向符号表的指针为NULL,也就是没有符号表,符号表是用于调试的,在这即使没有这个东西也不影响调试 符号表的数目为0 OptionHeader成员的大小为0x00e0 文件的属性值为0x0102
2017年03月13日
6 阅读
0 评论
0 点赞
2017-03-12
PE文件简介
PE文件的全称是Portable Executable,意为可移植的可执行的文件,常见的EXE、DLL、OCX、SYS、COM都是PE文件,PE文件是微软Windows操作系统上的程序文件(可能是间接被执行,如DLL)。它是跨win32平台的,只要运行在Windows上,不管是在什么体系的CPU上都可以运行PE文件使用平面的地址空间,所有代码和数据都合并在一起,组成一个很大的结构,文件的内容被分为不同的区块,块中包含代码和数据,每个区块在内存中都有其对应的权限,比如有的快只读,有的只写或者有的只可执行。一般有以下区块:.text : 是在编译或者汇编结束时产生的一种区块,是指令的代码.rdata :是运行时的只读数据,也就是所说的const常量.data:初始化的数据块,也就是全局变量.idata:包含其他外来dll的函数以及数据信息,即输入表.rsrc:包含全部的资源,如图标、菜单、位图等等每个区块在内存中按页边界对齐,区块没有大小限制,是一个连续的结构,每个块都有对应的属性pe文件的优势:磁盘上的数据结构与在内存中的结构是一致的相关名词:入口点(EntryPoint):程序执行的第一条指令所在的内存地址文件偏移地址(FileOffset)PE文件存储在磁盘上的时候,各个数据的地址相对于文件头的距离为文件的偏移地址虚拟地址(VirtuallAddress VA)应用程序访问的逻辑地址也就是它的虚拟地址基地址(ImageBase):文件被映射到内存时,初始地址叫做基地址pe文件大致结构如下图所示:一般在说到PE文件时都会涉及到以下几个名词基地址(ImageBase):PE文件被加载到内存中的首地址,是这个模块的句柄,可以使用函数GetModuleHandle来获取文件的偏移地址:PE文件中各个部分相对于文件头的偏移相对虚拟地址(RVA):PE结构被映射到内存中后,某个位置所在内存相对于基地址的偏移一般可执行文件被PE加载器加载到内存中后,文件的基本格式不会发生改变,只是会将各个块按照页来进行对其,PE文件在磁盘与在内存中的对应关系大致如下图所示:
2017年03月12日
2 阅读
0 评论
0 点赞
2017-03-08
hook键盘驱动中的分发函数实现键盘输入数据的拦截
我自己在看《寒江独钓》这本书的时候,书中除了给出了利用过滤的方式来拦截键盘数据之外,也提到了另外一种方法,就是hook键盘分发函数,将它替换成我们自己的,然后再自己的分发函数中获取这个数据的方式,但是书中并没有明确给出代码,我结合书中所说的一些知识加上网上找到的相关资料,自己编写了相关代码,并且试验成功了,现在给出详细的方法和代码。用这种方式时首先根据ObReferenceObjectByName函数来根据对应的驱动名称获取驱动的驱动对象指针。该函数是一个未导出函数,在使用时只需要先声明即可,函数原型如下:NTSTATUS ObReferenceObjectByName( PUNICODE_STRING ObjectName, //对应对象的名称 ULONG Attributes, //相关属性,一般给OBJ_CASE_INSENSITIVE PACCESS_STATE AccessState, //描述信息的一个结构体指针,一般给NULL ACCESS_MASK DesiredAccess, //以何种权限打开,一般给0如果或者FILL_ALL_ACCESS给它所有权限 POBJECT_TYPE ObjectType, //该指针是什么类型的指针,如果是设备对象给IoDeviceObjectType如果是驱动对象则给IoDriverObjectType KPROCESSOR_MODE AccessMode, //一般给NULL PVOID ParseContext, //附加参数,一般给NULL PVOID *pObject //用来接收相关指针的输出参数 );IoDeviceObjectType或者IoDriverObjectType也是未导出的,在使用之前需要先申明他们,例如extern POBJECT_TYPE IoDriverObjectType; extern POBJECT_TYPE IoDeviceObjectType;然后将该驱动对象中原始的分发函数保存起来,以便在hook之后调用或者在驱动卸载时恢复接下来hook相关函数,要截取键盘的数据,一般采用的是hook read函数在read函数中设置IRP的完成例程,然后调用原始的分发函数,一定要注意调用原始的分发函数,否则自己很难实现类似的功能,一旦实现不了,那么Windows上的键盘功能将瘫痪。在完成例程中解析穿回来的IRP就可得到对应键盘的信息。下面是具体的实现代码#define KDB_DRIVER_NAME L"\\Driver\\KbdClass" //键盘驱动的名称为KbdClass NTSTATUS ObReferenceObjectByName( PUNICODE_STRING ObjectName, ULONG Attributes, PACCESS_STATE AccessState, ACCESS_MASK DesiredAccess, POBJECT_TYPE ObjectType, KPROCESSOR_MODE AccessMode, PVOID ParseContext, PVOID *pObject); extern POBJECT_TYPE IoDriverObjectType; PDRIVER_OBJECT g_pKdbDriverObj; //键盘的驱动对象,保存这个是为了在卸载时还原它的分发函数 PDRIVER_DISPATCH g_oldDispatch[IRP_MJ_MAXIMUM_FUNCTION+1]; int g_KeyCount = 0; //记录键盘IRP的数量,当键盘的请求没有被处理完成时不能卸载这个驱动 VOID DriverUnload(PDRIVER_OBJECT DriverObject) { LARGE_INTEGER WaitTime; int i = 0; DbgPrint("KBD HOOK: Entry DriverUnload\n"); //等待5s WaitTime = RtlConvertLongToLargeInteger(-5 * 1000000000 / 100); //如果IRP没有被处理完成,等待5s再检测是否处理完成 while(0 != g_KeyCount) { KeDelayExecutionThread(KernelMode, FALSE, &WaitTime); } for(i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++) { //还原对应的分发函数 g_pKdbDriverObj->MajorFunction[i] = g_oldDispatch[i]; } } NTSTATUS c2cReadComplete( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PVOID Context ) { PUCHAR pBuffer; ULONG uLength; int i = 0; if(NT_SUCCESS(Irp->IoStatus.Status)) { pBuffer = (PUCHAR)(Irp->AssociatedIrp.SystemBuffer); uLength = Irp->IoStatus.Information; for(i = 0; i < uLength; i++) { //在完成函数中只是简单的输出了对应的16进制数 DbgPrint("cap2ctrl: Key %02x\r\n", pBuffer[i]); } } //每当一个IRP完成时,未完成的IRP数量都需要减一 g_KeyCount--; if(Irp->PendingReturned) { IoMarkIrpPending( Irp ); } return Irp->IoStatus.Status; } NTSTATUS c2cReadDispathc( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp ) { PIO_STACK_LOCATION pIroStack; DbgPrint("Hook By Me!\n"); //每当进入这个分发函数时都需要将这个未完成IRP数量加一 g_KeyCount++; //设置完成函数 //在这只能用这种方式,我自己试过用IoSetCompletionRoutine ,它注册的完成函数没有被调用,我也不知道为什么 pIroStack = IoGetCurrentIrpStackLocation(Irp); pIroStack->Control = SL_INVOKE_ON_SUCCESS|SL_INVOKE_ON_ERROR|SL_INVOKE_ON_CANCEL; pIroStack->CompletionRoutine = (PIO_COMPLETION_ROUTINE)c2cReadComplete; //调用原始的分发函数 return (g_oldDispatch[IRP_MJ_READ])(DeviceObject, Irp); } NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegistryPath) { int i = 0; PDRIVER_OBJECT pKbdDriverObj; UNICODE_STRING uKbdDriverName; NTSTATUS status; UNREFERENCED_PARAMETER(pRegistryPath); DbgPrint("cap2ctrl: Entry DriverEntry\n"); RtlInitUnicodeString(&uKbdDriverName, KDB_DRIVER_NAME); status = ObReferenceObjectByName(&uKbdDriverName, OBJ_CASE_INSENSITIVE, NULL, 0, IoDriverObjectType, KernelMode, NULL, &g_pKdbDriverObj); if(!NT_SUCCESS(status)) { return status; } //保存原始的派遣函数 for(i = 0; i < IRP_MJ_MAXIMUM_FUNCTION+1; i++) { g_oldDispatch[i] = g_pKdbDriverObj->MajorFunction[i]; } //HOOK读请求的派遣函数 g_pKdbDriverObj->MajorFunction[IRP_MJ_READ] = c2cReadDispathc; pDriverObject->DriverUnload = DriverUnload; //绑定设备 return STATUS_SUCCESS; }
2017年03月08日
4 阅读
0 评论
0 点赞
2017-03-01
遍历系统中加载的驱动程序以及通过设备对象指针获取设备对象名称
遍历系统中加载的驱动可以在R3层完成,通过几个未导出的函数:ZwOpenDirectoryObject、ZwQueryDirectoryObject,下面是具体的代码。//在这定义些基本的数据结构,这些本身是在R0层用的比较多的 typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING; typedef ULONG NTSTATUS; // 对象属性定义 typedef struct _OBJECT_ATTRIBUTES { ULONG Length; HANDLE RootDirectory; UNICODE_STRING *ObjectName; ULONG Attributes; PSECURITY_DESCRIPTOR SecurityDescriptor; PSECURITY_QUALITY_OF_SERVICE SecurityQualityOfService; } OBJECT_ATTRIBUTES, *POBJECT_ATTRIBUTES; // 基本信息定义 typedef struct _DIRECTORY_BASIC_INFORMATION { UNICODE_STRING ObjectName; UNICODE_STRING ObjectTypeName; } DIRECTORY_BASIC_INFORMATION, *PDIRECTORY_BASIC_INFORMATION; // 返回值或状态类型定义 #define OBJ_CASE_INSENSITIVE 0x00000040L #define DIRECTORY_QUERY (0x0001) #define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth #define STATUS_MORE_ENTRIES ((NTSTATUS)0x00000105L) #define STATUS_BUFFER_TOO_SMALL ((NTSTATUS)0xC0000023L) // 初始化对象属性宏定义 #define InitializeObjectAttributes( p, n, a, r, s ) { \ (p)->Length = sizeof(OBJECT_ATTRIBUTES); \ (p)->RootDirectory = r; \ (p)->Attributes = a; \ (p)->ObjectName = n; \ (p)->SecurityDescriptor = s; \ (p)->SecurityQualityOfService = NULL; \ } // 字符串初始化 //用来存储设备驱动对象名称的链表 extern vector<CString> g_DriverNameList; vector<DRIVER_INFO> g_DriverNameList; typedef VOID(CALLBACK* RTLINITUNICODESTRING)(PUNICODE_STRING, PCWSTR); RTLINITUNICODESTRING RtlInitUnicodeString; // 打开对象 typedef NTSTATUS(WINAPI *ZWOPENDIRECTORYOBJECT)( OUT PHANDLE DirectoryHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes ); ZWOPENDIRECTORYOBJECT ZwOpenDirectoryObject; // 查询对象 typedef NTSTATUS (WINAPI *ZWQUERYDIRECTORYOBJECT)( IN HANDLE DirectoryHandle, OUT PVOID Buffer, IN ULONG BufferLength, IN BOOLEAN ReturnSingleEntry, IN BOOLEAN RestartScan, IN OUT PULONG Context, OUT PULONG ReturnLength OPTIONAL ); ZWQUERYDIRECTORYOBJECT ZwQueryDirectoryObject; // 关闭已经打开的对象 typedef NTSTATUS (WINAPI *ZWCLOSE)(IN HANDLE Handle); ZWCLOSE ZwClose; BOOL EnumDriver() { HMODULE hNtdll = NULL; UNICODE_STRING strDirName; OBJECT_ATTRIBUTES oba; NTSTATUS ntStatus; HANDLE hDirectory; hNtdll = LoadLibrary(_T("ntdll.dll")); if (NULL == hNtdll) { return FALSE; } RtlInitUnicodeString = (RTLINITUNICODESTRING)GetProcAddress(hNtdll, "RtlInitUnicodeString"); ZwOpenDirectoryObject = (ZWOPENDIRECTORYOBJECT)GetProcAddress(hNtdll, "ZwOpenDirectoryObject"); ZwQueryDirectoryObject = (ZWQUERYDIRECTORYOBJECT)GetProcAddress(hNtdll, "ZwQueryDirectoryObject"); ZwClose = (ZWCLOSE)GetProcAddress(hNtdll, "ZwClose"); RtlInitUnicodeString(&strDirName, _T("\\Driver")); InitializeObjectAttributes(&oba, &strDirName, OBJ_CASE_INSENSITIVE, NULL, NULL); ntStatus = ZwOpenDirectoryObject(&hDirectory, DIRECTORY_QUERY, &oba); if (ntStatus != STATUS_SUCCESS) { return FALSE; } PDIRECTORY_BASIC_INFORMATION pBuffer = NULL; PDIRECTORY_BASIC_INFORMATION pBuffer2 = NULL; ULONG ulLength = 0x800; // 2048 ULONG ulContext = 0; ULONG ulRet = 0; // 查询目录对象 do { if (pBuffer != NULL) { free(pBuffer); } ulLength = ulLength * 2; pBuffer = (PDIRECTORY_BASIC_INFORMATION)malloc(ulLength); if (NULL == pBuffer) { if (pBuffer != NULL) { free(pBuffer); } if (hDirectory != NULL) { ZwClose(hDirectory); } return FALSE; } ntStatus = ZwQueryDirectoryObject(hDirectory, pBuffer, ulLength, FALSE, TRUE, &ulContext, &ulRet); } while (ntStatus == STATUS_MORE_ENTRIES || ntStatus == STATUS_BUFFER_TOO_SMALL); if (STATUS_SUCCESS == ntStatus) { pBuffer2 = pBuffer; while ((pBuffer2->ObjectName.Length != 0) && (pBuffer2->ObjectTypeName.Length != 0)) { CString strDriverName; strDriverName = pBuffer2->ObjectName.Buffer; g_DriverNameList.push_back(strDriverName); pBuffer2++; } } if (pBuffer != NULL) { free(pBuffer); } if (hDirectory != NULL) { ZwClose(hDirectory); } return TRUE; }通过设备对象的地址来获取设备对象的名称一般是在R0层完成,下面是具体的代码//定义相关的结构体和宏 typedef struct _OBJECT_CREATE_INFORMATION { ULONG Attributes; HANDLE RootDirectory; PVOID ParseContext; KPROCESSOR_MODE ProbeMode; ULONG PagedPoolCharge; ULONG NonPagedPoolCharge; ULONG SecurityDescriptorCharge; PSECURITY_DESCRIPTOR SecurityDescriptor; PSECURITY_QUALITY_OF_SERVICE SecurityQos; SECURITY_QUALITY_OF_SERVICE SecurityQualityOfService; } OBJECT_CREATE_INFORMATION, * POBJECT_CREATE_INFORMATION; typedef struct _OBJECT_HEADER { LONG PointerCount; union { LONG HandleCount; PSINGLE_LIST_ENTRY SEntry; }; POBJECT_TYPE Type; UCHAR NameInfoOffset; UCHAR HandleInfoOffset; UCHAR QuotaInfoOffset; UCHAR Flags; union { POBJECT_CREATE_INFORMATION ObjectCreateInfo; PVOID QuotaBlockCharged; }; PSECURITY_DESCRIPTOR SecurityDescriptor; QUAD Body; } OBJECT_HEADER, * POBJECT_HEADER; #define NUMBER_HASH_BUCKETS 37 typedef struct _OBJECT_DIRECTORY { struct _OBJECT_DIRECTORY_ENTRY* HashBuckets[NUMBER_HASH_BUCKETS]; struct _OBJECT_DIRECTORY_ENTRY** LookupBucket; BOOLEAN LookupFound; USHORT SymbolicLinkUsageCount; struct _DEVICE_MAP* DeviceMap; } OBJECT_DIRECTORY, * POBJECT_DIRECTORY; typedef struct _OBJECT_HEADER_NAME_INFO { POBJECT_DIRECTORY Directory; UNICODE_STRING Name; ULONG Reserved; #if DBG ULONG Reserved2 ; LONG DbgDereferenceCount ; #endif } OBJECT_HEADER_NAME_INFO, * POBJECT_HEADER_NAME_INFO; #define OBJECT_TO_OBJECT_HEADER( o ) \ CONTAINING_RECORD( (o), OBJECT_HEADER, Body ) #define OBJECT_HEADER_TO_NAME_INFO( oh ) ((POBJECT_HEADER_NAME_INFO) \ ((oh)->NameInfoOffset == 0 ? NULL : ((PCHAR)(oh) - (oh)->NameInfoOffset))) void GetDeviceName(PDEVICE_OBJECT pDeviceObj) { POBJECT_HEADER ObjectHeader; POBJECT_HEADER_NAME_INFO ObjectNameInfo; if ( pDeviceObj == NULL ) { DbgPrint( "pDeviceObj is NULL!\n" ); return; } // 得到对象头 ObjectHeader = OBJECT_TO_OBJECT_HEADER( pDeviceObj ); if ( ObjectHeader ) { // 查询设备名称并打印 ObjectNameInfo = OBJECT_HEADER_TO_NAME_INFO( ObjectHeader ); if ( ObjectNameInfo && ObjectNameInfo->Name.Buffer ) { DbgPrint( "Driver Name:%wZ - Device Name:%wZ - Driver Address:0x%x - Device Address:0x%x\n", &pDeviceObj->DriverObject->DriverName, &ObjectNameInfo->Name, pDeviceObj->DriverObject, pDeviceObj ); } // 对于没有名称的设备,则打印 NULL else if ( pDeviceObj->DriverObject ) { DbgPrint( "Driver Name:%wZ - Device Name:%S - Driver Address:0x%x - Device Address:0x%x\n", &pDeviceObj->DriverObject->DriverName, L"NULL", pDeviceObj->DriverObject, pDeviceObj ); } } }
2017年03月01日
5 阅读
0 评论
0 点赞
2017-02-16
驱动开发中的常用操作
这篇文章会持续更新,由于在驱动中,有许多常用的操作代码几乎不变,而我自己有时候长时间不用经常忘记,所以希望在这把一些常用的操作记录下来,当自己遗忘的时候,有个参考创建设备对象创建设备对象使用函数IoCreateDevice,它的参数如下:NTSTATUS IoCreateDevice( IN PDRIVER_OBJECT DriverObject, IN ULONG DeviceExtensionSize, IN PUNICODE_STRING DeviceName OPTIONAL, IN DEVICE_TYPE DeviceType, IN ULONG DeviceCharacteristics, IN BOOLEAN Exclusive, OUT PDEVICE_OBJECT *DeviceObject );第一个参数是驱动对象第二个参数是设备对象扩展的大小,它会自动根据大小生成一个内存空间,与对应设备绑定第三个参数是驱动名称第四个参数是驱动的类型,一般用作过滤设备的驱动类型为FILE_DEVICE_UNKNOWN第五个参数一般给FILE_DEVICE_SECURE_OPEN第六个参数表示设备是否为独占模式,一般给FALSE第七个参数是设备驱动的二级指针,用来返回生成的设备驱动的指针创建一个过滤设备的代码如下://创建设备对象 status = IoCreateDevice(pDriverObject, sizeof(LIST_ENTRY), &uDeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &pDeviceObject); //为设备对象设置相关标识 pDeviceObject->Flags |= DO_BUFFERED_IO;IRP的完成在某些我们不需要进行特殊处理,但是又得需要对这个IRP进行处理的时候,一般采用完成处理的方式,这种方式主要使用函数IoCompleteRequest,使用例子如下:Irp->IoStatus.Information = 0; //设置返回给应用层的缓冲区的大小 Irp->IoStatus.Status = STATUS_SUCCESS;//给应用层当前IO操作返回成功 IoCompleteRequest(Irp, IO_NO_INCREMENT);//结束IRP在派遣函数中拿IRP的主功能号IRP中保存了它的主功能号和副功能号,他们都被存储在IRP的栈中,下面是基本的代码pStack = IoGetCurrentIrpStackLocation(Irp); //获取IRP栈 IrpCode = pStack->MajorFunction;在MJ_DEVICE_CONTROL类型的IRP中得到对应的控制码CtrlCode = pStack->Parameters.DeviceIoControl.IoControlCode;获取驱动所在的进程这个方法目前只在XP上实验过,win7或者更高版本可能有些不同。获取当前进程主要在EPROCESS结构找到名为ProcessName的项,由于这个结构微软并没有公开,所以可能以后会根据系统版本的不同它的偏移可能也有些许不同。下面是具体的代码pCurrProcess = IoGetCurrentProcess(); RtlInitUnicodeString(&uProcessName, (PTSTR)((ULONG)pCurrProcess + 0x174)); //这个偏移量是在xp上有效,是通过WinDbg获取到的,如果有变化,也可以通过windbg重新得到数据 代码所处内存的划分在驱动程序中,一定要非常小心的为每个函数,数据划分内存块,否则会出现蓝屏现象,比如处在DISPATCH_LEVEL的代码,只能位于非分页内存,因为这个IRQL下的代码不能被打断,如果发生缺页中断,那么只会造成蓝屏现象。而PASSIVE_LEVLE的代码则没有这个限制。下面是定义函数和数据在不同内存页的写法#define PAGEDCODE code_seg("PAGE") //分页内存 #define LOCKEDCODE code_seg()//非分页内存 #define INITCODE code_seg("INIT")//处在这种类型的代码,当函数执行完成后,系统会立马回收它所在的内存页 #define PAGEDDATA data_seg("PAGE") #define LOCKEDDATA data_seg() #define INITDATA data_seg("INIT") //下面是使用这些宏的例子,使用时只需要在函数或者数据前加上对应的宏 LOCKEDCODE void test() { }给编译器提示,函数某些参数在函数中不使用一般在编译驱动时,如果函数参数或者在函数内部定义了某些变量在函数中没有使用的话,编译器会报错,但是有的函数原型是系统规定,但是有些参数又确实用不到,这个时候有两种方式,一种是关掉相关的编译选项,另一种是使用宏告知编译器,这个变量在函数中不使用.UNREFERENCED_PARAMETER(RegistryPath);获取系统时间这里的获取系统时间一般是指获取时间并将它转化我们熟悉的年月日、时分秒的格式,一般的步骤如下:利用函数KeGetSystemTime()获取系统时间,这个时间是格林尼治时间从1601年起至今经历的时间,单位为100ns利用ExSystemTimeToLocalTime()将上述的格林尼治时间转化为本时区的时间,这个值得含义和单位与上述的相同利用函数RtlTimeToTimeFields()将本地时间转化为带有年月日格式的时间函数的第二个参数是TIME_FIELDS结构,他的定义如下:typedef struct TIME_FIELDS { CSHORT Year; CSHORT Month; CSHORT Day; CSHORT Hour; CSHORT Minute; CSHORT Second; CSHORT Milliseconds; CSHORT Weekday; } TIME_FIELDS;下面是一个时间转化的例子 LARGE_INTEGER current_system_time; TIME_FIELDS time_fields; LARGE_INTEGER current_local_time; KeQuerySystemTime(¤t_system_time); ExSystemTimeToLocalTime(¤t_system_time, ¤t_local_time); RtlTimeToTimeFields(¤t_local_time, &time_fields); DbgPrint("Current Time: %d/%d/%d %d:%d:%d\n", time_fields.Year, time_fields.Month, time_fields.Day, time_fields.Hour, time_fields.Minute, time_fields.Second);他们三个可以互相转化,下面是它们之间转化的一个示意图:文件读写文件读写一般需要进行这样几步使用InitializeObjectAttributes初始化一个OBJECT_ATTRIBUTES对象使用ZwCreateFile创建一个文件句柄调用ZwReadFile或者ZwWriteFile读写文件这里面复杂的是InitializeObjectAttributes和ZwCreateFile传参的问题,好在这两个函数在调用时,一般传参都是固定的。VOID InitializeObjectAttributes( OUT POBJECT_ATTRIBUTES InitializedAttributes, IN PUNICODE_STRING ObjectName, //传希望打开的文件名称或者设备对象名称 IN ULONG Attributes, //权限一般给OBJ_CASE_INSENSITIVE IN HANDLE RootDirectory, //根目录,一般给NULL IN PSECURITY_DESCRIPTOR SecurityDescriptor//安全属性,一般给NULL );NTSTATUS ZwCreateFile( __out PHANDLE FileHandle, //返回的文件句柄 __in ACCESS_MASK DesiredAccess, //权限,如果希望对文件进行同步操作,需要额外加上SYNCHRONIZE __in POBJECT_ATTRIBUTES ObjectAttributes, __out PIO_STATUS_BLOCK IoStatusBlock, //一般不怎么用这个输出参数,但是的给值 __in_opt PLARGE_INTEGER AllocationSize,//一般给NULL __in ULONG FileAttributes,//文件属性,一般给FILE_ATTRIBUTE_NORMAL __in ULONG ShareAccess,//共享属性一般给0 __in ULONG CreateDisposition,//创建的描述信息,根据MSDN很容易决定 __in ULONG CreateOptions, //如果是同步操作,一般加上FILE_SYNCHRONOUS_IO_NONALERT,如果是异步操作一般给0 __in_opt PVOID EaBuffer, //一般给NULL __in ULONG EaLength//一般给0 );下面是读写不同设备的相关代码//同步读取驱动的设备对象 NTSTATUS status; HANDLE hDeviceA; OBJECT_ATTRIBUTES ObjAtrribute; UNICODE_STRING uDeviceName; IO_STATUS_BLOCK status_block; RtlInitUnicodeString(&uDeviceName, DEVICE_NAME); InitializeObjectAttributes(&ObjAtrribute, &uDeviceName, OBJ_CASE_INSENSITIVE, NULL, NULL); status = ZwCreateFile(&hDeviceA, SYNCHRONIZE | FILE_READ_ATTRIBUTES, &ObjAtrribute,&status_block, NULL, FILE_ATTRIBUTE_NORMAL, FILE_SHARE_READ, FILE_OPEN, FILE_SYNCHRONOUS_IO_NONALERT, NULL, 0); if(!NT_SUCCESS(status)) { return status; } ZwReadFile(hDeviceA, NULL, NULL, NULL, &status_block, NULL, 0, NULL, NULL); ZwClose(hDeviceA); return STATUS_SUCCESS;//同步读取文件 HANDLE hFile = NULL; OBJECT_ATTRIBUTES ObjAttribute; IO_STATUS_BLOCK IoStatusBlock; UNICODE_STRING uFileName; WCHAR wFname[] = L"\\??\\C:\\log.txt"; CHAR buf[] = "Hello World\r\n"; NTSTATUS status = STATUS_SUCCESS; FILE_STANDARD_INFORMATION fsi = {0}; PCHAR pBuffer = NULL; RtlInitUnicodeString(&uFileName, wFname); InitializeObjectAttributes(&ObjAttribute, &uFileName, OBJ_CASE_INSENSITIVE, NULL, NULL); //打开文件或者创建文件 status = ZwCreateFile(&hFile, GENERIC_WRITE | GENERIC_READ, &ObjAttribute, &IoStatusBlock, NULL, 0, 0, FILE_OPEN_IF, FILE_SYNCHRONOUS_IO_NONALERT, NULL, NULL); if(!NT_SUCCESS(status)) { DbgPrint("Create File Error\n"); return; } //写文件 status = ZwWriteFile(hFile, NULL, NULL, NULL, &IoStatusBlock, buf, sizeof(buf), NULL, NULL); if(NT_SUCCESS(status)) { DbgPrint("Write File Success%u", IoStatusBlock.Information); } //读取文件长度 status = ZwQueryInformationFile(hFile, &IoStatusBlock, &fsi, sizeof(fsi), FileStandardInformation); if(NT_SUCCESS(status)) { DbgPrint("file length:%u\n", fsi.EndOfFile.QuadPart); } //读文件 pBuffer = (PCHAR)ExAllocatePoolWithTag(PagedPool, fsi.EndOfFile.QuadPart * sizeof(CHAR), 'eliF'); if(NULL != pBuffer) { status = ZwReadFile(hFile, NULL, NULL, NULL, &IoStatusBlock, pBuffer, fsi.EndOfFile.QuadPart * sizeof(CHAR), NULL, NULL); if(NT_SUCCESS(status)) { DbgPrint("Read File %s lenth: %u", pBuffer, fsi.EndOfFile.QuadPart * sizeof(CHAR)); } } //关闭文件句柄 ZwClose(hFile); ExFreePool(pBuffer);
2017年02月16日
4 阅读
0 评论
0 点赞
1
...
12
13
14
...
19