首页
论坛
课程
招聘
[原创]内核NTSTATUS状态码和应用层错误代码的关系:
2022-4-15 16:00 9277

[原创]内核NTSTATUS状态码和应用层错误代码的关系:

2022-4-15 16:00
9277

内核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;
}

​ 结果如下:

 

图片描述


【看雪培训】《Adroid高级研修班》2022年夏季班招生中!

收藏
点赞4
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回