Every once in awhile, I’ll come across an app that implements some hardening techniques that make reversing a little more interesting. This was the case when I recently tried proxying the API requests for Yik Yak, a popular social media application exclusively available for mobile platforms that allows semi-anonymous user communication across a 5-mile radius (typically college areas).

Opening the app while performing a self-MitM attack effectively kills all API communication — typically, an indicator of SSL pinning.

After decompiling the APK and reviewing the Java source code, it was clear that the developers had also used an obfuscation tool to optimize/protect their build. Here’s an example method:

public int hashCode() { if (e == null) { i1 = 0; } else { i1 = e.hashCode(); } l2 = f; i3 = g; if (h) { c1 = '\u04CF'; } else { c1 = '\u04D5'; } j3 = fs.a(i); if (j == null) { j1 = 0; } else { j1 = j.hashCode(); } k3 = Arrays.hashCode(k); l3 = Arrays.hashCode(l); i4 = Arrays.hashCode(m); if (n == null) { k1 = 0; } else { k1 = n.hashCode(); } if (o == null) { l1 = 0; } else { l1 = o.hashCode(); } j4 = (int) (p ^ p >>> 32); if (q != null) { i2 = q.hashCode(); } return ((((((l1 + (k1 + ((((j1 + ((c1 + (((i1 + ((j2 + 527) * 31 + k2) * 31) * 31 + l2) * 31 + i3) * 31) * 31 + j3) * 31) * 31 + k3) * 31 + l3) * 31 + i4) * 31) * 31) * 31 + j4) * 31 + i2) * 31 + Arrays.hashCode(r)) * 31 + s) * 31 + fs.a(t)) * 31 + b(); }

Note that the variables, classes, and methods have been renamed from their original, human-friendly forms. As I looked over more of the code, it also seemed like string constants were obfuscated — a feature found in third party tools like DexGuard. While obfuscators are generally a good practice for several reasons, they only marginally delay the reversing process by making the code harder to follow. In my experience, obfuscators also make Java decompilation less reliable, so I’ll mostly be working with smali (examples are in Java when available).

Next, I began grepping the smali source files for strings relating to common SSL pinning implementations. I quickly found what I thought would be the pinning check:

