1 00:00:00,640 --> 00:00:02,480 Alright, welcome everyone. 2 00:00:02,480 --> 00:00:07,340 My name is Jack Baker, and I'm going to be talking about bugs in Game Engine. 3 00:00:07,380 --> 00:00:15,360 So as a little bit of background, over the last few months, I've found more than 10 remotely exploitable bugs while looking at two different game engines. 4 00:00:15,360 --> 00:00:18,300 And I'm going to be talking about four of those bugs today. 5 00:00:19,160 --> 00:00:28,600 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 top of. 6 00:00:28,600 --> 00:00:32,600 So if you're building a video game, you're probably not doing it from scratch. 7 00:00:32,600 --> 00:00:38,820 You're using a pre-made set of tools and software that are built to make that process easier. 8 00:00:38,820 --> 00:00:42,000 And that software is called your Game Engine. 9 00:00:42,500 --> 00:00:48,620 And so the popularity of many Game Engines means that a lot of games share the exact same bugs. 10 00:00:48,760 --> 00:00:57,220 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. 11 00:00:57,700 --> 00:01:01,560 And games don't usually get security patches after release. 12 00:01:01,560 --> 00:01:12,860 Maybe a bigger game will have some support, but an independent release is not going to get patches a year or two years after release just to fix some security bug. 13 00:01:13,920 --> 00:01:23,300 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. 14 00:01:23,300 --> 00:01:26,480 Those are Unreal Engine 4 and Unity. 15 00:01:26,740 --> 00:01:34,300 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. 16 00:01:34,300 --> 00:01:42,220 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. 17 00:01:42,860 --> 00:01:46,840 So Unreal Engine 4 is created by Epic Games. 18 00:01:46,840 --> 00:01:50,000 It's named for its roots in the Unreal series. 19 00:01:50,160 --> 00:01:51,320 It is open source. 20 00:01:51,320 --> 00:01:59,900 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. 21 00:02:00,280 --> 00:02:08,700 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. 22 00:02:10,560 --> 00:02:13,980 Unity is built by Unity Technologies. 23 00:02:13,980 --> 00:02:19,880 There are some open source components of it, but the core components of Unity are closed source. 24 00:02:19,920 --> 00:02:23,740 The core networking library we're going to be looking at is called U-Net. 25 00:02:23,920 --> 00:02:33,260 And while I couldn't find a whole lot of big games that are using U-Net, there are countless amount of indie releases on Steam and everywhere else that are built using U-Net. 26 00:02:34,020 --> 00:02:41,180 Now I should say that U-Net is deprecated, but there are a few reasons that I thought it would be a good target anyway. 27 00:02:41,180 --> 00:02:45,460 The first is that U-Net has not got an official replacement yet. 28 00:02:45,460 --> 00:02:48,140 Unity hasn't put out an alternative yet. 29 00:02:48,140 --> 00:02:55,540 So if you're using Unity and you're doing multiplayer, you're either using U-Net still or you're using a third-party solution. 30 00:02:55,820 --> 00:03:02,440 Also, U-Net still does receive patches and occasionally new features, even though it's deprecated. 31 00:03:02,440 --> 00:03:07,300 So the encryption API was added to U-Net after deprecation. 32 00:03:08,360 --> 00:03:14,060 And so a ton of new, and more importantly I think, existing games use U-Net. 33 00:03:14,060 --> 00:03:17,780 So I think bugs in U-Net still have some value. 34 00:03:18,400 --> 00:03:21,700 So let's talk about multiplayer protocols. 35 00:03:21,700 --> 00:03:29,800 As game engines have evolved, and as multiplayer protocols have evolved, there's really been a focus on two things. 36 00:03:29,800 --> 00:03:34,140 The first is obvious, it's increasing performance, increasing speed. 37 00:03:34,140 --> 00:03:40,160 The other is moving trust away from the client in order to prevent hacking. 38 00:03:40,700 --> 00:03:44,520 But as we're going to see, these can sometimes be conflicting goals. 39 00:03:45,020 --> 00:03:52,980 But to really understand multiplayer protocols, I think it's worth understanding the types of attacks that they're aiming to prevent. 40 00:03:53,180 --> 00:03:58,980 And I think the best example of this is to talk about the evolution of what we'll call movement hacking. 41 00:03:59,300 --> 00:04:07,700 So movement hacking is the process of manipulating the player's location in some way that the game normally wouldn't allow. 42 00:04:07,700 --> 00:04:12,800 In the old days, this was really easy because player location was just trusted to the client. 43 00:04:12,800 --> 00:04:18,240 So if you manipulate your location client side, you can teleport server side. 44 00:04:18,400 --> 00:04:28,440 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. 45 00:04:28,440 --> 00:04:32,080 So the client can no longer say, I'm at XYZ. 46 00:04:32,080 --> 00:04:44,040 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. 47 00:04:44,500 --> 00:04:47,860 This led to a new type of attack called speed hacking. 48 00:04:47,900 --> 00:04:51,340 So it's sort of the next evolution of movement hacking. 49 00:04:51,340 --> 00:04:57,600 Speed hacking, the goal is not to teleport necessarily, but just to move extremely fast. 50 00:04:57,600 --> 00:05:09,120 And typically the way this works is you send that movement request excessively fast, faster than a normal client ever would, and more requests then means more speed. 51 00:05:09,720 --> 00:05:15,600 This was prevented by basically giving more authority and more context to the server. 52 00:05:15,600 --> 00:05:23,840 So the server should be able to understand what is a realistic distance that a character can move in a given time frame. 53 00:05:24,000 --> 00:05:30,080 And if a client is attempting to move beyond that time frame, it can stop that from happening. 54 00:05:30,100 --> 00:05:37,000 And so by giving this context to the server, we're able to prevent this type of attack. 55 00:05:37,000 --> 00:05:39,960 And this is really how game engines have evolved. 56 00:05:39,960 --> 00:05:52,880 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. 57 00:05:53,600 --> 00:05:59,680 So with all of this said, let's talk about some of the technical specifics of multiplayer protocols. 58 00:05:59,680 --> 00:06:08,740 Now there's no real like published standard of what a multiplayer protocol looks like, but there are a few things that are pretty consistent between different protocols. 59 00:06:08,740 --> 00:06:15,160 And first of those is that most multiplayer protocols use some form of distributed architecture. 60 00:06:15,840 --> 00:06:27,440 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. 61 00:06:27,440 --> 00:06:34,520 And actions between objects and between systems are performed through what are called remote procedure calls. 62 00:06:34,900 --> 00:06:44,180 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. 63 00:06:44,180 --> 00:06:51,200 So this is really easy for the programmer because it's just like you're calling a regular function of your code. 64 00:06:51,200 --> 00:06:56,780 The difference is that that function is executing on someone else's system, not your local system. 65 00:06:56,800 --> 00:07:03,020 This is really convenient, but there's a lot of complexity that goes into this process on the back end. 66 00:07:03,860 --> 00:07:17,620 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. 67 00:07:17,620 --> 00:07:26,260 And so the way this usually works is each player has ownership of their character and maybe some associated sub-objects like your inventory. 68 00:07:26,340 --> 00:07:30,280 But player A can only issue RPCs on character A. 69 00:07:30,280 --> 00:07:34,760 He can't issue RPCs on character B and vice versa. 70 00:07:35,360 --> 00:07:43,340 Another interesting technical detail of multiplayer protocols is that they're usually implemented over UDP. 71 00:07:43,340 --> 00:07:51,600 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. 72 00:07:51,600 --> 00:07:58,240 But when we're about desktop games, we're usually talking about protocols that are implemented over UDP. 73 00:07:58,240 --> 00:08:12,640 And 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. 74 00:08:12,640 --> 00:08:18,340 So the protocol gets more complex because it has to deal with these types of problems itself. 75 00:08:19,240 --> 00:08:24,380 So now that we've talked a little bit about how these protocols work, let's look at our first bug. 76 00:08:24,380 --> 00:08:28,420 And this is an Unreal Engine 4 bug, and it's actually a file pathing bug. 77 00:08:29,040 --> 00:08:36,400 So Unreal Engine uses its own type of URL, and it uses this to communicate details between the server and the client. 78 00:08:36,400 --> 00:08:44,200 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. 79 00:08:44,200 --> 00:08:54,880 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. 80 00:08:55,440 --> 00:08:58,960 And so one of these URLs might look something like this. 81 00:08:58,960 --> 00:09:12,720 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. 82 00:09:13,200 --> 00:09:15,300 So the bug here is pretty simple. 83 00:09:15,300 --> 00:09:22,740 If you use a malicious URL, you can cause a server or a client to access any local file path. 84 00:09:23,120 --> 00:09:30,400 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. 85 00:09:30,400 --> 00:09:38,300 This is pretty boring on its own, but it gets more interesting when we start talking about Universal Naming Convention, or UNC paths. 86 00:09:38,440 --> 00:09:46,140 So UNC paths are special Windows paths that are used to access networked resources as if they were regular files. 87 00:09:46,240 --> 00:09:48,540 So regular local files. 88 00:09:49,120 --> 00:09:52,080 Typically a UNC path will look something like this. 89 00:09:52,080 --> 00:10:02,340 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. 90 00:10:03,180 --> 00:10:11,500 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. 91 00:10:11,500 --> 00:10:12,600 This is pretty simple. 92 00:10:12,600 --> 00:10:22,240 The one trick here is we do need to include the part that says .umap, where umap is a file extension used by Unreal Engine. 93 00:10:22,240 --> 00:10:29,400 And by having this somewhere in the domain name, here we use it as a subdomain, we can bypass some of the filtration. 94 00:10:29,400 --> 00:10:42,580 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 hi.txt file. 95 00:10:43,460 --> 00:10:49,940 And so what this does is it opens up the affected system to the world of SMB related attacks. 96 00:10:50,400 --> 00:10:55,080 Typically when a Windows system connects to an SMB share, it'll try to authenticate with it. 97 00:10:55,080 --> 00:10:59,340 So this can be used for credential harvesting or authentication relaying. 98 00:10:59,340 --> 00:11:02,120 There's a whole world of different attacks using this. 99 00:11:02,120 --> 00:11:10,180 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. 100 00:11:10,180 --> 00:11:17,060 So if you cause that SMB connection to deliberately take a long time, you can lock up the server for a while. 101 00:11:17,300 --> 00:11:21,300 So this was fixed in Unreal Engine 4.25.2. 102 00:11:21,300 --> 00:11:25,380 With this commit, it's really easy to backport this. 103 00:11:25,380 --> 00:11:28,520 So if you're on an older version, this isn't too hard to apply. 104 00:11:28,900 --> 00:11:33,520 So this is a pretty fun bug, but let's talk about something a little bit flashier. 105 00:11:33,520 --> 00:11:36,580 And let's start talking about our first UNet bug. 106 00:11:37,400 --> 00:11:44,940 So UNet packets are packed in such a format that you can put multiple RPCs into a single packet. 107 00:11:44,940 --> 00:11:47,360 And that looks a little bit like this. 108 00:11:47,380 --> 00:11:53,300 So the first thing you've got is the packet header, which for our purposes, we don't really care about. 109 00:11:53,300 --> 00:11:57,120 But then you have each message within that packet. 110 00:11:57,120 --> 00:12:09,600 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. 111 00:12:09,600 --> 00:12:14,460 And then at the end of that body, you see you have the next message just concatenated on. 112 00:12:15,320 --> 00:12:17,820 The bug here, again, is pretty simple. 113 00:12:17,820 --> 00:12:29,840 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. 114 00:12:29,860 --> 00:12:31,940 So that looks a little something like this. 115 00:12:32,000 --> 00:12:39,780 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. 116 00:12:39,780 --> 00:12:53,960 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. 117 00:12:54,600 --> 00:13:04,760 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. 118 00:13:04,760 --> 00:13:12,880 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. 119 00:13:14,000 --> 00:13:20,620 And so to do this, it's not enough to just cause the server to act on that old memory. 120 00:13:20,620 --> 00:13:25,100 We need to convince the server to actually send that memory back to us. 121 00:13:25,160 --> 00:13:30,220 And so the type of RPC that is really good for this is chat messages. 122 00:13:30,220 --> 00:13:34,900 So let's look at what an example chat RPC might look like. 123 00:13:34,900 --> 00:13:43,020 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. 124 00:13:43,020 --> 00:13:52,460 But if we just apply 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. 125 00:13:52,460 --> 00:14:00,820 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 content. 126 00:14:00,820 --> 00:14:05,980 And those contents will contain data from previous RPCs. 127 00:14:06,440 --> 00:14:12,240 But even in the absence of chat messages, there are some other RPCs we can use. 128 00:14:12,240 --> 00:14:23,500 Movement or spawning a new object are both good ones because they typically involve giving some form of vector, some location, as an argument. 129 00:14:23,500 --> 00:14:32,420 And we can use this to leak either 8 or 12 bytes, depending on whether or not this vector is 2D or 3D, if it's a 3D game or a 2D game. 130 00:14:32,420 --> 00:14:36,840 And then there's always game-specific RPCs that we could potentially use for this. 131 00:14:36,840 --> 00:14:38,620 We just have to get creative. 132 00:14:39,780 --> 00:14:42,740 So let's talk about what we can leak with this. 133 00:14:42,740 --> 00:14:44,720 Now, there can be a lot of things. 134 00:14:44,720 --> 00:14:48,280 If you're doing authentication over U-Net, this could be passwords. 135 00:14:48,280 --> 00:14:50,260 It could also be private messages. 136 00:14:50,260 --> 00:14:54,480 It could be game-specific stuff like player locations or player actions. 137 00:14:54,480 --> 00:14:59,760 Really anything that gets sent over U-Net by the client could be leaked this way. 138 00:14:59,800 --> 00:15:06,220 And this was fixed pretty recently at the end of May with U-Net version 1.0.6. 139 00:15:07,020 --> 00:15:16,420 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 warrant going into a little bit more technical depth. 140 00:15:16,640 --> 00:15:19,520 This first one is another Unreal Engine bug. 141 00:15:19,520 --> 00:15:25,040 And I really like this bug because it is a universal speed hack for Unreal Engine. 142 00:15:25,440 --> 00:15:33,240 So we talked just briefly about this, but movement in Unreal Engine is what we call server authoritative. 143 00:15:33,240 --> 00:15:37,860 That means that the client cannot just say, I'm at XYZ. 144 00:15:37,860 --> 00:15:46,040 Instead, the client has to issue a movement RPC and ask the server to update the player's location. 145 00:15:47,740 --> 00:15:51,180 And so this movement RPC has two important parts to it. 146 00:15:51,180 --> 00:15:59,220 I am oversimplifying this quite a bit, but I only want to talk about the parts that are actually relevant to our bug. 147 00:15:59,220 --> 00:16:02,040 So the first argument is the movement vector. 148 00:16:02,040 --> 00:16:06,680 And this is a vector that says the direction that we're moving and the speed we're moving at. 149 00:16:06,680 --> 00:16:14,620 And the second argument is a timestamp that just says we sent this RPC at this time relevant to client time. 150 00:16:15,160 --> 00:16:33,500 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. 151 00:16:33,720 --> 00:16:43,220 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. 152 00:16:43,740 --> 00:16:53,350 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. 153 00:16:54,110 --> 00:16:58,070 Now we need to make a slight digression and talk about floating point. 154 00:16:58,530 --> 00:17:03,750 So when I say floating point, I'm specifically referring to IEEE 754. 155 00:17:03,750 --> 00:17:15,050 And this is how most computer systems represent rational numbers, numbers that can be non-whole, such as 1, 2, 3, 4, or 1, 2, dot, 3, 4, anything with a period in it. 156 00:17:16,110 --> 00:17:20,570 And so what's interesting about floating point is it has some special values. 157 00:17:20,570 --> 00:17:24,830 Floating point can be used to represent infinity, either positive or negative. 158 00:17:24,830 --> 00:17:30,250 It also has a special value called not a number, which I choose to verbalize as NAN. 159 00:17:30,290 --> 00:17:37,270 And for completion's sake, I should say that NAN can be positive or negative, but it doesn't typically matter. 160 00:17:38,050 --> 00:17:43,210 So these special values usually are the result of some undefined mathematical operation. 161 00:17:43,470 --> 00:17:51,950 So if you do any non-zero number divided by zero in floating point, you get infinite, either positive or negative. 162 00:17:51,950 --> 00:17:55,610 If you do zero divided by zero in floating point, you get NAN. 163 00:17:55,770 --> 00:17:59,870 Similarly, if you do square root of negative one, you get NAN. 164 00:18:00,650 --> 00:18:08,030 Now NAN is really the more interesting of these two special values, and it has a couple properties that are really unique. 165 00:18:08,030 --> 00:18:13,550 The first is that any affirmative comparison against NAN will evaluate to false. 166 00:18:13,550 --> 00:18:17,090 So we can compare NAN to zero in any way we want. 167 00:18:17,090 --> 00:18:18,350 It'll always be false. 168 00:18:18,350 --> 00:18:20,610 We can do NAN equals equals zero. 169 00:18:20,610 --> 00:18:21,370 False. 170 00:18:21,370 --> 00:18:22,650 Is NAN greater than zero? 171 00:18:22,650 --> 00:18:23,110 False. 172 00:18:23,110 --> 00:18:24,450 Is NAN less than zero? 173 00:18:24,450 --> 00:18:24,890 False. 174 00:18:24,890 --> 00:18:28,950 We can even compare NAN to itself, and it'll still be false. 175 00:18:29,750 --> 00:18:40,530 The other thing is that NAN has a tendency to propagate, by that I mean any mathematical operation where NAN is an operand will evaluate to NAN. 176 00:18:40,530 --> 00:18:45,530 So NAN plus one, minus one, times two, divided by two, these are all NAN. 177 00:18:45,530 --> 00:18:53,950 And no matter how complex you make this mathematical operation, if one operand is NAN, the entire thing will become NAN. 178 00:18:54,890 --> 00:19:04,570 So this brings up the term NAN poisoning, which is a condition where the unique properties of NAN cause some sort of intended effect. 179 00:19:04,570 --> 00:19:08,990 So let's look at the following code as a really simple example of this. 180 00:19:08,990 --> 00:19:17,590 So in this case, the programmer is trying to ensure that the floating point number NAN is between zero and 100. 181 00:19:18,530 --> 00:19:33,330 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. 182 00:19:33,330 --> 00:19:45,930 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. 183 00:19:47,650 --> 00:19:54,470 So NAN poisoning attacks are pretty rare because it's typically difficult to actually introduce NAN in the first place. 184 00:19:54,470 --> 00:20:02,470 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. 185 00:20:02,470 --> 00:20:11,610 These aren't common bugs, but when we're doing remote procedure calls, we can use any argument of the correct type. 186 00:20:11,610 --> 00:20:19,010 So if the RPC calls for a floating point number, we can give it any floating point number, including NAN or infinite. 187 00:20:19,910 --> 00:20:27,110 So going back to our movement RPC, there's only one argument that is a floating point number, and that's the timestamp. 188 00:20:27,110 --> 00:20:30,210 So what happens if our timestamp is NAN? 189 00:20:30,210 --> 00:20:38,610 Well, to figure this out, we have to look at this mouthful of a function, uCharacterMovementComponent , isClientTimeStamp valid? 190 00:20:39,570 --> 00:20:43,350 So this is a lot to look at, so we're going to go through it together. 191 00:20:43,350 --> 00:20:50,330 But let's assume that our timestamp is NAN, because that is the value we've provided to our RPC. 192 00:20:50,330 --> 00:20:55,810 So we get to this first check, which is intended to ensure that the timestamp is greater than zero. 193 00:20:55,850 --> 00:21:02,870 And because this is an affirmative comparison, this will evaluate to false, and we just skip right past to the next line. 194 00:21:03,150 --> 00:21:14,930 Then we're going to calculate our delta timestamp, and this is done by subtracting our provided timestamp with the last valid timestamp that was received from our connection. 195 00:21:14,970 --> 00:21:26,450 But in this case, it does not matter what that last valid timestamp was, because our provided timestamp is NAN, and NAN minus anything will always be NAN. 196 00:21:26,450 --> 00:21:29,490 So delta timestamp evaluates to NAN. 197 00:21:29,790 --> 00:21:32,170 Then we get to these next two checks. 198 00:21:32,170 --> 00:21:38,930 In the first case, again, we're going to pass right by because timestamp is NAN, and this is an affirmative comparison. 199 00:21:39,410 --> 00:21:43,110 And for the next check, delta timestamp is also NAN. 200 00:21:43,110 --> 00:21:45,270 This is another affirmative comparison. 201 00:21:45,390 --> 00:21:50,470 We pass right by, and our timestamp is considered to be quote-unquote valid. 202 00:21:51,730 --> 00:21:58,990 So this is all written in such a way that just by pure luck, NAN will just pass right through and be considered valid. 203 00:21:59,030 --> 00:22:03,870 And so the next thing we do is we generate our delta time using NAN. 204 00:22:03,870 --> 00:22:13,930 So our delta time is calculated, again, by subtracting our last valid timestamp from our provided timestamp. 205 00:22:13,930 --> 00:22:23,250 And because our provided timestamp is NAN, delta time will evaluate to NAN regardless of what our last valid timestamp was. 206 00:22:23,770 --> 00:22:31,070 Now the server will use our delta time to attempt to apply our movement, but this is where we run into our first issue. 207 00:22:31,070 --> 00:22:43,050 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. 208 00:22:43,050 --> 00:22:48,490 So our movement is not applied, and even though our timestamp was considered valid, we don't go anywhere. 209 00:22:48,490 --> 00:22:49,630 We don't move. 210 00:22:50,690 --> 00:22:55,090 But we're not quite done yet because we've caused another value to be poisoned. 211 00:22:55,090 --> 00:23:03,590 We've caused server data current client timestamp to become NAN, and this is the saved version of the last valid timestamp. 212 00:23:03,590 --> 00:23:13,290 Because our timestamp was considered valid, even though we didn't apply our movement, NAN was shifted into that current client timestamp variable. 213 00:23:13,290 --> 00:23:18,110 Now we need to look at that is client timestamp valid function again. 214 00:23:18,430 --> 00:23:24,530 So we're looking at the same function again, but this time we're going to assume that our given timestamp is not NAN. 215 00:23:24,530 --> 00:23:26,430 It's not any special number. 216 00:23:26,430 --> 00:23:30,050 It's just a regular floating point number greater than zero. 217 00:23:30,050 --> 00:23:31,710 So we bypass this first check. 218 00:23:31,710 --> 00:23:35,190 We get to the delta timestamp calculation. 219 00:23:35,190 --> 00:23:43,470 And again, we're going to do our given timestamp minus our last valid timestamp, but this time our last valid timestamp is NAN. 220 00:23:43,470 --> 00:23:47,750 So delta time is still going to calculate as NAN. 221 00:23:47,750 --> 00:23:58,770 Then we get to these two comparisons, which just as before, we're going to bypass because server data current client timestamp and delta time are both NAN. 222 00:23:58,770 --> 00:24:00,250 So we pass these checks. 223 00:24:00,250 --> 00:24:03,130 And again, our timestamp is considered valid. 224 00:24:03,250 --> 00:24:09,510 So on this second RPC call, any timestamp greater than zero will pass the validity check. 225 00:24:09,510 --> 00:24:11,530 We could say it's 30 years in the future. 226 00:24:11,530 --> 00:24:14,810 It doesn't matter as long as it is greater than zero. 227 00:24:14,810 --> 00:24:18,490 And our first RPC call use NAN as a timestamp. 228 00:24:18,490 --> 00:24:21,850 Our second call will always pass the validation check. 229 00:24:22,450 --> 00:24:30,070 Unfortunately, our delta time is still going to calculate as NAN because our old client timestamp was NAN. 230 00:24:30,070 --> 00:24:31,510 So still nothing happens. 231 00:24:31,510 --> 00:24:36,730 We haven't moved an inch, but fortunately we've poisoned one more value now. 232 00:24:36,730 --> 00:24:43,950 So while all this is going on, the server is trying to determine if our time has drifted from server time. 233 00:24:43,950 --> 00:24:52,230 Essentially the server wants to make sure that we're not doing anything tricky or that we're not, our clock isn't drifting. 234 00:24:52,230 --> 00:25:03,590 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. 235 00:25:03,590 --> 00:25:09,970 But because our delta time is NAN, our client error is also going to be NAN. 236 00:25:09,970 --> 00:25:16,710 And then this client error is used to build up our cumulative value new time discrepancy. 237 00:25:16,710 --> 00:25:22,490 And this value is used to detect when we have drifted too far from server time. 238 00:25:22,490 --> 00:25:24,990 This is used to detect speed hacking attempts. 239 00:25:24,990 --> 00:25:30,730 It's also used to detect when a client might just be lagging too much. 240 00:25:30,730 --> 00:25:40,330 But because our client error is NAN, when our client error is added, our new time discrepancy value also becomes NAN. 241 00:25:41,550 --> 00:25:57,970 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 the time discrepancy for our connection. 242 00:25:57,970 --> 00:26:11,710 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. 243 00:26:11,870 --> 00:26:20,150 And more importantly, no mathematical operation will ever cause new time discrepancy to become a regular floating point number again. 244 00:26:20,150 --> 00:26:22,030 It is stuck as NAN. 245 00:26:22,030 --> 00:26:37,610 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. 246 00:26:37,610 --> 00:26:44,390 And what this allows us to do is it allows us to move significantly faster than our built-in limitations would ever allow. 247 00:26:44,690 --> 00:26:47,730 So I've oversimplified this process a bit. 248 00:26:47,730 --> 00:27:03,390 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. 249 00:27:03,390 --> 00:27:06,890 The second, the timestamp is just slightly greater than zero. 250 00:27:06,890 --> 00:27:11,010 And the third, the timestamp is some value well in the future. 251 00:27:11,010 --> 00:27:16,170 And every time you send this grouping, it will move you forward some amount. 252 00:27:16,170 --> 00:27:21,410 And because the server cannot detect the discrepancy anymore, you can do this as often as you want. 253 00:27:21,410 --> 00:27:27,250 And the only real limitation is how quickly you can send those RPCs and have them be processed. 254 00:27:27,650 --> 00:27:32,310 So saying all that, let's actually look at this in action. 255 00:27:32,310 --> 00:27:37,830 And I think you can probably imagine what this is going to look like, but I worked hard on it, so humor me. 256 00:27:38,910 --> 00:27:40,310 This is our first demo. 257 00:27:40,310 --> 00:27:47,550 This is built on a stock Unity, or I'm stock Unreal Engine 4 game template. 258 00:27:47,550 --> 00:27:59,690 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. 259 00:28:00,110 --> 00:28:06,990 And this is filmed from the server perspective, just so that you can see that this is actually happening server-side. 260 00:28:06,990 --> 00:28:10,110 It doesn't just look like we're moving fast client-side. 261 00:28:10,370 --> 00:28:12,750 So in the background, we've got our hacker. 262 00:28:12,750 --> 00:28:14,690 As you can see, he's vibrating. 263 00:28:14,690 --> 00:28:16,530 He's very excited to be here. 264 00:28:16,730 --> 00:28:20,750 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. 265 00:28:21,370 --> 00:28:25,250 And in just a second, we are going to see him blast off. 266 00:28:25,990 --> 00:28:26,870 And he's gone. 267 00:28:26,990 --> 00:28:34,470 Okay, so that is a little bit faster movement than intended, even with the speed hacking protections on. 268 00:28:34,490 --> 00:28:36,330 I really like this bug. 269 00:28:36,330 --> 00:28:44,730 I love floating point bugs, and the great thing about this one is that it actually does something other than just being a denial of service. 270 00:28:44,810 --> 00:28:48,110 I also think that this type of attack can apply in other ways. 271 00:28:48,110 --> 00:28:59,350 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. 272 00:28:59,350 --> 00:29:00,730 It's not always useful. 273 00:29:00,730 --> 00:29:04,510 It doesn't always do anything for us for an attacker, but it usually does something. 274 00:29:04,970 --> 00:29:19,810 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. 275 00:29:20,790 --> 00:29:25,030 So with that out of the way, let's talk about our final bug. 276 00:29:25,030 --> 00:29:28,810 We're going to go back to UNet, and this is a session hijacking bug. 277 00:29:29,530 --> 00:29:50,310 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. 278 00:29:50,310 --> 00:29:55,390 So packets are not validated by their source IP address, or their source port, or anything like that. 279 00:29:55,390 --> 00:29:59,350 They're only validated by values within the packet itself. 280 00:29:59,970 --> 00:30:08,450 So knowing this, it is at least theoretically possible that someone could hijack another player's session totally remotely, totally over the internet. 281 00:30:08,450 --> 00:30:12,670 We don't need a man in the middle, we don't need to be over LAN, or anything like that. 282 00:30:12,670 --> 00:30:14,190 And that's the plan here. 283 00:30:14,630 --> 00:30:21,750 So when UNet validates an incoming packet, there are three important values it looks at. 284 00:30:21,750 --> 00:30:25,510 The first is the host ID, then the session ID, and the packet ID. 285 00:30:25,510 --> 00:30:29,570 And these names don't mean anything on their own, so let's look at each one in detail. 286 00:30:30,670 --> 00:30:39,050 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. 287 00:30:39,210 --> 00:30:42,330 Host IDs are assigned sequentially, starting at 1. 288 00:30:42,330 --> 00:30:48,330 So the first client to connect gets host ID 1, the second client gets host ID 2, etc. 289 00:30:48,810 --> 00:30:54,050 Now host IDs aren't really intended to be a secret, so in a way we can sort of just ignore them. 290 00:30:54,050 --> 00:30:57,710 It's also really easy to enumerate the host ID of another player. 291 00:30:57,710 --> 00:31:03,750 If we're the second player in a game and we get host ID 2, the other player is probably host ID 1. 292 00:31:03,870 --> 00:31:12,330 If we're in a battle royale and we're host ID 47, the other host IDs are probably 1 through 46 and 48 through 100. 293 00:31:13,110 --> 00:31:18,370 Things get a little more complicated when we talk about the next value, which is the session ID. 294 00:31:18,530 --> 00:31:27,830 The session ID is the primary authenticating secret of a connection, and the session ID is randomly generated by the client when they connect. 295 00:31:27,830 --> 00:31:34,170 And every packet received for that client has to have the correct session ID, or the packet will be discarded. 296 00:31:34,850 --> 00:31:37,410 There are a couple problems with the session ID though. 297 00:31:37,410 --> 00:31:41,630 The first is that the session ID is also a 16-bit integer. 298 00:31:41,650 --> 00:31:43,530 It also can't be zero. 299 00:31:43,530 --> 00:31:48,630 This means that there's only 65,535 possible session IDs. 300 00:31:48,730 --> 00:31:56,690 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. 301 00:31:56,690 --> 00:32:02,870 So we can easily brute force the whole range of session IDs, even over the open internet. 302 00:32:02,870 --> 00:32:03,610 It doesn't matter. 303 00:32:03,610 --> 00:32:05,170 It doesn't take long at all. 304 00:32:05,650 --> 00:32:12,930 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. 305 00:32:13,010 --> 00:32:18,770 So session IDs are generated with a function named unet getRandNotZero. 306 00:32:18,770 --> 00:32:22,010 And what this function does is exactly what the name says. 307 00:32:22,010 --> 00:32:25,970 It gets a random number and ensures that that random number is not zero. 308 00:32:26,110 --> 00:32:30,610 And this is used for session IDs because session IDs cannot be zero. 309 00:32:31,550 --> 00:32:39,390 But so the way this function actually works is it takes the end result, that random number, and it ORs it with one. 310 00:32:39,390 --> 00:32:44,850 It essentially ensures that the least significant bit of that output will always be one. 311 00:32:44,850 --> 00:32:52,270 This has the effect of ensuring that a legitimate unet client will only ever generate an odd-numbered session ID. 312 00:32:52,270 --> 00:33:01,990 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. 313 00:33:02,290 --> 00:33:08,590 So this reduces the possible session IDs down to 32,768, so 50%. 314 00:33:10,010 --> 00:33:19,570 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. 315 00:33:19,570 --> 00:33:22,950 But there is one more hiccup, that's the packet ID. 316 00:33:22,950 --> 00:33:29,290 The packet ID is an integer that's incremented with each packet sent by the client, basically a sequence number. 317 00:33:29,290 --> 00:33:31,530 And again, it's 16 bits long. 318 00:33:32,450 --> 00:33:41,530 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. 319 00:33:41,650 --> 00:33:44,670 It's also used to determine the rate of packet loss. 320 00:33:44,670 --> 00:33:58,830 So if the last packet ID received by the server is one and the next packet ID it gets is 1000, the server is going to assume that it's lost 998 packets in the meantime. 321 00:34:00,970 --> 00:34:07,910 So if we can determine the host ID and guess the session ID, what can we do with the packet ID? 322 00:34:07,910 --> 00:34:12,330 I guess a better question might be, what happens if we just send a random packet ID? 323 00:34:12,330 --> 00:34:16,410 So let's read Unity's documentation on exactly this. 324 00:34:17,170 --> 00:34:21,370 So according to the documentation, there are a few conditions. 325 00:34:21,370 --> 00:34:30,310 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. 326 00:34:30,390 --> 00:34:38,370 If the packet ID is more than 512 behind the current packet ID, we're going to discard it because it's too old. 327 00:34:38,370 --> 00:34:39,830 We don't want it anymore. 328 00:34:40,710 --> 00:34:47,370 If the packet ID is in a list of packets we've seen recently, it's a duplicate, let's discard it. 329 00:34:47,370 --> 00:34:52,190 Otherwise, if none of these conditions are met, we're going to accept and process that packet. 330 00:34:52,990 --> 00:34:55,370 So there are a couple interesting things about this. 331 00:34:55,370 --> 00:35:03,570 The first is that if our guest packet ID is greater than our last packet ID plus 512, the connection will be disconnected. 332 00:35:03,570 --> 00:35:07,010 I want to emphasize that this is not our connection we're talking about. 333 00:35:07,010 --> 00:35:09,470 This is the other player's connection. 334 00:35:09,470 --> 00:35:15,370 So this is useful because it means we can pretty trivially kick other players off the server. 335 00:35:15,810 --> 00:35:27,130 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. 336 00:35:27,350 --> 00:35:32,430 But reading the documentation, it seems like the odds of this happening are pretty low. 337 00:35:32,430 --> 00:35:41,110 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. 338 00:35:41,110 --> 00:35:42,670 That's pretty bad. 339 00:35:43,230 --> 00:35:48,490 But when we look at the actual implementation, it tells a slightly different story. 340 00:35:48,510 --> 00:35:54,590 So packet ID validation is done by the function unet replay protector is packet replayed. 341 00:35:54,590 --> 00:36:00,990 In practice, this function actually does not discard packets that are more than 512 packets old. 342 00:36:00,990 --> 00:36:08,350 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. 343 00:36:08,350 --> 00:36:11,970 But no, the logic for discarding old packets just isn't there. 344 00:36:12,350 --> 00:36:18,250 Instead, old packets are accepted as if they weren't more than 512 packets old. 345 00:36:18,670 --> 00:36:22,050 So unfortunately, it's a little more complicated than it sounds. 346 00:36:22,050 --> 00:36:29,130 We can't just use packet ID zero every time, even though that would be the lowest packet ID. 347 00:36:29,130 --> 00:36:37,730 And the reason for that is that the server has to account for cases where the packet ID overflows, goes from FFFF to zero. 348 00:36:37,730 --> 00:36:40,510 And this happens pretty often over the course of a game. 349 00:36:40,510 --> 00:36:52,550 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. 350 00:36:52,550 --> 00:36:59,790 And so doing the math here, we have what's very close to a 50-50 shot that a packet ID will be accepted. 351 00:37:00,490 --> 00:37:08,130 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. 352 00:37:08,530 --> 00:37:18,170 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. 353 00:37:19,250 --> 00:37:21,150 Okay, so let's look at our second demo. 354 00:37:21,150 --> 00:37:26,230 And for this, I had to use an actual game, not just something I came up with myself. 355 00:37:26,230 --> 00:37:28,530 And this game is called Streets of Rogue. 356 00:37:28,530 --> 00:37:29,970 It's very cool. 357 00:37:29,970 --> 00:37:31,310 You should buy it. 358 00:37:31,310 --> 00:37:36,490 And I do want to emphasize that this is not a bug in the game itself. 359 00:37:36,490 --> 00:37:38,910 There's nothing wrong with how this game is programmed. 360 00:37:38,910 --> 00:37:40,330 This is a bug in U-Net. 361 00:37:40,330 --> 00:37:51,490 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. 362 00:37:51,530 --> 00:37:58,210 So this little guy in the red shirt is me, and these two identical looking people are our target players. 363 00:37:58,210 --> 00:38:00,690 And what we're going to do is we're going to inject packets. 364 00:38:00,690 --> 00:38:02,430 We're going to see them do a few things. 365 00:38:02,430 --> 00:38:04,330 First, they're going to say hi to everyone. 366 00:38:05,350 --> 00:38:06,990 Then we're going to kill them both. 367 00:38:07,450 --> 00:38:10,850 Then I started to feel a little bit bad, so we're going to resurrect them both. 368 00:38:10,910 --> 00:38:12,310 And then we're going to kill them again. 369 00:38:14,030 --> 00:38:17,210 Okay, so this is a pretty simple demo. 370 00:38:17,210 --> 00:38:20,490 Kind of stupid, but it demonstrates a few things. 371 00:38:20,490 --> 00:38:28,590 The first is that we're able to inject packets that are actually accepted within the context of another player's session. 372 00:38:28,590 --> 00:38:40,450 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. 373 00:38:40,650 --> 00:38:47,110 And finally, we're able to do it consistently enough that we're able to inject multiple packets into each player's session. 374 00:38:47,110 --> 00:38:58,050 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. 375 00:38:58,050 --> 00:39:00,950 So this bug is pretty reliable. 376 00:39:00,950 --> 00:39:11,130 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. 377 00:39:11,830 --> 00:39:14,550 So let's talk about remediations for this bug. 378 00:39:14,770 --> 00:39:18,750 This is considered to be an architectural weakness with U-Net. 379 00:39:18,770 --> 00:39:25,550 The actual fixes that would be required to prevent this entirely are not going to happen. 380 00:39:25,550 --> 00:39:31,490 The only mitigation, aside from moving away from U-Net, is to actually encrypt U-Net. 381 00:39:32,070 --> 00:39:38,350 Unity does provide a reference implementation that does a decent job of this, but it's not complete. 382 00:39:38,350 --> 00:39:40,730 It doesn't do key exchange for you. 383 00:39:40,730 --> 00:39:44,350 So to an extent, you are still on your own with this. 384 00:39:44,410 --> 00:39:49,970 And I've looked a lot, but I've not found a single game implementing encryption over U-Net. 385 00:39:49,990 --> 00:39:52,670 And I should emphasize that this is a mitigation. 386 00:39:52,670 --> 00:39:54,110 It is not a fix. 387 00:39:54,110 --> 00:40:07,390 So if your encryption is not complete, if an attacker can bypass this encryption, these bugs still exist exactly the same as they exist now. 388 00:40:07,750 --> 00:40:10,010 Okay, so that was my final bug. 389 00:40:10,010 --> 00:40:12,150 So let's talk about some future work. 390 00:40:12,150 --> 00:40:23,130 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. 391 00:40:23,130 --> 00:40:26,230 So both of these protocols have other transport modes. 392 00:40:26,230 --> 00:40:31,210 I think the most interesting of these are WebSockets, because that's what browser games are going to be using. 393 00:40:32,190 --> 00:40:36,470 There's also third-party networking plugins for some of these engines. 394 00:40:36,470 --> 00:40:40,370 Photon and Mirror are two relatively common ones for Unity. 395 00:40:40,550 --> 00:40:45,290 And there's just other engines, GameMaker Studio, Godot, stuff like that. 396 00:40:46,530 --> 00:40:52,170 Finally, I want to thank a few people, both Epic Games and Unity Technologies. 397 00:40:52,170 --> 00:41:03,350 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. 398 00:41:03,350 --> 00:41:06,550 I have nothing but good things to say about both of them. 399 00:41:06,550 --> 00:41:12,070 I also want to thank the artist who made the background art for my presentation, Grigorin. 400 00:41:12,070 --> 00:41:13,070 He's awesome. 401 00:41:14,190 --> 00:41:21,570 And finally, all the scripts, everything I've put together for this work, I've got up on GitHub. 402 00:41:21,570 --> 00:41:23,850 I've got POCs for most of these issues. 403 00:41:23,850 --> 00:41:28,890 I've got libraries for interfacing with UNet and Unreal's protocol. 404 00:41:29,570 --> 00:41:38,330 And I hope that if I can ask anything of you, it's that you go, you use something that you've learned here to go get banned from some video game. 405 00:41:38,830 --> 00:41:39,430 All right. 406 00:41:39,430 --> 00:41:40,630 Thank you, everyone.