July 8th, 2026
0 reactions

The other kind of control flow guard check: The combined validate and call

Some time ago, I discussed how to extract the function pointer from the control flow guard check. I gave the code for LdrpValidateUserCallTarget, but there’s another version of the function that combines the validation with a call. I assume this version exists because after validating a function pointer, you nearly always call it, so you may as well combine the two operations.

But this does mean that the calling convention has to change, because the registers need to be set up for the final call, meaning that the parameters to the combined validate-and-call cannot overlap with registers used by the calling convention. (Sound familiar?)

Here’s an x86-64 version.

    mov     r11, [ntdll!....]
    mov     r10,rax
    shr     r10,9
    mov     r11,qword ptr [r11+r10*8]
    mov     r10,rax
    shr     r10,3
    test    al,0Fh
    jne     @1
    bt      r11,r10
    jae     @2
    jmp     rax
@1: btr     r10,0
    bt      r11,r10
    jae     @3
@2: or      r10,1
    bt      r11,r10
    jae     @3
    jmp     rax
@3: xor     r10d, r10d
    jmp     bad

Let’s put this side-by-side with the validate-only version:

Validate only Validate and call
    mov     rdx,qword ptr [ntdll!....]
    mov     rax,rcx
    shr     rax,9
    mov     rdx,qword ptr [rdx+rax*8]
    mov     rax,rcx
    shr     rax,3
    test    cl,0Fh
    jne     @1
    bt      rdx,rax
    jae     @2
    ret
@1: btr     rax,0
    bt      rdx,rax
    jae     @3
@2: or      rax,1
    bt      rdx,rax
    jae     @3
    ret
@3: mov     rax,rcx
    xor     r10d,r10d
    jmp     bad
    mov     r11, [ntdll!....]
    mov     r10,rax
    shr     r10,9
    mov     r11,qword ptr [r11+r10*8]
    mov     r10,rax
    shr     r10,3
    test    al,0Fh
    jne     @1
    bt      r11,r10
    jae     @2
    jmp     rax
@1: btr     r10,0
    bt      r11,r10
    jae     @3
@2: or      r10,1
    bt      r11,r10
    jae     @3
    jmp     rax
@3:
    xor     r10d, r10d
    jmp     bad

The logic is the same; the functions merely use different registers.

The validate-only version receives the address in rcx and uses rax and rdx as scratch registers. The validate-and-call version receives the address in rax and uses r10 and r11 as scratch registers. (There’s also a small change when a bad pointer is detected: The validate-and-call version already has the bad pointer in the rax register, so it doesn’t have to do anything to move it there.)

The validate-and-call version shifts its parameter and scratch registers to those not used by the x86-64 Windows calling convention, so that it can finish with a jmp rax to jump to the validated function with all function parameters intact.

For AArch64, the story is similar.

Validate only Validate and call
    adrp        xip0,ntdll!....
    ldr         xip0,[xip0,#0x598]

    lsr         xip1,x15,#6
    tst         x15,#0xF
    ldrb        wip1,[xip0,xip1]
    ubfx        xip0,x15,#3,#3
    bne         @2

    lsr         xip1,xip1,xip0
    tbz         wip1,#0,@3
@1: ret

@2: and         xip0,xip0,#-2
    lsr         xip1,xip1,xip0
    tbz         wip1,#0,@4
@3: tbnz        wip1,#1,@1
@4: mov         xip0,#0
    b           @5
@5: b           bad
    adrp        xip0,ntdll!....
    ldr         xip0,[xip0,#0x598]

    lsr         xip1,x9,#6
    tst         x9,#0xF
    ldrb        wip1,[xip0,xip1]
    ubfx        xip0,x9,#3,#3
    bne         @2

    lsr         xip1,xip1,xip0
    tbz         wip1,#0,@3
@1: br          x9

@2: and         xip0,xip0,#-2
    lsr         xip1,xip1,xip0
    tbz         wip1,#0,@4
@3: tbnz        wip1,#1,@1
@4: mov         xip0,#1
    mov         x15,x9
    b           bad

Again, the code sequences are the same; it’s just the register usage. (And the code sequence when a bad call is detected.) The validate-only version takes the address in x15, whereas the validate-and-call version takes the address in x9. (Both use xip0 and xip1 as scratch registers.) And the validate-and-call version finishes with a b r9 to jump directly to the validated address instead of returning.

Again, you can extract the bad pointer from the thing that is shifted. For x86-64 validate-and-call, it’s rax, and for Aarch64 validate-and-call, it’s r9.

Topics

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments