Windows Crash Dumps: Function Pointers and Control Flow Guard
Function Pointers
A function pointer is just an address to a function that’s calculated at runtime. They are vulnerable, though, because an attacker could replace the address while the program is running and redirect its calls to malicious code.
Control Flow Guard
Windows Control Flow Guard injects hooks into your program to make sure calls made via function pointers invoke the correct code.
You can read about Control Flow Guard (CFG) from Microsoft, here: https://learn.microsoft.com/en-us/windows/win32/secbp/control-flow-guard.
The Problem
Control Flow Guard hooks can obscure function names making it difficult to track the source of an error in a crash dump. When viewing a disassembly, instead of seeing the name of the function in a call
instruction, you will see something similar to below:
1
call qword ptr [mymodule!__guard_dispatch_icall_fptr]
It takes a few more steps to find out what is actually being called.
An Example Program
We’ll write a small program to demonstrate.
In the source, below, there are three functions that output a simple string and return an int
value. Two of them return 0
and one of them returns 0x80000000
, a fake 32-bit error code.
Their addresses are loaded into an array and a loop calls them randomly until it receives the fake error code. Then print_error()
is called and the program terminates.
The program is compiled with Windows Control Flow Guard enabled (see https://learn.microsoft.com/en-us/windows/win32/secbp/control-flow-guard#how-can-i-enable-cfg)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <time.h>
#include <stdbool.h>
int print_claudio_1() {
printf("O, what men dare do!\n");
return 0;
}
int print_claudio_2() {
printf("What men may do!\n");
return 0;
}
int print_claudio_3() {
printf("What men daily do, not knowing what they do!\n");
// We return a fake Windows error code on this one!
return 0x80000000;
}
void print_error(int errno) {
printf("** Oh no! Received %x error! **\n", errno);
getchar();
}
int main() {
// An array of function pointers.
// The functions return int and take no parameters
int (*functions[3])();
functions[0] = print_claudio_1;
functions[1] = print_claudio_2;
functions[2] = print_claudio_3;
// Seed the randomiser
srand(time(NULL));
int findex;
int result;
while (true) {
findex = rand() % 3;
result = functions[findex](findex);
if (result < 0) {
print_error(result);
exit(result);
}
}
}
Program Output
When we run the program we see output similar to the following:
At this point we use Windows Task Manager to take a dump file and pretend it’s a crash dump. We also pretend that we don’t have the source code. Yay, imagination!
Analysing the Dump File
To start with, lets dump the stack trace.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Child-SP RetAddr Call Site
00 0000004a`4a5af658 00007ffc`a54c6b2b ntdll!NtReadFile+0x14
01 0000004a`4a5af660 00007ffb`4ac1760c KERNELBASE!ReadFile+0x7b
02 0000004a`4a5af6d0 00007ffb`4ac16a53 ucrtbased!_read_nolock+0xb9c [minkernel\crts\ucrt\src\appcrt\lowio\read.cpp @ 566]
03 0000004a`4a5af7d0 00007ffb`4abbfc26 ucrtbased!_read+0x333 [minkernel\crts\ucrt\src\appcrt\lowio\read.cpp @ 396]
04 0000004a`4a5af830 00007ffb`4abc04d0 ucrtbased!common_refill_and_read_nolock<char>+0x1f6 [minkernel\crts\ucrt\src\appcrt\stdio\_filbuf.cpp @ 146]
05 0000004a`4a5af8c0 00007ffb`4ab7717e ucrtbased!__acrt_stdio_refill_and_read_narrow_nolock+0x20 [minkernel\crts\ucrt\src\appcrt\stdio\_filbuf.cpp @ 181]
06 0000004a`4a5af900 00007ffb`4ab7749e ucrtbased!_fgetc_nolock+0x10e [minkernel\crts\ucrt\src\appcrt\stdio\fgetc.cpp @ 23]
07 0000004a`4a5af970 00007ffb`4ab771e3 ucrtbased!fgetc+0x28e [minkernel\crts\ucrt\src\appcrt\stdio\fgetc.cpp @ 52]
08 0000004a`4a5afa00 00007ffb`4ab774f9 ucrtbased!_fgetchar+0x13 [minkernel\crts\ucrt\src\appcrt\stdio\fgetc.cpp @ 74]
09 0000004a`4a5afa30 00007ff6`ee32209b ucrtbased!getchar+0x9 [minkernel\crts\ucrt\src\appcrt\stdio\fgetc.cpp @ 81]
0a 0000004a`4a5afa60 00007ff6`ee32216d hello_cfg!print_error+0x2b [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 25]
0b 0000004a`4a5afa90 00007ff6`ee322ed9 hello_cfg!main+0xbd [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 48]
0c 0000004a`4a5afb00 00007ff6`ee322d7e hello_cfg!invoke_main+0x39 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 79]
0d 0000004a`4a5afb50 00007ff6`ee322c3e hello_cfg!__scrt_common_main_seh+0x12e [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
0e 0000004a`4a5afbc0 00007ff6`ee322f6e hello_cfg!__scrt_common_main+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331]
0f 0000004a`4a5afbf0 00007ffc`a764257d hello_cfg!mainCRTStartup+0xe [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17]
10 0000004a`4a5afc20 00007ffc`a7e4aa58 kernel32!BaseThreadInitThunk+0x1d
11 0000004a`4a5afc50 00000000`00000000 ntdll!RtlUserThreadStart+0x28
Frame 0b
transitions to frame 0a
with a call to print_error
. For anyone who spends any time analysing crash dumps this is known as a lovely big, fat clue that could lead us to the crash cause.
The next step is to disassemble main
and find out why it would ever want to call print_error
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
0:000> uf hello_cfg!main
...(Some output removed)...
hello_cfg!main+0x71 [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 43]:
43 00007ff6`ee322121 33c0 xor eax,eax
43 00007ff6`ee322123 83f801 cmp eax,1
43 00007ff6`ee322126 7450 je hello_cfg!main+0xc8 (00007ff6`ee322178) Branch
hello_cfg!main+0x78 [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 44]:
44 00007ff6`ee322128 e8bf3e0000 call hello_cfg!rand (00007ff6`ee325fec)
44 00007ff6`ee32212d 99 cdq
44 00007ff6`ee32212e b903000000 mov ecx,3
44 00007ff6`ee322133 f7f9 idiv eax,ecx
44 00007ff6`ee322135 8bc2 mov eax,edx
44 00007ff6`ee322137 89442444 mov dword ptr [rsp+44h],eax
45 00007ff6`ee32213b 4863442444 movsxd rax,dword ptr [rsp+44h]
45 00007ff6`ee322140 488b44c428 mov rax,qword ptr [rsp+rax*8+28h]
45 00007ff6`ee322145 4889442450 mov qword ptr [rsp+50h],rax
45 00007ff6`ee32214a 8b4c2444 mov ecx,dword ptr [rsp+44h]
45 00007ff6`ee32214e 488b442450 mov rax,qword ptr [rsp+50h]
45 00007ff6`ee322153 ff15b7fe0000 call qword ptr [hello_cfg!__guard_dispatch_icall_fptr (00007ff6`ee332010)]
45 00007ff6`ee322159 89442448 mov dword ptr [rsp+48h],eax
46 00007ff6`ee32215d 837c244800 cmp dword ptr [rsp+48h],0
46 00007ff6`ee322162 7d12 jge hello_cfg!main+0xc6 (00007ff6`ee322176) Branch
hello_cfg!main+0xb4 [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 47]:
47 00007ff6`ee322164 8b4c2448 mov ecx,dword ptr [rsp+48h]
47 00007ff6`ee322168 e853f5ffff call hello_cfg!ILT+1712(print_error) (00007ff6`ee3216c0)
48 00007ff6`ee32216d 8b4c2448 mov ecx,dword ptr [rsp+48h]
48 00007ff6`ee322171 e87c3e0000 call hello_cfg!exit (00007ff6`ee325ff2)
hello_cfg!main+0xc6 [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 50]:
50 00007ff6`ee322176 eba9 jmp hello_cfg!main+0x71 (00007ff6`ee322121) Branch
hello_cfg!main+0xc8 [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 51]:
51 00007ff6`ee322178 488bcc mov rcx,rsp
51 00007ff6`ee32217b 488d15fe8a0000 lea rdx,[hello_cfg!__xt_z+0x1e0 (00007ff6`ee32ac80)]
51 00007ff6`ee322182 e8c9f4ffff call hello_cfg!ILT+1600(_RTC_CheckStackVars) (00007ff6`ee321650)
51 00007ff6`ee322187 4883c460 add rsp,60h
51 00007ff6`ee32218b 5f pop rdi
51 00007ff6`ee32218c c3 ret
We’ll start at line 21 where a 64-bit pointer stored 0x50 bytes above the stack-pointer (a local variable) is loaded into the rax
register. Then on line 22, a Windows Control Flow Guard function pointer is invoked (__guard_dispatch_icall_fptr
).
Lines 23-29 process the result of that function by comparing it to 0
. If it’s greater than, or equal to, 0
there is a jmp
over the call to print_error
. If not, the program drops into print_error
and then terminates.
__guard_dispatch_icall_fptr
is Windows Control Flow Guard ‘hooking’ the original function call. It uses rax
to store the actual address of the function and, in a lot of scenarios, it’s possible to recover what was called.
Why the
rax
register? Windows 64-bit ABI uses the fastcall convention in which the first four parameters to all functions are stored in thercx
,rdx
,r8
andr9
registers. Any remaining parameters are passed on the stack. I suspect therax
register is used because it’s reserved for returning results and won’t get in the way of parameters to the actual function being called. Once the function has been called that address is no longer needed and can be replaced with the actual result.
Finding the Function (finally!)
First, we can try and check what was in rax
using the WinDbg .frame
command with the /r
switch. This prints out the status of all registers for a particular stack frame. In this case 0b
.
1
2
3
4
5
6
7
8
9
10
11
12
0:000> .frame /r 0b
0b 0000004a`4a5afa90 00007ff6`ee322ed9 hello_cfg!main+0xbd [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 48]
rax=0000000000000006 rbx=0000000000000000 rcx=0000000000000008
rdx=0000000000000000 rsi=0000000000000000 rdi=0000004a4a5afaf0
rip=00007ff6ee32216d rsp=0000004a4a5afa90 rbp=0000000000000000
r8=0000000000001000 r9=0000000000000000 r10=0000000000000001
r11=0000021c7ecb2b30 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl zr na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246
hello_cfg!main+0xbd:
00007ff6`ee32216d 8b4c2448 mov ecx,dword ptr [rsp+48h] ss:0000004a`4a5afad8=80000000
Hmm … The constant value 6
. Not really what we’re looking for. Remember these values are stored just before the stack frame changes to 0a
(the call to print_error
), so it’s not inconceivable that rax
has changed by then. In fact, it was probably Control Flow Guard that did it!
There is something useful in this output, though, and that’s the frame address (0000004a4a5afa90
). Notice that it’s the same value as the rsp
register, or the stack-pointer. In Windows 64-bit, rsp
is not manipulated inside a function. It’s set in the function prologue and reset in the function epilogue, but that’s it. Elements are not push
ed or pop
ped but mov
ed to a slot calculated on the fly.
We know that a value 0x50 bytes above rsp
was was loaded into rax
before the call and we know that rsp
will keep the base address of the frame throughout the life of a function (it’s value certainly seems to match the address we see when using the .frame
command), so let’s see what is actually at rsp+0x50
, or 0000004a4a5afa90 + 50h
.
1
2
0:000> dps 0000004a4a5afa90+50h L1
0000004a`4a5afae0 00007ff6`ee321160 hello_cfg!ILT+336(print_claudio_3)
There it is. print_claudio_3
. We can now disassemble and take a look.
1
2
3
4
5
6
7
8
9
10
11
12
0:000> uf print_claudio_3
hello_cfg!print_claudio_3 [C:\Users\chris\source\repos\hello-cfg\hello-cfg\hello-cfg.c @ 15]:
15 00007ff6`ee322040 4057 push rdi
15 00007ff6`ee322042 4883ec20 sub rsp,20h
15732480 00007ff6`ee322046 488d0dbeef0000 lea rcx,[hello_cfg!_guard_fids_table <PERF> (hello_cfg+0x1100b) (00007ff6`ee33100b)]
15732480 00007ff6`ee32204d e8bef6ffff call hello_cfg!ILT+1792(__CheckForDebuggerJustMyCode) (00007ff6`ee321710)
16 00007ff6`ee322052 488d0dd7af0000 lea rcx,[hello_cfg!$xdatasym <PERF> (hello_cfg+0xd030) (00007ff6`ee32d030)]
16 00007ff6`ee322059 e802f3ffff call hello_cfg!ILT+848(printf) (00007ff6`ee321360)
19 00007ff6`ee32205e b800000080 mov eax,80000000h
20 00007ff6`ee322063 4883c420 add rsp,20h
20 00007ff6`ee322067 5f pop rdi
20 00007ff6`ee322068 c3 ret
You can even see a Control Flow Guard artefact on line 5.
Line 9 shows our error code being loaded into the eax
register (it’s only 32-bits wide, hence the 32-bit register name) as the function result.
If the value in the rsp
register was different to the frame address then try the above with just the frame address, instead.
32-bit Windows programs do manipulate the stack-pointer register (
esp
) and are constantlypush
ing andpop
ping, but they also use theebp
register as the base-pointer which is set during function prologue and reset during function epilogue. This method should also work for 32-bit programs, then, just useebp
instead ofesp
or in-fact, the address of the stack-frame from.frame
!
Finally…
Obviously, this is a contrived example, but I have come across this in the course of real-world scenarios and it shows a quick way around Control Flow Guards side-effect of obfuscation.