WEBVTT

00:00.640 --> 00:02.480
Alright, welcome everyone.

00:02.480 --> 00:07.340
My name is Jack Baker, and I'm going to be talking about bugs in Game Engine.

00:07.380 --> 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.

00:15.360 --> 00:18.300
And I'm going to be talking about four of those bugs today.

00:19.160 --> 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.

00:28.600 --> 00:32.600
So if you're building a video game, you're probably not doing it from scratch.

00:32.600 --> 00:38.820
You're using a pre-made set of tools and software that are built to make that process easier.

00:38.820 --> 00:42.000
And that software is called your Game Engine.

00:42.500 --> 00:48.620
And so the popularity of many Game Engines means that a lot of games share the exact same bugs.

00:48.760 --> 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.

00:57.700 --> 01:01.560
And games don't usually get security patches after release.

01:01.560 --> 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.

01:13.920 --> 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.

01:23.300 --> 01:26.480
Those are Unreal Engine 4 and Unity.

01:26.740 --> 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.

01:34.300 --> 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.

01:42.860 --> 01:46.840
So Unreal Engine 4 is created by Epic Games.

01:46.840 --> 01:50.000
It's named for its roots in the Unreal series.

01:50.160 --> 01:51.320
It is open source.

01:51.320 --> 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.

02:00.280 --> 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.

02:10.560 --> 02:13.980
Unity is built by Unity Technologies.

02:13.980 --> 02:19.880
There are some open source components of it, but the core components of Unity are closed source.

02:19.920 --> 02:23.740
The core networking library we're going to be looking at is called U-Net.

02:23.920 --> 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.

02:34.020 --> 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.

02:41.180 --> 02:45.460
The first is that U-Net has not got an official replacement yet.

02:45.460 --> 02:48.140
Unity hasn't put out an alternative yet.

02:48.140 --> 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.

02:55.820 --> 03:02.440
Also, U-Net still does receive patches and occasionally new features, even though it's deprecated.

03:02.440 --> 03:07.300
So the encryption API was added to U-Net after deprecation.

03:08.360 --> 03:14.060
And so a ton of new, and more importantly I think, existing games use U-Net.

03:14.060 --> 03:17.780
So I think bugs in U-Net still have some value.

03:18.400 --> 03:21.700
So let's talk about multiplayer protocols.

03:21.700 --> 03:29.800
As game engines have evolved, and as multiplayer protocols have evolved, there's really been a focus on two things.

03:29.800 --> 03:34.140
The first is obvious, it's increasing performance, increasing speed.

03:34.140 --> 03:40.160
The other is moving trust away from the client in order to prevent hacking.

03:40.700 --> 03:44.520
But as we're going to see, these can sometimes be conflicting goals.

03:45.020 --> 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.

03:53.180 --> 03:58.980
And I think the best example of this is to talk about the evolution of what we'll call movement hacking.

03:59.300 --> 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.

04:07.700 --> 04:12.800
In the old days, this was really easy because player location was just trusted to the client.

04:12.800 --> 04:18.240
So if you manipulate your location client side, you can teleport server side.

04:18.400 --> 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.

04:28.440 --> 04:32.080
So the client can no longer say, I'm at XYZ.

04:32.080 --> 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.

04:44.500 --> 04:47.860
This led to a new type of attack called speed hacking.

04:47.900 --> 04:51.340
So it's sort of the next evolution of movement hacking.

04:51.340 --> 04:57.600
Speed hacking, the goal is not to teleport necessarily, but just to move extremely fast.

04:57.600 --> 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.

05:09.720 --> 05:15.600
This was prevented by basically giving more authority and more context to the server.

05:15.600 --> 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.

05:24.000 --> 05:30.080
And if a client is attempting to move beyond that time frame, it can stop that from happening.

05:30.100 --> 05:37.000
And so by giving this context to the server, we're able to prevent this type of attack.

05:37.000 --> 05:39.960
And this is really how game engines have evolved.

05:39.960 --> 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.

05:53.600 --> 05:59.680
So with all of this said, let's talk about some of the technical specifics of multiplayer protocols.

05:59.680 --> 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.

06:08.740 --> 06:15.160
And first of those is that most multiplayer protocols use some form of distributed architecture.

06:15.840 --> 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.

06:27.440 --> 06:34.520
And actions between objects and between systems are performed through what are called remote procedure calls.

06:34.900 --> 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.

06:44.180 --> 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.

06:51.200 --> 06:56.780
The difference is that that function is executing on someone else's system, not your local system.

06:56.800 --> 07:03.020
This is really convenient, but there's a lot of complexity that goes into this process on the back end.

07:03.860 --> 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.

07:17.620 --> 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.

07:26.340 --> 07:30.280
But player A can only issue RPCs on character A.

07:30.280 --> 07:34.760
He can't issue RPCs on character B and vice versa.

07:35.360 --> 07:43.340
Another interesting technical detail of multiplayer protocols is that they're usually implemented over UDP.

07:43.340 --> 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.

07:51.600 --> 07:58.240
But when we're about desktop games, we're usually talking about protocols that are implemented over UDP.

07:58.240 --> 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.

08:12.640 --> 08:18.340
So the protocol gets more complex because it has to deal with these types of problems itself.

08:19.240 --> 08:24.380
So now that we've talked a little bit about how these protocols work, let's look at our first bug.

08:24.380 --> 08:28.420
And this is an Unreal Engine 4 bug, and it's actually a file pathing bug.

08:29.040 --> 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.

08:36.400 --> 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.

08:44.200 --> 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.

08:55.440 --> 08:58.960
And so one of these URLs might look something like this.

08:58.960 --> 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.

09:13.200 --> 09:15.300
So the bug here is pretty simple.

09:15.300 --> 09:22.740
If you use a malicious URL, you can cause a server or a client to access any local file path.

09:23.120 --> 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.

09:30.400 --> 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.

09:38.440 --> 09:46.140
So UNC paths are special Windows paths that are used to access networked resources as if they were regular files.

09:46.240 --> 09:48.540
So regular local files.

09:49.120 --> 09:52.080
Typically a UNC path will look something like this.

09:52.080 --> 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.

10:03.180 --> 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.

10:11.500 --> 10:12.600
This is pretty simple.

10:12.600 --> 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.

10:22.240 --> 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.

10:29.400 --> 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.

10:43.460 --> 10:49.940
And so what this does is it opens up the affected system to the world of SMB related attacks.

10:50.400 --> 10:55.080
Typically when a Windows system connects to an SMB share, it'll try to authenticate with it.

10:55.080 --> 10:59.340
So this can be used for credential harvesting or authentication relaying.

10:59.340 --> 11:02.120
There's a whole world of different attacks using this.

11:02.120 --> 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.

11:10.180 --> 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.

11:17.300 --> 11:21.300
So this was fixed in Unreal Engine 4.25.2.

11:21.300 --> 11:25.380
With this commit, it's really easy to backport this.

11:25.380 --> 11:28.520
So if you're on an older version, this isn't too hard to apply.

11:28.900 --> 11:33.520
So this is a pretty fun bug, but let's talk about something a little bit flashier.

11:33.520 --> 11:36.580
And let's start talking about our first UNet bug.

11:37.400 --> 11:44.940
So UNet packets are packed in such a format that you can put multiple RPCs into a single packet.

11:44.940 --> 11:47.360
And that looks a little bit like this.

11:47.380 --> 11:53.300
So the first thing you've got is the packet header, which for our purposes, we don't really care about.

11:53.300 --> 11:57.120
But then you have each message within that packet.

11:57.120 --> 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.

12:09.600 --> 12:14.460
And then at the end of that body, you see you have the next message just concatenated on.

12:15.320 --> 12:17.820
The bug here, again, is pretty simple.

12:17.820 --> 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.

12:29.860 --> 12:31.940
So that looks a little something like this.

12:32.000 --> 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.

12:39.780 --> 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.

12:54.600 --> 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.

13:04.760 --> 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.

13:14.000 --> 13:20.620
And so to do this, it's not enough to just cause the server to act on that old memory.

13:20.620 --> 13:25.100
We need to convince the server to actually send that memory back to us.

13:25.160 --> 13:30.220
And so the type of RPC that is really good for this is chat messages.

13:30.220 --> 13:34.900
So let's look at what an example chat RPC might look like.

13:34.900 --> 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.

13:43.020 --> 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.

13:52.460 --> 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.

14:00.820 --> 14:05.980
And those contents will contain data from previous RPCs.

14:06.440 --> 14:12.240
But even in the absence of chat messages, there are some other RPCs we can use.

14:12.240 --> 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.

14:23.500 --> 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.

14:32.420 --> 14:36.840
And then there's always game-specific RPCs that we could potentially use for this.

14:36.840 --> 14:38.620
We just have to get creative.

14:39.780 --> 14:42.740
So let's talk about what we can leak with this.

14:42.740 --> 14:44.720
Now, there can be a lot of things.

14:44.720 --> 14:48.280
If you're doing authentication over U-Net, this could be passwords.

14:48.280 --> 14:50.260
It could also be private messages.

14:50.260 --> 14:54.480
It could be game-specific stuff like player locations or player actions.

14:54.480 --> 14:59.760
Really anything that gets sent over U-Net by the client could be leaked this way.

14:59.800 --> 15:06.220
And this was fixed pretty recently at the end of May with U-Net version 1.0.6.

15:07.020 --> 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.

15:16.640 --> 15:19.520
This first one is another Unreal Engine bug.

15:19.520 --> 15:25.040
And I really like this bug because it is a universal speed hack for Unreal Engine.

15:25.440 --> 15:33.240
So we talked just briefly about this, but movement in Unreal Engine is what we call server authoritative.

15:33.240 --> 15:37.860
That means that the client cannot just say, I'm at XYZ.

15:37.860 --> 15:46.040
Instead, the client has to issue a movement RPC and ask the server to update the player's location.

15:47.740 --> 15:51.180
And so this movement RPC has two important parts to it.

15:51.180 --> 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.

15:59.220 --> 16:02.040
So the first argument is the movement vector.

16:02.040 --> 16:06.680
And this is a vector that says the direction that we're moving and the speed we're moving at.

16:06.680 --> 16:14.620
And the second argument is a timestamp that just says we sent this RPC at this time relevant to client time.

16:15.160 --> 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.

16:33.720 --> 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.

16:43.740 --> 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.

16:54.110 --> 16:58.070
Now we need to make a slight digression and talk about floating point.

16:58.530 --> 17:03.750
So when I say floating point, I'm specifically referring to IEEE 754.

17:03.750 --> 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.

17:16.110 --> 17:20.570
And so what's interesting about floating point is it has some special values.

17:20.570 --> 17:24.830
Floating point can be used to represent infinity, either positive or negative.

17:24.830 --> 17:30.250
It also has a special value called not a number, which I choose to verbalize as NAN.

17:30.290 --> 17:37.270
And for completion's sake, I should say that NAN can be positive or negative, but it doesn't typically matter.

17:38.050 --> 17:43.210
So these special values usually are the result of some undefined mathematical operation.

17:43.470 --> 17:51.950
So if you do any non-zero number divided by zero in floating point, you get infinite, either positive or negative.

17:51.950 --> 17:55.610
If you do zero divided by zero in floating point, you get NAN.

17:55.770 --> 17:59.870
Similarly, if you do square root of negative one, you get NAN.

18:00.650 --> 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.

18:08.030 --> 18:13.550
The first is that any affirmative comparison against NAN will evaluate to false.

18:13.550 --> 18:17.090
So we can compare NAN to zero in any way we want.

18:17.090 --> 18:18.350
It'll always be false.

18:18.350 --> 18:20.610
We can do NAN equals equals zero.

18:20.610 --> 18:21.370
False.

18:21.370 --> 18:22.650
Is NAN greater than zero?

18:22.650 --> 18:23.110
False.

18:23.110 --> 18:24.450
Is NAN less than zero?

18:24.450 --> 18:24.890
False.

18:24.890 --> 18:28.950
We can even compare NAN to itself, and it'll still be false.

18:29.750 --> 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.

18:40.530 --> 18:45.530
So NAN plus one, minus one, times two, divided by two, these are all NAN.

18:45.530 --> 18:53.950
And no matter how complex you make this mathematical operation, if one operand is NAN, the entire thing will become NAN.

18:54.890 --> 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.

19:04.570 --> 19:08.990
So let's look at the following code as a really simple example of this.

19:08.990 --> 19:17.590
So in this case, the programmer is trying to ensure that the floating point number NAN is between zero and 100.

19:18.530 --> 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.

19:33.330 --> 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.

19:47.650 --> 19:54.470
So NAN poisoning attacks are pretty rare because it's typically difficult to actually introduce NAN in the first place.

19:54.470 --> 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.

20:02.470 --> 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.

20:11.610 --> 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.

20:19.910 --> 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.

20:27.110 --> 20:30.210
So what happens if our timestamp is NAN?

20:30.210 --> 20:38.610
Well, to figure this out, we have to look at this mouthful of a function, uCharacterMovementComponent , isClientTimeStamp valid?

20:39.570 --> 20:43.350
So this is a lot to look at, so we're going to go through it together.

20:43.350 --> 20:50.330
But let's assume that our timestamp is NAN, because that is the value we've provided to our RPC.

20:50.330 --> 20:55.810
So we get to this first check, which is intended to ensure that the timestamp is greater than zero.

20:55.850 --> 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.

21:03.150 --> 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.

21:14.970 --> 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.

21:26.450 --> 21:29.490
So delta timestamp evaluates to NAN.

21:29.790 --> 21:32.170
Then we get to these next two checks.

21:32.170 --> 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.

21:39.410 --> 21:43.110
And for the next check, delta timestamp is also NAN.

21:43.110 --> 21:45.270
This is another affirmative comparison.

21:45.390 --> 21:50.470
We pass right by, and our timestamp is considered to be quote-unquote valid.

21:51.730 --> 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.

21:59.030 --> 22:03.870
And so the next thing we do is we generate our delta time using NAN.

22:03.870 --> 22:13.930
So our delta time is calculated, again, by subtracting our last valid timestamp from our provided timestamp.

22:13.930 --> 22:23.250
And because our provided timestamp is NAN, delta time will evaluate to NAN regardless of what our last valid timestamp was.

22:23.770 --> 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.

22:31.070 --> 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.

22:43.050 --> 22:48.490
So our movement is not applied, and even though our timestamp was considered valid, we don't go anywhere.

22:48.490 --> 22:49.630
We don't move.

22:50.690 --> 22:55.090
But we're not quite done yet because we've caused another value to be poisoned.

22:55.090 --> 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.

23:03.590 --> 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.

23:13.290 --> 23:18.110
Now we need to look at that is client timestamp valid function again.

23:18.430 --> 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.

23:24.530 --> 23:26.430
It's not any special number.

23:26.430 --> 23:30.050
It's just a regular floating point number greater than zero.

23:30.050 --> 23:31.710
So we bypass this first check.

23:31.710 --> 23:35.190
We get to the delta timestamp calculation.

23:35.190 --> 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.

23:43.470 --> 23:47.750
So delta time is still going to calculate as NAN.

23:47.750 --> 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.

23:58.770 --> 24:00.250
So we pass these checks.

24:00.250 --> 24:03.130
And again, our timestamp is considered valid.

24:03.250 --> 24:09.510
So on this second RPC call, any timestamp greater than zero will pass the validity check.

24:09.510 --> 24:11.530
We could say it's 30 years in the future.

24:11.530 --> 24:14.810
It doesn't matter as long as it is greater than zero.

24:14.810 --> 24:18.490
And our first RPC call use NAN as a timestamp.

24:18.490 --> 24:21.850
Our second call will always pass the validation check.

24:22.450 --> 24:30.070
Unfortunately, our delta time is still going to calculate as NAN because our old client timestamp was NAN.

24:30.070 --> 24:31.510
So still nothing happens.

24:31.510 --> 24:36.730
We haven't moved an inch, but fortunately we've poisoned one more value now.

24:36.730 --> 24:43.950
So while all this is going on, the server is trying to determine if our time has drifted from server time.

24:43.950 --> 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.

24:52.230 --> 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.

