-
-
[原创]内核NTSTATUS状态码和应用层错误代码的关系:
-
2022-4-15 16:00 15997
-
内核NTSTATUS状态码和应用层错误代码的关系:
(注:以下为win32环境下的结果。)
今天我在编写一个内核和应用层通信代码时出现了一个问题,当我在应用层使用DeviceIoControl来进行IRP操作时,如果DeviceIoControl调用失败不会得到错误码,需要调用GetLastError函数才能得到错误码,而这个错误码和内核的错误码不相同我根本无法定位。
DeviceIoControl function (ioapiset.h) - Win32 apps | Microsoft Docs
GetLastError function (errhandlingapi.h) - Win32 apps | Microsoft Docs
而且我得到的应用层错误码也很令人疑惑:
1 2 3 4 5 | ERROR_MORE_DATA 234 ( 0xEA ) More data is available. |
有更多数据可用?这是啥错误。于是乎我就开始了逆向分析了,找到该错误代码和我的内核代码中的关联。
(注:以下源代码参考WRK和ReactOS)
结论:
首先抛出结论方便大家直接使用。
内核中会通过RtlNtStatusToDosErrorNoTeb函数来将NTSTATUS错误码设置成GetLastError中的操作系统错误码,并将错误码设置成线程环境快TEB中的LastErrorValue字段的内容。
调用GetLastError函数得到的错误码就是线程环境快Teb中LastErrorValue字段的内容。
函数原型:(参考wrk和ReactOS)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 | ULONG RtlNtStatusToDosErrorNoTeb ( IN NTSTATUS Status ) / * + + Routine Description: This routine converts an NT status code to its DOS / OS 2 equivalent and returns the translated value. Arguments: Status - Supplies the status value to convert. Return Value: The matching DOS / OS 2 error code. - - * / { ULONG Offset; ULONG Entry; ULONG Index; / / / / Convert any HRESULTs to their original form of a NTSTATUS or a / / WIN32 error / / if (Status & 0x20000000 ) { / / / / The customer bit is set so lets just pass the / / error code on thru / / return Status; } else if ((Status & 0xffff0000 ) = = 0x80070000 ) { / / / / The status code was a win32 error already. / / return (Status & 0x0000ffff ); } else if ((Status & 0xf0000000 ) = = 0xd0000000 ) { / / / / The status code is a HRESULT from NTSTATUS / / Status & = 0xcfffffff ; } / / / / Scan the run length table and compute the entry in the translation / / table that maps the specified status code to a DOS error code. / / Entry = 0 ; Index = 0 ; do { if ((ULONG)Status > = RtlpRunTable[Entry + 1 ].BaseCode) { Index + = (RtlpRunTable[Entry].RunLength * RtlpRunTable[Entry].CodeSize); } else { Offset = (ULONG)Status - RtlpRunTable[Entry].BaseCode; if (Offset > = RtlpRunTable[Entry].RunLength) { break ; } else { Index + = (Offset * (ULONG)RtlpRunTable[Entry].CodeSize); if (RtlpRunTable[Entry].CodeSize = = 1 ) { return (ULONG)RtlpStatusTable[Index]; } else { return (((ULONG)RtlpStatusTable[Index + 1 ] << 16 ) | (ULONG)RtlpStatusTable[Index]); } } } Entry + = 1 ; } while (Entry < (sizeof(RtlpRunTable) / sizeof(RUN_ENTRY))); / / / / The translation to a DOS error code failed. / / / / The redirector maps unknown OS / 2 error codes by ORing 0xC001 into / / the high 16 bits. Detect this and return the low 16 bits if true. / / if (((ULONG)Status >> 16 ) = = 0xC001 ) { return ((ULONG)Status & 0xFFFF ); } return ERROR_MR_MID_NOT_FOUND; } |
1 2 3 4 5 6 7 | DWORD WINAPI GetLastError(VOID) { / * Return the current value * / return NtCurrentTeb() - >LastErrorValue; } |
逆向分析过程:
1 2 3 4 5 | / / 假设内核中的DeviceIoControl返回时的代码如下: su = STATUS_BUFFER_OVERFLOW; Irp - >IoStatus.Status = su; IoCompleteRequest(Irp, IO_NO_INCREMENT); return su; |
首先添加一个断点来方便调试:
1 2 3 4 5 | su = STATUS_BUFFER_OVERFLOW; DbgBreakPoint(); Irp - >IoStatus.Status = su; IoCompleteRequest(Irp, IO_NO_INCREMENT); return su; |
然后通过双机调试断在该断点处,并查看栈空间:
(注:打码的地方为自己的代码空间)
这里可以看到在真正调用到我们内核写的DeviceIoControl函数时前面的函数:
1 2 3 4 5 6 7 8 9 | 02 a7b04af4 84083eec nt!IofCallDriver + 0x63 03 a7b04b14 840872a8 nt!IopSynchronousServiceTail + 0x1f8 04 a7b04bd0 840ce7d3 nt!IopXxxControlFile + 0x82f 05 a7b04c04 83e8ca6a nt!NtDeviceIoControlFile + 0x2a 06 a7b04c04 77c86c04 nt!KiSystemServicePostCall 07 0028f790 77c853ec ntdll!KiFastSystemCallRet 08 0028f794 75e0ab4d ntdll!ZwDeviceIoControlFile + 0xc 09 0028f7f4 77babbc6 KERNELBASE!DeviceIoControl + 0xf6 0a 0028f820 00f228bc kernel32!DeviceIoControlImplementation + 0x80 |
因为DeviceIoControl是有一个status作为返回值的所以往上肯定有代码会接受这个返回值,于是我就开始往上一个一个分析函数:
首先是IofCallDriver,可以看到它的前缀是nt!,表明是nt内核模块,采用lm来查看该模块对应的文件:
1 2 3 4 | 0 : kd> lm start end module name 83e4c000 84274000 nt (pdb symbols) d:\symbol\ntkrpamp.pdb\ 5D110DC0022948A3B3FAF52F08E778402 \ntkrpamp.pdb |
可以看到该nt模块对应的是ntkrpamp.exe,该exe包含了Windows NT 内核空间的内核和执行层等很多东西。
这里我采用wrk的源码来分析该函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | NTSTATUS __fastcall IofCallDriver(PDEVICE_OBJECT DeviceObject, PIRP Irp) { NTSTATUS result; / / eax unsigned int v4; / / eax unsigned __int8 v5; / / cl char v6; / / al void * retaddr; / / [esp + Ch] [ebp + 4h ] if ( pIofCallDriver ) return pIofCallDriver(DeviceObject, Irp, retaddr); if ( - - Irp - >CurrentLocation < = 0 ) KeBugCheckEx( 0x35u , (ULONG_PTR)Irp, 0 , 0 , 0 ); v4 = Irp - >Tail.Overlay.PacketType - 36 ; Irp - >Tail.Overlay.PacketType = v4; v5 = * (_BYTE * )v4; * (_DWORD * )(v4 + 20 ) = DeviceObject; if ( v5 = = 22 && ((v6 = * (_BYTE * )(v4 + 1 ), v6 = = 2 ) || v6 = = 3 ) ) result = IopPoHandleIrp(); else result = DeviceObject - >DriverObject - >MajorFunction[v5](DeviceObject, Irp); return result; } |
可以看到该函数就调用了我们内核代码中的DeviceIocontrol然后返回对应的返回值,所以继续往上查看IopSynchronousServiceTail函数的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | PDEVICE_OBJECT __userpurge IopSynchronousServiceTail@<eax>( int a1@<eax>, struct _DEVICE_OBJECT * DeviceObject, PVOID Object , int a4, KPROCESSOR_MODE WaitMode, char a6, NTSTATUS a7) { PDEVICE_OBJECT DeviceObjecta; / / [esp + 20h ] [ebp + 8h ] ... 省略 ... LABEL_47: a7 = IofCallDriver(DeviceObject, (PIRP)a1); if ( !a6 ) ObDereferenceObjectDeferDelete(v7); DeviceObjecta = (PDEVICE_OBJECT)a7; if ( (_BYTE)a4 && a7 ! = 259 ) { HIBYTE(a4) = KfRaiseIrql( 1u ); IopCompleteRequest(a1 + 64 , v17, v18, & Object , v18); KfLowerIrql(HIBYTE(a4)); } if ( a6 ) { if ( a7 = = 259 ) { v15 = KeWaitForSingleObject(v7 + 92 , Executive, WaitMode, ( * ((_DWORD * )v7 + 11 ) & 4 ) ! = 0 , 0 ); if ( v15 = = 257 || v15 = = 192 ) IopCancelAlertedRequest(a1); DeviceObjecta = (PDEVICE_OBJECT) * ((_DWORD * )v7 + 7 ); } _InterlockedExchange((volatile __int32 * )v7 + 17 , 0 ); if ( * ((_DWORD * )v7 + 16 ) ) KeSetEvent((PRKEVENT)(v7 + 76 ), 0 , 0 ); ObfDereferenceObject(v7); } return DeviceObjecta; } |
在IDA中该函数莫名其妙用了一个PDEVICE_OBJECT变量来接受前面本应返回NTSTATUS变量的IofCallDriver函数的返回值,这里让我觉得很诡异,所以我就用WRK来看了,就没用IDA分析了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | / / wrk中 NTSTATUS IopSynchronousServiceTail( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp, IN PFILE_OBJECT FileObject, IN BOOLEAN DeferredIoCompletion, IN KPROCESSOR_MODE RequestorMode, IN BOOLEAN SynchronousIo, IN TRANSFER_TYPE TransferType ) { ... status = IoCallDriver( DeviceObject, Irp ); ... ... return status; } |
该函数虽然调用了IoCallDriver函数,不是我们前面那个函数,但是在WRK中有一个宏定义
1 2 | #define IoCallDriver(a,b) \ IofCallDriver(a,b) |
这样就解释得通了,IopSynchronousServiceTail函数还是得到了一个返回值,然后继续返回,所以继续向上查看IopXxxControlFile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | NTSTATUS IopXxxControlFile( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, IN ULONG IoControlCode, IN PVOID InputBuffer OPTIONAL, IN ULONG InputBufferLength, OUT PVOID OutputBuffer OPTIONAL, IN ULONG OutputBufferLength, IN BOOLEAN DeviceIoControl ) { ... ... return IopSynchronousServiceTail( deviceObject, irp, fileObject, (BOOLEAN)!DeviceIoControl, requestorMode, synchronousIo, OtherTransfer ); } |
该函数结束时返回了IopSynchronousServiceTail函数的值,所以继续往上查看NtDeviceIoControlFile的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | NTSTATUS NtDeviceIoControlFile ( __in HANDLE FileHandle, __in_opt HANDLE Event, __in_opt PIO_APC_ROUTINE ApcRoutine, __in_opt PVOID ApcContext, __out PIO_STATUS_BLOCK IoStatusBlock, __in ULONG IoControlCode, __in_bcount_opt(InputBufferLength) PVOID InputBuffer, __in ULONG InputBufferLength, __out_bcount_opt(OutputBufferLength) PVOID OutputBuffer, __in ULONG OutputBufferLength ) { ... ... return IopXxxControlFile( FileHandle, Event, ApcRoutine, ApcContext, IoStatusBlock, IoControlCode, InputBuffer, InputBufferLength, OutputBuffer, OutputBufferLength, TRUE ); } |
结果同上,再往上就是:
1 2 3 | 06 a7b04c04 77c86c04 nt!KiSystemServicePostCall 07 0028f790 77c853ec ntdll!KiFastSystemCallRet 08 0028f794 75e0ab4d ntdll!ZwDeviceIoControlFile + 0xc |
这三个函数,这两个没用实质性内容,主要是用来从用户层进入内核层,当在应用层调用ZwDeviceIoControl后会通过这上面的函数来进入内核层调用NtDeviceIoControlFile,具体原理与这里的内容无关,暂时跳过。
所以现在需要分析:
1 | 09 0028f7f4 77babbc6 KERNELBASE!DeviceIoControl + 0xf6 |
函数的内容,它是在KERNELBASE.dll模块中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | BOOL __stdcall DeviceIoControl(HANDLE hDevice, DWORD dwIoControlCode, LPVOID lpInBuffer, DWORD nInBufferSize, LPVOID lpOutBuffer, DWORD nOutBufferSize, LPDWORD lpBytesReturned, LPOVERLAPPED lpOverlapped) { ... 省略 ... v11 = NtDeviceIoControlFile( hDevice, 0 , 0 , 0 , &IoStatusBlock, dwIoControlCode, lpInBuffer, nInBufferSize, lpOutBuffer, nOutBufferSize); LABEL_15: if ( (v11 & 0xC0000000 ) ! = - 1073741824 ) * lpBytesReturned = IoStatusBlock.Information; BaseSetLastNTError(v11); return 0 ; } v11 = IoStatusBlock.Status; } if ( v11 > = 0 ) { * lpBytesReturned = IoStatusBlock.Information; return 1 ; } goto LABEL_15; } lpOverlapped - >Internal = 259 ; v8 = lpOverlapped - >hEvent; BaseSetLastNTError(v9); return 0 ; } |
首先说明在ntdll.dll中 Zw开头和Nt开头后面名称一样的函数内容是相同的,比如这里:
然后 DeviceIoControl函数,在调用NtDeviceIoControlFile函数得到返回值NTSTAUTS类型时调用了BaseSetLastNTError函数来处理该返回值,步入查看BaseSetLastNTError函数:
1 2 3 4 5 6 7 8 | ULONG __stdcall BaseSetLastNTError(NTSTATUS Status) { ULONG v1; / / esi v1 = RtlNtStatusToDosError(Status); RtlSetLastWin32Error(v1); return v1; } |
这个函数调用RtlNtStatusToDosError来处理NTSTATUS Status参数,然后返回了一个ULONG类型的变量,就很像我们想要的内容,将内核的NTSTATUS对应的错误代码转换成GetLastError对应的错误代码。在ntdll.dll中逆向分析该函数:
1 2 3 4 5 6 7 8 9 | ULONG __stdcall RtlNtStatusToDosError(NTSTATUS Status) { struct _TEB * v1; / / eax v1 = NtCurrentTeb(); if ( v1 ) v1 - >LastStatusValue = Status; return RtlNtStatusToDosErrorNoTeb(Status); } |
返回了一个RtlNtStatusToDosErrorNoTeb函数,继续分析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | unsigned int __stdcall RtlNtStatusToDosErrorNoTeb(unsigned int a1) { unsigned int result; / / eax int v2; / / ecx int v3; / / esi unsigned int v4; / / ecx unsigned int v5; / / edx unsigned __int16 v6; / / ax int v7; / / esi result = a1; if ( (a1 & 0x20000000 ) = = 0 ) { if ( (a1 & 0xFFFF0000 ) = = - 2147024896 ) { result = (unsigned __int16)a1; } else { if ( (a1 & 0xF0000000 ) = = - 805306368 ) result = a1 & 0xCFFFFFFF ; v2 = 0 ; v3 = 0 ; while ( result > = dword_77F131E8[ 2 * v2] ) { v3 + = (unsigned __int16)word_77F131E4[ 4 * v2] * (unsigned __int16)word_77F131E6[ 4 * v2]; if ( (unsigned int ) + + v2 > = 0xC9 ) goto LABEL_8; } v4 = 4 * v2; v5 = result - RtlpRunTable[v4 / 2 ]; if ( v5 > = (unsigned __int16)word_77F131E4[v4] ) { LABEL_8: if ( (result & 0xFFFF0000 ) = = - 1073676288 ) return (unsigned __int16)result; DbgPrint( "RTL: RtlNtStatusToDosError(0x%lx): No Valid Win32 Error Mapping\n" , result); DbgPrint( "RTL: Edit ntos\\rtl\\generr.c to correct the problem\n" ); DbgPrint( "RTL: ERROR_MR_MID_NOT_FOUND is being returned\n" ); return 317 ; } v6 = word_77F131E6[v4]; v7 = v5 * v6 + v3; if ( v6 = = 1 ) result = (unsigned __int16)RtlpStatusTable[v7]; else result = (unsigned __int16)RtlpStatusTable[v7] | ((unsigned __int16)word_77F1394A[v7] << 16 ); } } return result; } |
这个函数就是关键函数了,一看就是将传进来的NTSTAUTS变量一顿操作后,变成了一个int类型的,然后返回了。
测试:
我的DeviceIoControl中有三种我自己指定的返回值:
1 | STATUS_SUCCESS STATUS_UNSUCCESSFUL 和和STATUS_BUFFER_OVERFLOW |
我猜234对应的是和STATUS_BUFFER_OVERFLOW的值,然后编写一个简单的驱动代码来打印该NTSTATUS对应的应用层error:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #include <ntifs.h> extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { DbgBreakPoint(); UNREFERENCED_PARAMETER(DriverObject); UNREFERENCED_PARAMETER(RegistryPath); KdPrint(( "驱动加载成功\n" )); auto temp = RtlNtStatusToDosErrorNoTeb(STATUS_BUFFER_OVERFLOW); KdPrint(( "%d\n" , temp)); return STATUS_SUCCESS; } |
结果如下: