模糊测试崩溃分析

问题

公司QA引入模糊测试,测试出来我们很多界面崩溃,崩溃信息如下:

Didn't find class "com.android.intentfuzzer.util.SerializableTest" on path: DexPathList[[zip file "apk路径手动隐藏"],nativeLibraryDirectories=[/apk路径/lib/arm, /system/fake-libs, /apk路径//base.apk!/lib/armeabi-v7a, /system/lib, /vendor/lib]]
    at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:380)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
    at java.lang.Class.classForName(Native Method)
    at java.lang.Class.forName(Class.java:400)
    at android.os.Parcel$2.resolveClass(Parcel.java:2616)
    at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1613)
    at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1518)
    at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1772)
    at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1351)
    at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
    at android.os.Parcel.readSerializable(Parcel.java:2624)
    at android.os.Parcel.readValue(Parcel.java:2416)
    at android.os.Parcel.readArrayMapInternal(Parcel.java:2732)
    at android.os.BaseBundle.unparcel(BaseBundle.java:269)
    at android.os.BaseBundle.getString(BaseBundle.java:992)
    at android.content.Intent.getStringExtra(Intent.java:6289)
    at 业务界面通过intent的getStringExtra获取参数

分析

什么是模糊测试?

百科 第一句话 :模糊测试[Fuzzing],是一种通过向目标系统提供非预期的输入并监视异常结果来发现软件漏洞的方法。

为什么崩溃?

从崩溃栈可以看出,原因在于在通过intent获取参数时,有些参数找不到对应的类型

为什么我们调用getStringExtra会需要某些不存在的类型呢?

源码分析

我们一步步查看源码来解析

// Intent.java
    public String getStringExtra(String name) {
        return mExtras == null ? null : mExtras.getString(name); // 调用的Bundle的getString方法
    }

// Bundle继承自BaseBundle, BaseBundle.java
    @Nullable
    public String getString(@Nullable String key) {
        unparcel(); //先解析bundle数据
        final Object o = mMap.get(key); // 再从解析后的Map中获取数据
        try {
            return (String) o;
        } catch (ClassCastException e) {
            typeWarning(key, o, "String", e);
            return null;
        }
    }

    /* package */ void unparcel() {
        synchronized (this) {
            final Parcel parcelledData = mParcelledData;
            if (parcelledData == null) {
                if (DEBUG) Log.d(TAG, "unparcel "
                        + Integer.toHexString(System.identityHashCode(this))
                        + ": no parcelled data");
                return;
            }

            int N = parcelledData.readInt();
            // 省略一些非关键代码
            ArrayMap<String, Object> map = mMap;
            // 省略一些非关键代码
            try {
                parcelledData.readArrayMapInternal(map, N, mClassLoader); // 调用此方法解析数据
            } catch (BadParcelableException e) {
                // 省略一些非关键代码
            } finally {
                mMap = map;
                parcelledData.recycle();
                mParcelledData = null;
            }
            // 省略一些非关键代码
        }
    }

    /* package */ void readArrayMapInternal(ArrayMap outVal, int N,
        ClassLoader loader) {
       // 省略一些非关键代码
        int startPos;
        while (N > 0) { 
            if (DEBUG_ARRAY_MAP) startPos = dataPosition();
            String key = readString(); // 这里读取key
            Object value = readValue(loader);  // 这里读取value
            // 省略一些非关键代码
            outVal.append(key, value);
            N--;
        }
        outVal.validate();
    } 

    /**
     * Read a typed object from a parcel.  The given class loader will be
     * used to load any enclosed Parcelables.  If it is null, the default class
     * loader will be used.
     */
    public final Object readValue(ClassLoader loader) {
        int type = readInt(); // 每个value 首先有个int值表示类型

        switch (type) {
        case VAL_NULL:
            return null;

        case VAL_STRING:
            return readString();

        case VAL_INTEGER:
            return readInt();

        case VAL_MAP:
            return readHashMap(loader);

        case VAL_PARCELABLE:
            return readParcelable(loader);

        case VAL_SHORT:
            return (short) readInt();

        case VAL_LONG:
            return readLong();

        case VAL_FLOAT:
            return readFloat();

        case VAL_DOUBLE:
            return readDouble();

        case VAL_BOOLEAN:
            return readInt() == 1;

        case VAL_CHARSEQUENCE:
            return readCharSequence();

        case VAL_LIST:
            return readArrayList(loader);

        case VAL_BOOLEANARRAY:
            return createBooleanArray();

        case VAL_BYTEARRAY:
            return createByteArray();

        case VAL_STRINGARRAY:
            return readStringArray();

        case VAL_CHARSEQUENCEARRAY:
            return readCharSequenceArray();

        case VAL_IBINDER:
            return readStrongBinder();

        case VAL_OBJECTARRAY:
            return readArray(loader);

        case VAL_INTARRAY:
            return createIntArray();

        case VAL_LONGARRAY:
            return createLongArray();

        case VAL_BYTE:
            return readByte();

        case VAL_SERIALIZABLE:
            return readSerializable(loader); // 这里就是我们报错的地方

        case VAL_PARCELABLEARRAY:
            return readParcelableArray(loader);

        case VAL_SPARSEARRAY:
            return readSparseArray(loader);

        case VAL_SPARSEBOOLEANARRAY:
            return readSparseBooleanArray();

        case VAL_BUNDLE:
            return readBundle(loader); // loading will be deferred

        case VAL_PERSISTABLEBUNDLE:
            return readPersistableBundle(loader);

        case VAL_SIZE:
            return readSize();

        case VAL_SIZEF:
            return readSizeF();

        default:
            int off = dataPosition() - 4;
            throw new RuntimeException(
                "Parcel " + this + ": Unmarshalling unknown type code " + type + " at offset " + off);
        }
    }

    /**
     * Read and return a new Serializable object from the parcel.
     * @return the Serializable object, or null if the Serializable name
     * wasn't found in the parcel.
     */
    public final Serializable readSerializable() {
        return readSerializable(null);
    }

    private final Serializable readSerializable(final ClassLoader loader) {
        String name = readString();
        if (name == null) {
            // For some reason we were unable to read the name of the Serializable (either there
            // is nothing left in the Parcel to read, or the next value wasn't a String), so
            // return null, which indicates that the name wasn't found in the parcel.
            return null;
        }

        byte[] serializedData = createByteArray();
        ByteArrayInputStream bais = new ByteArrayInputStream(serializedData);
        try {
            ObjectInputStream ois = new ObjectInputStream(bais) {
                @Override
                protected Class<?> resolveClass(ObjectStreamClass osClass)
                        throws IOException, ClassNotFoundException {
                    // try the custom classloader if provided
                    if (loader != null) {
                        Class<?> c = Class.forName(osClass.getName(), false, loader); // 此处会根据类名查找类
                        if (c != null) {
                            return c;
                        }
                    }
                    return super.resolveClass(osClass);
                }
            };
            return (Serializable) ois.readObject();
        } catch (IOException ioe) {
            throw new RuntimeException("Parcelable encountered " +
                "IOException reading a Serializable object (name = " + name +
                ")", ioe);
        } catch (ClassNotFoundException cnfe) { // 崩溃最开始的异常就是此处抛出的
            throw new RuntimeException("Parcelable encountered " +
                "ClassNotFoundException reading a Serializable object (name = "
                + name + ")", cnfe);
        }
    }

流程简单总结:

  1. 请求Inent的getStringExtra
  2. 请求Bundle的getString
  3. Bundle首先解析Parcel数据
  4. Parcel会把按照格式 读出每个数据的类型和值, 对于Serializable类型,会确认Class
  5. Bundle通过解析后的Map返回数据

回到最开始的问题:为什么我们调用getStringExtra会需要某些不存在的类型呢?
原因在于

  1. Bundle数据中存在我们代码里没有的Serializable类型
  2. 虽然我们只想获取String,但是Bundle会先解析所有数据

难道每次获取数据,都会先解析一遍所有数据吗?
不会,我们再看解析数据unparcel的实现,就会发现,解析完之后就会把Parcel置空,下次就不解析了,而数据都已经缓存到Map中了

面对这样的模糊测试,我们应该怎么办?

也许你会想, 我们把异常try catch就行了
曾经我们的方案如下:

  1. 所有的intent获取参数都添加try catch(通过工具类封装)
  2. 必选的参数如果获取不到,就finish界面,不响应
  3. 可选的参数如果获取不到,就使用默认参数

但是这个方案还是有问题的!!
为什么?

原因在于 Parcel是顺序解析,一旦有异常,就会抛出中断解析,这样其实是只解析了前半部分数据,如果我们需要的数据在后半段,就获取不到了。
本质上,一旦这个Parcel有异常数据,那么其实整个Parcel的数据都应该是不可信的了。
所以想一想 觉得还是出异常直接finish比较好。

额外的收获Parcel的数据存储结构

结构大概如下:
count + key type value + key type value
count是Parcel键值对的总个数 int类型,
key type value 是一个键值对
key是String类型
type是int, 所有的类型可以看readValue源码
value的类型取决与type

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353

推荐阅读更多精彩内容

  • 这里强烈建议把前面两篇文章看一遍,因为前面两篇文章对后面大家对android的IPC的理解帮助很大,本片文章主要内...
    Sophia_dd35阅读 907评论 0 4
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,401评论 0 17
  • 1、Intent 可以传递的数据类型 在 Intent和IntentFilters(1) 中我们知道了通过 Int...
    CnPeng阅读 2,769评论 0 10
  • 今天戴老师给我们借来了三年级的书,让我们记笔记,这是在为写春天做准备。借这个机会我还读了其他的课文,又一...
    小王子WXN阅读 216评论 0 1
  • 最好的月光 /小聪 听见夜的酣声 关上了盼望的门 这个夜晚 依然写满了宁静 微风凌乱了谁的深情 闪烁的星光 孜孜不...
    令狐小聪阅读 224评论 0 0