 All right, welcome everyone. My name is Jack Baker and I'm going to be talking about bugs in game engine. So as a little bit of background over the last few months, I found more than 10 remotely exploitable bugs while looking at two different game engines. And I'm going to be talking about four of those bugs today. So just to level set things, when I use the term game engine, I'm referring to the base software that most games are built on, built on top of. So if you're building a video game, you're probably not doing it from scratch. You're using a pre-made set of tools and software that are built to make that process easier. And that software is called your game engine. And so the popularity of many game engines means that a lot of games share the exact same bugs. This is made worse by the fact that updating your game engine can be a huge pain, as anyone who's done a lot of game dev can probably tell you. And games don't usually get security patches after release. Maybe like a bigger game will have some support, but like an independent release is not going to get patches a year or two years after release just to fix some security bug. So there aren't a whole lot of good statistics on what game engines are the most popular, but there's a general understanding that two of them are more common than others. And those are Unreal Engine 4 and Unity. So as sort of a rule of thumb, if you're a solo developer or a small team, there's a pretty good chance you're using Unity. Whereas if you're a larger team but not so large that you've built your own engine from scratch, you're probably using Unreal Engine 4. So Unreal Engine 4 is created by Epic Games. It's named for its roots in the Unreal series. It is open source. There are some licensing restrictions on how you can use that source, but for our perspective of just looking for bugs, all the code is out there for us to look at. And there are really two big notable games, I think, right now that are built using Unreal Engine 4. And those are Fortnite and PUBG. Unity is built by Unity Technologies. There are some open source components of it, but the core components of Unity are closed source. The core networking library we're going to be looking at is called UNET. And while I couldn't find a whole lot of like big games that are using UNET, there are countless amount of indie releases on Steam and everywhere else that are built using UNET. Now I should say that UNET is deprecated, but there are a few reasons that I thought it would be a good target anyway. The first is that UNET has not got an official replacement yet. Unity hasn't put out an alternative yet. If you're using Unity and you're doing multiplayer, you're either using UNET still or you're using third-party solution. Also, UNET still does receive patches and occasionally new features, even though it's deprecated. So the encryption API was added to UNET after deprecation. And so a ton of new and more importantly, I think, existing games use UNET. The bugs in UNET still have some value. So let's talk about multiplayer protocols. As game engines have evolved and as multiplayer protocols have evolved, there's really been a focus on two things. The first is obvious. It's increasing performance, increasing speed. The other is moving trust away from the client in order to prevent hacking. But as we're going to see, these can sometimes be conflicting goals. But to really understand multiplayer protocols, I think it's worth understanding the types of attacks that they're aiming to prevent. And I think the best example of this is to talk about the evolution of what we'll call movement hacking. So movement hacking is the process of manipulating the player's location in some way that the game normally wouldn't allow. In the old days, this was really easy because player location was just trusted to the client. So if you manipulate your location client side, you can teleport server side. And this got more difficult as game engines got more complicated and trust was taken from the client and put on the server for player location. So the client can no longer say, I'm at XYZ. The client, all I can do is make a request saying, I would like to move. I'm moving in this direction at this speed and the server will update their position accordingly. This led to a new type of attack called speed hacking. So it's sort of the next evolution of movement hacking. Speed hacking, the goal is not to teleport necessarily, but just to move extremely fast. And typically the way this works is you send that movement request excessively fast faster than a normal client ever would. And more requests than means more speed. This was prevented by basically giving more authority and more context to the server. So the server should be able to understand what is a realistic distance that a character can move in a given time frame. And if a client is attempting to move beyond that time frame, it can stop that from happening. And so by giving this context to the server, we're able to prevent this type of attack. And this is really how game engines have evolved. It's been a constant process of moving trust away from the client and giving more authority, more responsibility and more context to the server to understand what is normal. So with all of this said, let's talk about some of the technical specifics of multiplayer protocols. Now, there's no real like publish standard of what a multiplayer protocol looks like, but there are a few things that are pretty consistent between different protocols. And the first of those is that most multiplayer protocols use some form of distributed architecture. And essentially what this means is each system, whether that be a client or a server, each system has a copy of every networked object, every object that's connected to the network in the game world. And actions between objects and between systems are performed through what are called remote procedure calls. So remote procedure calls, as the name sort of suggests, are a way of calling functions on a remote system as if you were calling them locally. So this is really easy for the programmer because it's just like you're calling a regular function of your code. The difference is that that function is executing on someone else's system, not your local system. This is really convenient, but there's a lot of complexity that goes into this process on the back end. And so with this concept of the distributed architecture usually comes some sort of concept of ownership, where owning an object usually means having the authority to issue RPCs on that object. And so the way this usually works is each player has ownership of their character and maybe some associated sub-objects like your inventory. But player A can only issue RPCs on character A. You can't issue RPCs on character B and vice versa. Another interesting technical detail of multiplayer protocols is that they're usually implemented over UDP. The one major exception to this are browser games where you can't access operating system sockets, so you have to use something like web sockets. But when we're talking about desktop games, we're usually talking about protocols that are implemented over UDP. This puts extra requirements on the protocol itself because UDP won't do things like validate that a packet is part of a session that you've already authenticated or identify when a packet is either a duplicate or out of order. So the protocol gets more complex because it has to deal with these types of problems itself. So now that we've talked a little bit about how these protocols work, let's look at our first bug. This is an Unreal Engine 4 bug and it's actually a file pathing bug. So Unreal Engine uses its own type of URL and it uses this to communicate details between the server and the client. So this can be stuff like the server can send a URL that says, we're playing on this map. You need to load these packages or the client could send something saying, I'm joining the game. My player name is Jack and I'm joining with two other players who are playing split screen on the same computer. And so one of these URLs might look something like this. You start with the IP address of the server, then you've got a file path that corresponds to a particular asset. And then just like an HTTP URL, you've got key value pairs that are separated by an equal sign. So the bug here is pretty simple. If you use a malicious URL, you can cause a server or a client to access any local file path. It's not going to try to write to that file or even read from that file. It's just going to try to determine if that file exists. This is pretty boring on its own, but it gets more interesting when we start talking about universal naming convention or UNC paths. The UNC paths are special windows paths that are used to access networked resources as if they were regular files. So regular local files. Typically, UNC path will look something like this. You've got two slashes, then the host name or IP address, another slash, then the share name, then another slash, and then either the file name or the full file path. So using this crafted URL or one that looks like it, we can cause a server or client to connect to a remote SMB share. This is pretty simple. The one trick here is we do need to include the part that says dot UMAP, where UMAP is a file extension used by Unreal Engine. And by having this somewhere in the domain name here, we use it as a subdomain. We can bypass some of the filtration. And if we provide this URL to a system, it will cause it to make an outgoing SMB connection and try to determine the existence of this high dot txt file. And so what this does is it opens up the affected system to the world of SMB related attacks. Typically, when a Windows system connects to an SMB share, it'll try to authenticate with it. So this can be used for credential harvesting or authentication relaying. There's a whole world of different attacks using this. This can also be used pretty trivially as a server denial of service because the whole server will lock up as it's making this request. So if you cause that SMB connection to deliberately take a long time, you can lock up the server for a while. So this was fixed in Unreal Engine 4.25.2 with this commit. It's really easy to backport this. So if you're on an older version, this isn't too hard to apply. So this is a pretty fun bug, but let's talk about something a little bit flashier. And let's start talking about our first unit bug. So unit packets are packed in such a format that you can put multiple RPCs into a single packet. And that looks a little bit like this. So the first thing you've got is the packet header, which for our purposes, we don't really care about. But then you have each message within that packet. And each message consists of a 16-bit message length, a 16-bit value that I call the message type, and then the message body, the actual contents of that message. And then at the end of that body, you see you have the next message just concatenated on. The bug here, again, is pretty simple. If we supply a message size that's larger than the actual payload of our message, the actual body of our message, we can convince the server to act on extra data that's already in memory. So that looks a little something like this. If we imagine this is just a regular RPC, we start with the length, and then we've got the body, which is four bytes. But if we were to manipulate that length without actually increasing the length of our body, we'll see that all this other data that was already in memory, that's not part of our message, is now within scope of our RPC. So what's interesting about this is that this old memory actually comes from previous RPCs, not just from ours, but from other connections, from other players. So what we want to do here is we want to create an RPC that will leak this old memory to us, kind of like Heartbleed would. And so to do this, it's not enough to just cause the server to act on that old memory. We need to convince the server to actually send that memory back to us. And so the type of RPC that is really good for this is chat messages. So let's look at what an example chat RPC might look like. In this case, again, we've got the length and then we've got the body of the RPC, which is just made up of a string length and then a string body. But if we just supply that string length and we don't supply a body, then all the rest of that memory is going to be treated as part of that string. And if that RPC is accepted, the server is going to send back a message that says there's a new chat message and here's its contents. And those contents will contain data from previous RPCs. But even in the absence of chat messages, there are some other RPCs we can use. Movement or spawning a new object are both good ones because they typically involve giving some form of vector, some location as an argument. And we can use this to leak either eight or 12 bytes depending on whether or not this vector is 2D or 3D, if it's a 3D game or a 2D game. And then there's always game specific RPCs that we could potentially use for this. We just have to get creative. So let's talk about what we can leak with this. Now, there can be a lot of things. If you're doing authentication over UNET, this could be passwords. It could also be private messages. It could be game specific stuff like player locations or player actions. Really, anything that gets sent over UNET by the client could be leaked this way. And this was fixed pretty recently at the end of May with UNET version 1.0.6. So I went kind of quickly through those first two, but I really wanted to save time for these last two bugs, which I think weren't going into a little bit more technical depth. This first one is another Unreal Engine bug. And I really like this bug because it is a universal speed hack for Unreal Engine. So we talked just briefly about this, but movement in Unreal Engine is what we call server authoritative. That means that the client cannot just say, I'm at XYZ. Instead, the client has to issue a movement RPC and ask the server to update the player's location. And so this movement RPC has two important parts to it. I am oversimplifying this quite a bit, but I only want to talk about the parts that are actually relevant to our bug. So the first argument is the movement vector. And this is a vector that says the direction that we're moving and the speed we're moving at. And the second argument is a timestamp that just says we sent this RPC at this time relevant to client time. And so the actual math here is that the server will calculate what's called a movement delta by taking our provided timestamp and subtracting from it the last valid timestamp that we provided to the server. And then it's going to take our movement vector and multiply it by our movement delta to calculate the actual movement that should be applied. And so if the server can properly validate that both the movement delta and the movement vector are sane, there's not a whole lot we can do to manipulate our movement. Now we need to make a slight digression and talk about floating point. So when I say floating point, I'm specifically referring to IEEE 754. And this is how most computer systems represent rational numbers, numbers that can be non-whole, such as 1234 or 12.34, anything with a period in it. And so what's interesting about floating point is it has some special values. Floating point can be used to represent infinity, either positive or negative. It also has a special value called not a number, which I choose to verbalize as nan. And for completion's sake, I should say that nan can be positive or negative, but it doesn't typically matter. So these special values usually are the result of some undefined mathematical operation. So if you do any non-zero number divided by zero in floating point, you get infinite, either positive or negative. If you do zero divided by zero in floating point, you get nan. Similarly, if you do square root of negative one, you get nan. Now nan is really the more interesting of these two special values, and it has a couple of properties that are really unique. The first is that any affirmative comparison against nan will evaluate to false. So we can compare nan to zero in any way we want. It will always be false. If you do nan equals equals zero, false. Is nan greater than zero false? Is nan less than zero false? We can even compare nan to itself, and it will still be false. The other thing is that nan has a tendency to propagate. And by that, I mean any mathematical operation where nan is an operand will evaluate to nan. So nan plus one, minus one, times two, divided by two, these are all nan. And no matter how complex you make this mathematical operation, if one operand is nan, the entire thing will become nan. So this brings up the term nan poisoning, which is a condition where the unique properties of nan cause some sort of intended effect. So let's look at the following code as a really simple example of this. So in this case, the programmer is trying to ensure that the floating point number nan is between zero and 100. The problem here is that if num is nan, both of these conditions will evaluate to false because any affirmative comparison with nan will evaluate to false. So nan will pass these validations and we will end up acting on nan in some way, assuming that it is a legitimate regular floating point number within the range zero and 100. So nan poisoning attacks are pretty rare because it's typically difficult to actually introduce nan in the first place. You don't usually have an opportunity to divide by zero or do square root of a negative, square root of a negative number or anything like that. These aren't common bugs, but when we're doing remote procedure calls, we can use any argument of the correct type. So if the RPC calls for a floating point number, we can give it any floating point number including nan or infinite. So going back to our movement RPC, there's only one argument that is a floating point number and that's the time stamp. So what happens if our time stamp is nan? Well, to figure this out, we have to look at this mouthful of a function. U character movement component is client time stamp valid. So this is a lot to look at. So we're going to go through it together, but let's assume that our time stamp is nan because that is the value we've provided to our RPC. So we get to this first check, which is intended to ensure that the time stamp is greater than zero. And because this is an affirmative comparison, this will evaluate to false and we just skip right past to the next line. Then we're going to calculate our delta time stamp. And this is done by subtracting our provided time stamp with the last valid time stamp that was received from our connection. But in this case, it does not matter what that last valid time stamp was because our provided time stamp is nan and nan minus anything will always be nan. So delta time stamp evaluates to nan. Then we get to these next two checks. In the first case, again, we're going to pass right by because time stamp is nan and this is an affirmative comparison. And for the next check, delta time stamp is also nan. This is another affirmative comparison we pass right by and our time stamp is considered to be quote unquote valid. So this is all written in such a way that just by pure luck, nan will just pass right through and be considered valid. And so the next thing we do is we generate our delta time using nan. So our delta time is calculated again by subtracting our subtracting our last valid time stamp from our provided time stamp. And because our provided time stamp is nan, delta time will evaluate to nan regardless of what our last valid time stamp was. Now the server will use our delta time to attempt to apply our movement. But this is where we've run into our first issue. There's one last sanity check here to ensure that the delta time is greater than zero. And we will never pass this check when delta time is nan because it's another affirmative comparison. So our movement is not applied. And even though our time stamp was considered valid, we don't go anywhere. But we're not quite done yet because we've caused another value to be poisoned. We've caused server data current client time stamp to become nan. And this is the saved version of the last valid time stamp because our time stamp was considered valid. Even though we didn't apply our movement, nan was shifted into that current client time stamp variable. Now we need to look at that is client time stamp valid function again. So we're looking at the same function again. But this time we're going to assume that our given time stamp is not nan. It's not any special number. It's just a regular floating point number greater than zero. So we bypass this first check. We get to the delta time stamp calculation. And again, we're going to do our given time stamp minus our last valid time stamp. But this time our last valid time stamp is nan. So delta time is still going to calculate as nan. Then we get to these two comparisons, which just as before we're going to bypass because server data current client time stamp and delta time are both nan. So we pass these checks and again, our time stamp is considered valid. So on this second RPC call, any time stamp greater than zero will pass the validity check. We could say it's 30 years in the future. It doesn't matter as long as it is greater than zero. And our first RPC call use nan as a time stamp. Our second call will always pass the validation check. Unfortunately, our delta time is still going to calculate as nan because our old client time stamp was nan. So still nothing happens. We haven't moved an inch. But fortunately, we've poisoned one more value now. So while all this is going on, the server is trying to determine if our time has drifted from server time. Essentially, the server wants to make sure that we're not doing anything tricky or that we're not our clock isn't drifting. And it does this by independently calculating its own delta time and calculating an error rate, a difference between our delta time and the server's delta time. But because our delta time is nan, our client error is also going to be nan. And then this client error is used to build up our cumulative value new time discrepancy. And this value is used to detect when we have drifted too far from server time. This is used to detect speed hacking attempts. It's also used to detect when a client might just be lagging too much. But because our client error is nan, when our client error is added, our new time discrepancy value also becomes nan. And because new time discrepancy is what's used to determine a difference between client time and server time, once we've poisoned this value, we can essentially disable the server's ability to detect a time discrepancy for our connection. And so when we actually get to the point when the server would attempt to detect a time discrepancy between our time and server time, this check will never pass because new time discrepancy is nan. And more importantly, no mathematical operation will ever cause new time discrepancy to become a regular floating point number again. It is stuck as nan. So at this point, because we've neutered the server's ability to detect a time discrepancy, we can now pull off like an old school speed hack where we just speed up time in order to move faster than we should. And what this allows us to do is it allows us to move significantly faster than our built-in limitations would ever allow. So I've oversimplified this process a bit. I know it's still pretty complicated. But what I've come up with as not the most efficient, but the most straightforward way of exploiting this is to just send RPCs in groups of three where the first RPC, the timestamp, is nan. The second, the timestamp is just slightly greater than zero. And the third, the timestamp is some value well in the future. And every time you send this grouping, it will move you forward some amount. And because the server cannot detect a discrepancy anymore, you can do this as often as you want. And the only real limitation is how quickly you can send those RPCs and have them be processed. So saying all that, let's actually look at this in action. And I think you can probably imagine what this is going to look like. But I worked hard on it, so humor me. This is our first demo. This is built on a stock Unreal Engine 4 game template. All I've done with it is I've enabled all the speed hacking protections. I've opened up the game world just a bit so we can run around some more. And I've modified the client to actually pull off our attack. And this is filmed from the server perspective just so that you can see that this is actually happening server side. It doesn't just look like we're moving fast client side. So in the background, we've got our hacker. As you can see, he's vibrating. He's very excited to be here. And what we're going to do is we're just going to get out of the way so we can get a good view of him running. And in just a second, we are going to see him blast off. And he's gone. Okay, so that is a little bit faster movement than intended, even with the speed hacking protections on. I really like this bug. I love floating point bugs. And the great thing about this one is that it actually does something other than just being like a denial of service. I also think that this type of attack can apply in other ways. I've looked at other games that use floating point in its RPCs and you can usually get some sort of unintended behavior by using these special floating point numbers. It's not always useful. It doesn't always do anything for us for an attacker, but it usually does something. I should also say that this does still apply to unity. But with UNET, it doesn't limit your movement in the first place so you don't have to go through this complicated process to speed hack. So with that out of the way, let's talk about our final bug. We're going to go back to UNET and this is a session hijacking bug. So UNET uses a protocol level process to authenticate incoming packets because UNET is implemented over UDP so you don't get the benefits of TCP where a stream is already authenticated and you can assume that a packet is part of a stream that you've already seen before. So packets are not validated by their source IP address or their source port or anything like that. They're only validated by values within the packet itself. So knowing this, it is at least theoretically possible that someone could hijack another player's session totally remotely, totally over the internet. We don't need a man in the middle. We don't need to be over LAN or anything like that. And that's the plan here. So when UNET validates an incoming packet, there are three important values it looks at. The first is the host ID, then the session ID and the packet ID. And these names don't mean anything on their own. So let's look at each one in detail. So the first of these values is the host ID. And this is a 16-bit integer that's used to associate a packet with a given client. Host IDs are assigned sequentially starting at one. So the first client to connect gets host ID one. The second client gets host ID two, etc. Now, host IDs aren't really intended to be a secret. So in a way, we can sort of just ignore them. It's also really easy to enumerate the host ID of another player. If we're the second player in a game and we get host ID two, the other player is probably host ID one. If we're in a battle royale and we're host ID 47, the other host IDs are probably one through 46 and 48 through 100. Things get a little more complicated when we talk about the next value, which is the session ID. The session ID is the primary authenticating secret of a connection. And the session ID is randomly generated by the client when they connect. And every packet received for that client has to have the correct session ID or the packet will be discarded. There are a couple of problems with the session ID though. The first is that the session ID is also a 16-bit integer. It also can't be zero. This means that there's only 65,535 possible session IDs. There's also no penalty for incorrectly guessing a session ID, other than the fact that our packet will just be discarded by the server. So we can easily brute force the whole range of session IDs even over the open internet. It doesn't matter. It doesn't take long at all. Now, we don't really need to do this part, but there is one more thing we can do to narrow down that search even more. So session IDs are generated with a function named unet get ran not zero. And what this function does is exactly what the name says. It gets a random number and ensures that that random number is not zero. And this is used for session IDs because session IDs cannot be zero. But so the way this function actually works is it takes the end result, that random number, and it orders it with one. It essentially ensures that the least significant bit of that output will always be one. This has the effect of ensuring that a legitimate unet client will only ever generate an odd numbered session ID. Technically, a session ID can be any 16-bit value other than zero, but a legitimate client is programmed to never actually generate one unless it's odd. So this reduces the possible session IDs down to 32,768. So 50%. So if we know the host ID and we can guess the session ID, that's all we need for our spoof packet to be accepted within the context of another player's session. But there is one more hiccup, and that's the packet ID. The packet ID is an integer that's incremented with each packet sent by the client, basically a sequence number, and again, it's 16 bits long. So what the packet ID is for is it's used to detect duplicate or out-of-order packets because, again, UDP isn't doing this for us. It's also used to determine the rate of packet loss. So if the last packet ID received by the server is one and the next packet ID it gets is 1,000, the server is going to assume that it's lost 9,998 packets in the meantime. So if we can guess the host ID, I'm sorry, determine the host ID and guess the session ID, what can we do with the packet ID? I guess a better question might be what happens if we just send a random packet ID. So let's read Unity's documentation on exactly this. So according to the documentation, there are a few conditions. If the new packet ID is greater than the last packet ID plus 512, we're going to disconnect the session because we've lost too many packets. If the packet ID is more than 512 behind the current packet ID, we're going to discard it because it's too old. We don't want it anymore. If the packet ID is in a list of packets we've seen recently, it's a duplicate, let's discard it. Otherwise, if none of these conditions are met, we're going to accept and process that packet. So there are a couple interesting things about this. The first is that if our guest packet ID is greater than our last packet ID plus 512, the connection will be disconnected. And I want to emphasize that this is not our connection we're talking about. This is the other player's connection. So this is useful because it means we can pretty trivially kick other players off the server. However, it would be a lot more interesting if we could bypass this check and inject a packet that would be actually executed within another player's session. But reading the documentation seems like the odds of this happening are pretty low. Guest packet ID must be last packet ID plus or minus 512, which doing the math gives us less than a 7% chance of success. That's pretty bad. But when we look at the actual implementation, it tells a slightly different story. So packet ID validation is done by the function unet replay protector is packet replayed. In practice, this function actually does not discard packets that are more than 512 packets old, like the documentation said. I spent a lot of time thinking that I was just misunderstanding or that it was more complicated than I thought. But no, the logic for discarding old packets just isn't there. Instead, old packets are accepted as if they weren't more than 512 packets old. So unfortunately, it's a little more complicated than it sounds. We can't just use packet ID zero every time, even though that would be the lowest packet ID. And the reason for that is that the server has to account for cases where the packet ID overflows, goes from FFFF to zero. And this happens pretty often over the course of a game. So instead of just directly comparing the numeric value, the server has to keep like a rolling window of packet IDs to determine if a packet is old or new. And so doing the math here, we have what's very close to a 50-50 shot that a packet ID will be accepted. Most of the rest of the time, our packet is going to cause the other player to get kicked out of the game, which is still pretty useful. And I say most of the rest of the time because occasionally, we'll guess a packet ID that was actually seen recently and it'll be seen as a duplicate and just be discarded. Okay, so let's look at our second demo. And for this, I had to use an actual game, not just something I came up with myself. And this game is called Streets of Rogue. It's very cool. You should buy it. And I do want to emphasize that this is not a bug in the game itself. There's nothing wrong with how this game is programmed. This is a bug in UNIT. But even still, what we're going to do with this bug is we're going to inject packets to cause two other players to do actions that we tell them to. So this little guy in the red shirt is me and these two identical-looking people are target players. And what we're going to do is we're going to inject packets. We're going to see them do a few things. First, they're going to say hi to everyone. Then we're going to kill them both. Then I started to feel a little bit bad, so we're going to resurrect them both. And then we're going to kill them again. Okay, so this is a pretty simple demo, kind of stupid, but it demonstrates a few things. The first is that we're able to inject packets that are actually accepted within the context of another player's session. The second is that we're able to do it consistently enough that we're able to do it for two players at the exact same time, even though those two players have different host IDs, different session IDs, different packet IDs, everything. And finally, we're able to do it consistently enough that we're able to inject multiple packets into each player's session. We were able to inject a packet to say something, pause, inject another packet to cause them to explode, pause, and then do this four times, injecting four different packets. So this bug is pretty reliable. And as I said, even if you lose the coin toss, all that happens is you kick another player out of the game, which has its utilities on its own. So let's talk about remediations for this bug. This is considered to be an architectural weakness with UNET. The actual fixes that would be required to prevent this entirely are not going to happen. The only mitigation aside from moving away from UNET is to actually encrypt UNET. Unity does provide a reference implementation that does a decent job of this, but it's not complete. It doesn't do key exchange for you. So to an extent, you are still on your own with this. And I have looked a lot, but I've not found a single game implementing encryption over UNET. And I should emphasize that this is a mitigation. It is not a fix. Encryption is not complete. If an attacker can bypass this encryption, these bugs still exist exactly the same as they exist now. Okay, so that was my final bug. So let's talk about some future work. I don't think I've found all the bugs, even in the components that I have looked at, but there are a few components that I haven't looked at at all that I think are worth looking at. So both of these protocols have other transport modes. I think the most interesting of these are WebSockets, because that's what browser games are going to be using. There's also third-party networking plugins for some of these engines. Photon and Mirror are two relatively common ones for Unity. And there's just other engines, GameMaker Studio, Godot, stuff like that. Finally, I want to thank a few people. Both Epic Games and Unity Technologies, their security teams were absolutely fantastic, super communicative, kept me up-to-date the entire time, put up with a lot of me, which can be very annoying. I have nothing but good things to say about both of them. I also want to thank the artist who made the background art for my presentation, Grigoreen, he's awesome. And finally, all the scripts, everything I've put together for this work, I've got up on GitHub, I've got POCs for most of these issues, I've got libraries for interfacing with Unet and Unreal's protocol. And I hope that if I can ask anything of you, is that you go, you use something that you've learned here to go get banned from some video game.