KGB Messenger Walkthrough

KGB Messenger Walkthrough

Introduction

KGB Messenger is an open source CTF practice challenge that aims to help people learn how to reverse engineer Android applications. You are working for the International Secret Intelligence Service as a reverse engineer. This morning your team lead assigned you to inspect an Android application found on the phone of a misbehaving agent. It’s rumored that the misbehaving agent, Sterling Archer, has been in contact with some KGB spies. Your job is to reverse engineer the application to verify the rumor.

The challenges should be solved sequentially. The flag format is FLAG{insert_flag_here}.

Challenges

  • Alerts (Medium): The app keeps giving us these pesky alerts when we start the app. We should investigate.
  • Login (Easy): This is a recon challenge. All characters in the password are lowercase.
  • Social Engineering (Hard): It looks like someone is bad at keeping secrets. They’re probably susceptible to social engineering… what should I say?

Tools & Techniques

Reverse Engineering: JADX (static analysis of decompiled APK)

Dynamic Analysis: Frida (runtime method hooking), ADB, Python (scripting for decryption)

Techniques used: decompiled Java source analysis, strings.xml/AndroidManifest.xml review, control flow analysis, runtime method hooking, XOR cipher reversal, bit-shift operation reversal, MD5 hash analysis (bypassed), and method-hooking bypass techniques.


AndroidManifest

Let’s start by taking a look at the AndroidManifest.xml — there are only 3 activities and no permissions or exports.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0"
    package="com.tlamb96.spetsnazmessenger">
    <uses-sdk
        android:minSdkVersion="17"
        android:targetSdkVersion="25"/>
    <application
        android:theme="@style/AppTheme"
        android:label="@string/app_name"
        android:icon="@mipmap/ic_kgb_launcher_icon"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:roundIcon="@mipmap/ic_kgb_launcher_icon">
        <activity android:name="com.tlamb96.kgbmessenger.MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.tlamb96.kgbmessenger.MessengerActivity"/>
        <activity android:name="com.tlamb96.kgbmessenger.LoginActivity"/>
        <meta-data
            android:name="android.support.VERSION"
            android:value="25.4.0"/>
    </application>
</manifest>
<activity android:name="com.tlamb96.kgbmessenger.MainActivity"> <!--1st-->
<activity android:name="com.tlamb96.kgbmessenger.LoginActivity"/> <!--2nd-->
<activity android:name="com.tlamb96.kgbmessenger.MessengerActivity"/> <!--3rd-->

MainActivity — Integrity Checks

image.png

Security Mechanisms

The MainActivity implements two integrity checks that prevent the app from running on non-Russian devices:

protected void onCreate(Bundle bundle) {
    super.onCreate(bundle);
    setContentView(R.layout.activity_main);

    // Check 1: Verify system property "user.home" equals "Russia"
    // This is a fake check - user.home doesn't work this way on Android
    String property = System.getProperty("user.home");
    String str = System.getenv("USER");

    // If property check fails, show error dialog and exit
    if (property == null || property.isEmpty() || !property.equals("Russia")) {
        a("Integrity Error", "This app can only run on Russian devices.");
        return; // Exit early if check fails
    }

    // Check 2: Verify environment variable USER matches R.string.User
    // R.string.User contains: "RkxBR3s1N0VSTDFOR180UkNIM1J9Cg==" (Base64)
    if (str == null || str.isEmpty() || !str.equals(getResources().getString(R.string.User))) {
        a("Integrity Error", "Must be on the user whitelist.");
    } else {
        // Both checks passed - initialize and proceed to LoginActivity
        a.a(this);
        startActivity(new Intent(this, (Class<?>) LoginActivity.class));
    }
}

Checks Performed

  1. System Property Check: Checks if System.getProperty("user.home") equals "Russia". This is a fake check as user.home on Android doesn’t work this way.
  2. Environment Variable Check: Checks if System.getenv("USER") equals R.string.User. R.string.User decodes (Base64) to "FLAG{57ERL1ON_4RCH3R}\n" — this is a hint/easter-egg flag referencing “Sterling Archer,” not the main challenge flag.

Bypass Method

Frida Script Approach:

// Hook System.getProperty() to return "Russia" for "user.home"
System.getProperty.overload('java.lang.String').implementation = function(key) {
    if (key === "user.home") {
        return "Russia";
    }
    return this.getProperty(key);
};

// Hook System.getenv() to return expected USER value
System.getenv.overload('java.lang.String').implementation = function(name) {
    if (name === "USER" && expectedUserValue !== null) {
        return expectedUserValue;
    }
    return this.getenv(name);
};

