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

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
- System Property Check: Checks if
System.getProperty("user.home")equals"Russia". This is a fake check asuser.homeon Android doesn’t work this way. - Environment Variable Check: Checks if
System.getenv("USER")equalsR.string.User.R.string.Userdecodes (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

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.

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
- First Message (
p):"Boris, give me the password"— encrypted with methoda(). When sent, Boris responds: “Only if you ask nicely.” - Second Message (
r):"May I *PLEASE* have the password?"— encrypted with methodb(). 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.

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:
- Decode encrypted string
p→"Boris, give me the password". - Send first message to Boris; he responds “Only if you ask nicely.”
- Decode encrypted string
r→"May I *PLEASE* have the password?". - Send second message to Boris.
- 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!")