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.
0 comments
Be the first to start the discussion.