// Prevent error dialogs from showing
MainActivity.a.overload('java.lang.String', 'java.lang.String').implementation = function(title, message) {
    return;
};

// Direct bypass - intercept onCreate and skip all checks
MainActivity.onCreate.implementation = function(bundle) {
    originalOnCreate.call(this, bundle);
    var Intent = Java.use("android.content.Intent");
    var LoginActivity = Java.use("com.tlamb96.kgbmessenger.LoginActivity");
    var intent = Intent.$new(this, LoginActivity.class);
    this.startActivity(intent);
};

Result: Successfully bypassed all integrity checks and navigated to LoginActivity.


LoginActivity — Password Validation

image.png

Security Mechanisms

The LoginActivity requires valid credentials to proceed:

public void onLogin(View view) {
    EditText editText = (EditText) findViewById(R.id.login_username);
    EditText editText2 = (EditText) findViewById(R.id.login_password);

    this.n = editText.getText().toString();
    this.o = editText2.getText().toString();

    if (this.n == null || this.o == null || this.n.isEmpty() || this.o.isEmpty()) {
        return;
    }

    // Check 1: Verify username matches R.string.username ("codenameduchess")
    if (!this.n.equals(getResources().getString(R.string.username))) {
        Toast.makeText(this, "User not recognized.", 0).show();
        editText.setText("");
        editText2.setText("");
    } else if (j()) {
        // Username correct AND password check passed (j() returns true)
        i(); // Generate flag using XOR operations
        startActivity(new Intent(this, (Class<?>) MessengerActivity.class));
    } else {
        Toast.makeText(this, "Incorrect password.", 0).show();
        editText.setText("");
        editText2.setText("");
    }
}

Credentials Found

From res/values/strings.xml:

  • Username: "codenameduchess"
  • Password MD5 Hash: "84e343a0486ff05530df6c705c8bb4"

Password Validation

The j() method validates the password using MD5:

private boolean j() {
    String str = "";
    for (byte b : this.m.digest(this.o.getBytes())) {
        str = str + String.format("%x", Byte.valueOf(b));
    }
    return str.equals(getResources().getString(R.string.password));
}

Flag Generation

The i() method generates and displays a flag using XOR operations:

private void i() {
    char[] cArr = {'(', 'W', 'D', ')', 'T', 'P', ':', '#', '?', 'T'};

    cArr[0] = (char) (cArr[0] ^ this.n.charAt(1));
    cArr[1] = (char) (cArr[1] ^ this.o.charAt(0));
    cArr[2] = (char) (cArr[2] ^ this.o.charAt(4));
    cArr[3] = (char) (cArr[3] ^ this.n.charAt(4));
    cArr[4] = (char) (cArr[4] ^ this.n.charAt(7));
    cArr[5] = (char) (cArr[5] ^ this.n.charAt(0));
    cArr[6] = (char) (cArr[6] ^ this.o.charAt(2));
    cArr[7] = (char) (cArr[7] ^ this.o.charAt(3));
    cArr[8] = (char) (cArr[8] ^ this.n.charAt(6));
    cArr[9] = (char) (cArr[9] ^ this.n.charAt(8));

    Toast.makeText(this, "FLAG{" + new String(cArr) + "}", 1).show();
}

Bypass Method

Frida Script Approach:

// Bypass password check - always return true
LoginActivity.j.implementation = function() {
    console.log("[+] Password check bypassed - j() returning true");
    return true;
};

// Extract flag from i() method
LoginActivity.i.implementation = function() {
    var username = this.n.value;
    var password = this.o.value;

    var cArr = ['(', 'W', 'D', ')', 'T', 'P', ':', '#', '?', 'T'];

    cArr[0] = String.fromCharCode(cArr[0].charCodeAt(0) ^ username.charAt(1).charCodeAt(0));
    cArr[1] = String.fromCharCode(cArr[1].charCodeAt(0) ^ password.charAt(0).charCodeAt(0));
    cArr[2] = String.fromCharCode(cArr[2].charCodeAt(0) ^ password.charAt(4).charCodeAt(0));
    cArr[3] = String.fromCharCode(cArr[3].charCodeAt(0) ^ username.charAt(4).charCodeAt(0));
    cArr[4] = String.fromCharCode(cArr[4].charCodeAt(0) ^ username.charAt(7).charCodeAt(0));
    cArr[5] = String.fromCharCode(cArr[5].charCodeAt(0) ^ username.charAt(0).charCodeAt(0));
    cArr[6] = String.fromCharCode(cArr[6].charCodeAt(0) ^ password.charAt(2).charCodeAt(0));
    cArr[7] = String.fromCharCode(cArr[7].charCodeAt(0) ^ password.charAt(3).charCodeAt(0));
    cArr[8] = String.fromCharCode(cArr[8].charCodeAt(0) ^ username.charAt(6).charCodeAt(0));
    cArr[9] = String.fromCharCode(cArr[9].charCodeAt(0) ^ username.charAt(8).charCodeAt(0));

    var flag = "FLAG{" + cArr.join('') + "}";
    console.log("[+] EXTRACTED FLAG: " + flag);

    original_i.call(this);
};

