Android Intent從入門到熟練以及Parcelable序列化傳遞複雜數據容易引發的安全問題

0x10 Intent 組件消息傳遞

Intent是一個重要的類,用於Android組件之間的消息傳遞。我相信你會在任何一個正常的Android應用中發現它的足跡。

Intent是Android程序中各組件之間進行交互的一種重要方式,它不僅可以指明當前組件想要執行的動作,還可以在不同組件之間傳遞數據。Intent一般可被用於啓動活動、啓動服務以及發送廣播等場景

Intent大致可以分爲兩種:顯式Intent 和隱式Intent。

0x11 顯式Intent

顯式Intent,也就是很明顯的使用Intent告訴系統我想啓動哪個組件。新建一個項目,命名爲SendIntent,在MainActivity輸入以下代碼。

button.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, SecondActivity.class);
                startActivity(intent);
            }
        });

Intent的構造方法有很多重載,這裏,我們用的是其中的一個構造方法,第一個參數是當前活動,第二個參數是想要拉起的Activity。

再建立一個空的Activity,命名爲SecondActivity,這樣就是用第一個Activity顯示的啓動第二個Activity。比較簡單,不再贅述。
在這裏插入圖片描述

0x12 隱式Intent

實際的項目當中,較少使用這種顯式Intent傳遞,我們更多的可能會遇到隱藏意圖的Intent。這種用法,不會直接說明我們要啓動哪個活動,或者發送消息給哪個組件。

新建一個項目,作爲一個單獨的,要被拉起的App,在其中新建SecondActivity,佈局文件請隨意,也不需要在MainActivity裏面加入過多的,只需要讓我們能夠識別到這是第二個App(TestApplication)中的活動。那麼,怎麼讓別的App識別到這個App中的相關組件呢?最關鍵的就是在Manifest.xml文件中,加入以下代碼

<activity android:name=".SecondActivity" android:exported="true" android:label="@string/title_activity_second" android:theme="@style/AppTheme.NoActionBar">
	<intent-filter>
	    <action android:name="android.intent.action.SECOND_START" />
	    <category android:name="android.intent.category.DEFAULT" />
	    <category android:name="android.intent.category.MY_CATEGORY" />
	</intent-filter>
</activity>

要想讓該應用的SecondActivity被其他應用識別到,必須加入 android:exported=“true”action標籤指明瞭當前活動可以響應 android.intent.action.MAIN_START這個action,category標籤是對intent更加細粒度的劃分。每個Intent只能指定一個action,但卻可以指定多個category

如果想拉起這個應用的該活動,我們需要在自己的應用SendIntent加入以下代碼

button.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View view) {
        Intent intent = new Intent("android.intent.action.SECOND_START");
        intent.addCategory("android.intent.category.MY_CATEGORY");
        startActivity(intent);
    }
});

這樣就是一個隱式的Intent傳遞,可使用SendIntent輕鬆拉起我們的TestApplication中的SecondActivity。
在這裏插入圖片描述

0x13 組件間消息傳遞

說了這麼多,Intent怎麼傳遞消息呢?在啓動活動時傳遞數據的思路很簡單,Intent中提供了一系列putExtra() 方法的重載,可以把我們想要傳遞的數據暫存在Intent中,啓動了另一個活動後,只需要把這些數據再從Intent中取出就可以了。也是類似的,我們修改SendIntent的代碼如下

button.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View view) {
        Intent intent = new Intent("android.intent.action.SECOND_START");
        intent.addCategory("android.intent.category.MY_CATEGORY");
        intent.putExtra("TestKey", "我是SendIntent傳遞的數據!");
        startActivity(intent);
    }
});

再編寫TestApplication中的SecondActivity,添加如下代碼

Intent intent = getIntent();
String data = intent.getStringExtra("TestKey");
Toast.makeText(SecondActivity.this, data, Toast.LENGTH_LONG).show();

這樣就完成了從一個活動向另外一個應用的活動傳遞數據的功能,我們使用的是隱式的方式。
在這裏插入圖片描述

0x20 本應用內傳遞與跨應用傳遞

我們對第一節的內容進行一下總結,可以得到這麼一個結論:如果只是一個應用內的組件之間的消息傳遞,那麼使用顯示的Intent就可以完成,這種方式直接調用Intent(FirstActivity.this, SecondActivity.class)的構造方法就可以完成;如果是跨應用的組件消息傳遞呢?

0x21 跨應用組件消息傳遞

  • 方法一:使用我們在0x13節,隱式Intent
  • 方法二:使用Component類就可以指定哪個包名下的哪個組件。(相當於顯式Intent)

修改我們的SendIntent如下

button.setOnClickListener(new View.OnClickListener(){
    @Override
    public void onClick(View view) {
        Intent intent = new Intent();
        ComponentName componentName = new ComponentName("com.example.testapplication", "com.example.testapplication.SecondActivity");
        intent.setComponent(componentName);
        intent.putExtra("TestKey", "我是SendIntent傳遞的數據!");
        startActivity(intent);
    }
});

運行該應用,達到的效果是和0x13節介紹的一樣。

0x30 使用Parcelable序列化傳遞複雜數據

進行Android開發的時候,無法將對象的引用傳給Activities或者Fragments,我們需要將這些對象放到一個Intent或者Bundle裏面,然後再傳遞。簡單來說就是將對象轉換爲可以傳輸的二進制流(二進制序列)的過程,這樣我們就可以通過序列化,轉化爲可以在網絡傳輸或者保存到本地的流(序列),從而進行傳輸數據 ,那反序列化就是從二進制流(序列)轉化爲對象的過程。

