Break things, write reports

How to make wallhacks for Among Us

Alternatively: ghetto protocol analysis and how to hack your way to cheat in Among Us

Tags — | Among Us | hacking | amongus | Categories: — lol |
Posted at — Oct 18, 2020

“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:

It’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:

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.

Graphing coordinates of Player

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:

Graphing coordinates of Player, post-game

Overlayed, this looks like the following:

Graphing coordinates of Player, with overlay!

If one was so inclined, a dodgy player could decide to draw a graph and show where the players are at all times…