CQ备忘录

一间存放故事的仓库

GitHub LinkedIn
10 July 2020

Android:csair逆向工程

by ChenQi

今天没写新东西,继续搬运。知乎上面没有存货了,剩下的都是很简短或者过时的内容。翻出三年前的这篇技术文档和代码,发现还能用,勉强充数吧。

中国航空公司的技术体系在互联网行业算是比较差了,再加上Android这种开源操作系统,几乎没有秘密可言。曾经想把所有航司App和官网都破解一遍,一直没行动。一是没必要,大前端逆向工程就几种固定的套路,翻来覆去折腾。二是系统天天变,今天破解了,明天又更新。三是没收益,不想违法,也不想从中盈利。
以下是2017年夏天对南方航空App的一次逆向分析记录。


0. 常规套路

1. APK 包体结构分析

APK 解压后的 DEX 文件:

DEX SIZE
classes.dex 14 KB
classes2.dex 6340 KB
classes3.dex 4021 KB

assets 目录结构:

FILE SIZE
classes.dgc 1358 KB
classes0.jar 6182 KB

标准的梆梆加固技术方案,用的还是付费企业版。

脱壳后:

DEX SIZE 用途
classes.dex 6517 KB 主要业务逻辑和UI
classes2.dex 6342 KB 其他业务逻辑和UI
classes3.dex 4017 KB 第三方SDK和开源库

2. 脱壳

过程省略,安卓第三方安全加固厂商就那么几家,常规固定套路。有耐心就自己搞,嫌麻烦就上淘宝找店铺,几十块到几百块,二十分钟搞定。

3. Manifest.xml 结构分析

Activity UI 入口必须有明文静态声明的,躲不了。摘录一段值机相关逻辑代码片段(快速跳过):

<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.csair.mbp" platformBuildVersionCode="25" platformBuildVersionName="7.1.1">
    <application android:name="com.csair.mbp.CSMBPApplication" android:networkSecurityConfig="@xml/b" android:persistent="true">
        <activity android:name="com.csair.mbp.WelcomeActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinLoginActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinListActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinDetailActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinApiActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinChooseSeatActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinCancleSeatActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinChooseAirlineActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinDialogListActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinSuccessActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckInTypeActivity"/>
        <activity android:name="com.csair.mbp.checkin.activity.CheckinLoginAllActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.CheckinNewLoginActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.CheckinNewLoginAllActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.MyBoardPassMemberOrNonOrderListActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.MyBoardPassMemberOrNonOrderRVActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.BoardingPassBundleOpenActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.CheckInAddAddressActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.CheckInAddUSAddressActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.SendEmailActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.BoardingPassCardActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.BoardingPassQrCardActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.PaymentForSeatActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.CheckinSelectPassengerActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.SelectSeatActivity"/>
        <activity android:name="com.csair.mbp.checkin.input.activity.SelectSeatActivityNew"/>
    </application>
</manifest>

4. NDK SO 文件结构

