Hi, Recently there's been a decent amount of Windows reverse engineering and research in order to come up with some rather simple but important results. I figure it might be useful to document parts of this somewhere.
On Windows, when an interface connects to an endpoint, it generates a "Network Signature", which is supposed to uniquely identify a network. This is done through the conjunction of the nla and network list services, and the kernel. For wifi, it uses aspects about the AP and SSID. For wwan, it uses details from there. And in general, for layer 2 connections, it hashes the mac address of the default gateway. This is sort of a terrible way to identify a network, since mac addresses can be spoofed. It looks like Microsoft has toyed with trying to make this less bad, with various papers and patents and whatnot for "authenticated dhcp" with some fancy/dated crypto, but it doesn't appear that these have gone anywhere. For layer 3 networks, nla doesn't really know what to do and so winds up just hashing the GUID of the NetCfgInstanceId for the interface. This means that every particular layer 3 interface instance implies only a single network. This Network Signature stuff matters because Windows will automatically fiddle with firewall rules based on which network you're connected to. Forge a network, and you can perhaps get a victim to have a more permissive firewall. For WireGuard, we're using Wintun interfaces [1]. We could roll with a single interface, which then would have the same Network Signature and hence firewall settings, no matter which WireGuard peers its hooked up with. Or, we could have a new interface for every new connection event, but this means adding an unbounded amount of garbage to the registry and cluttering people's interface names with things like "Local Area Connection 28912", because this is the 28912th time you've used WireGuard. Neither is so ideal. Since Wintun is a layer 3 interface, it turns out "all" we need to do is deterministically generate the GUID at adapter installation time. I couldn't find any documented way of doing this. After reversing 3 kernel drivers and 5 userland DLLs, I found that Windows is using a certain registry value as a sort of temporary storage in between two phases that it runs serially. By using the super old and ugly SetupAPI, we're actually able to split these phases up, so that we can modify this registry key before the next phase sees it. This is sort of like exploiting a race condition, except we're able to entirely pause the race while making our modifications, so we have 100% reliability. This winds up looking something like: CallClassInstaller(DIF_REGISTER_COINSTALLERS) key = OpenDevRegKey(DICS_FLAG_GLOBAL, DIREG_DRV) key.SetString("NetSetupAnticipatedInstanceId", deterministicGUID) CallClassInstaller(DIF_INSTALLINTERFACES) At the time of writing, other than our own source code, there are no hits for NetSetupAnticipatedInstanceId on Google, Bing, Yandex, or Baidu, so this might be the first description of such a technique. So, now that we can control the GUID and hence the NetworkSignature, we have to decide what determines a network. It turns out that in WireGuard, we can do this with much higher cryptographic assurance than any of the crazy "authenticated dhcp" proposals of Microsoft. Specifically, we know our own interface public key, the public keys of everyone we're willing to talk to, and which IP addresses we'll accept from those peers. If that doesn't perfectly define a network, I don't know what else does. Therefore, we sort the public keys, and sort the allowed IPs for each one, and then hash together a big block that looks like: label || len(interface name) || interface name || interface public key || number of peers || peer public key || number of peer allowed ips || len(allowed ip string) || allowed ip/cidr in canonical string notation || len(allowed ip string) || allowed ip/cidr in canonical string notation || len(allowed ip string) || allowed ip/cidr in canonical string notation || ... peer public key || number of peer allowed ips || len(allowed ip string) || allowed ip/cidr in canonical string notation || len(allowed ip string) || allowed ip/cidr in canonical string notation || len(allowed ip string) || allowed ip/cidr in canonical string notation || ... ... We pick a stable encoding for each of those elements (32-bit little endian integers, canonical IP address strings, well-known label identifier, etc), pass it all to BLAKE2, and mint ourselves a fully network-determined GUID. Hopefully this should eliminate a class of firewall attacks on Windows systems when using WireGuard. Perhaps this post will help others out somewhere down the line. By the way, since Windows security is pretty complex and hard to get right -- and hard to even keep all of it in your head at the same time -- we're maintaining an attacksurface.md [2] file, which evolves over time to contain our latest understanding. Hopefully this will help people point out where we're wrong, and also what areas we've neglected to consider. If something there piques your interest, don't hesitate to get in touch with security {at} wireguard ^dot^ com. Regards, Jason [1] https://www.wintun.net/ [2] https://git.zx2c4.com/wireguard-windows/about/attacksurface.md _______________________________________________ WireGuard mailing list WireGuard@lists.zx2c4.com https://lists.zx2c4.com/mailman/listinfo/wireguard