2013年7月12日星期五

WinDbg远程调试

注意windbg的版本要尽量相同。

第一步:在被调试的机器上,也就是服务端。
一种是命令行方式启动windbgWinDBG.exe -server tcp:port=8888 -p 2010
    注释:(使用TCP协议,在端口8888建立调试服务, 调试机器上进程号为2010的程序)
另一种是启动windbg,打开要调试的程序,然后在命令窗口输入.server tcp:port=8888来建立server,
    成功后windbg会提示你Client如何连接到Server,例如:

0:000> .server tcp:port=8888
Server started.  Client can connect with any of these command lines
0: <debugger> -remote tcp:Port=8888,Server=KOUDAQIANG-WIN8


第二步:在本地
命令行:windbg -remote tcp:Port=8888,Server=KOUDAQIANG-WIN8
或者启动后用菜单文件->Connect to remote session(CTRL+R)来连接,输入: tcp:Port=8888,Server=KOUDAQIANG-WIN8
显示的内容和运行的命令如下:

Microsoft (R) Windows Debugger Version 6.2.9200.16384 X86
Copyright (c) Microsoft Corporation. All rights reserved.

Server started.  Client can connect with any of these command lines
0: <debugger> -remote tcp:Port=8888,Server=KOUDAQIANG-WIN8
KOUDAQIANG-WIN7\Administrator (tcp 10.121.23.3:3544) connected at Fri Jul 12 15:09:46 2013
0:000> t
eax=00000000 ebx=00000003 ecx=3ba60000 edx=00000000 esi=00000000 edi=001b0000
eip=772a04cb esp=0018fac4 ebp=0018faf0 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000244
ntdll!LdrInitShimEngineDynamic+0x2ed:
772a04cb eb07            jmp     ntdll!LdrInitShimEngineDynamic+0x2f6 (772a04d4)
0:000> p
eax=00000000 ebx=00000003 ecx=3ba60000 edx=00000000 esi=00000000 edi=001b0000
eip=772a04d4 esp=0018fac4 ebp=0018faf0 iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000246
ntdll!LdrInitShimEngineDynamic+0x2f6:
772a04d4 c745fcfeffffff  mov     dword ptr [ebp-4],0FFFFFFFEh ss:002b:0018faec=00000000

同时另一个计算机的windbg上也会显示同样的内容,并且同步.

ok!
会者不难,难者不会。

但是要源代码的符号调试,需在被调试的机器上设置符号路径,这时的符号路径大多是远程的,也就是本台的地址.

参考:
http://msdn.microsoft.com/en-us/library/windows/hardware/hh451173(v=vs.85).aspx
http://msdn.microsoft.com/en-us/library/ff554390.aspx
windbg帮助文件等。

其实还有一种办法:就是在内核调试的时候,在应用层的代码中加入:DebugBreak();
不过这种办法调试的速度很慢,比vs双机调试和上面的办法要慢,
不过这个办法也有个好处可以同时调试r3和r0的代码,在这种情况下vs双机调试容易中断,我想也有解决的办法.

made by correy
made at 2013.02.05

2013年7月4日星期四

编程注意事项

这里记载一些心得经验和想法(没有实验).

这里大多是血的教训!请大家谨记.


1.ExAllocatePoolWithTag不但检查成功否和释放ExFreePoolWithTag,更重要的是要RtlZeroMemory,不然会有乱码等奇怪的现象.最好立马使用,最近使用.

不会C++,没有C++的思想。不过写下面的两个函数倒有进步的思想。

#define TAG 'tset' //驱动在内存的标志,即test

//new delete

PVOID allocate(IN SIZE_T NumberOfBytes)
/*
不建议对申请内存函数的封装,这样对内存泄漏不好定位.
*/
{
    PVOID p = NULL;

    //PAGED_CODE();
    if (KeGetCurrentIrql() > DISPATCH_LEVEL)
    {
        KdBreakPoint();//DbgBreakPoint() 
    }

    /*
    Callers of ExAllocatePoolWithTag must be executing at IRQL <= DISPATCH_LEVEL. 
    A caller executing at DISPATCH_LEVEL must specify a NonPagedXxx value for PoolType.
    A caller executing at IRQL <= APC_LEVEL can specify any POOL_TYPE value, but the IRQL and environment must also be considered for determining the page type.
    */

    p = ExAllocatePoolWithTag(NonPagedPool, NumberOfBytes, TAG); 
    if (p == NULL ) {
        return p;
    }

    /*
    Warning  Memory that ExAllocatePoolWithTag allocates is uninitialized. 
    A kernel-mode driver must first zero this memory if it is going to make it visible to user-mode software (to avoid leaking potentially privileged contents).
    */

    RtlZeroMemory(p, NumberOfBytes);

    return p;
}


VOID free(IN PVOID p)
{
    unsigned long r;

    /*
    Callers of ExFreePoolWithTag must be running at IRQL <= DISPATCH_LEVEL.
    A caller at DISPATCH_LEVEL must have specified a NonPagedXxx PoolType when the memory was allocated. 
    Otherwise, the caller must be running at IRQL <= APC_LEVEL.
    */

    //PAGED_CODE();

    if (KeGetCurrentIrql() > DISPATCH_LEVEL)
    {
        KdBreakPoint();//DbgBreakPoint() 
    }

    if (p) //防止多次释放导致的蓝屏。
    {        
        __try //防止传入非法的地址。
        {
            r = MmIsAddressValid(p);
            ExFreePoolWithTag(p, TAG);//KeGetCurrentIrql() > DISPATCH_LEVEL时依旧蓝屏。
        }
        __except (EXCEPTION_EXECUTE_HANDLER)
        {            
            r = GetExceptionCode();//啥也不做。
        }

        p = NULL;
    }
}    




2.不可用BOOL与true或者TRUE比较,因为:typedef int BOOL;
  所以:
  BOOL b = PathIsDirectory(buffer);
  //if (b == true) //00B4161A  cmp         dword ptr [ebp-268h],1
  //if (b == TRUE) //00B4161A  cmp         dword ptr [ebp-268h],1
  if (b)           //cmp         dword ptr [ebp-268h],0





3.要对用:RtlAppendUnicodeStringToString或者RtlAppendUnicodeToString或者RtlAppendStringToString对STRING或者UNICODE_STRING追加字符,原有字符结构必须有内存,不能是初始化的.
   就是初始化的时候要使用:RtlInitEmptyUnicodeString,而不能使用:RtlInitUnicodeString(&us1,L"\\REGISTRY\\USER\\");

   系统生成的UNICODE_STRING,如文件对象(FileObject)里面的,这是最好传递UNICODE_STRING指针,前提是不能修改这个输入参数,如要修改请备份或者复制一份.
   如果传递字符串的地址,再用RtlInitUnicodeString初始化,不仅麻烦而且易出错,因为:字符串的地址没有结束标志,且字符串后面的地址有可能不可以访问,所以会蓝屏.
   如果非要传递字符串的地址,建议一定加上字符串的长度,用原始的方法初始UNICODE_STRING,不要用RtlInitUnicodeString了.

   


4.FltRegisterFilter函数返回STATUS_OBJECT_NAME_NOT_FOUND
  #define STATUS_OBJECT_NAME_NOT_FOUND     ((NTSTATUS)0xC0000034L)
  原因是注册表中没有如下内容:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\xxxxxx\Instances]
"DefaultInstance"="xxxxxx"

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\services\xxxxxx\Instances\xxxxxx]
"Altitude"="371100"
"Flags"=dword:00000000

并注意:Altitude的值和注册时要一致.

说明:必须有一个名为"Instances"的子项用于存放驱动的实例信息,该子项下面的字符串值"DefaultInstance"指定了默认实例的名称。
     "Instances"项下面的每一个子项表示一个实例,每个实例子项必须有一个字符串值"Altitude"。
     FltRegisterFilter函数执行时,如果在注册表中没有找到默认实例的"Altitude"值,将会返回STATUS_OBJECT_NAME_NOT_FOUND错误。





5.ObRegisterCallbacks返回STATUS_ACCESS_DENIED (0xc0000022)
解决办法:
首选办法:sources里面加入:LINKER_FLAGS = $(LINKER_FLAGS)/INTEGRITYCHECK
第二个方案: 注册回调前加段代码,改一个比特位就解决了    
代码:
PLDR_DATA_TABLE_ENTRY pLdrEntry=(PLDR_DATA_TABLE_ENTRY)pDrvObj->DriverSection;
pLdrEntry->Flags |=0x20;  