解压缩 /lib/armeabi/*.so

SO文件 望文生义猜测用途
libAndroidBankCard.so 南航银行卡逻辑
libAndroidIDCard.so 南航用户ID逻辑
libencrypt_release.so 南航网络协议加解密逻辑
libapp_BaiduPanoramaAppLib.so 百度全景图
libBaiduMapSDK_base_v4_1_1.so 百度地图
libBaiduMapSDK_cloud_v4_1_1.so 百度地图
libBaiduMapSDK_map_v4_1_1.so 百度地图
libBaiduMapSDK_radar_v4_1_1.so 百度地图
libBaiduMapSDK_search_v4_1_1.so 百度地图
libBaiduMapSDK_util_v4_1_1.so 百度地图
liblocSDK7.so 百度地图
libbsdiff.so bsdiff增量更新
libDexHelper-x86.so 梆梆加固
libDexHelper.so 梆梆加固
libentryexpro.so 银联支付
libjcore110.so 极光推送
libmsc.so 科大讯飞语音
libMtaNativeCrash_v2.so 腾讯移动分析统计
libnbsdc2.1.0.so 听云应用性能监控
libtencentloc.so 腾讯地图
libuptsmaddon.so 银联支付
libweibosdkcore.so 微博

5. Java 字节码反编译

6. Java 代码分析启动流程

graph TB
A[CSMBPApplication 启动进程]
B[WelcomeActivity 欢迎页面]
C[CSMBPActivity 首屏主页面]
D[CheckinNewLoginActivity 选座&办登机牌页面]
E[MyBoardPassMemberOrNonOrderListActivity 值机查询页]
F[HttpQuery]
G[NetworkUtils]
H[CheckCodeUtil JNI]
A-->B
B-->C
C-->D
D-->E
  subgraph HTTP协议收发
    E-->F
    F-->G
    G-->H
  end

7

CheckinNewLoginActivity.java
枯燥的部分,跳过。

package com.csair.mpb.checkin.input.activity;

@Deprecated
@NBSInstrumented
public class CheckinNewLoginActivity extends AppCompatActivity implements TextWatcher, TraceFieldInterface {

    //UI Button Click 事件
    private void a(View view) {
        if (c()) {
            //MTA埋点统计
            b.a((int) j.f.New_MTA_061001001, "001", "CheckLogin");

            //获取UI控件字符串
            String trim = ((AQuery) this.a.id(2131758702)).getText().toString().trim();
            String replaceAll = ((AQuery) this.a.id(2131758704)).getText().toString().trim().replaceAll(" ", "");
            String trim2 = ((AQuery) this.a.id(2131758713)).getText().toString().trim();

            //SharedPreferences.edit().putString("login_phone", trim2);
            ai.a().a("login_phone", trim2);

            /*
            @com.csair.mbp.base.d.a.a(a =
            "com.csair.mbp.checkin.input.activity.MyBoardPassMemberOrNonOrderListActivity"
            )
            public interface ep {
                a a(
                @com.csair.mbp.base.d.a.k(a = "retrieveby") String str,
                @com.csair.mbp.base.d.a.k(a = "accountNo") String str2,
                @com.csair.mbp.base.d.a.k(a = "supplementInfo") String str3,
                @com.csair.mbp.base.d.a.k(a = "mobile") String str4
                );
            }
            */
            ((ep) d.b(ep.class, this)).a("IDFN", trim, replaceAll, trim2).b();

        }
    }

    //UI Button Click 事件
    private void b(View view) {
        if (d() && c() && e()) {
            String replaceAll;
            //MTA埋点统计
            b.a((int) j.f.New_MTA_061001001, "001", "ID/TicketNo");

            //获取UI控件字符串
            String trim = ((AQuery) this.a.id(2131758708)).getText().toString().trim();
            String trim2 = ((AQuery) this.a.id(2131758711)).getText().toString().trim();
            if (v.c()) {
                replaceAll = trim2.replaceAll(" ", "");
            } else {
                replaceAll = trim2;
            }
            String trim3 = ((AQuery) this.a.id(2131758713)).getText().toString().trim();

            //SharedPreferences.edit().putString("login_phone", trim2);
            ai.a().a("login_phone", trim3);
            if (this.b == null) {
                this.b = new ArrayList();
                this.b.add(replaceAll.toUpperCase());
                //SharedPreferences.edit().putString()
                this.d.a(this.b);
            } else if (!this.b.contains(replaceAll.toUpperCase())) {
                if (this.b.size() < 3) {
                    this.b.add(replaceAll.toUpperCase());
                } else {
                    this.b.remove(0);
                    this.b.add(replaceAll.toUpperCase());
                }
                //SharedPreferences.edit().putString()
                this.d.a(this.b);
            }
            if (this.c == null) {
                this.c = new ArrayList();
                this.c.add(trim);
                this.d.b(this.c);
            } else if (!this.c.contains(trim)) {
                if (this.c.size() < 3) {
                    this.c.add(trim);
                } else {
                    this.c.remove(0);
                    this.c.add(trim);
                }
                //SharedPreferences.edit().putString()
                this.d.b(this.c);
            }
            /*
            @com.csair.mbp.base.d.a.a(a =
            "com.csair.mbp.checkin.input.activity.MyBoardPassMemberOrNonOrderListActivity"
            )
            public interface ep {
                a a(
                @com.csair.mbp.base.d.a.k(a = "retrieveby") String str,
                @com.csair.mbp.base.d.a.k(a = "accountNo") String str2,
                @com.csair.mbp.base.d.a.k(a = "supplementInfo") String str3,
                @com.csair.mbp.base.d.a.k(a = "mobile") String str4
                );
            }
            */
            ((ep) d.b(ep.class, this)).a("TNNM", trim, replaceAll, trim3).b();
        }
    }
}

8

MyBoardPassMember…Activity.java
继续跳过。

package com.csair.mbp.checkin.input.activity;

@NBSInstrumented
public class MyBoardPassMemberOrNonOrderListActivity extends AppCompatActivity implements TraceFieldInterface {

