CuteFrida
Points 215
Solves 57
This app is so cute yet so susy, isn’t it?
The app contains an encrypted flag stored in the What_do_you_mean_by_encrypted_flag variable in MainActivity.java. The flag is obfuscated using a custom random number generator and a deobfuscation algorithm defined in the com.joom.paranoid package.
Analysis
Since im not the one who solved this challenge but my teammate SeaSir?! (boppind) did, I will explain as his perspective.
The MainActivity.java file contains the following code:
package com.pwnsec.cutefrida;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.OnApplyWindowInsetsListener;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.joom.paranoid.Deobfuscator$app$Release;
/* loaded from: classes.dex */
public class MainActivity extends AppCompatActivity {
private static final String What_do_you_mean_by_encrypted_flag = Deobfuscator$app$Release.getString(-548601664941L);
@Override // androidx.fragment.app.FragmentActivity, androidx.activity.ComponentActivity, androidx.core.app.ComponentActivity, android.app.Activity
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
EdgeToEdge.enable(this);
setContentView(C0814R.layout.activity_main);
String.format(Deobfuscator$app$Release.getString(-3140818349L), Deobfuscator$app$Release.getString(-28910622125L));
Log.d(Deobfuscator$app$Release.getString(-308083496365L), Deobfuscator$app$Release.getString(-338148267437L));
ViewCompat.setOnApplyWindowInsetsListener(findViewById(C0814R.id.main), new OnApplyWindowInsetsListener() { // from class: com.pwnsec.cutefrida.MainActivity$$ExternalSyntheticLambda0
@Override // androidx.core.view.OnApplyWindowInsetsListener
public final WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat windowInsetsCompat) {
return MainActivity.lambda$onCreate$0(view, windowInsetsCompat);
}
});
}
static /* synthetic */ WindowInsetsCompat lambda$onCreate$0(View view, WindowInsetsCompat windowInsetsCompat) {
Insets insets = windowInsetsCompat.getInsets(WindowInsetsCompat.Type.systemBars());
view.setPadding(insets.left, insets.top, insets.right, insets.bottom);
return windowInsetsCompat;
}
}All the strings in the MainActivity are obfuscated using the Deobfuscator$app$Release class from the com.joom.paranoid package. The encrypted flag is stored in the What_do_you_mean_by_encrypted_flag variable, which is decrypted using the Deobfuscator$app$Release.getString method with the seed -548601664941L.
private static final String What_do_you_mean_by_encrypted_flag = Deobfuscator$app$Release.getString(-548601664941L);The deobfuscation process involves two main classes: RandomHelper and DeobfuscatorHelper. The RandomHelper class implements a custom random number generator, while the DeobfuscatorHelper class uses that generator to decrypt the obfuscated strings.
package com.joom.paranoid;
/* loaded from: classes.dex */
public class RandomHelper {
private static short rotl(short s, int i) {
return (short) ((s >>> (32 - i)) | (s << i));
}
public static long seed(long j) {
long j2 = (j ^ (j >>> 33)) * 7109453100751455733L;
return ((j2 ^ (j2 >>> 28)) * (-3808689974395783757L)) >>> 32;
}
private RandomHelper() {
}
public static long next(long j) {
short s = (short) (j & 65535);
short s2 = (short) ((j >>> 16) & 65535);
short rotl = (short) (rotl((short) (s + s2), 9) + s);
short s3 = (short) (s2 ^ s);
return ((rotl(s3, 10) | (rotl << 16)) << 16) | ((short) (((short) (rotl(s, 13) ^ s3)) ^ (s3 << 5)));
}
}package com.joom.paranoid;
/* loaded from: classes.dex */
public class DeobfuscatorHelper {
public static final int MAX_CHUNK_LENGTH = 8191;
private DeobfuscatorHelper() {
}
public static String getString(long j, String[] strArr) {
long next = RandomHelper.next(RandomHelper.seed(4294967295L & j));
long j2 = (next >>> 32) & 65535;
long next2 = RandomHelper.next(next);
int i = (int) (((j >>> 32) ^ j2) ^ ((next2 >>> 16) & (-65536)));
long charAt = getCharAt(i, strArr, next2);
int i2 = (int) ((charAt >>> 32) & 65535);
char[] cArr = new char[i2];
for (int i3 = 0; i3 < i2; i3++) {
charAt = getCharAt(i + i3 + 1, strArr, charAt);
cArr[i3] = (char) ((charAt >>> 32) & 65535);
}
return new String(cArr);
}
private static long getCharAt(int i, String[] strArr, long j) {
return (strArr[i / MAX_CHUNK_LENGTH].charAt(i % MAX_CHUNK_LENGTH) << 32) ^ RandomHelper.next(j);
}
}The deobfuscation process involves seeding a custom random number generator with a modified version of the input seed. The generator produces pseudo-random values that are used to reconstruct the original string character by character. The getString method retrieves the length of the string and then iteratively fetches each character using the getCharAt method, which combines the character’s value with the next random number generated.
package com.joom.paranoid;
/* loaded from: classes.dex */
public class Deobfuscator$app$Release {
private static final String[] chunks = {"\ufffaᆴᅤ\uffdfᅳフ\uffbfルモ゙リト\uffc8ラレᅠロレノレモミマレヘᅠヨフᅠᆲᅬᅠマᅨヘᅨムᅬᅫロᅠᅫヒᅠヒラᅫムヤᅧᅠᄍヘᅫロᅨᅠᅫフᅠᅩノᅩヘニネラᅩヘᅩツ\ufff9ᄋヨムヒᅤ\uffdfᅬᄐラレワヤ\uffdfヒラレ\uffdf゙フフレヒ\uffdfルミモロレヘᅮ\uffdfニミハ\uffdfメヨリラヒ\uffdfルヨムロ\uffdfフミメレヒラヨムリ\ufffaᆴᅤ\uffdfᅳフ"};
public static String getString(long j) {
return DeobfuscatorHelper.getString(j, chunks);
}
}The obfuscated strings are stored in chunks, and the getString method retrieves the appropriate chunk based on the input seed. The deobfuscation process reconstructs the original string using the custom random number generator and the character retrieval method.
Solution by Reconstructing the Deobfuscation Algorithm
Solver by my teammate SeaSir?! (boppind):
s = "\ufffa\uffae\uffc5\uffdf\uffda\uff8c\uffbf\uff99\uff93\uff9e\uff98\uff84\uffc8\uff97\uff9a\uffa0\uff9b\uff9a\uff89\uff9a\uff93\uff90\uff8f\uff9a\uff8d\uffa0\uff96\uff8c\uffa0\uffac\uffcf\uffa0\uff8f\uffcb\uff8d\uffcb\uff91\uffcf\uffce\uff9b\uffa0\uffce\uff8b\uffa0\uff8b\uff97\uffce\u0091\u0094\u00ca\u00a0\u00b9\u008d\u00ce\u009b\u00cb\u00a0\u00ce\u008c\u00a0\u00cc\u0089\u00cc\u008d\u0086\u0088\u0097\u00cc\u008d\u00cc\u0082\u00f9\u00b7\u0096\u0091\u008b\u00c5\u00df\u00cf\u00bc\u0097\u009a\u009c\u0094\u00df\u008b\u0097\u009a\u00df\u009e\u008c\u008c\u009a\u008b\u00df\u0099\u0090\u0093\u009b\u009a\u008d\u00d3\u00df\u0086\u0090\u008a\u00df\u0092\u0096\u0098\u0097\u008b\u00df\u0099\u0096\u0091\u009b\u00df\u008c\u0090\u0092\u009a\u008b\u0097\u0096\u0091\u0098\u00fa\u00ae\u00c5\u00df\u00da\u008c"
MASK64 = (1 << 64) - 1
def rotl16(x, n):
n %= 16
return ((x << n) & 0xffff) | ((x & 0xffff) >> (16 - n))
def seed(j):
j &= MASK64
v0 = (j >> 33) & MASK64
j ^= v0
j = (j * 0x62a9d9ed799705f5) & MASK64
v0 = (j >> 28) & MASK64
j ^= v0
j = (j * ((-0x34db2f5a3773ca4d) & MASK64)) & MASK64
v0 = (j >> 32) & MASK64
j ^= v0
return j & MASK64
def next_rand(j):
j &= MASK64
mask = 0xffff
v2 = j & mask
p0 = (j >> 16) & mask
p1 = (v2 + p0) & 0xffff
p1 = rotl16(p1, 9)
p1 = (p1 + v2) & 0xffff
p0_xor = p0 ^ v2
v0 = rotl16(v2, 13)
v0 ^= p0_xor
v0 &= 0xffff
v1 = (p0 << 5) & 0xffff
v0 ^= v1
p0 = rotl16(p0, 10)
high = p1 & 0xffff
res = ((high << 32) & MASK64) | ((p0 & 0xffff) << 16) | (v0 & 0xffff)
return res & MASK64
def getCharAt_single(i, s, j):
rnd = next_rand(j)
pos = i % len(s)
c = ord(s[pos]) & 0xffff
val = (c << 32) ^ rnd
return val & MASK64
def getString_lowbytes(j, s):
j &= MASK64
v = seed(j)
# derive p0
v = next_rand(v)
ushr32 = (v >> 32) & 0xffff
v = next_rand(v)
ushr16 = (v >> 16) & 0xffff0000
p0 = (((j >> 32) & MASK64) ^ ushr32 ^ (ushr16 >> 16)) & 0xffffffff
# first call gives us length
val = getCharAt_single(p0, s, j)
length = (val >> 32) & 0xffff
out = []
for k in range(length):
idx = p0 + k + 1
val = getCharAt_single(idx, s, j)
ch = (val >> 32) & 0xffff
out.append(ch & 0xff)
return bytes(out)
# candidate 64-bit seeds (from reversing)
keys = [
-0x7fbb3515ad,
-0xbb3515ad,
-0x6bb3515ad,
-0x47bb3515ad,
-0x4ebb3515ad,
]
import re
for k in keys:
j = k & MASK64
b = getString_lowbytes(j, s)
print('--- KEY', hex(j), 'LEN', len(b))
hits = 0
for key in range(1, 256):
xb = bytes(c ^ key for c in b)
if re.search(b'(?i)(ctf|flag)', xb):
m = re.search(rb'[\x20-\x7e]{8,}', xb)
if m:
snippet = m.group(0).decode('ascii', errors='ignore')
else:
snippet = xb[:80].hex()
print('XOR', hex(key), 'FOUND_SNIPPET:', snippet)
hits += 1
if hits == 0:
for m in re.finditer(rb'[\x20-\x7e]{8,}', b):
print('PLAINTEXT_CAND:', m.group(0).decode('ascii', errors='ignore'))
breakSolution with Frida
To extract the flag using Frida, we can hook into the onCreate method of the MainActivity class and retrieve the value of the What_do_you_mean_by_encrypted_flag variable after it has been decrypted. Below is a Frida script that accomplishes this:
Or you can just log the variable directly by decompiling the APK and editing the smali code to print it out.
sget-object v0, Lcom/pwnsec/cutefrida/MainActivity;->What_do_you_mean_by_encrypted_flag:Ljava/lang/String; invoke-static {v0}, Landroid/util/Log;->d(Ljava/lang/String;)IInject this code snippet into the
onCreatemethod of MainActivity.smali to log the decrypted flag.
Here’s the Frida script:
Java.perform(function () {
var MainActivity = Java.use('com.pwnsec.cutefrida.MainActivity');
MainActivity.onCreate.overload('android.os.Bundle').implementation = function (bundle) {
this.onCreate(bundle);
var flag = MainActivity.What_do_you_mean_by_encrypted_flag.value;
console.log('Decrypted Flag: ' + flag);
};
});flag{7he_developer_is_S0_p4r4n01d_1t_th1nk5_Fr1d4_1s_3v3rywh3r3}