⬆️ ⬇️

Where do the security gaps come from?

                                              “We have a security hole.”
                                              - Well, at least something is safe.
                                                                                   Joke


image If you are a Windows user, then you should be familiar with windows that pop up every second Tuesday of a month, reporting on the installation of "critical security updates." Microsoft is making a lot of effort working around fixing vulnerabilities in its operating systems, but it's worth it: in a world where the number of cyberattacks only increases every day, no loophole in the defense of our computers should remain open to potential intruders.



A recent discussion on the mailing list on the 66192nd SVN revision of ReactOS showed how easy it is to add a critical vulnerability to the kernel code. I will use this case as an example of a simple but security-related error, as well as a visual representation of some measures that need to expose the kernel code if you really want to get a secure system.



We will find vulnerability



Let's take a look at this code and see what's what:

')

NTSTATUS APIENTRY NtUserSetInformationThread(IN HANDLE ThreadHandle, IN USERTHREADINFOCLASS ThreadInformationClass, IN PVOID ThreadInformation, IN ULONG ThreadInformationLength) { [...] switch (ThreadInformationClass) { case UserThreadInitiateShutdown: { ERR("Shutdown initiated\n"); if (ThreadInformationLength != sizeof(ULONG)) { Status = STATUS_INFO_LENGTH_MISMATCH; break; } Status = UserInitiateShutdown(Thread, (PULONG)ThreadInformation); break; } [...] } 


This is a small piece of the NtUserSetInformationThread function, which is a system call in win32k.sys, which can be (more or less) directly caused by user programs. Here ThreadInformation is a pointer to a certain block with data, and the ThreadInformationClass parameter shows how this data should be interpreted. If it is equal to UserThreadInitiateShutdown, there must be a 4-byte integer in the block. The number of bytes transferred is stored in ThreadInformationLength, and, as is easy to see, the code really checks that there was exactly “4”, otherwise the execution will be interrupted with an error STATUS_INFO_LENGTH_MISMATCH. But note that both of these parameters come directly from the user program, which means that a malicious bookmark, calling this function, can transfer anything to it.



And now let's see what happens with ThreadInformation when it is passed to UserInitiateShutdown:



 NTSTATUS UserInitiateShutdown(IN PETHREAD Thread, IN OUT PULONG pFlags) { NTSTATUS Status; ULONG Flags = *pFlags; [...] *pFlags = Flags; [...] /* If the caller is not Winlogon, do some security checks */ if (PsGetThreadProcessId(Thread) != gpidLogon) { // FIXME: Play again with flags... *pFlags = Flags; [...] } [...] *pFlags = Flags; return STATUS_SUCCESS; } 


Since a fairly large part of this function has not yet been implemented, all that is happening above is only a few cycles of reading and writing the 4-byte value indicated by the user.



So what's the problem then?



Well, just dereferencing an unverified pointer is enough to make a DoS attack (denial of service) possible - a malicious program can simply shut down a computer without having the right to do so. For example, a program may simply pass a null (NULL) pointer and thus take advantage of the vulnerability. UserInitiateShutdown dereferences the aforementioned pointer, which will lead to a BSOD, commonly called “bug check” among kernel developers. In this case, the caller has the ability to write to the memory (here we recall that this is an arbitrary pointer - it can even refer to the core area!). At first glance, writing the value read from the specified memory area back to the same does not look so bad. But in reality can bring enough problems. Some sections of memory often change with high intensity, and restoring a previously stored value may, for example, reduce the entropy level of a random number generator of a cryptoalgorithm, or rewrite the table display of memory pages with its old version, which by this point should have already been destroyed, allowing access to more memory that can be used to compromise the system. But these are all just examples born along the way — and purposefully attacking may have months to come up with the best solution to achieve their goals, and a seemingly small security flaw, such as this one, may be sufficient for someone to get all the secrets out of your car and take full control of it. Of course, when the function is fully implemented, it will change the Flags variable before writing it back, allowing you to modify an arbitrary section of memory (core), and in a controlled way is a real feast for the hacker.



Knowing all this, what can be fixed?



To protect against this kind of problem, the NT kernel has two mechanisms: probing (checking) and SEH (Structured Exception Handling). Memory checking eliminates a large number of problems, allowing you to make sure that the pointer received from the application actually refers to the user's memory space. Performing such a check for all pointer parameters ensures that user-level programs cannot access the kernel memory in this way. However, this does not save from null, or any other invalid pointers. And here the second mechanism, SEH, comes to the rescue: wrapping up every access to data by questionable pointers (that is, received from user programs) in an exception-handling block ensures that the code is stable even if the pointer is invalid. The kernel-level code in this case provides an exception handler, which is called whenever protected code throws an exception (such as an access violation due to using the wrong pointer). The exception handler collects the available information (such as an exception code), performs all the necessary actions to clear the memory, and returns, in most cases, control to the user, along with the error code.



Let's look at the corrected sources ( r66223 commit ):



 ULONG CapturedFlags = 0; ERR("Shutdown initiated\n"); if (ThreadInformationLength != sizeof(ULONG)) { Status = STATUS_INFO_LENGTH_MISMATCH; break; } /* Capture the caller value */ Status = STATUS_SUCCESS; _SEH2_TRY { ProbeForWrite(ThreadInformation, sizeof(CapturedFlags), sizeof(PVOID)); CapturedFlags = *(PULONG)ThreadInformation; } _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { Status = _SEH2_GetExceptionCode(); } _SEH2_END; if (NT_SUCCESS(Status)) Status = UserInitiateShutdown(Thread, &CapturedFlags); /* Return the modified value to the caller */ _SEH2_TRY { *(PULONG)ThreadInformation = CapturedFlags; } _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { Status = _SEH2_GetExceptionCode(); } _SEH2_END; 




Notice that all calls to the unsafe ThreadInformation pointer are now made inside _SEH2_TRY blocks. The exceptions arising in them will be controlled intercepted by code from the _SEH2_EXCEPT block. In addition, before delicating the pointer for the first time, a call is made to ProbeForWrite, which raises the exception STATUS_ACCESS_VIOLATION or STATUS_DATATYPE_MISALIGNMENT if an invalid (belonging to the core region, for example) pointer is found, or memory protected from writing. In the end, pay attention to the entered variable CapturedFlags, which is passed to UserInitiateShutdown. This trick simplifies operations with an insecure parameter: in order not to use SEH every time pFlags is accessed within a function, this value is saved to the trusted area by NtUserSetInformationThread, and then written back to user memory when the UserInitiateShutdown is completed. This eliminates the need to edit UserInitiateShutdown itself, since now it receives a safe pointer from the kernel area (a pointer to CapturedFlags) to the input. The result of all these measures - the function can now work with absolutely any set of user data, correct, and not very much, without the risk of harming the system. It is done!



What is the lesson to learn from this?



Obviously, heightened vigilance at the development stage allows to notice in time the lines of code that can become a security threat in the future. We must not allow them to become too many, because, frankly speaking, without them there will surely be a lot of security problems. In the future, if everything goes according to plan, we will gradually search for them and fix them, releasing regular updates, such as those that come to you on Tuesdays from the Windows Update Center.



A margin note. As Alex Ionescu rightly pointed out, Windows itself has a vulnerability in the same function, NtUserSetInformationThread. Moreover, according to him, it has not yet been closed and actively exploited for all kinds of jailbreaks of devices like Surface RT. It was first described in 2012 by a well-known security researcher named Mateusz "jooro" Yurchik (Mateusz Jurczyk) (who, by the way, often hangs out with us in IRC;]). His article on this topic can be found in the blog: j00ru.vexillium.org/?p=1393



- Notes from the translator:

All typos, errors and inaccuracies please report in personal messages.

In the translation involved: Postscripter , al-tarakanoff , Alexey Bragin , Mabu

Source: https://habr.com/ru/post/250749/



All Articles