6.如果有两个桌面,在内核中判断或者区分?
1.进程回调加父子关系。我是用链表实现了。
2.未公开的函数,详细的请查询http://doxygen.reactos.org。
3.判断进程内的某个类型的对象的值,即:DeskTop。
4.对象里面有个结构,就是使用这个对象的所有的进程的链表。




7.判断一个文件或者文件夹是不是在另一个文件夹里面。
应用层和驱动层通吃,这是一个方法和思路。
1.判断是不是这个文件夹,之前最好判断一下是不是目录。
2.如果待判断的文件或者目录的长度大于特定的目录,取代判断的目录或者文件夹的长度加一,然后和特定的目录加"\\"比较。
如果是miniFilter,有更方便的办法,因为它解析好了。
另外内核还有一些特殊的函数,至少三类:
1.以str,wcs,_wcs开头的函数。由内核导出,但是不建议使用,因为好多字符串不是以0x00结尾的.
2.刚开始还以为是:RtlLeftChild呢?后来发现了RtlPrefixUnicodeString,实验成功。不过要调用两次,后一次加一个\.
3.FsRtlIsNameInExpression或者FsRtlIsDbcsInExpression等。这个实验始终失败.




8.KeSetEvent使用的一个要点.
LONG KeSetEvent(IN PRKEVENT  Event, IN KPRIORITY  Increment, IN BOOLEAN  Wait);
最后一个参数为真,必须马上调用等待函数,不然蓝屏,注意IRQL还会升高,具体的看说明.
这是加班到凌晨4点才解决,后来才明白的.




9.UserMode or KernelMode
很多函数的说明中有这么一句话:Lower-level drivers should specify KernelMode. 
实际要怎么做?其实大多说是UserMode.只有系统进程或者驱动专用的是KernelMode.
其实完美的办法是调用ObIsKernelHandle函数.ObOpenObjectByPointer这个函数有点麻烦.
彻底的解决办法是ExGetPreviousMode(VOID).




10.由于VS2013没有一键转换的功能,所以依旧用vs2012,但是要编译:Windows Driver Kit (WDK) 8.1 Preview Samples下的例子,要把工程的编译平台修改为8.0(WindowsKernelModeDriver8.0)即可,注意有两处.不然编译出错.
error MSB8020: The builds tools for WindowsKernelModeDriver8.1 (Platform Toolset = 'WindowsKernelModeDriver8.1') cannot be found. To build using the WindowsKernelModeDriver8.1 build tools, either 

click the Project menu or right-click the solution, and then select "Update VC++ Projects...". Install WindowsKernelModeDriver8.1 to build using the WindowsKernelModeDriver8.1 build tools.




11.数字签名.
这个我也不太懂,有的要5级签名,微软的在最上等.
今天遇到一个,即使签名了也会出现:577 = 0x241 .
其含义为:Windows 无法验证此文件的数字签名。某软件或硬件最近有所更改,可能安装了签名错误或损毁的文件,或者安装的文件可能是来路不明的恶意软件。
改进办法是用命令行签名,最好加上时间信息,不过这又要开发环境了.





12.minifilter在卷挂载(PFLT_INSTANCE_SETUP_CALLBACK)的时候,获取卷设备的一些信息,更多信息请自己扩展.
BOOLEAN PrintVolume(__in PCFLT_RELATED_OBJECTS FltObjects)
    /*
    功能:打印挂载的卷的信息。
    */
{
    NTSTATUS status;    
    PVOID Buffer;
    BOOLEAN r = FALSE;    
    ULONG BufferSizeNeeded;
    UNICODE_STRING Volume;

    status = FltGetVolumeName(FltObjects->Volume, NULL, &BufferSizeNeeded);
    if (status != STATUS_BUFFER_TOO_SMALL) {
        return FALSE;
    }

    Buffer = ExAllocatePoolWithTag(NonPagedPool, BufferSizeNeeded + 2, TAG);
    if (Buffer == NULL) {
        return FALSE;
    }
    RtlZeroMemory(Buffer,BufferSizeNeeded + 2);

    Volume.Buffer = Buffer;
    Volume.Length = (USHORT)BufferSizeNeeded;
    Volume.MaximumLength = (USHORT)BufferSizeNeeded + 2;

    status = FltGetVolumeName(FltObjects->Volume, &Volume, &BufferSizeNeeded);//最后一个参数为NULL失败。
    if (!NT_SUCCESS(status)) {
        KdPrint(("FltGetVolumeName fail with error 0x%x!\n",status));
        ExFreePoolWithTag(Buffer, TAG);
        return FALSE;
    }

    KdPrint(("挂载的卷为:%wZ\n",&Volume));
      
    ExFreePoolWithTag(Buffer, TAG);
    return r;
}

