Last week I joined a group of colleagues from Core Security to participate in the Ekoparty’s 2019 official CTF, organized by null life. I picked up one of the crypto challenges: chiperchat. This write-up is to described the process followed to solve it.
The starting point was a PCAP capture and a hint stating that there was encrypted traffic inside. First off, I opened the capture in Wireshark and verified that there was a perfectly well formed TCP stream between 2 parties. The data carried out by these TCP packets contained no ASCII characters. I was not sure, by then, if every single byte was encrypted or if there was a mix of a binary protocol and encrypted bytes.
Under the hypothesis of a binary protocol, I went through several open source projects at GitHub with name cipherchat. Contrary to my expectations, there wasn’t a good match -most of the protocols were text-based-. The hypothesis of a binary protocol and the advantage of having access to the source code started to vanish.
I started looking at the data bytes themselves -extracted out of the PCAP capture for better analysis- and found an interesting pattern: many messages, sent by both parties during the whole conversation, started with byte 0x94.
Putting together the assumptions that every single byte was encrypted, that there was a pattern and knowing that it was a network stream; I came to the first set of hypothesis: 1) a stream cipher was being used (key XOR plaint-text = cipher-text probably), 2) there was a symmetric key known by both parties (no key exchange), 3) the same key was being used for every message, and 4) there was repeated plain-text. I was not sure if the symmetric key had a length long enough not to repeat itself for each message, or if it was a shorter key repeated over and over.
I moved to the end of the conversation and focused on the last message: 0xf9 0xac 0x8a 0x00. The last byte (0x00) looked like a string null terminator for the encrypted text because every message had it. Trying some wild guessing I asked myself: what would a party say to the other when finishing a conversation, that is 3 bytes long? BYE, or bye, or Bye perhaps. The next question was then: what must the first bytes of the key be in order to get a “BYE” plain-text with an XOR operation? With Python, the answer is: hex(ord(“B”) ^ 0xf9), hex(ord(“Y”) ^ 0xac) and hex(ord(“E”) ^ 0x8a). That gives a key that starts with 0xbb, 0xf5, 0xcf. Before moving forward, let me point something out: backtracking and exploring “bye” and “Bye” paths may be needed in the future if a reasonable plain-text is not obtained with “BYE”.
When decrypting the first 3 bytes of every message in the conversation, interesting things popped up. The message previous to the last one started with “/QU” and had 2 unknown characters. It was /QUIT probably -which reminded me of IRC-. The same thing with a sequence of “/PING” and “/PONG” commands. I considered these findings a confirmation of being in the right path with the previous assumptions, and could discover 2 more bytes of the key.
I repeated the previous strategy over and over. It worked well as long as I was able to guess plain-text, jumping from a shorter to a longer message. /ECHO commands were particularly helpful because they could be used to learn 6 more key bytes at a time by looking at the next message (which was identical but without /ECHO at the beginning). Going back and forth, all /ECHO messages were finally decrypted. The “/HELP” message was also useful to guess plain-text because of its length -was the longest- and structure: /COMMAND: description. Commands were known at that point.
I had a lower-upper case hell in every decrypted message so I had to use the backtracking clause pointed out earlier; the final message in the conversation was “Bye” instead of “BYE”.
A ZIP file with the original PCAP capture and a helper Python script I developed is available for download here.
Just for the record, the flag was EKO{pseudo_perfect_secrecy_X0R} 🙂