gamehacker
驱动牛犊
驱动牛犊
  • 注册日期2010-12-29
  • 最后登录2012-01-01
  • 粉丝1
  • 关注2
  • 积分18分
  • 威望161点
  • 贡献值0点
  • 好评度0点
  • 原创分0分
  • 专家分5分
阅读:2907回复:5

【活动】菜鸟零基础入门内核驱动开发

楼主#
更多 发布于:2010-12-29 16:59
写的很菜,大牛表笑偶啊~~~

内核驱动入门——手把手零基础入门
    相信大家经常听到“内核级编程”、“主动防御”、“驱动开发”……,很多杀毒软件都使用了驱动技术来实现主动防御的。由于网上关于内核编程和驱动开发的资料不是很多,要么是代码的片段、要么是有文章无代码,入门级的资料就更少了,因此很多“门外的朋友”都望眼欲穿。使用自己编写的软件一直是我等小菜的梦想,通过一段时间的学习,把我的一些经验和方法和大家分享一下,希望能把各位“门外的朋友”领进门,并让带领广大小菜实现梦想。开始学习之前你可能没有接触过编程,没写过代码,不过没关系只要按照本系列的教程学习,相信你一定会有收获的。
    “工欲善其事必先利其器”首先是开发环境的搭建,网上有很多文章教大家如何搭建驱动开发环境,但是大多都需要安装多个软件并需要进行一系列的配置,操作也比较麻烦。对于初学者我是不推荐那么复杂的操作。下面我会教大家搭建一个最简单的驱动开发环境,以后随着对编程技术和驱动开发技术的了解,我们在逐步完善我们的开发环境。
    
    第一步:资源准备
    
    搭建最简单驱动开发和调试环境需要如下软件:
    WDK(官方下载地址:http://www.microsoft.com/downloads/details.aspx?displaylang=en&FamilyID=36a2630f-5d56-43b5-b996-7633f2ec14ff
    Dbgview.exe
    InstDrv.exe

第二步:搭建环境

    需要安装的只有WDK,其他两个直接打开使用即可。安装WDK也是比较简单的,双击安装包中的KitSetup.exe进行安装。

    安装界面都是英文界面,不过大家可以放心,只要按照上面勾选所有选项进行安装就可以了,其他一律点击下一步。勾选所有选项是将所有的功能全部安装,对于初学者来说避免以后发现缺少某个功能需要重新安装。安装完成后,可以在“开始”-->“所有程序”中看到如下图中的菜单。

    这就说明你已经安装好了WDK,已经构架好了windows驱动编译和调试的环境了。
    Dbgview.exe和InstDrv.exe是不需要安装的,直接复制过来就能用。
    Dbgview.exe的主要功能是将驱动执行的调试信息显示出来,因为大多数情况下驱动执行的效果是没有界面显示的,因此需要使用Dbgview.exe来监控查看。
    
    InstDrv.exe的主要功能是加载驱动程序,并启动驱动程序,驱动程序与我们平时使用的应用程序不一样,我们平时使用QQ时直接双击就可以运行程序了,但是驱动程序不可以用双击的方法来执行,这时就得用到InstDrv.exe了。

    第三部:编写代码
    到这里我们就搭建好了一个简单的windows驱动开发和调试的环境了,下面就可以进行驱动程序的开发了,开发主要分为三个步骤:编写代码、编译驱动程序、调试测试效果。
    首先是编写代码,编写代码可以使用任意一款文本编辑器(记事本、EditPlus、UE等等),代码主要分为三部分:

1、makefile文件:格式是固定的一半情况下不需要改动。代码如下:

    !IF DEFINED(_NT_TARGET_VERSION)
    !    IF $(_NT_TARGET_VERSION)>=0x501
    !        INCLUDE $(NTMAKEENV)\makefile.def
    !    ELSE
    !        message BUILDMSG: Warning : The sample "$(MAKEDIR)" is not valid for the current OS target.
    !    ENDIF
    !ELSE
    !    INCLUDE $(NTMAKEENV)\makefile.def
    !ENDIF

2、Sources文件:源代码列表文件。里面是一些配置信息,依次包括:
目标名称:就是要生成驱动的文件名(这个可以随便起)
目标类型:我们要编译出的是驱动程序,因此这个填写DRIVER
目标路径:就是生成编译好的驱动后,保存的路径,如下填写Driver的意思就是在该目录下的Driver子目录生成编译好的驱动程序。
源代码列表:将要编译的源代码写在这里,编译器会对该源代码进行编译。
代码如下:
    TARGETNAME=h4ck
    TARGETTYPE=DRIVER
    TARGETPATH=Driver
    
    SOURCES=h4ck.c  

源代码文件:这个是我们需要编写的,也就是上面列表中的h4ck.c文件了。先来一个简单的练练手吧,打开记事本编写如下代码:

    #include <ntddk.h>
    
    VOID Unload(IN PDRIVER_OBJECT DriverObject){
    
    }
    
    NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath){
        DriverObject->DriverUnload = Unload;
        DbgPrint("no hack no life! By h4ck.cn");
        return STATUS_SUCCESS;
    }
    我们下不管这些代码的含义,直接将其保存为h4ck.c文件。到写在为止我们已经编写好了编译驱动所需要的三个文件了,他们的文件名分别是:makefile、Sources、h4ck.c。将这三个文件保存在E:\mywork\demo1中,当然目录可以任意起名,不过路径中最好不要包含空格和汉字。

    第四步:编译驱动
    这步操作比较简单,依次点击“开始”-->“所有程序”-->“Windows Driver Kits”-->“WDK 7600.16385.1”-->“Build Environments”会看到四个子菜单,分别是Windows 7、Windows Server 2003、Windows Vista and Windows Server 2008、Windows XP。选一个目标操作系统(我是在xp下测试该驱动,因此我选择Windows XP,这个根据你自己的情况而选),选好后会有两个子菜单:x86 Checked Build Environment和x86 Free Build Environment,分别是生成测试版驱动和生成发布版驱动。一般我们在测试的时候使用前者,正式发布程序的时候使用后者,因为后者会对程序进行进一步的优化,前者优化较少主要是为了方便调试。我们这里选前者,然后会弹出一个cmd窗口,在窗口中输入“cd /d E:\mywork\demo1”命令后回车,将目录切换到E:\mywork\demo1下,然后输入“build”命令进行自动编译链接,如果看到如下图所示,说明驱动程序已经编译成功了。