.method public checkServerTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V .locals 3 .prologue const/4 v2, 0x0 .line 153 iget-object v0, p0, LCG;->e:Ljava/util/Set; aget-object v1, p1, v2 invoke-interface {v0, v1}, Ljava/util/Set;->contains(Ljava/lang/Object;)Z move-result v0 if-eqz v0, :cond_0 .line 163 :goto_0 return-void .line 160 :cond_0 invoke-direct {p0, p1, p2}, LCG;->a([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V .line 161 invoke-direct {p0, p1}, LCG;->a([Ljava/security/cert/X509Certificate;)V .line 162 iget-object v0, p0, LCG;->e:Ljava/util/Set; aget-object v1, p1, v2 invoke-interface {v0, v1}, Ljava/util/Set;->add(Ljava/lang/Object;)Z goto :goto_0 .end method

I bypassed the above method by editing it to immediately return void, but after building, signing, and installing the new APK, I had the same “Internet Connection” error as above. After trying a few different edits/builds with the same result, I began to suspect that Yik Yak was using some tamper detection logic (a package signature check) in an effort to prevent reverse engineering. I confirmed this by installing an unmodified, though resigned, APK that resulted in the same error.

In order to bypass the tamper detection, I changed focus and started searching for its decision point. In Android, developers can access the package’s signatures using the PackageManager class like this:

PackageManager pm = context.getPackageManager(); String packageName = context.getPackageName(); Signature[] sigs = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES).signatures;

Since signatures are returned as instances of android.content.pm.Signature , I searched the source and found the following method:

public static Signature[] a(Context context) { if (context != null) goto _L2; else goto _L1 _L1: PackageManager packagemanager; return null; _L2: if ((packagemanager = context.getPackageManager()) == null) goto _L1; else goto _L3 _L3: try { context = packagemanager.getPackageInfo(context.getPackageName(), 64); } // Misplaced declaration of an exception variable catch (Context context) { context.printStackTrace(); return null; } if (context == null) goto _L1; else goto _L4 _L4: context = ((PackageInfo) (context)).signatures; return context; }

It was pretty clear that this method was used as a wrapper to fetch the signatures of the current build. Note that on line 12, the value 64 correctly matches the constant value for PackageInfo.GET_SIGNATURES. Searching for usages of this class produced a few results:

Rather than attempt to workaround multiple decision points, I decided to spoof the package signature by altering the above method to return the signature of the official Yik Yak build. In order to do that, I needed to know the signature that the app was looking for. I briefly hunted through the code in the results above, but didn’t find the hard-coded signature (probably due to obfuscation). Instead of further searching (or debugging) the code, I ran a script that extracts the signature from a given APK:

After I had the expected signature, I patched the above smali method to return it:

.method public static a(Landroid/content/Context;)[Landroid/content/pm/Signature; .locals 4 .prologue const/4 v0, 0x0 const-string v0, "3082019d30820106a003020102020452ab687f300d06092a864886f70d010105050030123110300e0603550403130777696c6c69616d3020170d3133313231333230303531395a180f32313133313131393230303531395a30123110300e0603550403130777696c6c69616d30819f300d06092a864886f70d010101050003818d00308189028181008fdfd8a1c6319b8d45445dc9c28a89600062dd00ad14c5ee3fac8d4812d5dfa3a5c6e534f242d5e91d6acb1807d618d44731973c4f69c328b6b755962810ed2cf8ff19fa5c6de40a34be5e92c6686e772fa864784e74144465272c260f877395df37b897e8147bbcdce15b8f11ee125c82bf9d2de9beb92056edea6f301d15f70203010001300d06092a864886f70d0101050500038181000a2f44f1a8d78b4d1965f0e60f9ef10826827ae131e6c4a3f976fc85f36f94578a698f904fd0a37a690f3dd338c16c3e408d77670543bb5b022d7c1bc86a0574e3e593092f1e06de141f04f6a68d78dbc5aa36f0a82062ecb03c1e7285a55b5ccfea58c193572d8d7542ca7a31748aabc7edff7990048a11ae5ef090074c9b25" .line 11 .local v0, "fake":Ljava/lang/String; const/4 v2, 0x1 new-array v1, v2, [Landroid/content/pm/Signature; const/4 v2, 0x0 new-instance v3, Landroid/content/pm/Signature; invoke-direct {v3, v0}, Landroid/content/pm/Signature;-><init>(Ljava/lang/String;)V aput-object v3, v1, v2 .line 13 .local v1, "sig":[Landroid/content/pm/Signature; return-object v1 .end method

Here’s that same code in Java:

public static Signature[] a(Context context) { String fake = "3082019d30820106a003020102020452ab687f300d06092a864886f70d010105050030123110300e0603550403130777696c6c69616d3020170d3133313231333230303531395a180f32313133313131393230303531395a30123110300e0603550403130777696c6c69616d30819f300d06092a864886f70d010101050003818d00308189028181008fdfd8a1c6319b8d45445dc9c28a89600062dd00ad14c5ee3fac8d4812d5dfa3a5c6e534f242d5e91d6acb1807d618d44731973c4f69c328b6b755962810ed2cf8ff19fa5c6de40a34be5e92c6686e772fa864784e74144465272c260f877395df37b897e8147bbcdce15b8f11ee125c82bf9d2de9beb92056edea6f301d15f70203010001300d06092a864886f70d0101050500038181000a2f44f1a8d78b4d1965f0e60f9ef10826827ae131e6c4a3f976fc85f36f94578a698f904fd0a37a690f3dd338c16c3e408d77670543bb5b022d7c1bc86a0574e3e593092f1e06de141f04f6a68d78dbc5aa36f0a82062ecb03c1e7285a55b5ccfea58c193572d8d7542ca7a31748aabc7edff7990048a11ae5ef090074c9b25"; Signature[] sig = new Signature[]{new Signature(fake)}; return sig; }

Now that the package signature check had been bypassed, I installed the new build to test — unfortunately, I had the same error. Since this was likely due to some additional pinning code that I missed, I searched through the sources again and found this method:

public void a(String s, List list) { List list1; boolean flag; flag = false; list1 = (List)b.get(s); if (list1 != null) goto _L2; else goto _L1 _L1: return; _L2: int l = list.size(); int i = 0; label0: do { label1: { if (i >= l) { break label1; } if (list1.contains(a((X509Certificate)list.get(i)))) { break label0; } i++; } } while (true); if (true) goto _L1; else goto _L3 _L3: StringBuilder stringbuilder = (new StringBuilder()).append("Certificate pinning failure!").append("

Peer certificate chain:"); int i1 = list.size(); for (int j = 0; j < i1; j++) { X509Certificate x509certificate = (X509Certificate)list.get(j); stringbuilder.append("

").append(a(((Certificate) (x509certificate)))).append(": ").append(x509certificate.getSubjectDN().getName()); } stringbuilder.append("

Pinned certificates for ").append(s).append(":"); i1 = list1.size(); for (int k = ((flag) ? 1 : 0); k < i1; k++) { s = (Dx)list1.get(k); stringbuilder.append("

sha1/").append(s.b()); } throw new SSLPeerUnverifiedException(stringbuilder.toString()); }

Bypassing this method with return-void finally disabled the pinning implementation, allowing me to successfully proxy the app’s API requests.

GET https://notify.yikyakapi.net/api/getAllForUser/***REMOVED*** HTTP/1.1 Host: notify.yikyakapi.net Connection: Keep-Alive Accept-Encoding: gzip Cookie: __cfduid=***REMOVED*** User-Agent: Dalvik/2.1.0 (Linux; U; Android 5.1.1; Nexus 6 Build/LMY48M) 3.0

The infosec world is full of examples of companies poorly handling software security, but it’s certainly progress to see more efforts to improve such security by organizations like Yik Yak (at least as far as Android is concerned).

Share this: