ThaiFight Posted July 10, 2015 Report Posted July 10, 2015 (edited) It’s great to see the increasing adoption of certificate pinning in Android apps. When I run into an app that throws connection errors while attempting to proxy requests, I tend to become more interested in diving deeper. Such was the case when I recently used the Subway app. Reversing the APK revealed cert pinning among some other interesting findings.Starting the app while proxying requests caused this error:Pinning is simple enough to bypass. I started by decompiling the app and analyzing the source code for pinning keywords. I actually found pinning implementations in two separate classes that implemented X509TrustManager . Here is one of the methods that enforced pinning:Java public void checkServerTrusted(X509Certificate ax509certificate[], String s) throws CertificateException { if (ax509certificate == null || ax509certificate.length == 0) { throw new CertificateException(new IllegalArgumentException("No X509Certificates found.")); } javax.net.ssl.TrustManager atrustmanager[]; int i; int j; try { TrustManagerFactory trustmanagerfactory = TrustManagerFactory.getInstance("X509"); trustmanagerfactory.init((KeyStore)null); atrustmanager = trustmanagerfactory.getTrustManagers(); j = atrustmanager.length; } // Misplaced declaration of an exception variable catch (X509Certificate ax509certificate[]) { throw new CertificateException(ax509certificate); } // Misplaced declaration of an exception variable catch (X509Certificate ax509certificate[]) { throw new CertificateException(ax509certificate); } i = 0; if (i >= j) { break; /* Loop/switch isn't completed */ } ((X509TrustManager)atrustmanager).checkServerTrusted(ax509certificate, s); i++; if (true) goto _L2; else goto _L1_L2: break MISSING_BLOCK_LABEL_52;_L1: boolean flag;label0: { s = PaydiantApplicationConfig.getPaydiantApplicationConfig().getPinningCertInfo(); boolean flag1 = false; flag = flag1; if (s == null) { break label0; } s = s.iterator(); com.paydiant.android.config.er er; byte abyte0[]; do { flag = flag1; if (!s.hasNext()) { break label0; } er = (com.paydiant.android.config.Config)s.next(); if (er.print == null || er.print.length == 0) { throw new CertificateException(new IllegalArgumentException("Invalid X509Certificate info provided.")); } abyte0 = messageDigest.digest(ax509certificate[er.osition].getEncoded()); messageDigest.reset(); } while (!Arrays.equals(abyte0, er.print)); flag = true; } if (!flag) { throw new CertificateException("Invalid X509Certificate used."); } else { return; } }Bypassing this was as simple as adding a return statement in the smali code to skip the pinning code in the method above. Note the addition of the return-void statement below:.method public checkServerTrusted([Ljava/security/cert/X509Certificate;Ljava/lang/String;)V .locals 13 .param p1, "chain" # [Ljava/security/cert/X509Certificate; .param p2, "authType" # Ljava/lang/String; .annotation system Ldalvik/annotation/Throws; value = { Ljava/security/cert/CertificateException; } .end annotation .prologue .line 583 return-void if-eqz p1, :cond_0After recompiling the App and installing, I was surprised to see this new error:Subway was using a custom app signature verification process in order to prevent reversing of their APK. Grepping the source for mentions of this process, I traced it back to the following method: public static void verifyAppSignature(Context context) { AppVerificationUtils.verifyAppSignature(context, PaydiantApplicationContext.getPaydiantApplicationContext().getKeyFingerprint(), new com.paydiant.android.common.util.AppVerificationUtils.IVerificationCallback(context) { final Context val$context; public void onVerificationComplete(boolean flag) { if (!flag) { Object obj = PaydiantApplicationContext.getPaydiantApplicationContext().getApplictionLabel(); android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(context); builder.setTitle(0x7f0c02f5); builder.setMessage(context.getString(0x7f0c02f4, new Object[] { obj })); builder.setCancelable(false); builder.setPositiveButton(0x7f0c006c, ((_cls1) (obj)). new android.content.DialogInterface.OnClickListener() { final _cls1 this$0; final String val$appLabel; public void onClick(DialogInterface dialoginterface, int i) { dialoginterface.dismiss(); try { dialoginterface = Class.forName(new String(new byte[] { })); Object obj = new Intent((String)dialoginterface.getField(new String(new byte[] { })).get(null)); String s = context.getString(0x7f0c00ad); ((Intent) (obj)).putExtra((String)dialoginterface.getField(new String(new byte[] { })).get(null), new String[] { s }); ((Intent) (obj)).putExtra((String)dialoginterface.getField(new String(new byte[] { })).get(null), context.getString(0x7f0c02f6, new Object[] { appLabel })); ((Intent) (obj)).setType(new String(new byte[] { })); ((Intent) (obj)).addFlags(0x800000); ((Intent) (obj)).addFlags(0x10000000); context.startActivity(((Intent) (obj))); Log.d(SecurityUtils.TAG, "Invalid App signature. Terminating ..."); dialoginterface = Class.forName(new String(new byte[] { })); obj = dialoginterface.getMethod(new String(new byte[] { }), new Class[0]).invoke(null, new Object[0]); dialoginterface.getMethod(new String(new byte[] { }), new Class[] { Integer.TYPE, Integer.TYPE }).invoke(null, new Object[] { obj, Integer.valueOf(15) }); return; } // Misplaced declaration of an exception variable catch (DialogInterface dialoginterface) { Log.d(SecurityUtils.TAG, dialoginterface.getMessage(), dialoginterface); return; } // Misplaced declaration of an exception variable catch (DialogInterface dialoginterface) { Log.d(SecurityUtils.TAG, dialoginterface.getMessage(), dialoginterface); return; } // Misplaced declaration of an exception variable catch (DialogInterface dialoginterface) { Log.d(SecurityUtils.TAG, dialoginterface.getMessage(), dialoginterface); return; } // Misplaced declaration of an exception variable catch (DialogInterface dialoginterface) { Log.d(SecurityUtils.TAG, dialoginterface.getMessage(), dialoginterface); return; } // Misplaced declaration of an exception variable catch (DialogInterface dialoginterface) { Log.d(SecurityUtils.TAG, dialoginterface.getMessage(), dialoginterface); return; } // Misplaced declaration of an exception variable catch (DialogInterface dialoginterface) { Log.d(SecurityUtils.TAG, dialoginterface.getMessage(), dialoginterface); } } { this$0 = final__pcls1; appLabel = String.this; super(); } }); obj = builder.create(); ((AlertDialog) (obj)).getWindow().setType(2003); ((AlertDialog) (obj)).show(); context.sendBroadcast(new Intent(SecurityUtils.ACTION_APP_VERIFICATION_FAILED)); } } { context = context1; super(); } }); }This was an interesting attempt at preventing reverse engineering, though it actually only caused a slight delay. In order to bypass this process, I simply added a line to skip the method’s execution by adding another return-void line, similar to the pinning bypass process above..method public static verifyAppSignature(Landroid/content/Context;)V .locals 2 .param p0, "context" # Landroid/content/Context; .prologue .line 70 return-void invoke-static {}, Lcom/paydiant/common/PaydiantApplicationContext;->getPaydiantApplicationContext()Lcom/paydiant/common/PaydiantApplicationContext;After recompiling and installing the app, I was able to successfully proxy requests:During my research, I stumbled on this Reddit post. Apparently, Subway was also determining whether the user’s device had been rooted. I searched around in the source and confirmed mentions of root detection methods.Java public static boolean isDeviceRooted(Context context) { A aa[] = new A[12]; aa[0] = new t>("find /system/app/Superuser.apk"); aa[1] = new t>("busybox df"); aa[2] = new t>("/sbin/su"); aa[3] = new t>("/system/bin/su"); aa[4] = new t>("/system/xbin/su"); aa[5] = new t>("/system/su"); aa[6] = new t>("/system/bin/.ext/.su"); aa[7] = new t>("/system/usr/we-need-root/su-backup"); aa[8] = new t>("/system/xbin/mu"); aa[9] = new t>("id"); aa[10] = new t>("cat /system/build.prop | grep ro.build.tags"); aa[11] = new t>("pm list packages"); A aa1[][] = new t>[12][]; aa1[0] = null; aa1[1] = null; aa1[2] = null; aa1[3] = null; aa1[4] = null; aa1[5] = null; aa1[6] = null; aa1[7] = null; aa1[8] = null; aa1[9] = (new t>[] { new <init>(new String[] { "uid=0(root)" }, null, null) }); aa1[10] = (new <init>[] { new <init>(new String[] { "test-keys" }, null, null) }); aa1[11] = (new <init>[] { new <init>(new String[] { "eu.chainfire.supersu", "com.thirdparty.superuser", "com.koushikdutta.superuser", "com.zachspong.temprootremovejb", "com.ramdroid.appquarantine", "com.noshufou.android.su" }, null, "1") }); int j = 0; java.util.List list = null; int k = aa.length; for (int i = 0; i < k; i++) { A a = aa; if (aa1[j] != null) { list = Arrays.asList(aa1[j]); } if (a.ute(list)) { Log.d("com.paydiant.android.common.util.RootedDeviceUtils", "Device is Rooted"); return true; } j++; } if (isCyanogenmodSuperuserExist(context)) { Log.d("com.paydiant.android.common.util.RootedDeviceUtils", "Device is Rooted"); return true; } else { Log.d("com.paydiant.android.common.util.RootedDeviceUtils", "Device is Not Rooted"); return false; } }This is a great example of an app taking security very seriously, but I’m not quite sure of the reasoning behind the root checking process. Though certificate pinning and signature verification techniques are generally a good idea, they only slightly impede the reverse engineering process. Edited July 10, 2015 by ThaiFight Quote