meshtastic / firmware

Meshtastic device firmware
https://meshtastic.org
GNU General Public License v3.0
2.97k stars 706 forks source link

[Bug]: missing integrity checks let attacker forge arbitrary message content using a known plaintext attack and replaying messages even when PSK is not known #4030

Open Jorropo opened 3 weeks ago

Jorropo commented 3 weeks ago

Category

Other

Hardware

Not Applicable

Firmware Version

all ¿ (fundamental protocol issue)

Description

This was discussed with @caveman99 in #contributor-lounge he gave me the go ahead to post it here.

Here is pseudo code targeting the admin module, this would let an attacker observe an admin packet, modify it and change more or less any settings at will on all the nodes using this admin PSK:

def encryptPacket(nodeid, packetid, key, message):
 bitstream = generateAesCtrBitstream(uint64bytes(packetid) + uint32Bytes(nodeid) + uint32bytes(0), key, len(message))
 return bitstream ^ message

decryptPacket = encryptPacket

setOwnerPacket = createSetOwnerPacket(to, newName) # this assume this has predictable output, the attack is harder if this does not, but not impossible, the main drawback is that it would require many round trip with modified packets to guess the order of fields
packetId = generatePacketId()
cypherText = encryptPacket(sourceNodeId, packetId, key, setOwner)
sendLora(sourceNodeId, packetId, cypherText)

# let's assume I am a an attacker, all of the following code run on my computer, I just observed some packet I couldn't decrypt it, and I then see a new nodeinfo with a changed name
# all of this to say, let's assume I was able to learn `to` and `newName` from contextual clues
# I also know `sourceNodeId`, `packetId` and `cypherText` since theses are sent in clear text
setOwnerPacket = createSetOwnerPacket(to, newName) # recreate the plaintext, there are many other ways to achieve this even when the whole content cannot be guessed by doing trial and error
bitstream = cypherText ^ setOwnerPacket # the whole magic is happening here
# I can now forge abitrary content that is len(bitstream) long
maliciousPayload = createSetChannelPSK(to, index=1, psk=PSKControlledByAttacker) # assume this is setting admin channel psk
cypherText = bitstream[:len(maliciousPayload)] ^ maliciousPayload # encrypt it back
sendLora(sourceNodeId, packetId, cypherText) # replay spoofed packet with modified content
# you might need to send other packets first to make the node forget it has seen this packet else it might disregard it as duplicated
# you could also jam the original packet so it never reach the target node

The fix I recommend is to use AEAD. I have prior experience with AES-GCM it is AES-CTR (what we are currently using) with a bit of extra math, this adds a 16 bytes hash after the message, it is accelerated on ESP32 and the extra math is not extremely expensive. The main drawbacks:

  1. The biggest admin packets don't have enough room for a 16 bytes auth. This is not a very hard engineering problem, protobuf isn't that efficient and I guess we could find a way to gain a few bytes.
  2. Backward compat / extra 16 bytes cost on all packets. This can be solved by making this optional when configuring your channel, most channels like AQ== would gain nothing but we could enforce this for admin. So users would be able to toggle if they want integrity protection
  3. nrf52 chips don't support AES-GCM acceleration, they do support AES-CCM tho, which also do AEAD, I am not familiar as familiar with AES-CCM but my limited opinion is that it should be good enough, it is based on AES-CBC instead of AES-CTR which GCM is based on. ESP32 chips also support AES-CCM acceleration. So AES-CCM should be a good alternative, drawback 1 and 2 still apply.

AES-CCM is often what is used in WPA3. AES-GCM is used in an overwhelming of TLS1.3 connections (altho Chacha20-Poly1305 is also often implemented as a fallback for CPUs who lack AES hardware acceleration), updated TLS1.2 clients also often prefer AES-GCM based suites. You are extremely likely to be using AES-GCM to view this very same issue.

This would not solve all the issue present in meshtastic's encryption, this only solve Integrity but not Perfect-Forward-Secrecy or Authentication.

Relevant log output

N/A

Is it dangerous to use the admin module ?

Probably not: The admin module is most useful on unattended nodes where a physical attack is significantly easier and faster to perform. This require to first capture a valid packet sent on the admin channel which is unlikely because how often do you change configuration settings ? The attacker then need to identify which packet were sent on the admin channel, this is easier said than done, packets do not publicly indicate which channel they are a part of, there is no easy way to differentiate a packet on the admin channel to any random encrypted packet. Then the attacker must correctly guess some bits in the admin message, in general each bit guessed correctly can be changed to attacker's value of choice. Admin messages a fair bit complex.

Examples of things that are "easy":

You use the admin module to change the role to ROUTER_CLIENT. An attacker can then change the role CLIENT_MUTE.

This is because a huge portion of the message is identical and the device role is broadcasted in nodeinfo making guessing easier. Changing the target of an admin message that clearly reflect changes in public info is also "easy". For example you have two nodes using the same admin channel, you update some configs on node A, an attacker can then send the same configuration to node B. The problem with this is that the attacker does not know which nodes share the same admin channel.

Tl;Dr: the vast majority of attacks are harder and it take significantly more time than gaining physical access to your node. Most impacts of such attack would be making the mesh unreliable, using the same hardware this can be done by setting HOP_LIMIT=7, override the duty cycle checks and configure the node to spam random messages every second completely hogging the spectrum.

Workarounds:

Jorropo commented 3 weeks ago

If implementing AEAD is not possible, a correct MAC (which include both message content, PSK and use a cryptographically secure hashing algorithm) is also acceptable, this historically created Side-Channel-Attacks in TLS1.2 which required TLS1.2 implementations to add mitigations and TLS1.3 removed all non AEAD support.

Even in the worst correct case MAC with SCA is way better than the current state of things.

Jorropo commented 3 weeks ago

Actually I over-complicated padding. Message length is not encrypted (it is stored in the explicit LoRa header), you can simply truncate the message to the length you want. You can't forge a message longer than the longest message you were able to guess.

xnyhps commented 3 weeks ago

this adds a 16 bytes hash after the message

16 bytes is ideal, but shorter authentication tags can be used for AES-GCM/AES-CCM. 12 bytes would still be fine, and depending on the situation, even 8 bytes can be used. The slow rate at which messages can be sent could justify using shorter tags.

xloem commented 3 weeks ago

If the fundamental protocol will change, given issues like this have been so extant, I would propose finding existing cryptographic solutions and systems to use as much as possible, so as to share work in maintaining them with others. There is AI-driven attack research now.