可以看到提示信息:
    3 files compiled - 10 LPS
    1 executable built
完成三个文件,也就是我们前面准备的三个文件,生成一个可执行文件,也就是生成了编译成功的驱动程序,可以在E:\mywork\demo1下多了一个driver目录,是我们在makefile中设置的TARGETPATH=Driver。在该目录下的i386目录中看到了我们编译出来的驱动程序了。

    呵呵,是不是很有成就感呢。希望大家能亲自动手实践一下,熟悉一下驱动开发的流程。
    
    第五步:测试效果
    测试就需要用到Dbgview.exe和InstDrv.exe了,首先打开Dbgview程序,设置一下监控内核,如下方法即可设置完成。
   
    然后就可以开始监控内核了,开始监听后使用InstDrv加载和启动驱动。打开InstDrv,界面如下:

    可以直接将我们编译好的驱动程序h4ck.sys拖到InstDrv里,然后就可以对这个驱动进行安装、启动、停止和卸载操作了。操作的结果会实时显示在InstDrv的底部。安装完成后就可以启动了,点击启动后,看Dbgview的监听窗口,如下:

    可以看到是将源代码中的一句话打印了出来。说明我们的驱动运行成功了。驱动程序是怎么将那段字符串打印出来的呢?这就要阅读以下源代码了,用记事本打开h4ck.c文件:
        #include <ntddk.h>
    第一行的意思是包含WDK中的ntddh.h头文件,说白了就是把ntddk.h文件拷贝过来,因为后面是要调用其中的一些函数的。不把它包含进来在编译的时候就会出错。
        VOID Unload(IN PDRIVER_OBJECT DriverObject){
        }
    上面代码是定义了一个没有返回值的函数,定义函数的格式是这样的:
    函数类型 函数名(参数1,参数2……)
    VOID可以理解为没有返回值,函数名是我们自己起的,这里我起的是Unload,参数的类型是PDRIVER_OBJECT,也就是一个驱动对象,驱动对象呢可以理解为我们写的这个驱动程序。参数前面那个IN,是指这个参数是用来输入的。
    后面大括号{}里就是函数的实现代码了,你会发现这个Unload函数里上面也没有啊?是的,这个函数是没有代码,但是是可以完成驱动卸载的。当然你也可以不写这个函数,这样你的驱动程序一旦被系统加载,驱动自己就无法自己卸载了。所以在自己测试的时候还是写上的好。
    下面才是驱动程序最重要的部分,所以大家可要竖起耳朵、睁大眼睛认真听了。
        NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath){
    DriverEntry是驱动的主函数,也就是驱动的入口,当用InstDrv加载了驱动后,点击“启动”时,驱动就会从这里开始执行。也就是整个驱动开始执行的地方。DriverEntry是入口函数名,一般不要修改这个函数名,因为编译器默认将这个函数名作为主函数名。可以看到它有两个输入的参数,这个是也默认的,最好不要修改。函数类型是NTSTATUS,也即是说返回值应该是一个状态值,这个从下面的代码中可以看出来。
    DriverObject->DriverUnload = Unload;
    完成卸载例程,这里先说一下DriverEntry的第一个参数了,DriverObject是 一个结构,里面有很多成员,定义如下:
    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[主功能函数码];
    }
    可以看到其中一个就是DriverUnload成员(倒数第二个),它负责卸载驱动时要执行的函数。我可以这样理解,一个商场有很多商家(一个DriverObject结构有很多成员),每个商家经营的产品都不一样(DriverObject结构的每个成员都有不同的意义),而我们想要买什么东西就必须要知道哪个商家有我们需要的商品。就需要有一个商家的导航,而我们只关心我们需要的商品。因为我们需要驱动卸载功能,因此这里要做一个导航,将DriverObject结构的负责卸载功能的DriverUnload成员指向Unload函数。也就是说当驱动卸载的时候会根据我们设置的这个导航来执行Unload函数。
            DbgPrint("no hack no life! By h4ck.cn");
    这行代码是不是很眼熟啊,引号里的就是我们在Dbgview的监听窗口中看到的信息。这是因为调用了DbgPrint函数,而字符串就是它的参数。DbgPrint函数的作用就是将参数打印出来。
            return STATUS_SUCCESS;}
    最后一句是返回一个状态值。我们从前面可以看到,DriverEntry函数类型是NTSTATUS,也即是说要有一个状态值的,return来实现返回操作,STATUS_SUCCESS是一个状态值,表示成功状态。
    这样一个简单的驱动就算完成了,是不是觉得内核驱动编程也是很简单的呢,也许你会觉得这个驱动没有实现什么具体强大的功能,只是在内核中输出了一句调试信息,而且在用户界面还是看不到的。也许你会觉得这个太简单了,呵呵,如果你这么想,那恭喜你,你对驱动开发已经入门了,你已经不再觉得内核编程很神秘很难学了。有了上面的基础下面就跟我一起来编译一个具有实际功能的内核驱动吧。
    我们要实现的功能是隐藏进程。大家都知道木马运行后为了不让用户发现,一般都会将自己的进程隐藏起来。这样当用户使用任务管理器查看进程的时候就不会发现它了。实现进程隐藏的方法很多,今天我们就使用内核驱动来实现这个功能吧。也算是我们菜鸟晋级的一个实战。呵呵首先是编写代码,这次我们用EditPlus来编写代码,因为EditPlus可以将代码高亮显示,看起来是比较舒服的。
    首先我们来看makefile和sources文件,这两个文件是不用修改的直接使用前面的就可以,我们主要的工作就是编写h4ck.c源代码文件。用EditPlus打开h4ck.c,首先看驱动入口函数DriverEntry:
    NTSTATUS DriverEntry (IN PDRIVER_OBJECT DriverObject,IN PUNICODE_STRING RegistryPath){
        KdPrint(("驱动开始\n"));
        DriverObject->DriverUnload = Unload;
        HiddenProccess();
        KdPrint(("驱动完成\n"));
        return STATUS_SUCCESS;
    }
