Zrtp.cpp trusts Initiator Hello
The ZRTP 'Initiator Hello' message is not hashed into the handshake and could be vulnerable to tampering by a MITM. The current ZRTPCPP code obtains state (such as the ZID) from the Hello message. As best I can tell this information is /not/ compared to the Commit message in the event that the remote peer becomes the Initiator (due to a Commit clash). Hence it may be possible to tamper with the ZID in a MITM setting.
I suggest replacing the ZID collected from the Hello message with the ZID in a received Commit.
Actually, I don't think so. ZRTP protects every packet with an HMAC, and this is true the the Hello and commit packets as well. Thus if a MitM modifies the contents of a packet then ZRTP detects this when it processes the next protocol packet it receives. This would then lead to an HMAC error and cause ZRTP to stop and to report an error.
Please refer to the sections 5.3ff and 9 how ZRTP computes this MAC. Thus the data in Hello is trustworthy it the MAC check succeeds after receiving a Commit. Similar for other packets.
Werner
Am 30.06.2013 16:15, schrieb matthewdgreen:
The ZRTP 'Initiator Hello' message is not hashed into the handshake and could be vulnerable to tampering by a MITM. The current ZRTPCPP code obtains state (such as the ZID) from the Hello message. As best I can tell this information is /not/ compared to the Commit message in the event that the remote peer becomes the Initiator (due to a Commit clash). Hence it may be possible to tamper with the ZID in a MITM setting.
I suggest replacing the ZID collected from the Hello message with the ZID in a received Commit.
Reply to this email directly or view it on GitHub: https://github.com/wernerd/ZRTPCPP/issues/7
Werner Dittmann [email protected] Tel +49 173 44 37 659 PGP key: 82EF5E8B
Unfortunately this is not the case for the Hello packet. The key for the Hello MAC is actually revealed in the Commit packet, thanks to the way the Hash chain works. An MITM attacker can hold the peer's Hello until after the Commit packet ships, learn the MAC and then forge the Hello at will.
I've read this spec a disturbing number of times but I'm always welcome to be proven wrong. In this particular case I don't think I am.
Matt
On Jun 30, 2013, at 10:33 AM, wernerd [email protected] wrote:
Actually, I don't think so. ZRTP protects every packet with an HMAC, and this is true the the Hello and commit packets as well. Thus if a MitM modifies the contents of a packet then ZRTP detects this when it processes the next protocol packet it receives. This would then lead to an HMAC error and cause ZRTP to stop and to report an error.
Please refer to the sections 5.3ff and 9 how ZRTP computes this MAC. Thus the data in Hello is trustworthy it the MAC check succeeds after receiving a Commit. Similar for other packets.
Werner
Am 30.06.2013 16:15, schrieb matthewdgreen:
The ZRTP 'Initiator Hello' message is not hashed into the handshake and could be vulnerable to tampering by a MITM. The current ZRTPCPP code obtains state (such as the ZID) from the Hello message. As best I can tell this information is /not/ compared to the Commit message in the event that the remote peer becomes the Initiator (due to a Commit clash). Hence it may be possible to tamper with the ZID in a MITM setting.
I suggest replacing the ZID collected from the Hello message with the ZID in a received Commit.
Reply to this email directly or view it on GitHub: https://github.com/wernerd/ZRTPCPP/issues/7
Werner Dittmann [email protected] Tel +49 173 44 37 659 PGP key: 82EF5E8B — Reply to this email directly or view it on GitHub.
You are not alone - it's somewhat complex and I discussed with Phil several hours to get all this :-) .
Some points:
- you are right with the Hash chain and the keys until it comes to H0 which is transmitted encrypted in the confirm packets. And at least at this point ZRTP detects some attack and bails out. An attacker cannot learn H0 and thus cannot perform modifications to previous packets.
- ZRTP requires that we use the ZID from the Hello packet at an early point to construct a DHPart2 packet (prepared for the hash commit, 4.4.1.1) and this requires that we need the ZID to check for possible retained shared secrets. Forged ZID usually lead to wrong shared secret ids and this raise a severe waring at the other party because this party may expect a valid retained shared secret. In case the attacker modifies both ZIDs the ZRTP stack would handle it as a "first" call, displaying/reporting according informations.
BTW, ZRTP can run without using ZIDs at all, without cache. In this case the user shall compare the SAS on every call. The cache adds convenience but not security. Note: this option is not implemened, or only "half". In my code its called "paranoid" mode and this can force the GUI to show the SAS check for every call.
Werner
Am 30.06.2013 17:59, schrieb matthewdgreen:
Unfortunately this is not the case for the Hello packet. The key for the Hello MAC is actually revealed in the Commit packet, thanks to the way the Hash chain works. An MITM attacker can hold the peer's Hello until after the Commit packet ships, learn the MAC and then forge the Hello at will.
I've read this spec a disturbing number of times but I'm always welcome to be proven wrong. In this particular case I don't think I am.
Matt
On Jun 30, 2013, at 10:33 AM, wernerd [email protected] wrote:
Actually, I don't think so. ZRTP protects every packet with an HMAC, and this is true the the Hello and commit packets as well. Thus if a MitM modifies the contents of a packet then ZRTP detects this when it processes the next protocol packet it receives. This would then lead to an HMAC error and cause ZRTP to stop and to report an error.
Please refer to the sections 5.3ff and 9 how ZRTP computes this MAC. Thus the data in Hello is trustworthy it the MAC check succeeds after receiving a Commit. Similar for other packets.
Werner
Am 30.06.2013 16:15, schrieb matthewdgreen:
The ZRTP 'Initiator Hello' message is not hashed into the handshake and could be vulnerable to tampering by a MITM. The current ZRTPCPP code obtains state (such as the ZID) from the Hello message. As best I can tell this information is /not/ compared to the Commit message in the event that the remote peer becomes the Initiator (due to a Commit clash). Hence it may be possible to tamper with the ZID in a MITM setting.
I suggest replacing the ZID collected from the Hello message with the ZID in a received Commit.
Reply to this email directly or view it on GitHub: https://github.com/wernerd/ZRTPCPP/issues/7
Werner Dittmann [email protected] Tel +49 173 44 37 659 PGP key: 82EF5E8B — Reply to this email directly or view it on GitHub.
Reply to this email directly or view it on GitHub: https://github.com/wernerd/ZRTPCPP/issues/7#issuecomment-20249415
Werner Dittmann [email protected] Tel +49 173 44 37 659 PGP key: 82EF5E8B
ZRTP requires that we use the ZID from the Hello packet at an early point to construct a DHPart2 packet (prepared for the hash commit, 4.4.1.1) and this requires that we need the ZID to check for possible retained shared secrets.
And that's ok when the peer is acting as the Responder. If you hit commit contention and receive a Commit packet from the peer -- now acting as Initiator -- you would have to throw away the DHPart2 packet you constructed anyway.
It seems that in this case the right thing to do is to reset the ZID to whatever you receive in the new Commit packet from the peer (now Initiator), discarding any previous ZID you received in its Hello packet. This should involve only a small change in the prepareDHPart1() function.
I looked through the rest of that function and it does seem that you properly reset all of the other values (ciphers, hashes etc.). Resetting the ZID seems like a good practical fix.
Forged ZID usually lead to wrong shared secret ids and this raise a severe waring at the other party because this party may expect a valid retained shared secret.
I agree that this doesn't seem exploitable -- except as a kind of minimal DoS where you break the authentication cycle. But at the same time: ZRTP is very complicated! There are a lot of edge cases -- involving PBXes, say -- where the ability to cause an apparently valid connection to the wrong ZID might lead to some subtle flaw down the road.
Another possibility is that an application might peek into the ZID and use it to identify callers.
I understand that Phil has a different PoV on this than I do, but my argument is simple: if you have unauthenticated data and authenticated data -- use the authenticated data! Why wouldn't you?
I had some discussion with Phil about this topic and we came up with the following handling:
If the ZID in the commit does not match the ZID from the Hello we generate an error and stop further ZRTP processing.
Reason: Even if a different ZID is not a real security issue this indicates either some sort of an attack or the other party's ZRTP implementation is wrong/faulty. In both cases the ZRTP stack should not trust any other data anymore.
I'll build in that check, perform the necessary tests and push back the changes. This would take a day or two.
Thanks for pointing this out and for the good discussion on that issue.
Werner
Am 30.06.2013 20:06, schrieb matthewdgreen:
ZRTP requires that we use the ZID from the Hello packet at an early point to construct a DHPart2 packet (prepared for the hash commit, 4.4.1.1) and this requires that we need the ZID to check for possible retained shared secrets.
And that's ok when the peer is acting as the Responder. If you hit commit contention and receive a Commit packet from the peer -- now acting as Initiator -- you would have to throw away the DHPart2 packet you constructed anyway.
It seems that in this case the right thing to do is to reset the ZID to whatever you receive in the new Commit packet from the peer (now Initiator), discarding any previous ZID you received in its Hello packet. This should involve only a small change in the prepareDHPart1() function.
I looked through the rest of that function and it does seem that you properly reset all of the other values (ciphers, hashes etc.). Resetting the ZID seems like a good practical fix.
Forged ZID usually lead to wrong shared secret ids and this raise a severe waring at the other party because this party may expect a valid retained shared secret.
I agree that this doesn't seem exploitable -- except as a kind of minimal DoS where you break the authentication cycle. But at the same time: ZRTP is very complicated! There are a lot of edge cases -- involving PBXes, say -- where the ability to cause an apparently valid connection to the wrong ZID might lead to some subtle flaw down the road.
Another possibility is that an application might peek into the ZID and use it to identify callers.
I understand that Phil has a different PoV on this than I do, but my argument is simple: if you have unauthenticated data and authenticated data -- use the authenticated data! Why wouldn't you?
Reply to this email directly or view it on GitHub: https://github.com/wernerd/ZRTPCPP/issues/7#issuecomment-20252363
Werner Dittmann [email protected] Tel +49 173 44 37 659 PGP key: 82EF5E8B
Werner Dittmann wrote:
you are right with the Hash chain and the keys until it comes to H0 which is transmitted encrypted in the confirm packets. And at least at this point ZRTP detects some attack and bails out. An attacker cannot learn H0 and thus cannot perform modifications to previous packets.
You can't forge any packet other than the initiator's Hello because they're all included in the total_hash. If you forge any other packet you'll get mismatched s0 shared secrets and both parties will reject the Confirm packet anyway. You'll never even get to H0.
You can freely forge some fields in the initiator's Hello because the message was mistakenly left out of the total_hash and not all fields are reiterated in the Commit packet. Neither does the hash chain protect it because you can force the next message from the state machine with a simple HelloACK. In early drafts of ZRTP the initiator's Hello was optional and didn't include information that would be problematic if unauthenticated. When that changed the total_hash should have been updated. The failure to do this was a mistake, and we'll fix it in a future draft of ZRTP. It requires bumping our version number, so it's not trivial to fix.
This mistake was a good find by Matthew. It is to the best of my knowledge the most significant flaw anyone has spotted in the protocol. It was overlooked by a very large number of earlier eyes.
Matthew Green wrote:
Unfortunately this is not the case for the Hello packet. The key for the Hello MAC is actually revealed in the Commit packet, thanks to the way the Hash chain works. An MITM attacker can hold the peer's Hello until after the Commit packet ships, learn the MAC and then forge the Hello at will.
You actually can't just hold onto the Hello and wait. You have to send a forged HelloACK to the initiator to move the state machine forward. Otherwise you'll just sit there waiting forever to receive the Commit. Perhaps you noticed this, but I haven't seen you address it elsewhere. The forged HelloACK is required to make this attack actually work.
Matthew Green wrote:
Hence it may be possible to tamper with the ZID in a MITM setting.
I believe this is an overstatement. ZIDi and ZIDr are included in the total_hash. If you somehow manage to convince one side to use an incorrect ZID then the peers will get mismatched s0 shared secrets and the negotiation will fail when the peers reject each other's confirm_mac.
Werner Dittmann wrote:
If the ZID in the commit does not match the ZID from the Hello we generate an error and stop further ZRTP processing.
This actually does not meaningfully differ from the current behavior. If we currently allow a forged ZID to enter our state, then the negotiation will fail when our s0 values turn out to not match between the peers and they reject each other's confirm_mac. With this change, we simply reject the negotiation a bit earlier when we receive the Commit.
All the same, I agree that this check is prudent. Any other inconsistency between the Hello and the Commit messages, such as choosing an algorithm that wasn't in the initiator's Hello, should also be grounds for stopping the negotiation.
This is a good point. The inclusion of ZIDi/r does provide a final check on the Hello message in ZRTPCPP. And in fact the 'fix' I proposed the first time could have made things worse, since I was suggesting to reset the ZIDi/r when a Confirm packet came in.
I don't see anything exploitable here, though if you do submit a new ZRTP draft please remember that protocol version negotiation isn't authenticated either!
Yes; a downgrade attack was my very first thought when this came up. However I now believe this is infeasible against a wary implementation. The responder Hello has to be passed as-is before we can get the Commit and H2 from the initiator, so the initiator will use the higher version. Because of the planned change to total_hash the peers will have mismatched s0 values and the negotiation will fail.
I admit to being a bit flummoxed by the code for this. As best I can tell the current ZRTPCPP code receives a Hello, then looks to see if it supports a protocol version equal to or less than the received version. Then it sets its own Hello to support this version. Maybe I've got this backwards. (See https://github.com/wernerd/ZRTPCPP/blob/master/zrtp/ZrtpStateClass.cpp) That seemed to admit a downgrade attack.
(Now to be clear, the current ZRTPCPP only supports one version -- though two are defined -- so this is not a vulnerability.)
At this point the parties have not taken on definite roles as Initiator or Responder, so I was assuming the first Hello packet could be tampered. Moreover I was thinking it might even be possible for an MITM to push one party into Responder status by fiddling with the MITM flags.
Matt
On Jul 5, 2013, at 2:19 PM, traviscross [email protected] wrote:
Yes; a downgrade attack was my very first thought when this came up. However I now believe this is infeasible against a wary implementation. The responder Hello has to be passed as-is before we can get the Commit and H2 from the initiator, so the initiator will use the higher version. Because of the planned change to total_hash the peers will have mismatched s0 values and the negotiation will fail.
Any way you do it you have to forge a Hello from one (and only one) party. To that party you must send the other party's actual Hello and a forged HelloACK so you can get the Commit and calculate the MAC for the forged Hello. At that point, this party will not negotiate further about the version and will use the higher version. You can force the responder to a lower version but you'll get mismatched s0 values. If there is a Commit clash and the initiator changes you'll have forged the wrong Hello -- the now-responder's Hello, which is in total_hash.
In other words, if you only downgrade one peer the negotiation will fail. You can't downgrade both peers without either forging a message you can't forge or forging a message before you can actually forge it correctly.
Am 05.07.2013 20:39, schrieb matthewdgreen:
I admit to being a bit flummoxed by the code for this. As best I can tell the current ZRTPCPP code receives a Hello, then looks to see if it supports a protocol version equal to or less than the received version. Then it sets its own Hello to support this version. Maybe I've got this backwards. (See https://github.com/wernerd/ZRTPCPP/blob/master/zrtp/ZrtpStateClass.cpp) That seemed to admit a downgrade attack.
If I haven’t messed it up the code implements the version negotiation as defined in RFC6189, section 4.1.1
Werner
(Now to be clear, the current ZRTPCPP only supports one version -- though two are defined -- so this is not a vulnerability.)
At this point the parties have not taken on definite roles as Initiator or Responder, so I was assuming the first Hello packet could be tampered. Moreover I was thinking it might even be possible for an MITM to push one party into Responder status by fiddling with the MITM flags.
Matt
On Jul 5, 2013, at 2:19 PM, traviscross [email protected] wrote:
Yes; a downgrade attack was my very first thought when this came up. However I now believe this is infeasible against a wary implementation. The responder Hello has to be passed as-is before we can get the Commit and H2 from the initiator, so the initiator will use the higher version. Because of the planned change to total_hash the peers will have mismatched s0 values and the negotiation will fail.
Reply to this email directly or view it on GitHub: https://github.com/wernerd/ZRTPCPP/issues/7#issuecomment-20533225
Werner Dittmann [email protected] Tel +49 173 44 37 659 PGP key: 82EF5E8B
Ok, I had to read Travis's note several times before I realized he was discussing a future version in which both Hello messages are checked in total_hash. Yes, that would seem to be ok.
That said I'd still want to look through the code. What happens if a peer receives multiple Hello packets? Is it possible it might negotiate a version based on the first Hello received (and store some state) then later accept a second Hello? I looked through the ZrtpStateClass.cpp and saw some comments about "If the protcol engine receives another Hello it repeats the HelloAck/Hello response until Timer T1 exceeds its maximum." but I'm honestly not sure what happens in this case
Am 06.07.2013 09:56, schrieb matthewdgreen:
Ok, I had to read Travis's note several times before I realized he was discussing a future version in which both Hello messages are checked in total_hash. Yes, that would seem to be ok.
That said I'd still want to look through the code. What happens if a peer receives multiple Hello packets? Is it possible it might negotiate a version based on the first Hello received (and store some state) then later accept a second Hello? I looked through the ZrtpStateClass.cpp and saw some comments about "If the protcol engine receives another Hello it repeats the HelloAck/Hello response until Timer T1 exceeds its maximum." but I'm honestly not sure what happens in this case
Once the state engine accepts a Hello packet an processes it steps to the next protocol state and it ignores other Hello packets.
The comment that you mention is in the ackSent state. In this state either a HelloAck of a Commit triggers a protocol state change. If we receive Hello packets only then the other party does not send HelloAck/Commit or these packets are lost during transmission. After some retries the state engine terminate and reports "ZRTP not supported". This behaviour is part of the ZRTP event states.
Reply to this email directly or view it on GitHub: https://github.com/wernerd/ZRTPCPP/issues/7#issuecomment-20550785
Werner Dittmann [email protected] Tel +49 173 44 37 659 PGP key: 82EF5E8B
Ok, I concede defeat on this one. Good job guys.
Since we might as well be thorough: what about the MITM and Signature-capable flags? I know they're optional, but any applications have concerns there?
In short, yes. A couple days ago I wrote the following in an email to Werner and others. As I recall, I discussed these same points with Phil many months ago when this first came to light.
The best attack I can think of is that you could prevent a responder from using the optional SAS signing feature since we write in 7.2:
To avoid unnecessary signature calculations, a signature SHOULD NOT be sent if the other ZRTP endpoint did not set the (S) flag in the Hello message.
You could also prevent a responder from rendering the SAS in a relayed SAS call (5.13):
If the receiver of the SASrelay message did not previously receive a Hello message with the MiTM (M) flag set, the Relayed SAS SHOULD NOT be rendered.
Those may be the only two actual attacks other than denial of service.
Commit contention in this scenario is very likely. If a PBX sets pre-shared mode, or both sides do and a PBX sets the M flag, then you'll know who will be the initiator.
Otherwise you have even odds of the wrong side becoming the initiator and your forgery being detected. But since you're intermediating packets, you can tell when this is going to happen because you can check each side's hvi. If you're going to lose, then I'd just start dropping packets to force the parties to make a new call so you could try again.
Neither of those attacks is particularly scary. The SAS signing attack may prevent the use of that feature. As best I know, however, no one actually uses it, so there may be no affected applications.
The M flag attack may prevent parties from comparing the SAS in a trusted-PBX scenario. The trusted-PBX option isn't very popular either. Werner's library didn't implement it until fairly recently, and many clients don't support it.