打印信息有:
挂载的卷为:\Device\Mup
挂载的卷为:\Device\HarddiskVolume1
挂载的卷为:\Device\HarddiskVolume2
挂载的卷为:\Device\HarddiskVolume4
挂载的卷为:\Device\HarddiskVolume3
挂载的卷为:\Device\HarddiskVolume5
挂载的卷为:\Device\CdRom0






13.
BOOLEAN IsMyVolume(__in PCFLT_RELATED_OBJECTS FltObjects)
{
    BOOLEAN b = FALSE;
    PFILE_OBJECT    FileObject;
    UNICODE_STRING  uni_disk;
    UNICODE_STRING  Hide_name;
    NTSTATUS status = STATUS_SUCCESS;

    FileObject = FltObjects->FileObject;//sp->FileObject;

    //On Windows Vista and later operating systems, you must ensure that APCs are not disabled before calling this routine. 
    //Call KeAreAllApcsDisabled for this purpose.

    //ObReferenceObjectByPointer((PVOID)FileObject,0,NULL,KernelMode);
    status = IoVolumeDeviceToDosName(FileObject->DeviceObject,&uni_disk);//RtlVolumeDeviceToDosName 支持xp之前,但是prefast警告.
    //ObDereferenceObject(FileObject);
        
    if (!NT_SUCCESS( status )) //如果失败了puni_disk->buffer == 0,下面的比较会蓝屏.
    {
        return b;
    }

    RtlInitUnicodeString(&Hide_name,L"X:");

    if (RtlEqualUnicodeString(&Hide_name,&uni_disk,TRUE))
    {
        b = TRUE;
    }

    ExFreePool(uni_disk.Buffer);//或者下面的办法.
    //RtlFreeUnicodeString(&uni_disk);

    return b;
}





14.OACR的使用.
正常情况下,编译之后,双击图标即可显示.
但是非正常情况下:
1.编译驱动
2.check now -> 选项.
3.view warnings  -> 选项.
以上是个人理解,并非正确.





15.C和CPP与布尔变量的关系。
C中默认情况下只能使用大写的布尔变量。
C++中注意这是两种不同的数据类型。

在C中测试效果如下:
BOOLEAN BX;//BYTE
//BOOL B;//int 
//bool b;

BOOLEAN BX1 = TRUE;
//BOOLEAN BX2 = true;
注意注释的是错误的,不过在CPP中是都可以的。
谨记,这是一会开发驱动程序,一会写应用层代码所得的。





16.再论UNICODE_STRING。

//
// Unicode strings are counted 16-bit character strings. If they are
// NULL terminated, Length does not include trailing NULL.
//

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
#ifdef MIDL_PASS
    [size_is(MaximumLength / 2), length_is((Length) / 2) ] USHORT * Buffer;
#else // MIDL_PASS
    _Field_size_bytes_part_(MaximumLength, Length) PWCH   Buffer;
#endif // MIDL_PASS
} UNICODE_STRING;
//上面是文件中的定义。

