demoinfocs-golang: Parser errors on encrypted net-message when decryption key is provided

Describe the bug

Hello!

Some Valve demos can lead to an error when parsing them with the new NetMessageDecryptionKey property added in the last version. The error occurs in the CSVCMsg_EncryptedData handler because it’s trying to read out of the bit reader boundaries.

To Reproduce

This zip archive contains 2 affected demos.

Code:

Parsing one of the attached demos using the snippet from the last release should trigger a panic.

Expected behavior

I think errors occurring while reading an encrypted data message should not prevent a demo parsing. It’s supposed to be private and may change in the future.

Library version 2.13.0

Additional context

I’m currently using my own implementation to handle encrypted messages with AdditionalNetMessageCreators but since this project now support it, I would like to use it and remove unnecessary code 😄

I looked into a possible solution but the gobitread package doesn’t expose a public function to know how many reading bytes are available which prevent me to ignore a corrupted message easily.

What I tried:

+++ b/pkg/demoinfocs/net_messages.go
@@ -98,6 +98,10 @@ func (p *parser) handleEncryptedData(msg *msg.CSVCMsg_EncryptedData) {
 	br := bit.NewSmallBitReader(r)
 
 	paddingBytes := br.ReadSingleByte()
+	messageSize := len(b)
+	if 1+int(paddingBytes) > messageSize {
+		return
+	}
 	br.Skip(int(paddingBytes) << 3)
 
 	bBytesWritten := br.ReadBytes(4)
@@ -106,8 +110,16 @@ func (p *parser) handleEncryptedData(msg *msg.CSVCMsg_EncryptedData) {
 	unassert.Same(len(b), 5+int(paddingBytes)+nBytesWritten)
 
 	cmd := br.ReadVarInt32()
+
+	if messageSize-nBytesWritten-int(paddingBytes)-5 != 0 {
+		return
+	}
+
 	size := br.ReadVarInt32()
 
+	if nBytesWritten-2 != int(size) {
+		return
+	}
 	m := p.netMessageForCmd(int(cmd))
 
 	if m == nil {

But it’s not enough for the demo match730_003449478367177343081_1946274414_112.dem, I think it would easier to know how many bytes are available before actually reading it to prevent an error or maybe you have an other idea?

If that can help, here is the code of my own handler, I’m checking if reading bytes is safe before actually doing it at each step: Note: reader is a GO bytes.Reader small wrapper, let me know if you need more details.

parser.RegisterNetMessageHandler(func(m *msg.CSVCMsg_EncryptedData) {
	isPublicKey := m.KeyType == encryptedKeyTypePublic
	if !isPublicKey || analyzer.iceKey == nil {
		return
	}

	messageSize := len(m.Encrypted)
	isMessageMalFormed := messageSize%analyzer.iceKey.BlockSize() != 0
	if isMessageMalFormed {
		return
	}

	plaintext := make([]byte, messageSize)
	analyzer.iceKey.DecryptFullArray(m.Encrypted, plaintext)

	reader := bytesreader.NewBytesReader(plaintext)
	paddingBytes := reader.ReadUInt8()
	if 1+paddingBytes > uint8(messageSize) {
		return
	}
	reader.Skip(int64(paddingBytes))
	if reader.Remaining() <= 4 {
		return
	}
	bytesWritten := reader.ReadInt32BE()
	if reader.Remaining() != int(bytesWritten) {
		return
	}

	cmd := reader.ReadVarInt32()
	if cmd == uint32(msg.SVC_Messages_svc_UserMessage) {
		size := reader.ReadVarInt32()
		if reader.Remaining() != int(size) {
			return
		}
		currentOffset := reader.CurrentOffset()
		userMessage := new(msg.CSVCMsg_UserMessage)
		proto.Unmarshal(plaintext[currentOffset:], userMessage)
		switch userMessage.MsgType {
		case int32(msg.ECstrike15UserMessages_CS_UM_SayText2):
                        //  Chat message here
		}
	}
})

Thank you!

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 24 (14 by maintainers)

Commits related to this issue

Most upvoted comments

Nice catch with the 00 suffix on the hex versions of the key. Off the back of that I’ve just tried all key combinations of F5B4050C8CAF5800 + n where n is in the range [0…256), but the parser still can’t decrypt the encrypted messages.

Edit: just tested 84167DFF22E31800 + n on the other demo, but unfortunately also didn’t work.

so, I’ve investigated some more, and what jumps out at me is that the two broken demos @akiver linked have the following keys when translated to HEX (which is what we use for ICE)

F5B4050C8CAF5800 - 9517933397249562624 as uint64
84167DFF22E31800 - 17704781586558310400 as uint64

note both of the hex versions ending with 00

for reference, one demo that works had the key 07CA349329F7C253, not ending in 00

Playing these matches in the official game client shows that we’re calculating the same decryption key. If you place both the .dem and the .dem.info in the csgo/replays folder then play them from the main menu, once the demo starts you can see that cl_decryptdata_key_pub is the same as the key extracted by extractPublicEncryptionKey.

Note that the game doesn’t crash like our parsers do when it encounters bad encrypted data. I’m pretty sure the engine just skips over these messages, as when you play the demo back you can’t see any chat messages.

Also I tested my hypothesis that maybe the key had a bit flipped, but I tried all 64 combinations of the key but none of them decrypted correctly. So I’m pretty sure the encryption key is plainly wrong.

Is it possible that these demos are simply corrupt? A very insignificant change to an encrypted data blob (even just 1 bit) will avalanche into a big change in the decrypted data. Unfortunately demos have no checksums - so an error in any of these could cause corruption:

  • Cosmic ray hitting the RAM where the demo is buffered before being written to disk
  • Network corruption when uploading/downloading from the game server -> Valve CDN/Valve CDN -> your machine
  • Disk corruption on the game server/Valve CDN/your machine