Post

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:

Control Flow Guard Example

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!

Create Memory Dump File

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 the rcx, rdx, r8 and r9 registers. Any remaining parameters are passed on the stack. I suspect the rax 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 pushed or popped but moved 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 constantly pushing and popping, but they also use the ebp 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 use ebp instead of esp 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.

This post is licensed under CC BY 4.0 by the author.