有两处和我们前面写的那个驱动代码是不一样的。
KdPrint:其实它的效果和DbgPrint是一样的,区别在于KdPrint是在调试版中执行,在正式发布版中是被忽略的。而DbgPrint在两个版本中是都执行的。KdPrint的功能还是很好的,可以方便开发和调试,但是要注意KdPrint和DbgPrint的格式稍有不同,前者是双括号的,这个要注意。
“HiddenProccess();”这个函数就是实现隐藏进程功能的主要函数了。首先我们介绍一下隐藏进程的原理,在用户层实现隐藏进程的方法有很多,但是大多是方法都可以通过遍历ActiveProcessLinks(活动进程链)的方法检测出来。也许你要问“啥是活动进程链”呢,我们可以简单理解为,内核中有一个链表是用来记录所有执行着的进程的,这个链表是双向的,也就是说第一条记录和最后一条记录的连在一起的,以可以把这样的双向链表理解成一个“圆圈”所有进程一个一个的连续的排列在这个圆圈上。而我们隐藏进程的原理就是把我们要隐藏的进程从这个链表上拿掉,那就需要两个步骤:
找到要隐藏的进程在链表中的位置。
将进程从链表中摘除,并将原来链表接上。
完成第一个步骤可以用遍历整个表的方法来查找要隐藏进程的位置,第二步需要将摘除隐藏进程后的链表接上,这就需要我们将要隐藏进程两边的进程相互连接。具体我们来看代码:
    VOID HiddenProccess(){
        ULONG  Address;
        ULONG  oldAddress;
        char *processName;
        LIST_ENTRY*  listEntry;
    声明四个变量,前两个是长整型数,用来存放地址信息;第三个是一个字符串指针,是用来存放进程名称的;第四个是一个链表结构,是用来存放活动进程链表的。这些变量在程序运行时的变化请看下面代码。
        Address = (ULONG)PsGetCurrentProcess();
    使用PsGetCurrentProcess函数可以获得一个EPROCESS结构,这个结构里面包含了很多关于进程的信息,EPROCESS结构你可以理解为一个商场,商场里有很多卖笔记本的商家,比如二号商家是卖IBM笔记本的,八号商家是卖联想笔记本的,十六号是卖华硕笔记本的……,那么EPROCESS结构中0x88就是进程活动链,0x174是进程名,0x84是进程PID等等。上面代码是将Address赋值为EPROCESS结构的首地址。
        Address += 0x88;
        oldAddress = Address;
    这里首先将Address增加0x88,也就是将Address指向活动进程链,然后oldAddress也赋值为活动进程链的地址。赋值完成后进行下面的循环。Do……while循环是一种很常用的循环模式,他的特点就是首先执行do后面大括号里的函数指令,然后根据while后面括号里的条件判断是不是继续循环执行do后面大括号里的函数。直到while后面的条件判断结果为否就退出循环。下面先看看do里面的函数都是上面功能。
        do{
          processName = GetAllProcessListName(Address);
    首先是通过GetAllProcessListName来获得进程名称。GetAllProcessListName函数上面会在后面分析。
            if(!strcmp(processName, "cmd.exe")){
                KdPrint(("隐藏进程的名称:%s\n",processName));
                listEntry = (LIST_ENTRY *) (Address);
                *((ULONG*)listEntry->Blink) = (ULONG)listEntry->Flink;
                *((ULONG*)listEntry->Flink+1) = (ULONG)listEntry->Blink;
    然后判断进程名是不是cmd.exe,如果是就构造一个开始地址为Address的链表listEntry,其实也就是活动进程链,下面简单的看看LIST_ENTRY的结构。
    LIST_ENTRY的结构定义如下
    typedef struct _LIST_ENTRY
    {
            struct _LIST_ENTRY *Flink;
           struct _LIST_ENTRY *Blink;
    } LIST_ENTRY, *PLIST_ENTRY;
    内核使用该结构将所有对象维护在一个双向链表中。一个对象分属多个链表是很常见的, Flink 成员是一个向前链接,指向下一个 LIST_ENTRY 结构, Blink 成员则是一个向后链接,指向前一个 LIST_ENTRY 结构。通常情况下,这些链表都成环形,也就是说,最后一个 Flink 指向链表中的第一个 LIST_ENTRY 结构,而第一个 Blink 指向最后一个。这样就很容易双向遍历该链表。如果一个程序要遍历整个链表,它需要保存第一个 LIST_ENTRY 结构的地址,以判断是否已遍历了整个链表。如果链表仅包含一个 LIST_ENTRY 结构,那么该 LIST_ENTRY 结构必须引用其自身,也就是说, Flink 和 Blink 都指向其自己。
    那么就可以看出前面的代码是通过给LIST_ENTRY结构中Blink成员和Flink成员重新复制来实现断链功能的。
            }
            Address = *(ULONG*)Address;        
    上面一句循环条件的关键之处,将指针里的值给变量,这样能继续循环,直到循环到开始的第一个链表项。
        }while(oldAddress != Address);        
    }
    下面就是判断是否循环到了链表的第一项,也就是判断有没有循环完所有链表项目,如果不是第一项说明循环完了链表中的所有项则退出循环;如果不是第一项则条件oldAddress != Address为真则继续循环。
    最后看看GetAllProcessListName是怎么取得进程名称的吧。GetAllProcessListName函数的代码如下:
    char*  GetAllProcessListName(ULONG Address){
        char *processName;
    首先定义一个字符指针,用来指向进程的名称。
        Address -= 0x88;
    然后将Address减去0x88,也就是将Address重新指向EPROCESS结构的开始地址。
        processName = (char*)(Address + 0x174);
    然后把Address + 0x174处的字符赋值给processName,也就得到了进程的名字,因为EPROCESS结构得 0x174处存储的就是进程的名字,当然根据操作系统的版本不同,这个偏移地址是有所不同的,我们这里是WINDOWS XP sp3的系统。最后返回processName值即可。
        return processName;    
    }

    整个代码的分析到这里就结束了,下面就可以编译一下这个驱动了,方法和前面的一样,准备好Makefile、sources和源代码文件,然后使用wdk的命令提示行进入该目录使用build命令编译即可生成驱动程序文件了。
    验证这个驱动功能的时候到了,这是我们编写的第一个具有实际功能的驱动程序,对驱动入门来说意义重大啊。我们先打开一个cmd.exe程序,然后打开任务管理器窗口可以看到有cmd.exe进程的显示。

    然后使用InstDrv.exe加载并启动驱动程序,使用Dbgview.exe来查看调试信息。然后观察任务管理器中进程的变化。
   
    
    在调试信息中可以看到已经找到cmd.exe进程并已经成功隐藏了该进程。然后在任务管理器中你也会发现刚才还“活蹦乱跳”的cmd.exe进程神奇的消失了。
    到这里内核驱动编程的第一课已经结束了,希望广大小菜能有所收获,有什么问题可以到非安全论坛进行交流。