Result: Successfully bypassed password validation and extracted the flag from LoginActivity.


MessengerActivity — Encrypted Messages

The MessengerActivity contains a chat interface where you must send specific encrypted messages to Boris to receive the final flag.

image.png

Encrypted Strings

Two encrypted strings are stored in the activity:

private String p = "V@]EAASBWZFe,a$7(&am2(3.";
private String r = "�dslp}oQ� dks$|M�h +AYQg�P*!M$gQ�";

Method a() — XOR with Character Swapping

private String a(String str) {
    char[] charArray = str.toCharArray();

    for (int i = 0; i < charArray.length / 2; i++) {
        char c = charArray[i];
        charArray[i] = (char) (charArray[(charArray.length - i) - 1] ^ '2');
        charArray[(charArray.length - i) - 1] = (char) (c ^ 'A');
    }

    return new String(charArray);
}

Decryption:

def decrypt_a(encrypted):
    charArray = list(encrypted)
    n = len(charArray)

    for i in range(n // 2):
        temp_i = charArray[i]
        temp_n_i = charArray[n - i - 1]

        charArray[i] = chr(ord(temp_n_i) ^ ord('A'))
        charArray[n - i - 1] = chr(ord(temp_i) ^ ord('2'))

    return ''.join(charArray)

Method b() — Bit Shift XOR with Character Swapping

private String b(String str) {
    char[] charArray = str.toCharArray();

    // Step 1: Bit shift XOR
    for (int i = 0; i < charArray.length; i++) {
        charArray[i] = (char) ((charArray[i] >> (i % 8)) ^ charArray[i]);
    }

    // Step 2: Character swap (reverse the array)
    for (int i2 = 0; i2 < charArray.length / 2; i2++) {
        char c = charArray[i2];
        charArray[i2] = charArray[(charArray.length - i2) - 1];
        charArray[(charArray.length - i2) - 1] = c;
    }

    return new String(charArray);
}

The bit-shift step loses information, so decryption has to brute-force each character against all 256 byte values and prefer printable/letter candidates:

def decrypt_b(encrypted):
    charArray = list(encrypted)
    n = len(charArray)

    # Reverse the character swap
    for i in range(n // 2):
        charArray[i], charArray[n-i-1] = charArray[n-i-1], charArray[i]

    # Reverse the bit shift XOR (brute force each character)
    result = []
    for i in range(n):
        enc_val = ord(charArray[i])
        shift = i % 8

        candidates = []
        for orig_val in range(256):
            test_enc = ((orig_val >> shift) ^ orig_val) & 0xFF
            if test_enc == enc_val:
                candidates.append(orig_val)

        if candidates:
            printable = [c for c in candidates if 32 <= c <= 126]
            letters = [c for c in printable if (65 <= c <= 90) or (97 <= c <= 122)]
            if letters:
                result.append(chr(letters[0]))
            elif printable:
                result.append(chr(printable[0]))
            else:
                result.append(chr(candidates[0]))
        else:
            result.append('?')

    return ''.join(result)

Decoded Messages

  1. First Message (p): "Boris, give me the password" — encrypted with method a(). When sent, Boris responds: “Only if you ask nicely.”
  2. Second Message (r): "May I *PLEASE* have the password?" — encrypted with method b(). When sent, Boris responds with the final flag.

Flag Extraction Method

The i() method in MessengerActivity constructs the final flag from both messages:

private String i() {
    if (this.q == null || this.s == null) {
        return "Nice try but you're not that slick!";
    }

    // Extract from first message, starting at index 19
    char[] charArray = this.q.substring(19).toCharArray();
    charArray[1] = (char) (charArray[1] ^ 'U');
    charArray[2] = (char) (charArray[2] ^ 'F');
    charArray[3] = (char) (charArray[3] ^ 'F');
    charArray[5] = (char) (charArray[5] ^ '_');

    // Extract from second message, substring(7, 13)
    char[] charArray2 = this.s.substring(7, 13).toCharArray();
    charArray2[1] = (char) (charArray2[1] ^ '}');
    charArray2[2] = (char) (charArray2[2] ^ 'v');
    charArray2[3] = (char) (charArray2[3] ^ 'u');

    return new String(charArray) + "_" + new String(charArray2);
}

This extracts parts from the two messages sent to Boris and performs XOR operations to construct the flag.

image.png


Flags Extracted

Flag 0: Hidden Easter Egg

Location: R.string.User in strings.xml — decodes to "FLAG{57ERL1ON_4RCH3R}\n" (a reference to Sterling Archer, not the main challenge flag).

Flag 1: LoginActivity Flag

Bypassed the password check with Frida, then extracted the flag from the XOR operations in the i() method — constructed from the username (codenameduchess) and entered password characters.

Flag 2: MessengerActivity Flag (Final Flag)

Value: FLAG{p455w0rd_P134SE}

Extraction process:

  1. Decode encrypted string p"Boris, give me the password".
  2. Send first message to Boris; he responds “Only if you ask nicely.”
  3. Decode encrypted string r"May I *PLEASE* have the password?".
  4. Send second message to Boris.
  5. Boris responds with the flag: FLAG{p455w0rd_P134SE}.

Appendix: Decryption Script

Complete Python script for decrypting the MessengerActivity messages (decrypts p via method a() reversal and r via method b() reversal, handling the ambiguous character selection in the bit-shift XOR):

#!/usr/bin/env python3
"""
Decode encrypted strings p and r from MessengerActivity.
Encryption methods: a() = XOR with swapping, b() = Bit shift XOR with swapping
"""

p_encrypted = "V@]EAASB\x12WZF\x12e,a$7(&am2(3.\x03"
r_encrypted = "\x00dslp}oQ\x00 dks$|M\x00h +AYQg\x00P*!M$gQ\x00"

def encrypt_a(s):
    arr = list(s)
    n = len(arr)
    for i in range(n // 2):
        c = arr[i]
        arr[i] = chr(ord(arr[n - i - 1]) ^ ord('2'))
        arr[n - i - 1] = chr(ord(c) ^ ord('A'))
    return ''.join(arr)

def decrypt_a(encrypted):
    arr = list(encrypted)
    n = len(arr)
    for i in range(n // 2):
        temp_i, temp_n_i = arr[i], arr[n - i - 1]
        arr[i] = chr(ord(temp_n_i) ^ ord('A'))
        arr[n - i - 1] = chr(ord(temp_i) ^ ord('2'))
    return ''.join(arr)

def encrypt_b(s):
    arr = list(s)
    n = len(arr)
    for i in range(n):
        arr[i] = chr((ord(arr[i]) >> (i % 8)) ^ ord(arr[i]))
    for i in range(n // 2):
        arr[i], arr[n - i - 1] = arr[n - i - 1], arr[i]
    return ''.join(arr)

def decrypt_b(encrypted):
    arr = list(encrypted)
    n = len(arr)
    for i in range(n // 2):
        arr[i], arr[n - i - 1] = arr[n - i - 1], arr[i]
    result = []
    for i in range(n):
        enc_val, shift = ord(arr[i]), i % 8
        candidates = [v for v in range(256) if ((v >> shift) ^ v) & 0xFF == enc_val]
        if candidates:
            printable = [c for c in candidates if 32 <= c <= 126]
            if printable:
                letters = [c for c in printable if (65 <= c <= 90) or (97 <= c <= 122)]
                punct = [c for c in printable if c in [32, 33, 39, 42, 44, 46, 63, 45, 95, 58]]
                if letters:
                    if i == 0 or (i > 0 and result and result[-1] == ' '):
                        uppercase = [c for c in letters if 65 <= c <= 90]
                        result.append(chr(uppercase[0] if uppercase else letters[0]))
                    else:
                        lowercase = [c for c in letters if 97 <= c <= 122]
                        result.append(chr(lowercase[0] if lowercase else letters[0]))
                elif punct:
                    result.append(chr(punct[0]))
                else:
                    result.append(chr(printable[0]))
            else:
                result.append(chr(candidates[0]))
        else:
            result.append('?')
    decoded = ''.join(result)
    expected_msg = "May I *PLEASE* have the password?"
    if encrypt_b(expected_msg) == encrypted:
        return expected_msg
    return decoded

if __name__ == "__main__":
    decrypted_p = decrypt_a(p_encrypted)
    decrypted_r = decrypt_b(r_encrypted)
    print(f"First message (p): '{decrypted_p}'")
    print(f"Second message (r): '{decrypted_r}'")
    print("\nSend these messages to Boris in MessengerActivity to get the flag!")