ProbeForWrite vs ProbeForRead

This question is mainly out of curiosity.

This is the implementation of ProbeForRead:

VOID
ProbeForRead(
    IN CONST VOID *Address,
    IN SIZE_T Length,
    IN ULONG Alignment
    )
{
    PAGED_CODE();

    ASSERT(((Alignment) == 1) || ((Alignment) == 2) ||
           ((Alignment) == 4) || ((Alignment) == 8) ||
           ((Alignment) == 16));

    if ((Length) != 0) {
        if (((ULONG_PTR)(Address) & ((Alignment) - 1)) != 0) {
            ExRaiseDatatypeMisalignment();

        } else if ((((ULONG_PTR)(Address) + (Length)) < (ULONG_PTR)(Address)) ||
                   (((ULONG_PTR)(Address) + (Length)) > (ULONG_PTR)MM_USER_PROBE_ADDRESS)) {
            ExRaiseAccessViolation();
        }
    }
}

This is the implementation for ProbeForWrite:

VOID
ProbeForWrite (
    IN PVOID Address,
    IN SIZE_T Length,
    IN ULONG Alignment
    )
{

    ULONG_PTR EndAddress;
    ULONG_PTR StartAddress;

    if (Length != 0) {
        ASSERT((Alignment == 1) || (Alignment == 2) ||
               (Alignment == 4) || (Alignment == 8) ||
               (Alignment == 16));

        StartAddress = (ULONG_PTR)Address;
        if ((StartAddress & (Alignment - 1)) == 0) {

            EndAddress = StartAddress + Length - 1;
            if ((StartAddress <= EndAddress) &&
                (EndAddress < MM_USER_PROBE_ADDRESS)) {

                EndAddress = (EndAddress & ~(PAGE_SIZE - 1)) + PAGE_SIZE;
                do {
                    *(volatile CHAR *)StartAddress = *(volatile CHAR *)StartAddress;

                    StartAddress = (StartAddress & ~(PAGE_SIZE - 1)) + PAGE_SIZE;
                } while (StartAddress != EndAddress);

                return;

            } else {
                ExRaiseAccessViolation();
            }

        } else {
            ExRaiseDatatypeMisalignment();
        }
    }

    return;
}

So the implementation of ProbeForRead doesn’t even dereference the provided buffer (it only checks it resides in the user mode portion) but ProbeForWrite tries to perform memory writes in loops. My question is - Why is ProbeForWrite implemented like this? Why do we need to perform these writes for each page? Can’t we just test it resides in the user mode portion as well?

Because the pages could be mapped, but mapped as read-only.

Yeah but if the page is mapped as read-only an exception will be thrown and we catch it so it’s ok… We need to use __try anyway when writing to user memory. Moreover ProbeForWrite is not safe by design because it does not lock the pages so the user code can unmap or change the page protection in between so I don’t understand what is the point of performing a security check that’s not safe by design…

The main value (IMO) of these APIs is to determine if the specified address is within the user mode range. Read/write checks are of limited usage since the app could change the page protections a moment after this call is made. Keep in mind that __try/__except do NOT catch invalid kernel mode accesses, so this is the big gain from having called ProbeForXxx ahead of time. In other words, if you took the position that you’d just let the exception handler deal with lack of write access, but the user actually supplied a kernel address, this would either be a massive security hole (valid kernel address) or crash the system (invalid kernel address).

1 Like

I know that dereferencing arbitrary kernel addresses can cause an immediate blue-screen. My question is: Why do we need to try to write to the pages in ProbeForWrite? As far as I understand checking for the user mode range is enough…

You certainly don’t have to call this function. But given that it’s called ProbeForWrite it makes sense that it would check writability, no? It also provides an alignment check which the user can’t change after the fact. But if you just want to see if it’s a user mode address you can directly check the pointer against MmUserProbeAddress which is a public symbol.

ProbeForRead and ProbeForWrite are old and venerable functions. As such, I’d suggest that actually writing to the memory made sense back in a “simpler time” – it also has the advantage of checking the state of the pages prior to your attempt to USE the pages, and could (sort of) allow you to differentiate different causes for the failure.

In short, I don’t think we gain anything (from a security perspective) in 2022 by writing to each page. Of course, I’ll be happy to be proven wrong by somebody who has more insight into this particular design point that me.

Peter

Presumably, buffers that a driver cares about reading from represent input of some kind. Most input is read somewhere near the start of whatever a specific request is supposed to do, so if the pages in question can’t be read, the operation will fail quickly - shortly after the ProbeForRead call and before some expensive hardware operation has taken place. But the buffers that a driver cares about writing to represent output of some kind, and the data to be written into those buffers won’t be available until it is collected from the hardware in some way. Verifying that the output range is actually writable before initiating the hardware operation allows the call to fail quickly with invalid input and avoids work that would be useless.

This is a high level description of my understanding of the rationale. I have no direct knowledge, but this is how is makes sense to me.