Parcelable是Android爲我們提供的序列化的接口,Parcelable相對於Serializable的使用相對複雜一些,但Parcelable的效率相對Serializable也高很多,這一直是Google工程師引以爲傲的,有時間的可以看一下Parcelable和Serializable的效率對比 Parcelable vs Serializable 號稱快10倍的效率——《簡書:Android中Parcelable的原理和使用方法》

網絡上有關其詳細介紹不勝枚舉,在這裏也不展開來說了。

0x40 Intent與Parcelable暴露組件的安全問題

其實講了這麼多,都是爲了介紹暴露的組件可能引發的一種攻擊,DOS攻擊,當一個組件可被其他應用傳遞消息的時候,需要對接收的Intent進行過濾,不然很容易引發崩潰,我們舉例來說,剛好以實際項目中經常遇到的情況,來說明問題,也剛好說明0x30節,Parcelable帶來的隱藏風險。

0x41 案例分析

以下代碼是一個正常應用的一個組件

package com.lys.testapplication;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;

public class MainActivity extends AppCompatActivity {
    private final String TAG = "testapplication";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        a(getIntent());

    }

    private void a(Intent paramIntent){
        if(paramIntent == null){
            return;
        }

        if(paramIntent.getType() != null && "vnd.wfa.wsc".equals(paramIntent.getType())){
            Parcelable[] parcelable = paramIntent.getParcelableArrayExtra("android.test.extra.TEST_MESSAGES");
            Person b = (Person) parcelable[0];
        }else {
            Log.d(TAG, "NULL!");
        }
    }
}

這個MainActivity組件接收Intent,並且使用getType()設置了接收的MIME類型,關於該方法的詳細使用,網上也有例子。總之在發送端使用setType(),就可以設置MIME類型,以匹配getType()。這段代碼是一種常見的接收Parceable序列化對象的寫法,問題的根源在於沒有對異常進行處理,收到構造的Intent,程序會崩潰。那麼怎麼進行構造呢?我們編寫IntentDemo如下

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ComponentName componentName = new ComponentName("com.lys.testapplication", "com.lys.testapplication.MainActivity");
                Intent intent = new Intent();
                intent.setComponent(componentName);
                intent.setType("vnd.wfa.wsc");
                Person[] person = new Person[0];
                intent.putExtra("android.test.extra.TEST_MESSAGES", person);
                startActivity(intent);
            }
        });
    }
}

這個應用使用ComponentName類,顯示指定Intent要傳遞的對象,並且使用setType(),設置符合的MIME類型。注意,無論是目標應用com.lys.testapplication,還是我們的IntentDemo.apk,都需要新建一個共有的類,Person(),Parcelable序列化Intent傳遞的就是這個類構造的對象。

public class Person implements Parcelable {
    private String name;
    private int age;

    @Override
    public int describeContents(){
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags){
        dest.writeString(name);
        dest.writeInt(age);
    }

    public static final Creator<Person> CREATOR = new Creator<Person>() {
        @Override
        public Person createFromParcel(Parcel source) {
            Person person = new Person();
            person.name = source.readString();//讀取name
            person.age = source.readInt();//讀取age
            return person;
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };
}

運行我們的IntentDemo.apk,得到如下結果。點擊按鈕,我們觀察彈窗,發現是目標應用TestApplication已經崩潰,說明達到效果。
在這裏插入圖片描述

0x42 原因分析

觀察應用的日誌
在這裏插入圖片描述
取第一行日誌

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.lys.testapplication/com.lys.testapplication.MainActivity}: java.lang.ArrayIndexOutOfBoundsException: length=0; index=0

可以看到是數組越界引發的異常。也就是說,我們使用

Person[] person = new Person[0];

傳遞了一個長度爲0的空數組,但是目標應用並沒有對數組長度進行判斷,就直接引用

Person b = (Person) parcelable[0];

導致出現了數組越界。那麼我們對IntentDemo進行修改,傳遞一個數組長度大於0的對象呢?是不是目標應用TestApplication就不會產生異常了呢?答案是否定的。

0x43 另外一種情況

修改IntentDemo的Person[] person這一行代碼,修改成的代碼如下。這次我們傳遞了一個長度爲1的數組,按照正常情況來說,TestApplication應該就不會崩潰了。

Person[] person = new Person[1];

在這裏插入圖片描述
結果如下,也確實能夠成功拉其該應用,但是我們的目標應用只是個測試案例,接受了person對象,並沒有使用,正常的應用是會使用這個對象的,如果我們在TestApplication代碼中使用person對象,那麼仍然會造成異常。只是這次的異常並非數組越界,而是
在這裏插入圖片描述
我們取第一行的日誌

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.lys.testapplication/com.lys.testapplication.MainActivity}: java.lang.NullPointerException: Attempt to read from null array

儘管person[0]這次沒有越界了,但是元素內容爲空,也會導致空指針異常,這就類似於,我們修改IntentDemo的Person[] person如

String[] person = new String();

可以達到一樣的效果,目標應用並沒有對元素的內容進行檢查,導致應用崩潰。

0x50 總結

無論是使用Intent傳遞簡單的數據,還是使用Parcelable序列化以後的數據,對外部尤其是那些三方組件傳過來的對象,一定要進行異常檢測和數據類型校驗,否則三方應用可能發送一個簡單的Intent就會導致應用崩潰。

發佈了26 篇原創文章 · 獲贊 22 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章