   private void b(String str, String str2, String str3, String str4) {
        /*
        this.f is class BaseJsonQuery
        */
        if (!(this.f == null || this.f.isCancelled())) {
            this.f.cancel(true);
        }

        /*
        e is class HttpQuery
        public abstract class e extends AsyncTask<String, Integer, Object> implements TraceFieldInterface{}
        a is class BaseJsonQuery
        public class a extends e {}
        g is interface IJsonQueryDealer
        public interface g {}
        this.f = new BaseJsonQuery(Context).a( g );
        */
        this.f = new a(this).a(new e(str, str2, str3, str4));

        this.f.b(false).a(com.csair.mbp.base.i.a((int) f.URL_C064, new Object[0]), dw.a(this), dx.a(this), dy.a(this));
    }
}

9

HttpQuery.java
继续跳过。

package com.csair.mbp.c;

public abstract class e extends AsyncTask<String, Integer, Object> implements TraceFieldInterface {

    public void a(String str, f fVar, e eVar, c cVar) {
        this.k = fVar;
        this.l = eVar;
        this.m = cVar;

        /*
        加载CheckCodeUtil SO动态库,并初始化。
        */
        CheckCodeUtil.a(BaseApplication.a());

        //兼容Android 2.3系统版本,启动异步线程执行http请求。
        if (VERSION.SDK_INT < 11) {
            String[] strArr = new String[]{str};
            if (this instanceof AsyncTask) {
                NBSAsyncTaskInstrumentation.execute(this, strArr);
                return;
            } else {
                execute(strArr);
                return;
            }
        }

        //使用线程池,启动异步线程执行http请求。
        Executor newCachedThreadPool = Executors.newCachedThreadPool();
        String[] strArr2 = new String[]{str};
        if (this instanceof AsyncTask) {
            NBSAsyncTaskInstrumentation.executeOnExecutor(this, newCachedThreadPool, strArr2);
        } else {
            executeOnExecutor(newCachedThreadPool, strArr2);
        }
    }
}

10

NetworkUtils.java
继续跳过。

package com.csair.mbp.c;

@NBSInstrumented
/* compiled from: NetworkUtils */
public final class i {

    /* compiled from: NetworkUtils */
    private static class a extends Exception {
        a(String str) {
            super(str);
        }
    }

    public static String a(Context context, String str) throws Exception {
        JSONObject init = NBSJSONObjectInstrumentation.init(str);
        String optString = init.optString(dc.X);
        if (TextUtils.isEmpty(optString)) {
            return null;
        }
        String a = k.a("NETWORK_DESCRIPTION", "F");
        String substring = optString.substring(0, 1);
        if (a.equalsIgnoreCase(substring)) {
            String optString2 = init.optString(dc.Y);
            if (TextUtils.isEmpty(optString2)) {
                return null;
            }
            /*
            CheckCodeUtil 解密 optString
            */
            optString = CheckCodeUtil.a(context).a(optString);
            if (TextUtils.isEmpty(optString2)) {
                return null;
            }
            int parseInt = Integer.parseInt(optString.substring(0, 1));
            if (!optString2.startsWith(optString.substring(1, parseInt + 1))) {
                return null;
            }
            byte[] a2 = o.a(com.csair.mbp.base.f.a.a(optString2.getBytes(), optString.substring(parseInt + 1).getBytes()));
            return a2 != null ? new String(a2, "utf-8") : null;
        } else {
            k.a("NETWORK_DESCRIPTION", substring.toUpperCase());
            return null;
        }
    }

    /*
    CheckCodeUtil 加密 str
    o.b() is GZipUtil.GZip()
    */
    private static byte[] c(String str, int i) throws Exception {
        return o.b(CheckCodeUtil.a(BaseApplication.a()).a(k.a("NETWORK_DESCRIPTION", "") + str, i).getBytes("utf-8"));
    }

11

CheckCodeUtil.java
继续跳过。

package com.MobileTicket;

import android.content.Context;
import android.content.SharedPreferences;

public class CheckCodeUtil {
    private static CheckCodeUtil instance = null;
    private static SharedPreferences sharedPreferences = null;

    public static CheckCodeUtil getInstance(Context context) {
        if (instance == null) {
            instance = new CheckCodeUtil();
        }
        if (sharedPreferences == null && context != null) {
            sharedPreferences = context.getSharedPreferences("checkCodeUtilServiceInfo", 0);
        }
        return instance;
    }

    public String deCheckCode(String str) {
        byte[] decheckcode = decheckcode(str);
        return decheckcode != null ? new String(decheckcode) : null;
    }

