|  | 
|  | 1 | +// Licensed to the .NET Foundation under one or more agreements. | 
|  | 2 | +// The .NET Foundation licenses this file to you under the MIT license. | 
|  | 3 | + | 
|  | 4 | +using System; | 
|  | 5 | +using System.Diagnostics; | 
|  | 6 | +using System.Runtime.CompilerServices; | 
|  | 7 | +using System.Runtime.InteropServices; | 
|  | 8 | +using System.Text; | 
|  | 9 | + | 
|  | 10 | +// This test is testing control flow guard. | 
|  | 11 | +// | 
|  | 12 | +// Since the only observable behavior of control flow guard is that it terminates the process, | 
|  | 13 | +// to test various scenarios the test re-runs itself. The main executable finishes with | 
|  | 14 | +// exit code 100 on success. The subprocesses are all expected to be killed by control | 
|  | 15 | +// flow guard and exit with exit code C0000409. | 
|  | 16 | +// | 
|  | 17 | +// The "corrupted" indirect call target is located in a piece of memory we VirtualAlloc'd. | 
|  | 18 | +// By default VirtualAlloc'd RWX memory is considered valid call target. | 
|  | 19 | +// The s_armed static variable controls whether we ask the OS to consider the VirtualAlloc'd | 
|  | 20 | +// memory an invalid call target. | 
|  | 21 | +unsafe class ControlFlowGuardTests | 
|  | 22 | +{ | 
|  | 23 | +    static Func<int>[] s_scenarios = | 
|  | 24 | +        { | 
|  | 25 | +            TestFunctionPointer.Run, | 
|  | 26 | +            TestDelegate.Run, | 
|  | 27 | +            TestCorruptingVTable.Run, | 
|  | 28 | +        }; | 
|  | 29 | + | 
|  | 30 | +    static bool s_armed; | 
|  | 31 | + | 
|  | 32 | +    static int Main(string[] args) | 
|  | 33 | +    { | 
|  | 34 | +        // Are we running the control program? | 
|  | 35 | +        if (args.Length == 0) | 
|  | 36 | +        { | 
|  | 37 | +            // Dry run - execute all scenarios while s_armed is false. | 
|  | 38 | +            // | 
|  | 39 | +            // The replaced call target will not be considered invalid by CFG and none of this | 
|  | 40 | +            // should crash. This is a safeguard to make sure the only reason why the subordinate | 
|  | 41 | +            // programs could exit with a FailFast is CFG, and not some other bug in the test logic. | 
|  | 42 | +            Console.WriteLine("*** Dry run ***"); | 
|  | 43 | +            foreach (Func<int> scenario in s_scenarios) | 
|  | 44 | +                scenario(); | 
|  | 45 | + | 
|  | 46 | +            // Now launch subordinate processes and check they FailFast | 
|  | 47 | +            for (int i = 0; i < s_scenarios.Length; i++) | 
|  | 48 | +            { | 
|  | 49 | +                Console.WriteLine($"*** Scenario {i} ***"); | 
|  | 50 | +                Process p = Process.Start(new ProcessStartInfo(Environment.ProcessPath, i.ToString())); | 
|  | 51 | +                p.WaitForExit(); | 
|  | 52 | +                if ((p.ExitCode != -1073740791) && (p.ExitCode != 57005)) | 
|  | 53 | +                { | 
|  | 54 | +                    Console.WriteLine($"FAIL: Scenario exited with exit code {p.ExitCode}"); | 
|  | 55 | +                    return 1; | 
|  | 56 | +                } | 
|  | 57 | +                else | 
|  | 58 | +                { | 
|  | 59 | +                    Console.WriteLine($"Crashed as expected."); | 
|  | 60 | +                } | 
|  | 61 | +            } | 
|  | 62 | + | 
|  | 63 | +            return 100; | 
|  | 64 | +        } | 
|  | 65 | + | 
|  | 66 | +        [DllImport("kernel32", ExactSpelling = true)] | 
|  | 67 | +        static extern uint GetErrorMode(); | 
|  | 68 | + | 
|  | 69 | +        [DllImport("kernel32", ExactSpelling = true)] | 
|  | 70 | +        static extern uint SetErrorMode(uint uMode); | 
|  | 71 | + | 
|  | 72 | +        // Don't pop the WER dialog box that blocks the process until someone clicks Close. | 
|  | 73 | +        SetErrorMode(GetErrorMode() | 0x0002 /* NOGPFAULTERRORBOX */); | 
|  | 74 | + | 
|  | 75 | +        // VirtualAlloc should specify TARGETS_INVALID | 
|  | 76 | +        s_armed = true; | 
|  | 77 | + | 
|  | 78 | +        // Run specified subordinate program | 
|  | 79 | +        if (int.TryParse(args[0], out int index) && ((uint)index) < (uint)s_scenarios.Length) | 
|  | 80 | +            return s_scenarios[index](); | 
|  | 81 | + | 
|  | 82 | +        // Subordinate program unknown | 
|  | 83 | +        return 10; | 
|  | 84 | +    } | 
|  | 85 | + | 
|  | 86 | +    class TestFunctionPointer | 
|  | 87 | +    { | 
|  | 88 | +        public static int Run() | 
|  | 89 | +        { | 
|  | 90 | +            var target = (delegate*<void>)CreateNewMethod(); | 
|  | 91 | +            target(); | 
|  | 92 | +            Console.WriteLine("Was able to call the pointer"); | 
|  | 93 | +            return 1; | 
|  | 94 | +        } | 
|  | 95 | +    } | 
|  | 96 | + | 
|  | 97 | +    class TestDelegate | 
|  | 98 | +    { | 
|  | 99 | +        class RawData | 
|  | 100 | +        { | 
|  | 101 | +            public IntPtr FirstField; | 
|  | 102 | +        } | 
|  | 103 | + | 
|  | 104 | +        public static int Run() | 
|  | 105 | +        { | 
|  | 106 | +            Func<int> del = Run; | 
|  | 107 | + | 
|  | 108 | +            // Replace the delegate destination | 
|  | 109 | +            Span<IntPtr> delegateMemory = MemoryMarshal.CreateSpan(ref Unsafe.As<RawData>(del).FirstField, 4); | 
|  | 110 | +            int slotIndex = delegateMemory.IndexOf((IntPtr)(delegate*<int>)&Run); | 
|  | 111 | +            if (slotIndex < 0) | 
|  | 112 | +            { | 
|  | 113 | +                Console.WriteLine("Target not found in the delegate?"); | 
|  | 114 | +                return 1; | 
|  | 115 | +            } | 
|  | 116 | +            delegateMemory[slotIndex] = CreateNewMethod(); | 
|  | 117 | + | 
|  | 118 | +            del(); | 
|  | 119 | + | 
|  | 120 | +            Console.WriteLine("Was able to call the modified delegate"); | 
|  | 121 | +            return 2; | 
|  | 122 | +        } | 
|  | 123 | +    } | 
|  | 124 | + | 
|  | 125 | +    class TestCorruptingVTable | 
|  | 126 | +    { | 
|  | 127 | +        class Test<T> | 
|  | 128 | +        { | 
|  | 129 | +            public override string ToString() => "TotallyUniqueString"; | 
|  | 130 | +        } | 
|  | 131 | + | 
|  | 132 | +        public static int Run() | 
|  | 133 | +        { | 
|  | 134 | +            // Obscure `typeof(string)` so that dataflow analysis can't see it and the MakeGenericType | 
|  | 135 | +            // call produces a freshly allocated vtable (not a vtable in the readonly data segment of | 
|  | 136 | +            // the executable that we wouldn't be able to overwrite). | 
|  | 137 | +            Type stringType = Type.GetType(new StringBuilder("System.").Append("String").ToString()); | 
|  | 138 | +            Type testOfString = typeof(Test<>).MakeGenericType(stringType); | 
|  | 139 | + | 
|  | 140 | +            // Patch the MethodTable of Test<string>: find the vtable slot with the ToString method | 
|  | 141 | +            // and replace it with a new value that is not in the control flow guard bitmask. | 
|  | 142 | +            IntPtr toStringMethod = testOfString.GetMethod("ToString").MethodHandle.GetFunctionPointer(); | 
|  | 143 | +            var methodTableMemory = new Span<IntPtr>((void*)testOfString.TypeHandle.Value, 64); | 
|  | 144 | +            int slotIndex = methodTableMemory.IndexOf(toStringMethod); | 
|  | 145 | +            if (slotIndex < 0) | 
|  | 146 | +            { | 
|  | 147 | +                Console.WriteLine("ToString method not found in the MethodTable?"); | 
|  | 148 | +                return 1; | 
|  | 149 | +            } | 
|  | 150 | +            methodTableMemory[slotIndex] = CreateNewMethod(); | 
|  | 151 | + | 
|  | 152 | +            // Allocate the type and call the corrupted virtual slot | 
|  | 153 | +            object o = Activator.CreateInstance(testOfString); | 
|  | 154 | +            o.ToString(); | 
|  | 155 | + | 
|  | 156 | +            // CFG should have stopped the party | 
|  | 157 | +            Console.WriteLine("Was able to call the modified slot"); | 
|  | 158 | +            return 2; | 
|  | 159 | +        } | 
|  | 160 | +    } | 
|  | 161 | + | 
|  | 162 | +    static IntPtr CreateNewMethod() | 
|  | 163 | +    { | 
|  | 164 | +        [DllImport("kernel32", ExactSpelling = true, SetLastError = true)] | 
|  | 165 | +        static extern IntPtr VirtualAlloc(IntPtr lpAddress, nuint dwSize, int flAllocationType, int flProtect); | 
|  | 166 | + | 
|  | 167 | +        int flProtect = 0x40 /* EXEC_READWRITE */; | 
|  | 168 | + | 
|  | 169 | +        if (s_armed) | 
|  | 170 | +            flProtect |= 0x40000000 /* TARGETS_INVALID */; | 
|  | 171 | + | 
|  | 172 | +        IntPtr address = VirtualAlloc( | 
|  | 173 | +            lpAddress: IntPtr.Zero, | 
|  | 174 | +            dwSize: 4096, | 
|  | 175 | +            flAllocationType: 0x00001000 | 0x00002000 /* COMMIT+RESERVE*/, | 
|  | 176 | +            flProtect: flProtect); | 
|  | 177 | + | 
|  | 178 | +        switch (RuntimeInformation.ProcessArchitecture) | 
|  | 179 | +        { | 
|  | 180 | +            case Architecture.X64: | 
|  | 181 | +            case Architecture.X86: | 
|  | 182 | +                *((byte*)address) = 0xC3; // ret | 
|  | 183 | +                break; | 
|  | 184 | +            case Architecture.Arm64: | 
|  | 185 | +                *((uint*)address) = 0xD65F03C0; // ret | 
|  | 186 | +                break; | 
|  | 187 | +            case Architecture.Arm: | 
|  | 188 | +                *((ushort*)address) = 0x46F7; // mov pc, lr | 
|  | 189 | +                break; | 
|  | 190 | +            default: | 
|  | 191 | +                throw new NotSupportedException(); | 
|  | 192 | +        } | 
|  | 193 | + | 
|  | 194 | +        return address; | 
|  | 195 | +    } | 
|  | 196 | +} | 
0 commit comments