25:03.590 --> 25:09.970
But because our delta time is NAN, our client error is also going to be NAN.

25:09.970 --> 25:16.710
And then this client error is used to build up our cumulative value new time discrepancy.

25:16.710 --> 25:22.490
And this value is used to detect when we have drifted too far from server time.

25:22.490 --> 25:24.990
This is used to detect speed hacking attempts.

25:24.990 --> 25:30.730
It's also used to detect when a client might just be lagging too much.

25:30.730 --> 25:40.330
But because our client error is NAN, when our client error is added, our new time discrepancy value also becomes NAN.

25:41.550 --> 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.

25:57.970 --> 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.

26:11.870 --> 26:20.150
And more importantly, no mathematical operation will ever cause new time discrepancy to become a regular floating point number again.

26:20.150 --> 26:22.030
It is stuck as NAN.

26:22.030 --> 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.

26:37.610 --> 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.

26:44.690 --> 26:47.730
So I've oversimplified this process a bit.

26:47.730 --> 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.

27:03.390 --> 27:06.890
The second, the timestamp is just slightly greater than zero.

27:06.890 --> 27:11.010
And the third, the timestamp is some value well in the future.

27:11.010 --> 27:16.170
And every time you send this grouping, it will move you forward some amount.

27:16.170 --> 27:21.410
And because the server cannot detect the discrepancy anymore, you can do this as often as you want.

27:21.410 --> 27:27.250
And the only real limitation is how quickly you can send those RPCs and have them be processed.

27:27.650 --> 27:32.310
So saying all that, let's actually look at this in action.

27:32.310 --> 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.

27:38.910 --> 27:40.310
This is our first demo.

27:40.310 --> 27:47.550
This is built on a stock Unity, or I'm stock Unreal Engine 4 game template.

27:47.550 --> 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.

28:00.110 --> 28:06.990
And this is filmed from the server perspective, just so that you can see that this is actually happening server-side.

28:06.990 --> 28:10.110
It doesn't just look like we're moving fast client-side.

28:10.370 --> 28:12.750
So in the background, we've got our hacker.

28:12.750 --> 28:14.690
As you can see, he's vibrating.

28:14.690 --> 28:16.530
He's very excited to be here.

28:16.730 --> 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.

28:21.370 --> 28:25.250
And in just a second, we are going to see him blast off.

28:25.990 --> 28:26.870
And he's gone.

28:26.990 --> 28:34.470
Okay, so that is a little bit faster movement than intended, even with the speed hacking protections on.

28:34.490 --> 28:36.330
I really like this bug.

28:36.330 --> 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.

28:44.810 --> 28:48.110
I also think that this type of attack can apply in other ways.

28:48.110 --> 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.

28:59.350 --> 29:00.730
It's not always useful.

29:00.730 --> 29:04.510
It doesn't always do anything for us for an attacker, but it usually does something.

29:04.970 --> 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.

29:20.790 --> 29:25.030
So with that out of the way, let's talk about our final bug.

29:25.030 --> 29:28.810
We're going to go back to UNet, and this is a session hijacking bug.

29:29.530 --> 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.

29:50.310 --> 29:55.390
So packets are not validated by their source IP address, or their source port, or anything like that.

29:55.390 --> 29:59.350
They're only validated by values within the packet itself.

29:59.970 --> 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.

30:08.450 --> 30:12.670
We don't need a man in the middle, we don't need to be over LAN, or anything like that.

30:12.670 --> 30:14.190
And that's the plan here.

30:14.630 --> 30:21.750
So when UNet validates an incoming packet, there are three important values it looks at.

30:21.750 --> 30:25.510
The first is the host ID, then the session ID, and the packet ID.

30:25.510 --> 30:29.570
And these names don't mean anything on their own, so let's look at each one in detail.

30:30.670 --> 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.

30:39.210 --> 30:42.330
Host IDs are assigned sequentially, starting at 1.

30:42.330 --> 30:48.330
So the first client to connect gets host ID 1, the second client gets host ID 2, etc.

30:48.810 --> 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.

30:54.050 --> 30:57.710
It's also really easy to enumerate the host ID of another player.

30:57.710 --> 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.

31:03.870 --> 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.

31:13.110 --> 31:18.370
Things get a little more complicated when we talk about the next value, which is the session ID.

31:18.530 --> 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.

31:27.830 --> 31:34.170
And every packet received for that client has to have the correct session ID, or the packet will be discarded.

31:34.850 --> 31:37.410
There are a couple problems with the session ID though.

31:37.410 --> 31:41.630
The first is that the session ID is also a 16-bit integer.

31:41.650 --> 31:43.530
It also can't be zero.

31:43.530 --> 31:48.630
This means that there's only 65,535 possible session IDs.

31:48.730 --> 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.

31:56.690 --> 32:02.870
So we can easily brute force the whole range of session IDs, even over the open internet.

32:02.870 --> 32:03.610
It doesn't matter.

32:03.610 --> 32:05.170
It doesn't take long at all.

32:05.650 --> 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.

32:13.010 --> 32:18.770
So session IDs are generated with a function named unet getRandNotZero.

32:18.770 --> 32:22.010
And what this function does is exactly what the name says.

32:22.010 --> 32:25.970
It gets a random number and ensures that that random number is not zero.

32:26.110 --> 32:30.610
And this is used for session IDs because session IDs cannot be zero.

32:31.550 --> 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.

32:39.390 --> 32:44.850
It essentially ensures that the least significant bit of that output will always be one.

32:44.850 --> 32:52.270
This has the effect of ensuring that a legitimate unet client will only ever generate an odd-numbered session ID.

32:52.270 --> 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.

33:02.290 --> 33:08.590
So this reduces the possible session IDs down to 32,768, so 50%.

33:10.010 --> 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.

33:19.570 --> 33:22.950
But there is one more hiccup, that's the packet ID.

33:22.950 --> 33:29.290
The packet ID is an integer that's incremented with each packet sent by the client, basically a sequence number.

33:29.290 --> 33:31.530
And again, it's 16 bits long.

33:32.450 --> 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.

33:41.650 --> 33:44.670
It's also used to determine the rate of packet loss.

33:44.670 --> 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.

34:00.970 --> 34:07.910
So if we can determine the host ID and guess the session ID, what can we do with the packet ID?

34:07.910 --> 34:12.330
I guess a better question might be, what happens if we just send a random packet ID?

34:12.330 --> 34:16.410
So let's read Unity's documentation on exactly this.

34:17.170 --> 34:21.370
So according to the documentation, there are a few conditions.

34:21.370 --> 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.

34:30.390 --> 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.

34:38.370 --> 34:39.830
We don't want it anymore.

34:40.710 --> 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.

34:47.370 --> 34:52.190
Otherwise, if none of these conditions are met, we're going to accept and process that packet.

34:52.990 --> 34:55.370
So there are a couple interesting things about this.

34:55.370 --> 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.

35:03.570 --> 35:07.010
I want to emphasize that this is not our connection we're talking about.

35:07.010 --> 35:09.470
This is the other player's connection.

35:09.470 --> 35:15.370
So this is useful because it means we can pretty trivially kick other players off the server.

35:15.810 --> 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.

35:27.350 --> 35:32.430
But reading the documentation, it seems like the odds of this happening are pretty low.

35:32.430 --> 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.

35:41.110 --> 35:42.670
That's pretty bad.

35:43.230 --> 35:48.490
But when we look at the actual implementation, it tells a slightly different story.

35:48.510 --> 35:54.590
So packet ID validation is done by the function unet replay protector is packet replayed.

35:54.590 --> 36:00.990
In practice, this function actually does not discard packets that are more than 512 packets old.

36:00.990 --> 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.

36:08.350 --> 36:11.970
But no, the logic for discarding old packets just isn't there.

36:12.350 --> 36:18.250
Instead, old packets are accepted as if they weren't more than 512 packets old.

36:18.670 --> 36:22.050
So unfortunately, it's a little more complicated than it sounds.

36:22.050 --> 36:29.130
We can't just use packet ID zero every time, even though that would be the lowest packet ID.

36:29.130 --> 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.

36:37.730 --> 36:40.510
And this happens pretty often over the course of a game.

36:40.510 --> 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.

36:52.550 --> 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.

37:00.490 --> 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.

37:08.530 --> 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.

37:19.250 --> 37:21.150
Okay, so let's look at our second demo.

37:21.150 --> 37:26.230
And for this, I had to use an actual game, not just something I came up with myself.

37:26.230 --> 37:28.530
And this game is called Streets of Rogue.

37:28.530 --> 37:29.970
It's very cool.

37:29.970 --> 37:31.310
You should buy it.

37:31.310 --> 37:36.490
And I do want to emphasize that this is not a bug in the game itself.

37:36.490 --> 37:38.910
There's nothing wrong with how this game is programmed.

37:38.910 --> 37:40.330
This is a bug in U-Net.

37:40.330 --> 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.

37:51.530 --> 37:58.210
So this little guy in the red shirt is me, and these two identical looking people are our target players.

37:58.210 --> 38:00.690
And what we're going to do is we're going to inject packets.

38:00.690 --> 38:02.430
We're going to see them do a few things.

38:02.430 --> 38:04.330
First, they're going to say hi to everyone.

38:05.350 --> 38:06.990
Then we're going to kill them both.

38:07.450 --> 38:10.850
Then I started to feel a little bit bad, so we're going to resurrect them both.

38:10.910 --> 38:12.310
And then we're going to kill them again.

38:14.030 --> 38:17.210
Okay, so this is a pretty simple demo.

38:17.210 --> 38:20.490
Kind of stupid, but it demonstrates a few things.

38:20.490 --> 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.

38:28.590 --> 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.

38:40.650 --> 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.

38:47.110 --> 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.

38:58.050 --> 39:00.950
So this bug is pretty reliable.

39:00.950 --> 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.

39:11.830 --> 39:14.550
So let's talk about remediations for this bug.

39:14.770 --> 39:18.750
This is considered to be an architectural weakness with U-Net.

39:18.770 --> 39:25.550
The actual fixes that would be required to prevent this entirely are not going to happen.

39:25.550 --> 39:31.490
The only mitigation, aside from moving away from U-Net, is to actually encrypt U-Net.

39:32.070 --> 39:38.350
Unity does provide a reference implementation that does a decent job of this, but it's not complete.

39:38.350 --> 39:40.730
It doesn't do key exchange for you.

39:40.730 --> 39:44.350
So to an extent, you are still on your own with this.

39:44.410 --> 39:49.970
And I've looked a lot, but I've not found a single game implementing encryption over U-Net.

39:49.990 --> 39:52.670
And I should emphasize that this is a mitigation.

39:52.670 --> 39:54.110
It is not a fix.

39:54.110 --> 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.

40:07.750 --> 40:10.010
Okay, so that was my final bug.

40:10.010 --> 40:12.150
So let's talk about some future work.

40:12.150 --> 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.

40:23.130 --> 40:26.230
So both of these protocols have other transport modes.

40:26.230 --> 40:31.210
I think the most interesting of these are WebSockets, because that's what browser games are going to be using.

40:32.190 --> 40:36.470
There's also third-party networking plugins for some of these engines.

40:36.470 --> 40:40.370
Photon and Mirror are two relatively common ones for Unity.

40:40.550 --> 40:45.290
And there's just other engines, GameMaker Studio, Godot, stuff like that.

40:46.530 --> 40:52.170
Finally, I want to thank a few people, both Epic Games and Unity Technologies.

40:52.170 --> 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.

41:03.350 --> 41:06.550
I have nothing but good things to say about both of them.

41:06.550 --> 41:12.070
I also want to thank the artist who made the background art for my presentation, Grigorin.

41:12.070 --> 41:13.070
He's awesome.

41:14.190 --> 41:21.570
And finally, all the scripts, everything I've put together for this work, I've got up on GitHub.

41:21.570 --> 41:23.850
I've got POCs for most of these issues.

41:23.850 --> 41:28.890
I've got libraries for interfacing with UNet and Unreal's protocol.

41:29.570 --> 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.

41:38.830 --> 41:39.430
All right.

41:39.430 --> 41:40.630
Thank you, everyone.