排版好辛苦,呵呵
gamehacker
驱动牛犊
驱动牛犊
  • 注册日期2010-12-29
  • 最后登录2012-01-01
  • 粉丝1
  • 关注2
  • 积分18分
  • 威望161点
  • 贡献值0点
  • 好评度0点
  • 原创分0分
  • 专家分5分
沙发#
发布于:2010-12-29 17:01
这是偶投稿在非安全杂志的,呵呵
gamehacker
驱动牛犊
驱动牛犊
  • 注册日期2010-12-29
  • 最后登录2012-01-01
  • 粉丝1
  • 关注2
  • 积分18分
  • 威望161点
  • 贡献值0点
  • 好评度0点
  • 原创分0分
  • 专家分5分
板凳#
发布于:2010-12-29 17:01
不知能中奖不?貌似是第一个参与活动的哈
basketwill
驱动牛犊
驱动牛犊
  • 注册日期2005-09-02
  • 最后登录2016-04-05
  • 粉丝8
  • 关注0
  • 积分47分
  • 威望410点
  • 贡献值1点
  • 好评度6点
  • 原创分0分
  • 专家分0分
地板#
发布于:2010-12-30 16:41
不错
宛俊_三哥!
reludson
驱动牛犊
驱动牛犊
  • 注册日期2004-01-29
  • 最后登录2014-06-22
  • 粉丝1
  • 关注0
  • 积分25分
  • 威望209点
  • 贡献值0点
  • 好评度20点
  • 原创分0分
  • 专家分1分
地下室#
发布于:2011-01-06 22:12
非常 不错的!!!
wonsea
驱动牛犊
驱动牛犊
  • 注册日期2007-07-06
  • 最后登录2011-11-03
  • 粉丝0
  • 关注0
  • 积分4分
  • 威望33点
  • 贡献值0点
  • 好评度1点
  • 原创分0分
  • 专家分0分
5楼#
发布于:2011-01-20 23:35
我也是入门,
MARK!
游客

返回顶部