首頁>科技>

1、引言

隨著Android系統的不斷升級,即時通訊網技術群和社群裡的IM和推送開發的程式設計師們,對於程序保活這件事是越來越悲觀,必竟系統對各種保活黑科技的限制越來越多了,想超越系統的摯肘,難度越來越大。

但保活這件事就像“激情”之後的餘味,總是讓人慾罷不能,想放棄又不甘心。那麼,除了像上篇《2020年了,Android後臺保活還有戲嗎?看我如何優雅的實現!》這樣的正經白名單方式,不正經的“黑科技”是否還有發揮的餘地?

答案是肯定的,“黑科技”仍發揮的餘地。不是“黑科技”不行,而是技術沒到位。

研究TIM的保活是一次偶然機會,發現在安全中心關閉了它的自啟動功能的情況下, 一鍵清理、強力清理等各大招都無法徹底殺掉TIM,系統的自啟動攔截也沒能阻止TIM的永生,這引起了我強烈的興趣,於是便有了本文。

擴充套件知識:騰訊TIM是什麼?(以下文字來自百度百科)

TIM是由騰訊公司於2016年11月釋出的多平臺IM客戶端應用。TIM是在QQ輕聊版的基礎上加入了協同辦公服務的支援,可QQ號登入,以及好友、訊息同步等,適合辦公使用。

(本文同步釋出於:http://www.52im.net/thread-2893-1-1.html)

袁輝輝:2019年5月加入位元組跳動移動平臺部。畢業於西安電子科技大,曾就職於小米、聯想、IBM。

之前主要經歷從事Android手機系統研發,在上一份小米MIUI系統組工作期間主要負責小米手機Android Framework架構優化、系統穩定、技術預研、平臺建設等工作。熱衷於研究Android系統核心技術,對Android系統框架有著深刻理解與豐富的實戰經驗,編寫近200篇高品質文章,多次受邀參加業內Android技術大會演講。

3、保活技術回顧

Android保活技術的進化,可以分為幾個階段。

第一個階段:也就是各種“黑科技”盛行的時代,比如某Q搞出來的1畫素、後臺無聲音樂(某運動計步APP就幹過)等等。

這個階段的一些典型主要技術手段,可以看以下這幾篇文章:

《應用保活終極總結(一):Android6.0以下的雙程序守護保活實踐》

《Android程序保活詳解:一篇文章解決你的所有疑問》

第二個階段:到了Android 6.0時代以後,Android保活就開始有點技術難度了,之前的各種無腦保活方法開始慢慢失效。

這個階段的一些典型技術手段,可以讀讀以下這幾篇文章:

《應用保活終極總結(二):Android6.0及以上的保活實踐(程序防殺篇)》

《應用保活終極總結(三):Android6.0及以上的保活實踐(被殺復活篇)》

第三個階段:進入Android 8.0時代,Android直接在系統層面進行了各種越來越嚴格的管控,可以用的保活手段越來越少,保活技術的發展方向已發分化為兩個方向——要麼用白名單的方式走正經的保活路徑、要麼越來越“黑”一“黑”到底(比如本文將要介紹的TIM的保活手段)。

這個階段可以用的保活已經手段不多了,以下幾篇盤點了目前的一些技術可行性現狀等:

《Android P正式版即將到來:後臺應用保活、訊息推送的真正噩夢》

《全面盤點當前Android後臺保活方案的真實執行效果(截止2019年前)》

《2020年了,Android後臺保活還有戲嗎?看我如何優雅的實現!》

4、什麼是保活?

保活就是在使用者主動殺程序,或者系統基於當前記憶體不足狀態而觸發清理程序後,該程序設法讓自己免於被殺的命運或者被殺後能立刻重生的手段。

保活是”應用的蜜罐,系統的腫瘤“,應用高保活率給自己贏得線上時長,甚至做各種應用想做而使用者不期望的行為,給系統帶來的是不必要的耗電,以及系統額外的效能負擔。

保活方案一直就層出不窮,APP開發們不斷地絞盡腦汁讓自己的應用能存活得時間更長, 主要思路有以下兩個。

提升程序優先順序,降低被殺概率:

1)比如監聽SCREEN_ON/OFF廣播,啟動一畫素的透明Activity;

2)啟動空通知,提升fg-service;

... ...

程序被殺後,重新拉起程序:

1)監聽系統或者第3方廣播拉起程序。但目前安全中心/Whetstone已攔截;

2)Native fork程序相互監聽,監聽到父程序被殺,則通過am命令啟動程序。force-stop會殺整個程序組,所以這個方法幾乎很難生效了。

5、初步分析5.1 初識TIM

執行命令adb shell ps | grep tencent.tim,可見TIM共有4個程序, 其父程序都是Zygote:

root@gityuan:/ # ps | grep tencent.tim

u0_a146 27965 551 1230992 43964 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:Daemon

u0_a146 27996 551 1252492 54032 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:MSF

u0_a146 28364 551 1348616 89204 SyS_epoll_ 00f6df4bf0 S com.tencent.tim:mail

u0_a146 31587 551 1406128 147976 SyS_epoll_ 00f6df4bf0 S com.tencent.tim

5.2 一鍵清理看現象,排查初步懷疑

以下是對TIM執行一鍵清理後的日誌:

12-21 21:12:20.265 1053 1075 I am_kill : [0,4892,com.tencent.tim:Daemon,5,stop com.tencent.tim: from pid 4617]

12-21 21:12:20.272 1053 1075 I am_kill : [0,5276,com.tencent.tim:mail,2,stop com.tencent.tim: from pid 4617]

12-21 21:12:20.305 1053 1075 I am_kill : [0,4928,com.tencent.tim,2,stop com.tencent.tim: from pid 4617]

12-21 21:12:20.330 1053 1075 I am_kill : [0,4910,com.tencent.tim:MSF,0,stop com.tencent.tim: from pid 4617]

12-21 21:13:59.920 1053 1466 I am_proc_start: [0,5487,10146,com.tencent.tim:MSF,service,com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService]

12-21 21:13:59.984 1053 1604 I am_proc_start: [0,5516,10146,com.tencent.tim,content provider,com.tencent.tim/com.tencent.mqq.shared_file_accessor.ContentProviderImpl]

Force-stop是系統提供的殺程序最為徹底的方式,詳見文章《Android程序絕殺技–forceStop》。從日誌可以發現一鍵清理後TIM的4個程序全部都已被Force-stop。但程序com.tencent.tim:MSF立刻就被DaemonMsfService服務啟動過程而拉起。

問題1:安全中心已配置了禁止TIM的自啟動, 並且安全中心和系統都有對程序自啟動以及級聯啟動的嚴格限制,為何會有漏網之魚?

懷疑1: 是否安全中心自啟動沒能有效限制,以及微信/QQ跟TIM有所級聯,比如com.tencent.mobileqq.app.DaemonMsfService服務名中以com.tencent.mobileqq(QQ的包名)開頭。

經過dumpsys以及反覆驗證後排除了這種可能性,如下:

12-21 21:12:20.266 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

12-21 21:12:20.291 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

12-21 21:12:20.323 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

12-21 21:12:20.323 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

12-21 21:12:20.331 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

12-21 21:12:20.332 1053 1075 I AutoStartManagerService: MIUILOG- Reject RestartService packageName :com.tencent.tim uid : 10146

懷疑2: 是否在TIM程序被殺後, 收到BinderDied後的死亡回撥過程中將Service再次拉起,這個情況也很快就被排除, 因為force-stop這種冷麵強力殺手, 並不會等到死亡回撥再去清理程序相關資訊,而是直接連根拔起,並不會走到AMS的死亡回撥。

懷疑3: TIM設定了alarm機制,在callApp為空符合特徵, 但經過分析這裡就是普通的startService, 非startServiceInPackage(), 也排除了這種可能性:

//啟動DaemonAssistService時,callApp為空,只有通過PendingIntent方式才可能出現這種情況

12-21 21:56:54.653 3181 3195 I am_start_service: [-1,NULL,10146,com.tencent.tim:Daemon,com.tencent.tim/com.tencent.mobileqq.app.DaemonAssistService,{cmp=com.tencent.tim/com.tencent.mobileqq.app.DaemonAssistService}]

12-21 21:56:56.666 3181 3827 I am_start_service: [-1,NULL,10146,com.tencent.tim:MSF,com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService,{cmp=com.tencent.tim/com.tencent.mobileqq.app.DaemonMsfService}]

既然排除以上3種可能,直接上斷點來看看吧。

5.3 Android Studio斷點分析

一上斷點就發現了意外的一幕:

問題2:startService()的callingPid怎麼可能等於0?

5.3.1)分析callingPid=0:

為什麼說上面是意外的一幕呢?這需要對binder底層原理有一定深入理解,才能看出一些端倪,那就是此處callingPid=0是不合理邏輯的。很多人可能不太理解為何就不合乎邏輯, 這要從Binder原理說起, startService()這個Binder call是屬於同步binder呼叫, 對於binder呼叫過程,只有非同步Binder呼叫的情況下callingPid=0才會為空, 因為不需要reply應答資料給傳送binder請求的那一端。 但如果是同步的,則必須要給出callingPid,否則無法將應答資料回傳給傳送方。 這是由Binder Driver所決定的,見如下Binder Driver核心程式碼。

(1) Binder發起端:根據當前ONE_WAY來決定是否設定from執行緒

binder_transaction(...) {

...

if(!reply && !(tr->flags & TF_ONE_WAY))

t->from = thread;

else

t->from = NULL;

}

...

}

(2) Binder接收端: 根據from執行緒是否為空, 來決定sender_pid是否為0. 這便是Java層所說的callingPid

binder_thread_read(...) {

...

t_from = binder_get_txn_from(t);

if(t_from) {

structtask_struct *sender = t_from->proc->tsk;

tr.sender_pid = task_tgid_nr_ns(sender,

task_active_pid_ns(current));

} else{

tr.sender_pid = 0;

}

...

}

上述程式碼表明: 同步的Binder呼叫的情況下則callingPid必定不等於0。

下面告訴大家如何看一個Binder呼叫是否同步, 如下圖最後一個引數代表的是FLAG_ONEWAY值,等於0則代表的是同步, 等於1則代表的是非同步。

以上程式碼是framework的框架程式碼,startService最終都會呼叫到這裡來,所以callingPid必然是不可能出現為0的情況,讓我們看不透到底哪個程序把com.tencent.tim: Daemon拉起的。

5.3.2)揭祕:

從前面的分析來看callingPid是不可能為0的, 但從結果來看的確是0, 出現矛盾就一定有反常規存在,難道是存在同步的Binder呼叫,也存在同時callingPid=0的case?答案是No.

從原始碼角度來看是沒有這種可能性存在,後面再進一步追蹤flags值的變化,從如下的flags=17,可以確定的是此處的startService的binder call是ONE_WAY的,這就可以確定的確是發起了非同步的Binder呼叫。

程式碼如下:

雖然callingPid=0,但從callUid=10146可以確定的一點是com.tencent.tim: Daemon程序是被來自TIM應用自身的某個程序所拉起的。

5.4 小結

通過前面的初步分析,先整理一下思路,有以下初步結論:

1)TIM至少有4個程序,且都是由Zygote程序fork, 保活是通過startService被拉起;

2)排除 安全中心的對TIM限制自啟動功能失效的情況;

3)排除 TIM程序被殺後的Binder死亡回撥過程通過Service重新拉起程序;

4)排除 alarm機制 拉起程序;

5)從callingPid=0,可以得出TIM沒有走常規的系統框架中提供的startService()介面來啟動服務,而是自定義的方式;

6)從callingUid=10146, 可以得出TIM救活自己的方式,是通過TIM自身,而非系統或者第三方應用拉起。

到此不難得出一個猜想: 首先TIM應用能做到監聽應用程序被殺的情況, 其次是TIM應用自身替換掉或者自定義一套Binder呼叫,主動跟Binder驅動進行資料互動。

6、深入分析6.1 尋求規律

TIM應用有4個程序,不斷反覆地嘗試殺TIM每一個程序後,觀察自啟動的情況後。 發現了一個規律:com.tencent.tim: Daemon和com.tencent.tim:MSF程序任一被殺,都會先把對方程序拉起,然後跟著自殺後,再重啟。

接下來就把範圍鎖定在這兩個程序,然後來tracing訊號處理情況。

6.2 從signal角度來分析

開啟signal開關:

root@gityuan:/ # echo 1 > /d/tracing/events/signal/enable

root@gityuan:/ # echo 1 > /d/tracing/tracing_on

執行如下命令抓取tracing log:

root@cancro/: cat/d/tracing/trace_pipe

日誌如下:

//通過adb shell kill-9 10649, 將com.tencent.tim:Daemon程序殺掉

sh-22775 [000] d..2 18844.276419: signal_generate: sig=9 errno=0 code=0 comm=cent.tim:Daemon pid=10649 grp=1 res=0

//執行緒Thread-89 將tencent.tim:MSF程序也殺掉了

Thread-89-10712 [000] dn.2 18844.340735: signal_generate: sig=9 errno=0 code=0 comm=tencent.tim:MSF pid=10669 grp=1 res=0

Binder:14682_4-14845 [000] d..2 18844.340779: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0

Binder:14682_1-14694 [000] d..2 18844.341418: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0

Binder:14682_2-14697 [000] d..2 18844.345075: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=0

tencent.tim:MSF-14682 [000] dn.2 18844.345115: signal_deliver: sig=9 errno=0 code=0 sa_handler=0 sa_flags=

從這裡,可以發現com.tencent.tim: Daemon程序是由於其中一個執行緒Thread-89所殺,但從名字來看Thread-xxx,很明顯是系統自動生成的編號。

問題3:程序內的名叫“Thread-89”的執行緒具有什麼特點,如何做到把程序殺掉?

從下面的截圖,可以看出MSF程序的這個特殊的執行緒當前在執行flock_lock操作,這個明顯是一個檔案加鎖的操作, 這個方法很快就引起了我的注意。同理Daemon程序也有一個這樣的執行緒, 離真相有近了一步。

再來看看呼叫棧情況:

Cmd line: com.tencent.tim:Daemon

"Thread-89"prio=10 tid=12 Native

| group="main"sCount=1 dsCount=0 obj=0x32c07460 self=0xf3382000

| sysTid=10712 nice=-8 cgrp=bg_non_interactive sched=0/0handle=0xee824930

| state=S schedstat=( 44972457 14188383 124 ) utm=1 stm=3 core=0 HZ=100

| stack=0xee722000-0xee724000 stackSize=1038KB

| held mutexes=

kernel: __switch_to+0x74/0x8c

kernel: flock_lock_file_wait+0x2a4/0x318

kernel: SyS_flock+0x19c/0x1a8

kernel: el0_svc_naked+0x20/0x28

native: #00 pc 000423d4 /system/lib/libc.so (flock+8)

native: #01 pc 0000195d /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc+64)

...

native: #29 pc 0000191f /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc+2)

native: #30 pc 0000191d /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z9lock_filePc)

native: #31 pc 0000191b /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z18notify_and_waitforPcS_+102)

...

native: #63 pc 000018d1 /data/app/com.tencent.tim-1/lib/arm/libdaemon_acc.so (_Z18notify_and_waitforPcS_+28)

at com.libwatermelon.WaterDaemon.doDaemon2(Native method)

at com.libwatermelon.strategy.WaterStrategy2$2.run(WaterStrategy2.java:111)

從這個執行緒的呼叫棧中的名字, notify_and_waitfor讓我想到了這極有可能用於監聽檔案來獲知程序是否存活。 為了進一步觀察這個特殊執行緒的工作使命, 這裡還不需要GDB, 祭出strace大招應該就差不多。

6.3 利用strace分析

root@gityuan:/ # strace -CttTip 22829 -CttTip 22793

結果如下:

flock基礎知識簡介:

flock是Linux檔案鎖,用於多個程序同時操作同一個檔案時,通過加鎖機制保證資料的完整,flock使用場景之一,便是用於檢測程序是否存在。flock屬於建議性的鎖,而非強制性鎖,只是程序可以直接操作正被另一個程序用flock鎖住的檔案, 原因在於flock只檢測檔案是否加鎖,核心並不會強制阻塞其他程序的讀寫操作,這便是建議性鎖的核心策略。

方法原型: intflock(intfd, intoperation)

第一個引數是檔案描述符,第二引數指定鎖的型別,有以下3個可選值:

1)LOCK_SH: 共享鎖, 同一時間執行多個程序同時持有該共享鎖;

2)LOCK_EX: 排它鎖,只允許一個程序持有該鎖;

3)LOCK_UN: 移除該程序的該檔案所持有的鎖。

從strace可以推測出:com.tencent.tim:MSF程序的監控執行緒執行排它鎖LOCK_EX型別的flock,嘗試去獲取某個檔案,而該檔案已被com.tencent.tim: Daemon程序所持有,所以MSF程序會被阻塞知道鎖的釋放,而一旦Daemon程序被殺,系統就會回收所有資源(包括檔案),這是Linux核心負責完成的。

當Daemon程序的檔案被回收,就會釋放flock, 從而MSF程序可以獲取該鎖,從而吐出“lock file success”的資訊。 MSF得知Daemon程序被殺,然後執行一行ioctl(11, BINDER_WRITE_READ, 0xffffffffee823ed0) = 0 <0.000867> 。

這個應該就是TIM程序自身實現了一套執行startService的Binder呼叫,向Binder驅動傳送 BINDER_WRITE_READ的ioctl命令。 再然後傳送kill SIGKILL將自身MSF程序殺掉,同樣的道理可以再次被拉起。

分析到這裡,看執行了writev操作, 應該就是Log操作, 有一個關鍵詞到 Watermelon 吸引了我的注意力, 搜尋 Watermelon 關鍵詞,果然找到新的一片天地。

6.4 TIM日誌

//舊的MSF程序

24538 24562 D Watermelon: lock filesuccess >> /data/user/0/com.tencent.tim/app_indicators/indicator_p2

24538 24562 E Watermelon: Watch >>>>Daemon<<<<< Daed !!

24538 24562 E Watermelon: java_callback:onDaemonDead

24538 24562 V Watermelon: onDaemonDead

24576 24576 D Watermelon: lock filesuccess >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

24576 24576 E Watermelon: Watch >>>>Daemon<<<<< Daed !!

24576 24576 E Watermelon: process exit

//新daemon程序

25103 25103 V Watermelon: initDaemon processName=com.tencent.tim:Daemon

25103 25103 E Watermelon: onDaemonAssistantCreate

25134 25134 D Watermelon: start daemon24=/data/user/0/com.tencent.tim/app_bin/daemon2

//app_d程序

25137 25137 D Watermelon: pipe readdatasize >> 316 <<

25137 25137 D Watermelon: indicator_self_path >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

25137 25137 D Watermelon: observer_daemon_path >> /data/user/0/com.tencent.tim/app_indicators/observer_p1

25137 25137 I Watermelon: sIActivityManager==NULL

25137 25137 I Watermelon: BpActivityManager init

//新daemon

25103 25120 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_p2

25103 25120 D Watermelon: lock filesuccess >> /data/user/0/com.tencent.tim/app_indicators/indicator_p2

25137 25137 I Watermelon: BpActivityManager init end

//app_d程序

25137 25137 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

25137 25137 D Watermelon: lock filesuccess >> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

//新MSF程序

25119 25119 V Watermelon: initDaemon processName=com.tencent.tim:MSF

25119 25119 V Watermelon: mConfigurations.PERSISTENT_CONFIG.PROCESS_NAME=com.tencent.tim:MSF

25119 25119 E Watermelon: onPersistentCreate

25153 25153 D Watermelon: start daemon24=/data/user/0/com.tencent.tim/app_bin/daemon2

25119 25144 D Watermelon: pipe write len=324

25159 25159 D Watermelon: pipe readdatasize >> 324 <<

25159 25159 D Watermelon: indicator_self_path >> /data/user/0/com.tencent.tim/app_indicators/indicator_p1

25159 25159 D Watermelon: observer_daemon_path >> /data/user/0/com.tencent.tim/app_indicators/observer_d1

25159 25159 I Watermelon: sIActivityManager==NULL

25159 25159 I Watermelon: BpActivityManager init

25119 25144 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

25119 25144 D Watermelon: lock filesuccess >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

25159 25159 I Watermelon: BpActivityManager init end

//各程序進入監聽就緒狀態

25159 25159 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_p1

25159 25159 D Watermelon: lock filesuccess >> /data/user/0/com.tencent.tim/app_indicators/indicator_p1

25119 25144 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...

25119 25144 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_p2

25159 25159 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...

25159 25159 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d1

25137 25137 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...

25137 25137 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_p1

25103 25120 E Watermelon: Watched >>>>OBSERVER<<<< has been ready...

25103 25120 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

再從其中的擷取核心片段:

25159 25159 I Watermelon: BpActivityManager init

25119 25144 D Watermelon: start try to lock file>> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

25119 25144 D Watermelon: lock filesuccess >> /data/user/0/com.tencent.tim/app_indicators/indicator_d2

不難看出:

1)TIM自身通過向servicemanager查詢來獲取AMS的代理BpActivityManager, 然後自己去寫startService通訊過程的資料;

2)TIM通過兩個程序通過flock來相互監聽對方程序存活狀態;

3)監聽的檔案有比如:/data/user/0/com.tencent.tim/app_indicators/indicator_d2。

6.5 indicator檔案

進一步檢視TIM所監聽的路徑下/data/user/0/com.tencent.tim/app_indicators/, 發現有4個監聽檔案:

問題4:為何需要4個indicator檔案?

進一步延伸:通過檢視flock,再次發現新大陸,原來除了Daemon和MSF程序各有一個監聽檔案的執行緒, 還有兩個由init程序作為父程序的app_d程序也監聽檔案:

gityuan@13203:~/gityuan$ adb shell ps-t | grep-i flock

u0_a146 10668 10649 1143304 85876 flock_lock 00f6e1e3d8 S Thread-85

u0_a146 10712 10669 1158552 89664 flock_lock 00f6e1e3d8 S Thread-89

u0_a146 10687 1 12768 564 flock_lock 00f73113d8 S app_d

u0_a146 10717 1 12768 560 flock_lock 00f74353d8 S app_d

不難發現,以上幾個程序/執行緒的uid=10146,進一步通過ps命名查詢。

再一次重新整理對TIM應用的認識:原來TIM有6個程序,其中還有2個是掛在init程序下,名字跟tencent沒有關係,差點錯過了這兩個特殊的程序。

這兩個app_d程序其實也是做著同樣的相互監聽的工作, 應該是備選方案。當有概率恰巧Daemon和MSF程序同時被殺而來不及互保的情況下,那麼可以走緊急通道app_d 將TIM程序拉起。可謂是暗藏玄機, 6個程序中有4個程序可以相互保活, 以保證TIM程序永生。

問題5: 這4個程序到達是什麼如何相互監聽的呢?

通過不斷分析被殺與重啟前後的規律與特徵,得出程序與監聽檔案的關係圖:

進一步揭露面紗,得到如下結論:

1)Daemon與MSF兩程序等待對方所持有的鎖,兩個app_d程序相互等待對方所持有的鎖;

2)app_d1程序被殺, 則app_d2觀察後通過拉起DaemonMsfService服務來啟動MSF程序,然後跟著被殺;

3)app_d2程序被殺,則app_d1觀察後通過拉起DaemonAssistService服務來啟動Daemon程序,然後跟著被殺;

4)Daemon與MSF兩程序, 如果殺掉其中一個,則另個一個程序觀察後通過拉起服務方式來啟動對方程序,然後跟著被殺;然後app_d兩個程序也跟著重啟。

另外猜想:監測indicator_p1和indicator_p2的兩個程序有關聯,indicator_d1和indicator_d2的程序有關聯,後面會驗證。

到這裡又有出現新的疑問:Daemon程序死後,MSF程序通過flock能監測到該事件,可是app_d程序又是如何得知的呢? app_d得知之後,又為何要再次自殺重啟?

6.6 從cgroup角度來分析

root@gityuan:/acct/uid_10146/pid_10649# cat cgroup.procs

10649 //Daemon

10687 //app_d

root@gityuan:/acct/uid_10146/pid_10669# cat cgroup.procs

10669 //MSF

10717 //app_d

從而,進一步獲取更多關於TIM深層次的關聯,通過檢視cgroup發現,Daemon和app_d1是同一個group的, MSF和app_d2是同一個group的。

問題6: app_d到底是如何創建出來?又是如何成為init程序的子程序的?

從程序建立與退出的角度來看看來看:

//5170(MSF程序) --> 5192 --> 5201(退出) --> 5211(存活)

tencent.tim:MSF-5170 [001] ...1 55659.446062: sched_process_fork: comm=tencent.tim:MSF pid=5170 child_comm=tencent.tim:MSF child_pid=519

Thread-300-5192 [000] ...1 55659.489621: sched_process_fork: comm=Thread-300 pid=5192 child_comm=Thread-300 child_pid=5201

<...>-5201 [003] ...1 55659.501074: sched_process_exec: filename=/data/user/0/com.tencent.tim/app_bin/daemon2pid=5201 old_pid=5201

daemon2-5201 [009] ...1 55659.533492: sched_process_fork: comm=daemon2 pid=5201 child_comm=daemon2 child_pid=5211

daemon2-5201 [009] ...1 55659.535169: sched_process_exit: comm=daemon2 pid=5201 prio=120

daemon2-5201 [009] d..3 55659.535341: signal_generate: sig=17 errno=0 code=262145 comm=Thread-300 pid=5192 grp=1 res=1

說明:其中一個app_d程序是由MSF程序,通過兩次fork,然後父程序退出,從而成為了孤兒程序,然後託孤給init程序,這是Linux程序機制所保證的。 同理,另一個app_d程序是由Daemon程序所fork。到這裡,那麼總算是認清的app_d的由來。 app_d是由於cgroup關聯所以可以得知Daemon程序的情況。 關於重啟的原因是為了重新建立互動的關係。

問題7:為何單殺daemon,會牽連app_d程序被殺,這是什麼原理?

解答:從殺程序的日誌上來是呼叫killProcessGroup()殺程序,可事實上adb只調用kill -9 pid的方式,單殺一個程序,怎麼就牽連了app_d程序。 這是由於當daemon程序被殺後,死亡回撥會回來後,在binderDied()的過程執行了killProcessGroup()。

如果從Linux核心層面,研究過Binder死亡回撥機制的童鞋,到這裡還就會有想到一個新的疑問如下。

問題8:app_d是由daemon程序間接fork出來的, 會共享binder fd,所以即便daemon程序被殺,死亡回撥也不會觸發,這又是何觸發的呢?

解答:由於app_d程序被fork後,馬上執行了exec()系的函式, 而在ProcessState開啟Binder驅動的時候, 有一個非常重要的flag, 那就是O_CLOEXEC。

採用O_CLOEXEC方式開啟的問題,當新建立的程序呼叫exec()函式成功後,檔案描述符會自動關閉, 程式碼如下:

6.7 剖根問底

問題9:TIM到底對Binder框架做了什麼級別的修改?這4個互保程序,既然callingPid=0,有沒有辦法知道到底是由誰拉起誰的?

前面既然說了,TIM強行修改了ONEWAY的方式。可以去掉該flags, 為了除錯,這裡就針對TIM,並且code=34(即START_SERVICE_TRANSACTION), 並且修改flag的case下:

從實驗結果來看,通過修改IPCThreadState.cpp程式碼, 完成control住了 TIM的所有修改, 這裡可以說明:

TIM分別在Java層和Native層,主動向ServiceManager程序查詢AMS後,獲取BpActivityManager代理物件,然後繼續使用框架中的IPCThreadState跟Binder驅動互動,並沒有替換掉libbinder.so。

其實,還可以更高階的玩法,連IPCThreadState這些框架通訊程式碼也不使用, 徹底地去自定義Binder互動程式碼,類似於servicemanager的方式。可以自己封裝ioctl(),直接talkWithDriver。TIM保活還有改進空間, 提供保活變種方案,這樣的話,上面的除錯程式碼也攔截不了其對flags修改為ONEWAY的過程。 即使如此,一切都在Control之中, 完全可以在Binder Driver中攔截再定位其策略, 玩得再高階也主要活動在使用者態, 核心態的策略還是相對安全的, 此所謂“魔高一座,道高一尺”。

另外,通過增加上面的臨時程式碼,再次多次實驗對比,可以得出如下關係圖:

二度fork是指前面介紹了,fork後再fork,然後託孤,無論如何跟最初的程序都屬於同一個group,有著級聯被殺關係。

1)殺掉Daemon程序,則MSF程序觀察到會去拉起Daemon程序; 同時app_d1因為同一個group而被殺,則app_d2程序觀察到也拉起Daemon程序,這就是雙保險;

2)殺掉app_d1程序, 則app_d2程序觀察到會拉起MSF程序;

3)直接force-stop程序, 則6個程序都會被殺,只是殺的過程並非所有程序同一時刻點被殺,而是有前後順序,所以造成能自啟。

6.8 分析思路歸納

我們來回顧一下上面的過程:

1)先有了初步分析過程中對一些常規套路的可能性的排除,並嗅到callingPid=0的異常舉動;

2)沿著蛛絲馬跡,不斷反覆嘗試殺程序,從中尋找更多的規律,不斷地向自己提出疑問;

3)結合signal,strace, traces,ps,binder,linux,kill等技能 不斷地解答自己的疑惑。

解系統層的問題,更像是偵探破案的感覺,要有敏銳的嗅覺,抓住蛛絲馬跡,加上”大膽猜想,小心驗證“ , 終究能找到案件的真相。 此所謂”點動成線,線動成面,面動成體“, 從零星的點滴勾畫出全方面立體化的真相。

歸納下,主要提出過這些疑惑:

問題1:安全中心已配置了禁止TIM的自啟動, 並且安全中心和Whetstone都有對程序自啟動以及級聯啟動的嚴格限制, 為何會有漏網之魚?

問題2:startService()的callingPid怎麼可能等於0?

問題3:程序內的名叫“Thread-89”的執行緒具有什麼特點,如何做到把程序殺掉?

問題4:為何需要4個indicator檔案?

問題5: 這4個程序到達是什麼如何相互監聽的呢?

問題6: app_d到底是如何創建出來?又是如何成為init程序的子程序的?

問題7:為何單殺daemon,會牽連app_d程序被殺,這是什麼原理?

問題8:app_d是由daemon程序間接fork出來的, 會共享binder fd,所以即便daemon程序被殺,死亡回撥也不會觸發,這又是何觸發的呢?

問題9:TIM到底對Binder框架做了什麼級別的修改?這4個互保程序,既然callingPid=0,有沒有辦法知道到底是由誰拉起誰的?

7、本文總結

總結一下TIM的保活技術要點,我們可以得出以下經驗:

1)通過flock的檔案排它鎖方式來監聽程序存活狀態

1.1)先採用一對普通的程序Daemon和MSF相互監聽檔案的方式來獲得對方程序是否存活的狀態;

1.2)同時再採用一對退孤給init程序的app_d程序相互監聽檔案的方式來獲得對方程序是否存活的狀態; 而這兩個程序都有間接由Daemon和MSF程序所fork而來;雙重保險。

2)不採用系統框架中startService的Binder框架程式碼,而是自身在Native層通過自己去查詢獲取BpActivityManager代理物件, 然後自己實現startService介面,並修改為ONEWAY的binder呼叫,既增加分析問題的難度,也進一步隱藏自身策略;

3)當監聽程序死亡,則通過自身實現的StartService的Binder call去拉起對方程序,系統對於這種方式啟動程序並沒有攔截機制。

這種flock的方式至少比網上常說的通過迴圈監聽的方式,要強很多。

比往常的互保更厲害的是TIM共有6個程序(說明:使用過程也還會建立一些程序),其中4個程序,形成兩組互動程序,其中一組利用Linux程序託孤原理,可謂是隱藏得很深來互保,進一步確保程序永生。

當然,程序收到signal訊號後,如果恰巧這四個程序在同一個時刻點退出,那麼還是有概率會被殺。

不走系統框架程式碼,自己去實現啟動服務的binder call也是一大亮點,不過還有更高階的玩法,直接封裝ioctl跟驅動互動。之前針對這個問題,做過反保活方案,後來為了某些功能緣故又放開對這個的限制,這裡就不再繼續展開了。

附錄:有關IM/推送的程序保活/網路保活方成的文章彙總

《應用保活終極總結(一):Android6.0以下的雙程序守護保活實踐》

《應用保活終極總結(二):Android6.0及以上的保活實踐(程序防殺篇)》

《應用保活終極總結(三):Android6.0及以上的保活實踐(被殺復活篇)》

《Android程序保活詳解:一篇文章解決你的所有疑問》

《Android端訊息推送總結:實現原理、心跳保活、遇到的問題等》

《深入的聊聊Android訊息推送這件小事》

《為何基於TCP協議的移動端IM仍然需要心跳保活機制?》

《Android P正式版即將到來:後臺應用保活、訊息推送的真正噩夢》

《全面盤點當前Android後臺保活方案的真實執行效果(截止2019年前)》

《一文讀懂即時通訊應用中的網路心跳包機制:作用、原理、實現思路等》

《融雲技術分享:融雲安卓端IM產品的網路鏈路保活技術實踐》

《正確理解IM長連線的心跳及重連機制,並動手實現(有完整IM原始碼)》

《2020年了,Android後臺保活還有戲嗎?看我如何優雅的實現!》

《史上最強Android保活思路:深入剖析騰訊TIM的程序永生技術》

>> 更多同類文章 ……

(本文同步釋出於:http://www.52im.net/thread-2893-1-1.html)

最新評論
  • 整治雙十一購物亂象,國家再次出手!該跟這些套路說再見了
  • 再加10億!2020年京東物流將再投10億培養各類人才,你有機會嗎?