Jump to content
ThaiFight

Reverse Engineering the Subway Android App

Recommended Posts

Posted (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:

azQFb2U.png

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_0

After recompiling the App and installing, I was surprised to see this new error:

zzI8354.png

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.

7K6Sni0.png

.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:

7K6Sni0.png

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 by ThaiFight

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.



×
×
  • Create New...