start	end	text
640	6560	Alright, welcome everyone. My name is Jack Baker, and I'm going to be talking about bugs in Game
6560	11760	Engine. So as a little bit of background, over the last few months, I've found more than 10
11760	15940	remotely exploitable bugs while looking at two different game engines. And I'm going to be
15940	22420	talking about four of those bugs today. So just to level set things, when I use the term Game
22420	29500	Engine, I'm referring to the base software that most games are built on top of. So if you're
29500	34260	building a video game, you're probably not doing it from scratch. You're using a pre-made set of
34260	40900	tools and software that are built to make that process easier. And that software is called your
40900	47320	Game Engine. And so the popularity of many Game Engines means that a lot of games share the exact
47320	53160	same bugs. This is made worse by the fact that updating your Game Engine can be a huge pain,
53160	58840	as anyone who's done a lot of game dev can probably tell you. And games don't usually
58840	65080	get security patches after release. Maybe a bigger game will have some support, but an
65080	71340	independent release is not going to get patches a year or two years after release just to fix some
71340	78580	security bug. So there aren't a whole lot of good statistics on what Game Engines are the most
78580	83300	popular, but there's a general understanding that two of them are more common than others.
83300	90040	Those are Unreal Engine 4 and Unity. So as sort of a rule of thumb, if you're a solo developer
90040	95440	or a small team, there's a pretty good chance you're using Unity. Whereas if you're a larger
95440	100960	team but not so large that you've built your own engine from scratch, you're probably using Unreal
100960	109100	Engine 4. So Unreal Engine 4 is created by Epic Games. It's named for its roots in the Unreal
109100	114800	series. It is open source. There are some licensing restrictions on how you can use that source,
114800	119900	but for our perspective of just looking for bugs, all the code is out there for us to look at.
120280	123740	And there are really two big notable games, I think, right now
123740	128700	that are built using Unreal Engine 4, and those are Fortnite and PUBG.
130560	136060	Unity is built by Unity Technologies. There are some open source components of it,
136060	141840	but the core components of Unity are closed source. The core networking library we're going
141840	146600	to be looking at is called U-Net. And while I couldn't find a whole lot of big games that
146600	151760	are using U-Net, there are countless amount of indie releases on Steam and everywhere else that
151760	159180	are built using U-Net. Now I should say that U-Net is deprecated, but there are a few reasons that I
159180	164940	thought it would be a good target anyway. The first is that U-Net has not got an official replacement
164940	171260	yet. Unity hasn't put out an alternative yet. So if you're using Unity and you're doing multiplayer,
171260	177580	you're either using U-Net still or you're using a third-party solution. Also, U-Net still does
177580	183560	receive patches and occasionally new features, even though it's deprecated. So the encryption
183560	191380	API was added to U-Net after deprecation. And so a ton of new, and more importantly I think,
191380	197780	existing games use U-Net. So I think bugs in U-Net still have some value.
198400	205840	So let's talk about multiplayer protocols. As game engines have evolved, and as multiplayer
205840	211280	protocols have evolved, there's really been a focus on two things. The first is obvious, it's
211280	217240	increasing performance, increasing speed. The other is moving trust away from the client
217840	224520	in order to prevent hacking. But as we're going to see, these can sometimes be conflicting goals.
225020	230680	But to really understand multiplayer protocols, I think it's worth understanding the types of
230680	235820	attacks that they're aiming to prevent. And I think the best example of this is to talk about
235820	242820	the evolution of what we'll call movement hacking. So movement hacking is the process of manipulating
242820	248480	the player's location in some way that the game normally wouldn't allow. In the old days,
248480	253240	this was really easy because player location was just trusted to the client. So if you
253240	259960	manipulate your location client side, you can teleport server side. And this got more difficult
259960	266940	as game engines got more complicated and trust was taken from the client and put on the server
266940	274500	for player location. So the client can no longer say, I'm at XYZ. The client, all I can do is make
274640	280660	a request saying, I would like to move, I'm moving in this direction at this speed, and the server
280660	288040	will update their position accordingly. This led to a new type of attack called speed hacking. So
288040	293180	it's sort of the next evolution of movement hacking. Speed hacking, the goal is not to
293180	299560	teleport necessarily, but just to move extremely fast. And typically the way this works is you
299560	305920	send that movement request excessively fast, faster than a normal client ever would,
305920	313360	and more requests then means more speed. This was prevented by basically giving more authority and
313360	319980	more context to the server. So the server should be able to understand what is a realistic distance
319980	326580	that a character can move in a given time frame. And if a client is attempting to move
326580	333680	beyond that time frame, it can stop that from happening. And so by giving this context to
333680	339200	the server, we're able to prevent this type of attack. And this is really how game engines have
339200	344920	evolved. It's been a constant process of moving trust away from the client and giving more
344920	352880	authority, more responsibility, and more context to the server to understand what is normal.
353600	359680	So with all of this said, let's talk about some of the technical specifics of multiplayer protocols.
359680	364500	Now there's no real like published standard of what a multiplayer protocol looks like,
364500	368980	but there are a few things that are pretty consistent between different protocols. And
368980	375160	first of those is that most multiplayer protocols use some form of distributed architecture.
375840	381100	And essentially what this means is each system, whether that be a client or a server, each system
381100	387440	has a copy of every networked object, every object that's connected to the network in the game world.
387440	392800	And actions between objects and between systems are performed through what are called remote
392800	400040	procedure calls. So remote procedure calls, as the name sort of suggests, are a way of calling
400040	406020	functions on a remote system as if you were calling them locally. So this is really easy for
406020	411500	the programmer because it's just like you're calling a regular function of your code. The
411500	416780	difference is that that function is executing on someone else's system, not your local system.
416800	421620	This is really convenient, but there's a lot of complexity that goes into this process on the
421620	428200	back end. And so with this concept of the distributed architecture usually comes some
428200	434220	sort of concept of ownership, where owning an object usually means having the authority to
434220	441440	issue RPCs on that object. And so the way this usually works is each player has ownership of
441440	447360	their character and maybe some associated sub-objects like your inventory. But player A
447360	454760	can only issue RPCs on character A. He can't issue RPCs on character B and vice versa.
455360	461160	Another interesting technical detail of multiplayer protocols is that they're usually
461160	467580	implemented over UDP. The one major exception to this are browser games where you can't access
467580	472080	operating system sockets, so you have to use something like web sockets. But when we're
472080	478240	about desktop games, we're usually talking about protocols that are implemented over UDP.
478240	485160	And this puts extra requirements on the protocol itself because UDP won't do things like validate
485160	490340	that a packet is part of a session that you've already authenticated or identify when a packet
490340	495920	is either a duplicate or out of order. So the protocol gets more complex because it has to
495920	501640	deal with these types of problems itself. So now that we've talked a little bit about how
501640	506480	these protocols work, let's look at our first bug. And this is an Unreal Engine 4 bug, and it's
506480	513340	actually a file pathing bug. So Unreal Engine uses its own type of URL, and it uses this to
513340	518280	communicate details between the server and the client. So this can be stuff like the server can
518280	524940	send a URL that says, we're playing on this map, you need to load these packages. Or the client
524940	531500	could send something saying, I'm joining the game, my player name is Jack, and I'm joining with two
531500	538000	other players who are playing split screen on the same computer. And so one of these URLs might look
538000	543300	something like this. You start with the IP address of the server, then you've got a file path that
543300	550660	corresponds to a particular asset, and then just like an HTTP URL, you've got key value pairs that
550660	557660	are separated by an equal sign. So the bug here is pretty simple. If you use a malicious URL, you can
557660	565020	cause a server or a client to access any local file path. It's not going to try to write to that
565020	570940	file or even read from that file, it's just going to try to determine if that file exists. This is
570940	575260	pretty boring on its own, but it gets more interesting when we start talking about Universal
575260	582660	Naming Convention, or UNC paths. So UNC paths are special Windows paths that are used to access
582660	590540	networked resources as if they were regular files. So regular local files. Typically a UNC
590540	596260	path will look something like this. You've got two slashes, then the host name or IP address,
596260	600960	another slash, then the share name, then another slash, and then either the file name or the full
600960	609420	file path. So using this crafted URL, or one that looks like it, we can cause a server or client to
609420	615160	connect to a remote SMB share. This is pretty simple. The one trick here is we do need to
615160	622440	include the part that says .umap, where umap is a file extension used by Unreal Engine. And by
622440	628360	having this somewhere in the domain name, here we use it as a subdomain, we can bypass some of the
628360	637440	filtration. And if we provide this URL to a system, it will cause it to make an outgoing SMB
637440	645160	connection and try to determine the existence of this hi.txt file. And so what this does is it
645160	652060	opens up the affected system to the world of SMB related attacks. Typically when a Windows system
652060	656760	connects to an SMB share, it'll try to authenticate with it. So this can be used for credential
656760	662120	harvesting or authentication relaying. There's a whole world of different attacks using this.
662120	667940	This can also be used pretty trivially as a server denial of service because the whole server will lock
667940	673780	up as it's making this request. So if you cause that SMB connection to deliberately take a long
673780	682040	time, you can lock up the server for a while. So this was fixed in Unreal Engine 4.25.2. With this
682040	687640	commit, it's really easy to backport this. So if you're on an older version, this isn't too hard to
687640	693740	apply. So this is a pretty fun bug, but let's talk about something a little bit flashier. And let's
693740	701600	start talking about our first UNet bug. So UNet packets are packed in such a format that you can
701600	708480	put multiple RPCs into a single packet. And that looks a little bit like this. So the first thing
708480	713940	you've got is the packet header, which for our purposes, we don't really care about. But then
713940	721820	you have each message within that packet. And each message consists of a 16-bit message length,
722000	728300	a 16-bit value that I call the message type, and then the message body, the actual contents
728300	733500	of that message. And then at the end of that body, you see you have the next message just concatenated
733500	741640	on. The bug here, again, is pretty simple. If we supply a message size that's larger than the actual
741640	747080	payload of our message, the actual body of our message, we can convince the server to act on
747080	753240	extra data that's already in memory. So that looks a little something like this. If we imagine this
753240	759780	is just a regular RPC, we start with the length, and then we've got the body, which is four bytes.
759780	765840	But if we were to manipulate that length without actually increasing the length of our body,
765840	770720	we'll see that all this other data that was already in memory, that's not part of our message,
770720	777660	is now within scope of our RPC. So what's interesting about this is that this old memory
777660	783920	actually comes from previous RPCs, not just from ours, but from other connections, from other
783920	790840	players. So what we want to do here is we want to create an RPC that will leak this old memory to us
790840	798560	kind of like Heartbleed would. And so to do this, it's not enough to just cause the server to
798560	805100	act on that old memory. We need to convince the server to actually send that memory back to us.
805160	811940	And so the type of RPC that is really good for this is chat messages. So let's look at what an
811940	817700	example chat RPC might look like. In this case, again, we've got the length, and then we've got
817700	823620	the body of the RPC, which is just made up of a string length and then a string body. But if we
823620	830060	just apply that string length and we don't supply a body, then all the rest of that memory is going
830060	837140	to be treated as part of that string. And if that RPC is accepted, the server is going to send back
837280	842100	a message that says there's a new chat message and here's its content. And those contents will
842100	850320	contain data from previous RPCs. But even in the absence of chat messages, there are some other
850320	856500	RPCs we can use. Movement or spawning a new object are both good ones because they typically
857040	865340	involve giving some form of vector, some location, as an argument. And we can use this to leak either
865340	872420	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.
872420	876840	And then there's always game-specific RPCs that we could potentially use for this.
876840	882960	We just have to get creative. So let's talk about what we can leak with this. Now,
883480	887420	there can be a lot of things. If you're doing authentication over U-Net, this could be
887420	893040	passwords. It could also be private messages. It could be game-specific stuff like player locations
893040	899760	or player actions. Really anything that gets sent over U-Net by the client could be leaked this way.
899800	906220	And this was fixed pretty recently at the end of May with U-Net version 1.0.6.
907020	911640	So I went kind of quickly through those first two, but I really wanted to save time for these
911640	917360	last two bugs, which I think warrant going into a little bit more technical depth. This first one
917360	924040	is another Unreal Engine bug. And I really like this bug because it is a universal speed hack for
924040	931280	Unreal Engine. So we talked just briefly about this, but movement in Unreal Engine is what we
931280	938580	call server authoritative. That means that the client cannot just say, I'm at XYZ. Instead,
938580	946040	the client has to issue a movement RPC and ask the server to update the player's location.
947740	953660	And so this movement RPC has two important parts to it. I am oversimplifying this quite a bit,
953660	959800	but I only want to talk about the parts that are actually relevant to our bug. So the first
959800	964900	argument is the movement vector. And this is a vector that says the direction that we're moving
964900	970100	and the speed we're moving at. And the second argument is a timestamp that just says we sent
970100	979460	this RPC at this time relevant to client time. And so the actual math here is that the server
979460	985780	will calculate what's called a movement delta by taking our provided timestamp and subtracting
985780	995220	from it the last valid timestamp that we provided to the server. And then it's going to
995220	1001640	take our movement vector and multiply it by our movement delta to calculate the actual movement
1001640	1007820	that should be applied. And so if the server can properly validate that both the movement delta
1007820	1013350	and the movement vector are sane, there's not a whole lot we can do to manipulate our movement.
1014110	1018070	Now we need to make a slight digression and talk about floating point.
1018530	1025210	So when I say floating point, I'm specifically referring to IEEE 754. And this is how most
1025210	1030310	computer systems represent rational numbers, numbers that can be non-whole, such as 1, 2,
1030310	1037990	3, 4, or 1, 2, dot, 3, 4, anything with a period in it. And so what's interesting about floating
1037990	1043530	point is it has some special values. Floating point can be used to represent infinity, either
1043530	1048990	positive or negative. It also has a special value called not a number, which I choose to verbalize
1048990	1055490	as NAN. And for completion's sake, I should say that NAN can be positive or negative, but it
1055490	1062210	doesn't typically matter. So these special values usually are the result of some undefined mathematical
1062210	1069890	operation. So if you do any non-zero number divided by zero in floating point, you get
1069890	1075030	infinite, either positive or negative. If you do zero divided by zero in floating point, you get
1075030	1082410	NAN. Similarly, if you do square root of negative one, you get NAN. Now NAN is really the more
1082410	1088030	interesting of these two special values, and it has a couple properties that are really unique.
1088030	1094050	The first is that any affirmative comparison against NAN will evaluate to false. So we can
1094050	1100610	compare NAN to zero in any way we want. It'll always be false. We can do NAN equals equals zero.
1100610	1106110	False. Is NAN greater than zero? False. Is NAN less than zero? False. We can even compare NAN
1106110	1113350	to itself, and it'll still be false. The other thing is that NAN has a tendency to propagate,
1113350	1120930	by that I mean any mathematical operation where NAN is an operand will evaluate to NAN. So NAN
1120930	1127450	plus one, minus one, times two, divided by two, these are all NAN. And no matter how complex you
1127450	1133950	make this mathematical operation, if one operand is NAN, the entire thing will become NAN.
1134890	1141670	So this brings up the term NAN poisoning, which is a condition where the unique properties of NAN
1141670	1147570	cause some sort of intended effect. So let's look at the following code as a really simple
1147570	1153990	example of this. So in this case, the programmer is trying to ensure that the floating point number
1153990	1164650	NAN is between zero and 100. The problem here is that if NUM is NAN, both of these conditions
1164650	1173330	will evaluate to false because any affirmative comparison with NAN will evaluate to false.
1173330	1179570	So NAN will pass these validations and we will end up acting on NAN in some way,
1179570	1185930	assuming that it is a legitimate regular floating point number within the range zero and 100.
1187650	1192990	So NAN poisoning attacks are pretty rare because it's typically difficult to actually introduce
1192990	1199210	NAN in the first place. You don't usually have an opportunity to divide by zero or do square root
1199210	1203970	of a negative square root of a negative number or anything like that. These aren't common bugs,
1204510	1211610	but when we're doing remote procedure calls, we can use any argument of the correct type.
1211610	1216890	So if the RPC calls for a floating point number, we can give it any floating point number,
1216890	1224970	including NAN or infinite. So going back to our movement RPC, there's only one argument that is a
1224970	1230550	floating point number, and that's the timestamp. So what happens if our timestamp is NAN? Well,
1230550	1236270	to figure this out, we have to look at this mouthful of a function,
1236270	1241430	uCharacterMovementComponent, isClientTimeStamp valid?
1241430	1244350	So this is a lot to look at, so we're going to go through it together. But let's assume that
1244350	1251530	our timestamp is NAN, because that is the value we've provided to our RPC. So we get to this first
1251530	1256190	check, which is intended to ensure that the timestamp is greater than zero. And because
1256190	1261430	this is an affirmative comparison, this will evaluate to false, and we just skip right past
1261430	1268090	to the next line. Then we're going to calculate our delta timestamp, and this is done by subtracting
1268090	1274930	our provided timestamp with the last valid timestamp that was received from our connection.
1274970	1280690	But in this case, it does not matter what that last valid timestamp was, because our
1280690	1288290	provided timestamp is NAN, and NAN minus anything will always be NAN. So delta timestamp evaluates
1288290	1294430	to NAN. Then we get to these next two checks. In the first case, again, we're going to pass
1294430	1301190	right by because timestamp is NAN, and this is an affirmative comparison. And for the next check,
1301190	1306450	delta timestamp is also NAN. This is another affirmative comparison. We pass right by,
1306450	1313210	and our timestamp is considered to be quote-unquote valid. So this is all written in
1313210	1319310	such a way that just by pure luck, NAN will just pass right through and be considered valid. And so
1319310	1326230	the next thing we do is we generate our delta time using NAN. So our delta time is calculated,
1326230	1333930	again, by subtracting our last valid timestamp from our provided timestamp.
1333930	1341170	And because our provided timestamp is NAN, delta time will evaluate to NAN regardless of what our
1341170	1348490	last valid timestamp was. Now the server will use our delta time to attempt to apply our movement,
1348490	1353630	but this is where we run into our first issue. There's one last sanity check here to ensure that
1353630	1359930	the delta time is greater than zero, and we will never pass this check when delta time is NAN
1359930	1365330	because it's another affirmative comparison. So our movement is not applied, and even though
1365330	1371770	our timestamp was considered valid, we don't go anywhere. We don't move. But we're not quite
1371770	1377390	done yet because we've caused another value to be poisoned. We've caused server data current client
1377390	1384130	timestamp to become NAN, and this is the saved version of the last valid timestamp. Because our
1384130	1390630	timestamp was considered valid, even though we didn't apply our movement, NAN was shifted into
1390630	1396490	that current client timestamp variable. Now we need to look at that is client timestamp valid
1396490	1401750	function again. So we're looking at the same function again, but this time we're going to
1401750	1407950	assume that our given timestamp is not NAN. It's not any special number. It's just a regular floating
1407950	1414070	point number greater than zero. So we bypass this first check. We get to the delta timestamp
1414070	1420170	calculation. And again, we're going to do our given timestamp minus our last valid timestamp,
1420170	1427750	but this time our last valid timestamp is NAN. So delta time is still going to calculate as NAN.
1427750	1433450	Then we get to these two comparisons, which just as before, we're going to bypass because
1433450	1440310	server data current client timestamp and delta time are both NAN. So we pass these checks. And
1440310	1447430	again, our timestamp is considered valid. So on this second RPC call, any timestamp greater than
1447430	1452330	zero will pass the validity check. We could say it's 30 years in the future. It doesn't matter
1452330	1459350	as long as it is greater than zero. And our first RPC call use NAN as a timestamp. Our second call
1459350	1465350	will always pass the validation check. Unfortunately, our delta time is still going to
1465350	1471830	calculate as NAN because our old client timestamp was NAN. So still nothing happens. We haven't
1471830	1478690	moved an inch, but fortunately we've poisoned one more value now. So while all this is going on,
1478690	1484650	the server is trying to determine if our time has drifted from server time. Essentially the
1484650	1490290	server wants to make sure that we're not doing anything tricky or that we're not,
1490290	1496750	our clock isn't drifting. And it does this by independently calculating its own delta time
1496750	1503590	and calculating an error rate, a difference between our delta time and the server's delta time.
1503590	1510670	But because our delta time is NAN, our client error is also going to be NAN. And then this
1510670	1518010	client error is used to build up our cumulative value new time discrepancy. And this value is
1518010	1524290	used to detect when we have drifted too far from server time. This is used to detect speed hacking
1524290	1531970	attempts. It's also used to detect when a client might just be lagging too much. But because our
1531970	1539270	client error is NAN, when our client error is added, our new time discrepancy value also becomes
1539270	1547270	NAN. And because new time discrepancy is what's used to determine a difference between client
1547270	1553650	time and server time, once we've poisoned this value, we can essentially disable the server's
1553650	1559650	ability to detect the time discrepancy for our connection. And so when we actually get to the
1559650	1566910	point when the server would attempt to detect a time discrepancy between our time and server time,
1566910	1573790	this check will never pass because new time discrepancy is NAN. And more importantly, no
1573790	1579550	mathematical operation will ever cause new time discrepancy to become a regular floating point
1579550	1586710	number again. It is stuck as NAN. So at this point, because we've neutered the server's ability
1586710	1593130	to detect a time discrepancy, we can now pull off like an old school speed hack where we just
1593130	1599750	speed up time in order to move faster than we should. And what this allows us to do is it allows
1599750	1606570	us to move significantly faster than our built-in limitations would ever allow. So I've oversimplified
1606570	1612190	this process a bit. I know it's still pretty complicated, but what I've come up with as not
1612190	1618710	the most efficient but the most straightforward way of exploiting this is to just send RPCs in
1618710	1625450	groups of three, where the first RPC, the timestamp is NAN. The second, the timestamp is just slightly
1625450	1632190	greater than zero. And the third, the timestamp is some value well in the future. And every time you
1632190	1638130	send this grouping, it will move you forward some amount. And because the server cannot detect the
1638130	1643130	discrepancy anymore, you can do this as often as you want. And the only real limitation is how
1643130	1650570	quickly you can send those RPCs and have them be processed. So saying all that, let's actually look
1650570	1655350	at this in action. And I think you can probably imagine what this is going to look like, but I
1655350	1664290	worked hard on it, so humor me. This is our first demo. This is built on a stock Unity, or I'm
1664290	1670670	stock Unreal Engine 4 game template. All I've done with it is I've enabled all the speed
1670670	1675510	hacking protections, I've opened up the game world just a bit so we can run around some more,
1675510	1682170	and I've modified the client to actually pull off our attack. And this is filmed from the server
1682170	1687530	perspective, just so that you can see that this is actually happening server-side. It doesn't just
1687530	1693350	look like we're moving fast client-side. So in the background, we've got our hacker. As you can see,
1693350	1698510	he's vibrating. He's very excited to be here. And what we're going to do is we're just going to get
1698510	1703490	out of the way so we can get a good view of him running. And in just a second, we are going to
1703490	1712070	see him blast off. And he's gone. Okay, so that is a little bit faster movement than intended,
1712070	1718170	even with the speed hacking protections on. I really like this bug. I love floating point bugs,
1718170	1722790	and the great thing about this one is that it actually does something other than just being
1723050	1728570	a denial of service. I also think that this type of attack can apply in other ways. I've looked at
1728570	1735370	other games that use floating point in its RPCs, and you can usually get some sort of unintended
1736130	1741290	behavior by using these special floating point numbers. It's not always useful. It doesn't always
1741290	1747110	do anything for us for an attacker, but it usually does something. I should also say that this does
1747110	1756330	still apply to Unity, but with UNet, it doesn't limit your movement in the first place, so you
1756330	1763250	don't have to go through this complicated process to speed hack. So with that out of the way,
1763250	1768810	let's talk about our final bug. We're going to go back to UNet, and this is a session hijacking bug.
1769530	1776270	So UNet uses a protocol level process to authenticate incoming packets, because UNet
1776270	1784470	is implemented over UDP, so you don't get the benefits of TCP where a stream is already
1784470	1790310	authenticated, and you can assume that a packet is part of a stream that you've already seen before.
1790310	1795070	So packets are not validated by their source IP address, or their source port, or anything like
1795070	1801610	that. They're only validated by values within the packet itself. So knowing this, it is at least
1801610	1807210	theoretically possible that someone could hijack another player's session totally remotely, totally
1807210	1811750	over the internet. We don't need a man in the middle, we don't need to be over LAN, or anything
1811750	1819790	like that. And that's the plan here. So when UNet validates an incoming packet, there are three
1819790	1825510	important values it looks at. The first is the host ID, then the session ID, and the packet ID.
1825510	1829570	And these names don't mean anything on their own, so let's look at each one in detail.
1830670	1836690	So the first of these values is the host ID, and this is a 16-bit integer that's used to associate
1837150	1843570	a packet with a given client. Host IDs are assigned sequentially, starting at 1. So the first client
1843570	1850610	to connect gets host ID 1, the second client gets host ID 2, etc. Now host IDs aren't really intended
1850610	1856030	to be a secret, so in a way we can sort of just ignore them. It's also really easy to enumerate
1856030	1861570	the host ID of another player. If we're the second player in a game and we get host ID 2, the other
1861570	1868850	player is probably host ID 1. If we're in a battle royale and we're host ID 47, the other host IDs
1868850	1875730	are probably 1 through 46 and 48 through 100. Things get a little more complicated when we talk
1875730	1881970	about the next value, which is the session ID. The session ID is the primary authenticating secret
1881970	1887830	of a connection, and the session ID is randomly generated by the client when they connect.
1887830	1892650	And every packet received for that client has to have the correct session ID, or the packet
1892650	1898250	will be discarded. There are a couple problems with the session ID though. The first is that
1898250	1904610	the session ID is also a 16-bit integer. It also can't be zero. This means that there's only
1904610	1913150	65,535 possible session IDs. There's also no penalty for incorrectly guessing a session ID,
1913150	1918030	other than the fact that our packet will just be discarded by the server. So we can easily brute
1918030	1923930	force the whole range of session IDs, even over the open internet. It doesn't matter. It doesn't
1923930	1930070	take long at all. Now we don't really need to do this part, but there is one more thing we can do
1930070	1936310	to narrow down that search even more. So session IDs are generated with a function named
1936310	1942670	unet getRandNotZero. And what this function does is exactly what the name says. It gets a random
1942670	1948210	number and ensures that that random number is not zero. And this is used for session IDs because
1948210	1956810	session IDs cannot be zero. But so the way this function actually works is it takes the end result,
1956810	1962170	that random number, and it ORs it with one. It essentially ensures that the least significant bit
1962170	1968470	of that output will always be one. This has the effect of ensuring that a legitimate unet client
1968470	1974990	will only ever generate an odd-numbered session ID. Technically, a session ID can be any 16-bit
1974990	1980730	value other than zero, but a legitimate client is programmed to never actually generate one unless
1980730	1988590	it's odd. So this reduces the possible session IDs down to 32,768, so 50%.
1990010	1995710	So if we know the host ID and we can guess the session ID, that's all we need for our spoof
1995710	2001310	packet to be accepted within the context of another player's session. But there is one more hiccup,
2001310	2006370	that's the packet ID. The packet ID is an integer that's incremented with each packet
2006370	2011530	sent by the client, basically a sequence number. And again, it's 16 bits long.
2012450	2018150	So what the packet ID is for is it's used to detect duplicate or out-of-order packets because,
2018150	2024670	again, UDP isn't doing this for us. It's also used to determine the rate of packet loss.
2024670	2030570	So if the last packet ID received by the server is one and the next packet ID it gets is 1000,
2030570	2038830	the server is going to assume that it's lost 998 packets in the meantime.
2040970	2047910	So if we can determine the host ID and guess the session ID, what can we do with the packet ID?
2047910	2052330	I guess a better question might be, what happens if we just send a random packet ID?
2052330	2059550	So let's read Unity's documentation on exactly this. So according to the documentation,
2059550	2066050	there are a few conditions. If the new packet ID is greater than the last packet ID plus 512,
2066050	2070310	we're going to disconnect the session because we've lost too many packets.
2070390	2077230	If the packet ID is more than 512 behind the current packet ID, we're going to discard it
2077230	2083930	because it's too old. We don't want it anymore. If the packet ID is in a list of packets we've
2083930	2089710	seen recently, it's a duplicate, let's discard it. Otherwise, if none of these conditions are met,
2089710	2095090	we're going to accept and process that packet. So there are a couple interesting things about
2095090	2101810	this. The first is that if our guest packet ID is greater than our last packet ID plus 512,
2101810	2106410	the connection will be disconnected. I want to emphasize that this is not our connection we're
2106410	2111030	talking about. This is the other player's connection. So this is useful because it
2111030	2117630	means we can pretty trivially kick other players off the server. However, it would be a lot more
2117630	2124590	interesting if we could bypass this check and inject a packet that would be actually executed
2124590	2130610	within another player's session. But reading the documentation, it seems like the odds of this
2130610	2137710	happening are pretty low. Guest packet ID must be last packet ID plus or minus 512, which doing
2137710	2144550	the math gives us less than a 7% chance of success. That's pretty bad. But when we look at the actual
2144550	2150990	implementation, it tells a slightly different story. So packet ID validation is done by the
2150990	2157650	function unet replay protector is packet replayed. In practice, this function actually does not
2157650	2163730	discard packets that are more than 512 packets old. Like the documentation said, I spent a lot
2163730	2168350	of time thinking that I was just misunderstanding or that it was more complicated than I thought.
2168350	2174750	But no, the logic for discarding old packets just isn't there. Instead, old packets are accepted as
2174750	2181270	if they weren't more than 512 packets old. So unfortunately, it's a little more complicated
2181270	2187870	than it sounds. We can't just use packet ID zero every time, even though that would be the lowest
2187870	2194150	packet ID. And the reason for that is that the server has to account for cases where the packet
2194150	2200510	ID overflows, goes from FFFF to zero. And this happens pretty often over the course of a game.
2200510	2206410	So instead of just directly comparing the numeric value, the server has to keep
2206410	2213850	like a rolling window of packet IDs to determine if a packet is old or new. And so doing the math
2213850	2221250	here, we have what's very close to a 50-50 shot that a packet ID will be accepted. Most of the
2221250	2226010	rest of the time, our packet is going to cause the other player to get kicked out of the game,
2226010	2231470	which is still pretty useful. And I say most of the rest of the time, because occasionally
2231930	2236910	we'll guess a packet ID that was actually seen recently, and it'll be seen as a duplicate and
2236910	2242930	just be discarded. Okay, so let's look at our second demo. And for this, I had to use an actual
2242930	2248790	game, not just something I came up with myself. And this game is called Streets of Rogue. It's
2248790	2255750	very cool. You should buy it. And I do want to emphasize that this is not a bug in the game
2255750	2260630	itself. There's nothing wrong with how this game is programmed. This is a bug in U-Net. But even
2260630	2267490	still, what we're going to do with this bug is we're going to inject packets to cause
2267490	2273830	two other players to do actions that we tell them to. So this little guy in the red shirt is me,
2273830	2279450	and these two identical looking people are our target players. And what we're going to do is
2279450	2283250	we're going to inject packets. We're going to see them do a few things. First, they're going to say
2283250	2289210	hi to everyone. Then we're going to kill them both. Then I started to feel a little bit bad,
2289210	2292310	so we're going to resurrect them both. And then we're going to kill them again.
2294030	2300490	Okay, so this is a pretty simple demo. Kind of stupid, but it demonstrates a few things.
2300490	2306350	The first is that we're able to inject packets that are actually accepted within the context
2306350	2311270	of another player's session. The second is that we're able to do it consistently enough that
2311270	2315710	we're able to do it for two players at the exact same time, even though those two players have
2315710	2321330	different host IDs, different session IDs, different packet IDs, everything. And finally,
2321330	2326210	we're able to do it consistently enough that we're able to inject multiple packets into each
2326210	2332070	player's session. We were able to inject a packet to say something, pause, inject another packet to
2332610	2338050	cause them to explode, pause, and then do this four times, injecting four different packets.
2338050	2346430	So this bug is pretty reliable. And as I said, even if you lose the coin toss, all that happens
2346430	2351130	is you kick another player out of the game, which has its utilities on its own.
2351830	2357450	So let's talk about remediations for this bug. This is considered to be an architectural weakness
2357450	2364590	with U-Net. The actual fixes that would be required to prevent this entirely are not going
2364590	2371490	to happen. The only mitigation, aside from moving away from U-Net, is to actually encrypt U-Net.
2372070	2377790	Unity does provide a reference implementation that does a decent job of this, but it's not
2377790	2384350	complete. It doesn't do key exchange for you. So to an extent, you are still on your own with this.
2384410	2389970	And I've looked a lot, but I've not found a single game implementing encryption over U-Net.
2389990	2396910	And I should emphasize that this is a mitigation. It is not a fix. So if your encryption is not
2397570	2405410	complete, if an attacker can bypass this encryption, these bugs still exist exactly
2405410	2412150	the same as they exist now. Okay, so that was my final bug. So let's talk about some future work.
2412150	2418190	I don't think I've found all the bugs, even in the components that I have looked at,
2418190	2422730	but there are a few components that I haven't looked at at all that I think are worth looking
2422730	2427330	at. So both of these protocols have other transport modes. I think the most interesting
2427330	2431210	of these are WebSockets, because that's what browser games are going to be using.
2432190	2437930	There's also third-party networking plugins for some of these engines. Photon and Mirror are two
2437930	2443950	relatively common ones for Unity. And there's just other engines, GameMaker Studio, Godot,
2443950	2452170	stuff like that. Finally, I want to thank a few people, both Epic Games and Unity Technologies.
2452170	2458170	Their security teams were absolutely fantastic, super communicative, kept me up to date the entire
2458170	2465390	time, put up with a lot of me, which can be very annoying. I have nothing but good things to say
2465390	2470410	about both of them. I also want to thank the artist who made the background art for my
2470410	2477610	presentation, Grigorin. He's awesome. And finally, all the scripts, everything I've put together
2477610	2484130	for this work, I've got up on GitHub. I've got POCs for most of these issues. I've got
2484130	2492690	libraries for interfacing with UNet and Unreal's protocol. And I hope that if I can ask anything
2492690	2497050	of you, it's that you go, you use something that you've learned here to go get banned from
2497050	2500630	some video game. All right. Thank you, everyone.