//下面是文档中的说明。
typedef struct _UNICODE_STRING {
  USHORT  Length;
  USHORT  MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;


Length 
The length in bytes of the string stored in Buffer. 
MaximumLength 
The length in bytes of Buffer. 
Buffer 
Pointer to a buffer used to contain a string of wide characters. 

If the string is NULL-terminated, Length does not include the trailing NULL.

个人理解:
上面的必须看懂并记住。
尽管微软所这是安全的字符串,不会出所谓的问题。
但是使用不当还是会出现蓝屏和莫名奇怪的问题。
因为好像没有函数检验字符串的有效性。
至少在WINDBG的本地变量里面显示的字符串和长度是可以不相符的。
反过来说,理解了这个结构,可以写出一些技巧的代码。
再次重复,长度是字节的长度,
所以在内存地址中定位字符的时候要除以Buffer的单位再减一。
所以复制的时候千万不要长度再乘以Buffer的单位。
这些问题很难发现和定位,费了我两天的时间,才解决,所以写此心得。







17.看《windows内核情景分析》的9.12MDL章节:
更喜欢叫DeviceObject->Flags的:
DO_BUFFERED_IO为复制方式,特点:系统申请非分页内存,然后再复制。
DO_DIRECT_IO为映射方式,特点:获取用户地址的物理地址的内核地址。
neither buffered nor direct I/O为直接方式,特点:很少使用或者直接使用,注意DPC/ISR中不可以用。
更多的官方信息:
http://msdn.microsoft.com/en-us/library/windows/hardware/ff550869(v=vs.85).aspx Neither I/O Operations
http://msdn.microsoft.com/en-us/library/windows/hardware/ff565381(v=vs.85).aspx Using Direct I/O with PIO
http://msdn.microsoft.com/en-us/library/windows/hardware/ff565374(v=vs.85).aspx Using Direct I/O with DMA
http://msdn.microsoft.com/en-us/library/windows/hardware/ff565356(v=vs.85).aspx Using Buffered I/O
具体的例子可看:\7600.16385.1\src\general\ioctl\wdm\sys。






18.win32程序显示控制台

一个函数就ok了!
AllocConsole()  //这个会闪一下。
AttachConsole(-1) //这个正好。

 但是要想显示,还可以:
GetStdHandle
WriteFile 





19.不显示Console窗口的Console程序
/*
控制台程序不显示控制台窗口。
wmainCRTStartup对应:wmain 宽字符。
mainCRTStartup对应:main 单字符。

另一种思路是WIN32程序隐藏窗口。
*/
#pragma comment(linker, "/subsystem:\"windows\" /entry:\"wmainCRTStartup\"") 

其实如果不想看到Console窗口,还有一个更直接的方法:那就是直接在EXE文件中将PE文件头的Subsystem从3改成2。在EXE文件中,PE文件头的偏移地址是0x3c,Subsystem是一个WORD,它在PE文件头中的偏移是0x5c。





20.RtlIsNameLegalDOS8Dot3的说明。
1.第一个参数是文件名,不是路径。
2.凡是文件名长度小于12的都认为是的,包括NTFS的元文件,目录等。




21.minifilter中的IRP操作。
PIO_STACK_LOCATION irpSp = IoGetCurrentIrpStackLocation(Cbd->Iopb->Parameters.NetworkQueryOpen.Irp);





22.看UNICODE_STRING,一定不要在图形界面中看,如本地变量和全局变量。
1.可能不显示汉字。
2.可能显示的是错误的。
也不要用dt UNICODE_STRING XXXXXXXX查看,这个和上面的是一样的。
而要这样看:
db US.Buffer LUS.Length.
谨记:这是血的教训,很难查的问题。





23.高IRQL的注意事项,特别是IRQL >= DISPATCH_LEVEL,IRQL <= APC_LEVEL出问题的不多。
   大家都知道那些规则。
   但是这是我的心得和体会:
   1.IRQL >= DISPATCH_LEVEL时,不要调用PASSIVE_LEVEL,APC_LEVEL级别的函数。
     这个看似简单的问题你做到了吗?检查检查你的代码。
   2.IRQL >= DISPATCH_LEVEL时,不要访问分页内存,包括用户的内存/地址。
     如果非得访问用户的内存不可,可以考虑MDL,这肯定要锁定操作的。
     一般的访问用户地址建议用异常处理,可是此时IQRL低,对于同步处理不好,如链表操作。
   3.高IRQL的优点,大家都知道的,请不要忘了缺点。 
   4.XP是没有查看IRQL的WINDBG命令的,!PCR等查看的是不对的,永远为0,不信你试试看。