    public String checkCode(String str, int i) {
        long j = 0;
        long currentTimeMillis = System.currentTimeMillis();
        if (sharedPreferences != null) {
            j = sharedPreferences.getLong("timeDifference", 0);
        }
        byte[] checkcode = checkcode(str, i, (j + currentTimeMillis) + "");
        return checkcode != null ? new String(checkcode) : null;
    }

    public native byte[] checkcode(String str, int i, String str2);

    public native byte[] decheckcode(String str);

    static {
        System.loadLibrary("encrypt_release_crack");
    }
}

12. Wireshark 抓包分析HTTP协议

值机查询

request:

0000   1f 8b 08 00 00 00 00 00 00 00 05 c1 4b 12 82 20  ............K..
0010   00 00 d0 03 b9 10 2a 19 5d b8 c8 cc 10 bf 24 a6  ......*.].....$.
0020   b6 63 06 c9 a6 6c d4 d0 c4 d3 f7 5e b0 be ed a9  .c...l.....^....
0030   3f b7 0a 68 39 69 1e e5 41 b7 eb bf fb 4f 0b 9d  ?..h9i..A....O..
0040   a2 33 3c 40 a8 65 20 53 c4 f0 28 58 34 e0 a5 88  .3<@.e S..(X4...
0050   69 7d 82 64 6a 9d f1 26 ac 4c 2e 9a a8 94 b0 9c  i}.dj..&.L......
0060   aa a7 c0 35 13 8f 6a 64 57 d4 bc 50 38 8c e1 61  ...5..jdW..P8..a
0070   9b 2f 54 cb b2 f2 bd 39 e6 20 dd 92 9a 17 a8 37  ./T....9. .....7
0080   e7 3b 6f 58 f6 f3 4b a1 42 e0 6d 6a c5 4d 07 f1  .;oX..K.B.mj.M..
0090   0a 13 c9 6c df c8 b8 43 5d f7 0f dd 76 b8 6f 99  ...l...C]...v.o.
00a0   00 00 00                                         ...

标准GZIP格式:

header 10 bytes: 1f 8b 08 magic number + version number + timestamp  
footer 8 bytes: CRC-32 checksum  + uncompressed data length

解压缩:

Fxl8rmEet0yfryaKPFh2ms3ne19Sh+B0JQ5+6/dL1AdTKpHvSLQXC1Jre9qVd5OfvyJtNJTPQtidHXTdgWqTR6Yk6IpqI4zuGQyfUWDBuLa0NzMXaS6m/uZaYTOwDUdtI0BztxHYh1Hx1MfT8D+Oa9Q==  

标准Base64格式:

Base64编码由大写字母,小写字母,数字,’+’,’/’组成,尾部通常有1,2个’=’。

配合Java代码逻辑解读,这里使用AES加解密,解密后数据如下,仍然是二进制密文,思路卡壳,不过已知与 libencrypt_release.so 有关。

536BC2820A4F1D78063CC3A748C29DC28C6CC2AA74C289C2B77214062A4BC28B72C38C4D145BC2AEC3B2C2B2C3984D25C3BF58C39AC398C292C392C3A54011C3A0C293C3A8C28BC38238C3A0C392C2B0C385C2BC0DC2BB00C2B9C38535C3A44019C285C3842DC28B4DC3BE1EC3A7C28D4CC29F6CC3B913C3A5C2B2C3B664C28C14C38CC3A6C3AB242AC3903A4AC3880A79C386153DC3ADC39A6E3759C3B3C2A32913C381C29C4E6CC2A25AC389594705C283C2A15D00C3925E0F60C297C283

13. NDK SO 文件反汇编分析

目标:libencrypt_release.so

Linux ELF 格式的 SO文件,有很多现成分析工具。过程省略。
找到该文件的2个关键 JNI 方法: checkcode, decheckcode。

14. NDK SO文件签名校验绕过

直接调用该SO文件的2个JNI方法会静默报错。即运行正常,但解密内容错误,无法使用。
简单分析可知,其运行时通过 JNI 反射调用 Java 层的 Android Package Signature,检查合法性。非法即返回错误值。

祭出大杀器 IDA Pro,找到一个最大嫌疑犯,字符串”162cebab80ee4e308341350f2eeadd1d”,长度32。
继续验证,它就是南航APK包签名的MD5值。
接下来就简单了,使用任意二进制编辑器,替换新密钥MD5值,跳过验证逻辑。
(南航犯了小错误,这里不该明文保存完整hardcode,至少混淆拆分隐藏一下吧。)

15. End

虽然完整的加解密算法没有逆向出来,但是 HTTP 协议及其 NDK 加解密方法已经清晰,那么剩下的就是体力活了。

tags: