Android 插件化原理解析——Hook機(jī)制之AMS&PMS

轉(zhuǎn):http://weishu.me/2016/03/07/understand-plugin-framework-ams-pms-hook/

在前面的文章中我們介紹了DroidPlugin的Hook機(jī)制玉转,也就是代理方式Binder Hook突想;插件框架通過AOP實(shí)現(xiàn)了插件使用和開發(fā)的透明性。在講述DroidPlugin如何實(shí)現(xiàn)四大組件的插件化之前,有必要說明一下它對(duì)ActivityManagerServiche以及PackageManagerService的Hook方式(以下簡(jiǎn)稱AMS猾担,PMS)袭灯。

ActivityManagerService對(duì)于FrameWork層的重要性不言而喻,Android的四大組件無一不與它打交道:

startActivity最終調(diào)用了AMS的startActivity系列方法绑嘹,實(shí)現(xiàn)了Activity的啟動(dòng)稽荧;Activity的生命周期回調(diào),也在AMS中完成工腋;

startService,bindService最終調(diào)用到AMS的startService和bindService方法姨丈;

動(dòng)態(tài)廣播的注冊(cè)和接收在AMS中完成(靜態(tài)廣播在PMS中完成)

getContentResolver最終從AMS的getContentProvider獲取到ContentProvider

而PMS則完成了諸如權(quán)限校撿(checkPermission,checkUidPermission),Apk meta信息獲取(getApplicationInfo等)擅腰,四大組件信息獲取(query系列方法)等重要功能蟋恬。

在上文Android插件化原理解析——Hook機(jī)制之Binder Hook中講述了DroidPlugin的Binder Hook機(jī)制;我們知道AMS和PMS就是以Binder方式提供給應(yīng)用程序使用的系統(tǒng)服務(wù)趁冈,理論上我們也可以采用這種方式Hook掉它們歼争。但是由于這兩者使用得如此頻繁,F(xiàn)ramework給他們了一些“特別優(yōu)待”箱歧,這也給了我們相對(duì)于Binder Hook更加穩(wěn)定可靠的hook方式。

閱讀本文之前一膨,可以先clone一份understand-plugin-framework呀邢,參考此項(xiàng)目的ams-pms-hook模塊。另外豹绪,插件框架原理解析系列文章見索引价淌。

AMS獲取過程

前文提到Android的四大組件無一不與AMS相關(guān),也許讀者還有些許疑惑瞒津;這里我就挑一個(gè)例子蝉衣,依據(jù)Android源碼來說明,一個(gè)簡(jiǎn)單的startActivity是如何調(diào)用AMS最終通過IPC到system_server的巷蚪。

不論讀者是否知道病毡,我們使用startActivity有兩種形式:

直接調(diào)用Context類的startActivity方法;這種方式啟動(dòng)的Activity沒有Activity棧屁柏,因此不能以standard方式啟動(dòng)啦膜,必須加上FLAG_ACTIVITY_NEW_TASK這個(gè)Flag。

調(diào)用被Activity類重載過的startActivity方法淌喻,通常在我們的Activity中直接調(diào)用這個(gè)方法就是這種形式僧家;

Context.startActivity

我們查看Context類的startActivity方法,發(fā)現(xiàn)這竟然是一個(gè)抽象類裸删;查看Context的類繼承關(guān)系圖如下:

我們看到諸如Activity八拱,Service等并沒有直接繼承Context,而是繼承了ContextWrapper;繼續(xù)查看ContextWrapper的實(shí)現(xiàn):

@Override

publicvoidstartActivity(Intent intent){

mBase.startActivity(intent);

}

WTF!! 果然人如其名肌稻,只是一個(gè)wrapper而已清蚀;這個(gè)mBase是什么呢?這里我先直接告訴你灯萍,它的真正實(shí)現(xiàn)是ContextImpl類轧铁;至于為什么,有一條思路:mBase是在ContextWrapper構(gòu)造的時(shí)候傳遞進(jìn)來的旦棉,那么在ContextWrapper構(gòu)造的時(shí)候可以找到答案

什么時(shí)候會(huì)構(gòu)造ContextWrapper呢齿风?它的子類Application,Service等被創(chuàng)建的時(shí)候绑洛。

可以在App的主線程AcitivityThread的performLaunchActivit方法里面找到答案救斑;更詳細(xì)的解析可以參考老羅的Android應(yīng)用程序啟動(dòng)過程源代碼分析

好了,我們姑且當(dāng)作已經(jīng)知道Context.startActivity最終使用了ContextImpl里面的方法真屯,代碼如下:

publicvoidstartActivity(Intent intent, Bundle options){

warnIfCallingFromSystemProcess();

if((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) ==0) {

thrownewAndroidRuntimeException(

"Calling startActivity() from outside of an Activity "

+" context requires the FLAG_ACTIVITY_NEW_TASK flag."

+" Is this really what you want?");

}

mMainThread.getInstrumentation().execStartActivity(

getOuterContext(), mMainThread.getApplicationThread(),null,

(Activity)null, intent, -1, options);

}

代碼相當(dāng)簡(jiǎn)單脸候;我們知道了兩件事:

其一,我們知道了在Service等非Activity的Context里面啟動(dòng)Activity為什么需要添加FLAG_ACTIVITY_NEW_TASK绑蔫;

其二运沦,真正的startActivity使用了Instrumentation類的execStartActivity方法;繼續(xù)跟蹤:

publicActivityResultexecStartActivity(

Context who, IBinder contextThread, IBinder token, Activity target,

Intent intent,intrequestCode, Bundle options){

// ... 省略無關(guān)代碼

try{

intent.migrateExtraStreamToClipData();

intent.prepareToLeaveProcess();

// ----------------look here!!!!!!!!!!!!!!!!!!!

intresult = ActivityManagerNative.getDefault()

.startActivity(whoThread, who.getBasePackageName(), intent,

intent.resolveTypeIfNeeded(who.getContentResolver()),

token, target !=null? target.mEmbeddedID :null,

requestCode,0,null,null, options);

checkStartActivityResult(result, intent);

}catch(RemoteException e) {

}

returnnull;

}

到這里我們發(fā)現(xiàn)真正調(diào)用的是ActivityManagerNative的startActivity方法配深;如果你不清楚ActivityManager携添,ActivityManagerService以及ActivityManagerNative之間的關(guān)系;建議先仔細(xì)閱讀我之前關(guān)于Binder的文章Binder學(xué)習(xí)指南篓叶。

Activity.startActivity

Activity類的startActivity方法相比Context而言直觀了很多烈掠;這個(gè)startActivity通過若干次調(diào)用輾轉(zhuǎn)到達(dá)startActivityForResult這個(gè)方法,在這個(gè)方法內(nèi)部有如下代碼:

Instrumentation.ActivityResult ar =

mInstrumentation.execStartActivity(

this, mMainThread.getApplicationThread(), mToken,this,

intent, requestCode, options);

可以看到缸托,其實(shí)通過Activity和ContextImpl類啟動(dòng)Activity并無本質(zhì)不同左敌,他們都通過Instrumentation這個(gè)輔助類調(diào)用到了ActivityManagerNative的方法羽戒。

Hook AMS

OK梁棠,我們到現(xiàn)在知道彻桃;其實(shí)startActivity最終通過ActivityManagerNative這個(gè)方法遠(yuǎn)程調(diào)用了AMS的startActivity方法州邢。那么這個(gè)ActivityManagerNative是什么呢届巩?

ActivityManagerNative實(shí)際上就是ActivityManagerService這個(gè)遠(yuǎn)程對(duì)象的Binder代理對(duì)象避归;每次需要與AMS打交道的時(shí)候礼烈,需要借助這個(gè)代理對(duì)象通過驅(qū)動(dòng)進(jìn)而完成IPC調(diào)用峭拘。

我們繼續(xù)看ActivityManagerNative的getDefault()方法做了什么:

staticpublicIActivityManagergetDefault(){

returngDefault.get();

}

gDefault這個(gè)靜態(tài)變量的定義如下:

private static final Singleton gDefault = new Singleton() {

protected IActivityManager create() {

IBinder b = ServiceManager.getService("activity

IActivityManager am = asInterface(

return am;

}

};

由于整個(gè)Framework與AMS打交道是如此頻繁匹摇,framework使用了一個(gè)單例把這個(gè)AMS的代理對(duì)象保存了起來咬扇;這樣只要需要與AMS進(jìn)行IPC調(diào)用,獲取這個(gè)單例即可廊勃。這是AMS這個(gè)系統(tǒng)服務(wù)與其他普通服務(wù)的不同之處懈贺,也是我們不通過Binder Hook的原因——我們只需要簡(jiǎn)單地Hook掉這個(gè)單例即可经窖。

這里還有一點(diǎn)小麻煩:Android不同版本之間對(duì)于如何保存這個(gè)單例的代理對(duì)象是不同的;Android 2.x系統(tǒng)直接使用了一個(gè)簡(jiǎn)單的靜態(tài)變量存儲(chǔ)梭灿,Android 4.x以上抽象出了一個(gè)Singleton類画侣;具體的差異可以使用grepcode進(jìn)行比較:差異

我們以4.x以上的代碼為例說明如何Hook掉AMS;方法使用的動(dòng)態(tài)代理堡妒,如果有不理解的配乱,可以參考之前的系列文章Android插件化原理解析——Hook機(jī)制之動(dòng)態(tài)代理


Class activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");

// 獲取 gDefault 這個(gè)字段, 想辦法替換它

Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");

gDefaultField.setAccessible(true);

Object gDefault = gDefaultField.get(null);

// 4.x以上的gDefault是一個(gè) android.util.Singleton對(duì)象; 我們?nèi)〕鲞@個(gè)單例里面的字段

Class singleton = Class.forName("android.util.Singleton");

Field mInstanceField = singleton.getDeclaredField("mInstance");

mInstanceField.setAccessible(true);

// ActivityManagerNative 的gDefault對(duì)象里面原始的 IActivityManager對(duì)象

Object rawIActivityManager = mInstanceField.get(gDefault);

// 創(chuàng)建一個(gè)這個(gè)對(duì)象的代理對(duì)象, 然后替換這個(gè)字段, 讓我們的代理對(duì)象幫忙干活

Class iActivityManagerInterface = Class.forName("android.app.IActivityManager");

Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),

newClass[] { iActivityManagerInterface },newIActivityManagerHandler(rawIActivityManager));

mInstanceField.set(gDefault, proxy);

好了,我們hook成功之后啟動(dòng)Activity看看會(huì)發(fā)生什么:

D/HookHelper﹕ hey, baby; you are hook!!

D/HookHelper﹕method:activityResumed calledwithargs:[android.os.BinderProxy@9bc71b2]

D/HookHelper﹕ hey, baby; you are hook!!

D/HookHelper﹕method:activityIdle calledwithargs:[android.os.BinderProxy@9bc71b2, null,false]

D/HookHelper﹕ hey, baby; you are hook!!

D/HookHelper﹕method:startActivity calledwithargs:[android.app.ActivityThread$ApplicationThread@17e750c, com.weishu.upf.ams_pms_hook.app, Intent{ act=android.intent.action.VIEW dat=http://wwww.baidu.com/... }, null, android.os.BinderProxy@9bc71b2, null, -1,0, null, null]

D/HookHelper﹕ hey, baby; you are hook!!

D/HookHelper﹕method:activityPaused calledwithargs:[android.os.BinderProxy@9bc71b2]

可以看到皮迟,簡(jiǎn)單的幾行代碼搬泥,AMS已經(jīng)被我們完全劫持了!! 至于劫持了能干什么,自己發(fā)揮想象吧~

DroidPlugin關(guān)于AMS的Hook伏尼,可以查看IActivityManagerHook這個(gè)類忿檩,它處理了我上述所說的兼容性問題,其他原理相同爆阶。另外燥透,也許有童鞋有疑問了,你用startActivity為例怎么能確保Hook掉這個(gè)靜態(tài)變量之后就能保證所有使用AMS的入口都被Hook了呢辨图?

答曰:無他班套,唯手熟爾。

Android Framewrok層對(duì)于四大組件的處理故河,調(diào)用AMS服務(wù)的時(shí)候吱韭,全部都是通過使用這種方式;若有疑問可以自行查看源碼忧勿。你可以從Context類的startActivity, startService,bindService, registerBroadcastReceiver, getContentResolver 等等入口進(jìn)行跟蹤杉女,最終都會(huì)發(fā)現(xiàn)它們都會(huì)使用ActivityManagerNative的這個(gè)AMS代理對(duì)象來完成對(duì)遠(yuǎn)程AMS的訪問瞻讽。

PMS獲取過程

PMS的獲取也是通過Context完成的鸳吸,具體就是getPackageManager這個(gè)方法;我們姑且當(dāng)作已經(jīng)知道了Context的實(shí)現(xiàn)在ContextImpl類里面速勇,直奔ContextImpl類的getPackageManager方法:


publicPackageManagergetPackageManager(){

if(mPackageManager !=null) {

returnmPackageManager;

}

IPackageManager pm = ActivityThread.getPackageManager();

if(pm !=null) {

// Doesn't matter if we make more than one instance.

return(mPackageManager =newApplicationPackageManager(this, pm));

}

returnnull;

}

可以看到晌砾,這里干了兩件事:

真正的PMS的代理對(duì)象在ActivityThread類里面

ContextImpl通過ApplicationPackageManager對(duì)它還進(jìn)行了一層包裝

我們繼續(xù)查看ActivityThread類的getPackageManager方法,源碼如下:

publicstaticIPackageManagergetPackageManager(){

if(sPackageManager !=null) {

returnsPackageManager;

}

IBinder b = ServiceManager.getService("package");

sPackageManager = IPackageManager.Stub.asInterface(b);

returnsPackageManager;

}

可以看到烦磁,和AMS一樣养匈,PMS的Binder代理對(duì)象也是一個(gè)全局變量存放在一個(gè)靜態(tài)字段中;我們可以如法炮制都伪,Hook掉PMS呕乎。

現(xiàn)在我們的目的很明切,如果需要HookPMS有兩個(gè)地方需要Hook掉:

ActivityThread的靜態(tài)字段sPackageManager

通過Context類的getPackageManager方法獲取到的ApplicationPackageManager對(duì)象里面的mPM字段陨晶。

Hook PMS

現(xiàn)在使用代理Hook應(yīng)該是輕車熟路了吧猬仁,通過上面的分析帝璧,我們Hook兩個(gè)地方;代碼信手拈來:

// 獲取全局的ActivityThread對(duì)象

Class activityThreadClass = Class.forName("android.app.ActivityThread");

Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");

Object currentActivityThread = currentActivityThreadMethod.invoke(null);

// 獲取ActivityThread里面原始的 sPackageManager

Field sPackageManagerField = activityThreadClass.getDeclaredField("sPackageManager");

sPackageManagerField.setAccessible(true);

Object sPackageManager = sPackageManagerField.get(currentActivityThread);

// 準(zhǔn)備好代理對(duì)象, 用來替換原始的對(duì)象

Class iPackageManagerInterface = Class.forName("android.content.pm.IPackageManager");

Object proxy = Proxy.newProxyInstance(iPackageManagerInterface.getClassLoader(),

newClass[] { iPackageManagerInterface },

newHookHandler(sPackageManager));

// 1. 替換掉ActivityThread里面的 sPackageManager 字段

sPackageManagerField.set(currentActivityThread, proxy);

// 2. 替換 ApplicationPackageManager里面的 mPM對(duì)象

PackageManager pm = context.getPackageManager();

Field mPmField = pm.getClass().getDeclaredField("mPM");

mPmField.setAccessible(true);

mPmField.set(pm, proxy);

好了湿刽,Hook完畢我們驗(yàn)證以下結(jié)論的烁;調(diào)用一下PMS的getInstalledApplications方法,打印日志如下:

03-0715:07:27.1878306-8306/com.weishu.upf.ams_pms_hook.app D/IActivityManagerHandler﹕ hey, baby; you are hook!!

03-0715:07:27.1878306-8306/com.weishu.upf.ams_pms_hook.app D/IActivityManagerHandler﹕ method:getInstalledApplications called with args:[0,0]

OK诈闺,我們又成功劫持了PackageManager?是臁!DroidPlugin 處理PMS的代碼可以在IPackageManagerHook查看雅镊。

在結(jié)束講解PackageManager的Hook之前襟雷,我們需要說明一點(diǎn);那就是Context的實(shí)現(xiàn)類里面沒有使用靜態(tài)全局變量來保存PMS的代理對(duì)象漓穿,而是每擁有一個(gè)Context的實(shí)例就持有了一個(gè)PMS代理對(duì)象的引用嗤军;所以這里有個(gè)很蛋疼的事情,那就是我們?nèi)绻胍耆獺ook住PMS晃危,需要精確控制整個(gè)進(jìn)程內(nèi)部創(chuàng)建的Context對(duì)象叙赚;所幸,插件框架中僚饭,插件的Activity震叮,Service,ContentProvider鳍鸵,Broadcast等所有使用到Context的地方苇瓣,都是由框架控制創(chuàng)建的;因此我們要小心翼翼地替換掉所有這些對(duì)象持有的PMS代理對(duì)象偿乖。

我前面也提到過击罪,靜態(tài)變量和單例都是良好的Hook點(diǎn),這里很好地反證了這句話:想要Hook掉一個(gè)實(shí)例變量該是多么麻煩!

小結(jié)

寫到這里贪薪,關(guān)于DroidPlugin的Hook技術(shù)的講解已經(jīng)完結(jié)了媳禁;我相信讀者或多或少地認(rèn)識(shí)到,其實(shí)Hook并不是一項(xiàng)神秘的技術(shù)画切;一個(gè)干凈竣稽,透明的框架少不了AOP,而AOP也少不了Hook霍弹。

我所講解的Hook僅僅使用反射和動(dòng)態(tài)代理技術(shù)毫别,更加強(qiáng)大的Hook機(jī)制可以進(jìn)行字節(jié)碼編織,比如J2EE廣泛使用了cglib和asm進(jìn)行AOP編程典格;而Android上現(xiàn)有的插件框架還是加載編譯時(shí)代碼岛宦,采用動(dòng)態(tài)生成類的技術(shù)理論上也是可行的;之前有一篇文章Android動(dòng)態(tài)加載黑科技 動(dòng)態(tài)創(chuàng)建Activity模式耍缴,就講述了這種方式砾肺;現(xiàn)在全球的互聯(lián)網(wǎng)公司不排除有用這種技術(shù)實(shí)現(xiàn)插件框架的可能 齐佳;我相信不遠(yuǎn)的未來,這種技術(shù)也會(huì)在Android上大放異彩债沮。

了解完Hook技術(shù)之后炼吴,接下來的系列文章會(huì)講述DroidPlugin對(duì)Android四大組件在插件系統(tǒng)上的處理,插件框架對(duì)于這一部分的實(shí)現(xiàn)是DroidPlugin的精髓疫衩,Hook只不過是工具而已硅蹦。學(xué)習(xí)這部分內(nèi)容需要對(duì)于Activity,Service闷煤,Broadcast以及ContentProvider的工作機(jī)制有一定的了解童芹,因此我也會(huì)在必要的時(shí)候穿插講解一些Android Framework的知識(shí);我相信這一定會(huì)對(duì)讀者大有裨益鲤拿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末假褪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子近顷,更是在濱河造成了極大的恐慌生音,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件窒升,死亡現(xiàn)場(chǎng)離奇詭異缀遍,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)饱须,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門域醇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蓉媳,你說我怎么就攤上這事譬挚。” “怎么了酪呻?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵减宣,是天一觀的道長。 經(jīng)常有香客問我号杠,道長蚪腋,這世上最難降的妖魔是什么丰歌? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任姨蟋,我火速辦了婚禮,結(jié)果婚禮上立帖,老公的妹妹穿的比我還像新娘眼溶。我一直安慰自己,他們只是感情好晓勇,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布堂飞。 她就那樣靜靜地躺著灌旧,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绰筛。 梳的紋絲不亂的頭發(fā)上枢泰,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天,我揣著相機(jī)與錄音铝噩,去河邊找鬼衡蚂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛骏庸,可吹牛的內(nèi)容都是我干的毛甲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼具被,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼玻募!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起一姿,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤七咧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后叮叹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體坑雅,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年衬横,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了裹粤。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蜂林,死狀恐怖遥诉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情噪叙,我是刑警寧澤矮锈,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站睁蕾,受9級(jí)特大地震影響苞笨,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜子眶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一瀑凝、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧臭杰,春花似錦粤咪、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宪塔。三九已至,卻和暖如春囊拜,著一層夾襖步出監(jiān)牢的瞬間某筐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來泰國打工冠跷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留来吩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓蔽莱,卻偏偏與公主長得像弟疆,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子盗冷,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容