Alternatively: ghetto protocol analysis and how to hack your way to cheat in Among Us
“Among Us is an online multiplayer social deduction game developed and published by American game studio InnerSloth and released on June 15, 2018. The game takes place in a space-themed setting in which players each take on one of two roles, most being Crewmates, and a predetermined number being Impostors.”
It’s a pretty nifty game, where you need to figure out who the bad guys are, while completing mundane tasks. If you’re the imposter, you need to kill everyone else before they complete the tasks. Alternatively, if you’re super bored, and have a Friday afternoon, you can also use it as practice for protocol analysis. Ultimately, to cheat in this game you’d want at least one of the following:
After some quick time with procdump, observing the ‘Among Us.exe’ process, it appears that it has the following characteristics for communications:
Not uncommon for games to pick UDP over TCP; as you get better latency with UDP, and don’t need to worry about window sizes. You end up needing to handle packets that may be received out of order, so there may be a counter or other field to indicate some kind of ordering. After some quick fiddling with wireshark, we had some packet captures of an empty lobby. Curiously, without anything happening in the game, messages were being sent and received. These are likely some kind of keep-alive.
With a lot of fiddling with installing scapy on Windows 10, I ended up with a small script to start capturing and displaying messages, with the intent to identify commonalities between them. This gave the following outputs, noting SourcePort, DestinationPort, Size in bytes, and the raw payload:
PS D:\Among Us\script> python .\dump.py | Select -First 20
22023 -> 57201 [Size: 18] : 0a0017ff0000000000000000000000000000
22023 -> 57201 [Size: 18] : 0c000f00000000fc24683e6115ad0180b41a
57201 -> 22023 [Size: 4] : 0a000fff
57201 -> 22023 [Size: 3] : 0c0018
22023 -> 57201 [Size: 18] : 0a0018ff000000bf807f1f8700c1c28015bc
57201 -> 22023 [Size: 3] : 0c0019
22023 -> 57201 [Size: 18] : 0c0010000000008fd56c95a4d99840d52907
57201 -> 22023 [Size: 4] : 0a0010ff
22023 -> 57201 [Size: 18] : 0a0019ff0000000001000000005920ff980d
57201 -> 22023 [Size: 3] : 0c001a
22023 -> 57201 [Size: 18] : 0a001aff0000005000000000500204000850
22023 -> 57201 [Size: 18] : 0c00110000000090c855f73acc4dd1838144
57201 -> 22023 [Size: 4] : 0a0011ff
57201 -> 22023 [Size: 3] : 0c001b
22023 -> 57201 [Size: 18] : 0a001bff0000000f0abe2a5a59326aa2d905
57201 -> 22023 [Size: 3] : 0c001c
22023 -> 57201 [Size: 18] : 0c001200000000385d38433853384b385b38
57201 -> 22023 [Size: 4] : 0a0012ff
22023 -> 57201 [Size: 18] : 0a001cff000000b942fedac6c51b802d89b7
57201 -> 22023 [Size: 35] : 01001d1d00054a4a524d160002040d137465737420626c6f6220676f65732068657265
Curiously we can see a few interesting things here already:
57201 -> 22023
, we’re seeing 3 and 4 byte packets.0c0018
, 0c0019
, 0c001a
, 0c001b
.0x0c00
0xc0
, due to seeing value’s such as 0xc014c
, which appeared to increment0a000fff
, 0a0010ff
, 0a0011ff
, 0a0012ff
0x0a00
22023 -> 57201
, we’re seeing these 18 byte packets.0x0a00
, or an 0x0c00
, this could indicate some kind of type0x0c
and 0x0a
messages from the serverIt’s unlikely we care about these keep-alive like ticks, they don’t appear to have positional information or anything one would use for 1337 haxx.
Given we’re seeing obvious counters, and nothing that looks like a CRC/checksum, or any fields that seem to be changing seemingly randomly - it’s unlikely the protocol is encrypted, or at least these message types are not.
A strong indication that none of the protocol may not be encrypted is the 35 byte message observed, when observed in a hexdump output appears as the following:
00000000 01 00 1d 1d 00 05 4a 4a 52 4d 16 00 02 04 0d 13 |......JJRM......|
00000010 74 65 73 74 20 62 6c 6f 62 20 67 6f 65 73 20 68 |test blob goes h|
00000020 65 72 65 |ere|
Noting the ‘test blob goes here’ part, and that this was an in-game chat message sent by myself sitting in an empty lobby.
With a little bit of in-game knowledge of messages being capped to 100 characters, the single-byte field at 0xf
with value 0x13
is decimal 19, which is the length of the message.
The bytes at the start of the payload, from 0x00
to 0xE
don’t appear immediately obvious, other than perhaps another counter field or two, or maybe a game identifier?
Jumping on the more interesting bits, if one wanted to cheat they’d look for things like:
Looking at the game start, we usually see a single, fairly large packet come in of interest:
22023 -> 52484 [Size: 277] : 0100010f01100c0100160000ac6874da0756b25c0980056b6f7669640498030001051800008ba24475075631650480075245414348454c09970100010a190000ac69a3410756ddd40d8008686f74206769726c03b30200010a1b0000ac69e79a0756ae4808800a77696e79752073616d6101d30800010a180000ac69a1470756cf791080074b41525448494b01ef0100010a150000ac6872eb0756328d0e80056b72757469015800010a180000ac69da960756800a128007756420656b206806932b00010a190000ac68594007564f1c1580094d656761426c6173740733000108170000671d468a075660f80e80067a61696e616c01941600010a170000671d468a075657f80e8006506c6179657201e911000104
Again in a hexdump:
00000000 01 00 01 0f 01 10 0c 01 00 16 00 00 ac 68 74 da |............¬htÚ|
00000010 07 56 b2 5c 09 80 05 6b 6f 76 69 64 04 98 03 00 |.V²\...kovid....|
00000020 01 05 18 00 00 8b a2 44 75 07 56 31 65 04 80 07 |......¢Du.V1e...|
00000030 52 45 41 43 48 45 4c 09 97 01 00 01 0a 19 00 00 |REACHEL.........|
00000040 ac 69 a3 41 07 56 dd d4 0d 80 08 68 6f 74 20 67 |¬i£A.VÝÔ...hot g|
00000050 69 72 6c 03 b3 02 00 01 0a 1b 00 00 ac 69 e7 9a |irl.³.......¬iç.|
00000060 07 05 06 ae 48 08 80 0a 77 69 6e 79 75 20 73 61 |...®H...winyu sa|
00000070 6d 61 01 d3 08 00 01 0a 18 00 00 ac 69 a1 47 07 |ma.Ó.......¬i¡G.|
00000080 56 cf 79 10 80 07 4b 41 52 54 48 49 4b 01 ef 01 |VÏy...KARTHIK.ï.|
00000090 00 01 0a 15 00 00 ac 68 72 eb 07 56 32 8d 0e 80 |......¬hrë.V2...|
000000a0 05 6b 72 75 74 69 01 58 00 01 0a 18 00 00 ac 69 |.kruti.X......¬i|
000000b0 da 96 07 56 80 0a 12 80 07 75 64 20 65 6b 20 68 |Ú..V.....ud ek h|
000000c0 06 93 2b 00 01 0a 19 00 00 ac 68 59 40 07 56 4f |..+......¬hY@.VO|
000000d0 1c 15 08 00 09 4d 65 67 61 42 6c 61 73 74 07 33 |.....MegaBlast.3|
000000e0 00 01 08 17 00 00 67 1d 46 8a 07 56 60 f8 0e 80 |......g.F..V`ø..|
000000f0 06 7a 61 69 6e 61 6c 01 94 16 00 01 0a 17 00 00 |.zainal.........|
00000100 67 1d 46 8a 07 56 57 f8 0e 80 06 50 6c 61 79 65 |g.F..VWø...Playe|
00000110 72 01 e9 11 00 01 04 |r.é....|
We see the following little tidbits:
kovid
, REACHEL
, hot girl
)0x7
for REACHEL
)0x80
(except for MegaBlast?), and then is followed by the length of the character namePlayer
and the trailing bytes at 0x111
)zainal
and Player
distance, minus 0x6
bytes)0x00
, 0x00
has some kind of significance, as we’re seeing this near the presumed start of a record, and we’re observing 10 of these records (which lines up with our player count!)With this in mind, the player named Player
record would be as follows:
00000000 00 00 67 1d 46 8a 07 56 57 f8 0e 80 06 50 6c 61 |..g.F..VWø...Pla|
00000010 79 65 72 01 e9 11 00 01 04 |yer.é....|
REACHEL
as follows:
00000000 00 00 8b a2 44 75 07 56 31 65 04 80 07 52 45 41 |...¢Du.V1e...REA|
00000010 43 48 45 4c 09 97 01 00 01 0a |CHEL......|
And to confirm our suspicions, hot girl
:
00000000 00 00 ac 69 a3 41 07 56 dd d4 0d 80 08 68 6f 74 |..¬i£A.VÝÔ...hot|
00000010 20 67 69 72 6c 03 b3 02 00 01 0a | girl.³....|
Curiously with this, we have 1 byte between player records. These bytes are just before the 0x00
, 0x00
record. The value’s above appear to be sporadic but not random, and are as follows:
kovid: 0x16, 0x00, 0x00
REACHEL: 0x18, 0x00, 0x00
Hot Girl: 0x19, 0x00, 0x00
MegaBlast: 0x19, 0x00, 0x00
winyu sama.: 0x1b, 0x00, 0x00
KARTHIK: 0x18, 0x00, 0x00
kruti: 0x15, 0x00, 0x00
zainal: 0x17, 0x00, 0x00
ud ek h: 0x18, 0x00, 0x00
Player: 0x17, 0x00, 0x00
This preamble byte doesn’t stand out as an immediate candidate for faction, although it could be a bitmask for tasking?
With the preamble out of the way, the start of the payload is as follows:
00000000 01 00 01 0f 01 10 0c 01 00
I’m still unsure what it means sadly!
Moving off to another part of the game, I started observing what was happening in real-time with scapy and ignoring things like heartbeats. This came out of a small script made just so I could observe messages on my second monitor, shown below:
from scapy.all import sniff, Packet, UDP, PcapReader
import sys
def process(p: Packet):
"""
Hacky little format string printer.
"""
if len(p[UDP].payload) == 3 or len(p[UDP].payload) == 4 or len(p[UDP].payload) == 18:
pass
print(p[UDP].sport,"->", p[UDP].dport, "[Size: {}]".format(len(p[UDP].payload)),":", bytes(p[UDP].payload).hex())
sniff(filter="udp port {}".format(sys.argv[1]), prn=process)
Starting up a server and starting to move around, I saw the following communications of interest:
PS D:\Among Us\script> python .\livedump.py 22523
61235 -> 22523 [Size: 22] : 00120005f069a48c0b0001062a00b182cd82ff77ff7f
61235 -> 22523 [Size: 22] : 00120005f069a48c0b0001062b005c81cd82ff77ff7f
61235 -> 22523 [Size: 22] : 00120005f069a48c0b0001062c001881cd82ff7fff7f
61235 -> 22523 [Size: 22] : 00120005f069a48c0b0001062d001881cd82ff7fff7f
61235 -> 22523 [Size: 22] : 00120005f069a48c0b0001062e001881cd82ff7fff7f
61235 -> 22523 [Size: 22] : 00120005f069a48c0b0001062f0018815683ff7fff87
Making it a bit easier on myself, I started moving downward only:
52300 -> 22423 [Size: 22] : 001200052210cf8c0b0001065f014380c583ff7fff77
52300 -> 22423 [Size: 22] : 001200052210cf8c0b000106600143807082ff7fff77
52300 -> 22423 [Size: 22] : 001200052210cf8c0b000106610143801b81ff7fff77
52300 -> 22423 [Size: 22] : 001200052210cf8c0b00010662014380c57fff7fff77
52300 -> 22423 [Size: 22] : 001200052210cf8c0b000106630143803d7fff7fff7f
52300 -> 22423 [Size: 22] : 001200052210cf8c0b000106640143803d7fff7fff7f
52300 -> 22423 [Size: 22] : 001200052210cf8c0b000106650143803d7fff7fff7f
The following observations could be made:
Testing the hypothesis of the 15th, 17th and 12th bytes being significant, the following modfifications were made:
from scapy.all import sniff, Packet, UDP, PcapReader
import sys
def processMovement(p: Packet):
"""
Figures out player ID and movement
Outputs in a neat little CSV for graphing
"""
playerID = bytes(p[UDP].payload)[11:12].hex()
playerX = bytes(p[UDP].payload)[15]
playerY = int(bytes(p[UDP].payload)[17:18].hex(),16)
return playerID, playerX, playerY
def process(p: Packet):
"""
Hacky little format string printer.
"""
if len(p[UDP].payload) == 22:
id, x, y = processMovement(p)
print(p[UDP].sport,"->", p[UDP].dport, "{},{},{}".format(id,x,y))
sniff(filter="udp port {}".format(sys.argv[1]), prn=process)
With the following outputs:
52300 -> 22423 06,125,137
52300 -> 22423 06,124,137
52300 -> 22423 06,123,136
52300 -> 22423 06,122,135
52300 -> 22423 06,121,134
52300 -> 22423 06,120,133
52300 -> 22423 06,120,132
52300 -> 22423 06,120,131
52300 -> 22423 06,120,129
52300 -> 22423 06,121,128
52300 -> 22423 06,122,127
52300 -> 22423 06,123,127
52300 -> 22423 06,124,127
This appeared to be on the right track for the following reasons:
Other fields were observed but unclear what they meant:
To ensure the data doesn’t seem broken, I moved the character around the lobby walls and graphed the data.
Fairly confident this maps out with the lobby walls.
Doing this with a real game, isolating to just a single player ID we get the following:
Overlayed, this looks like the following:
If one was so inclined, a dodgy player could decide to draw a graph and show where the players are at all times…