反編譯、分析、閱讀 Android 應用程式原始碼,以 17直播為例

有些 Android 應用程式會要求過多的權限,如果不給它的話還不讓你使用應用程式,但明明這權限就很不合理啊。

以 17 直播為例,看個直播幹嘛要存取我的檔案?

https://dl2.pushbulletusercontent.com/N86YBB9oAlvoEPmGO6QyO9NJvSDoFD9c/Screenshot_20180225-141150.png

看了就很不爽,就是這樣的應用程式訓練出毫不眨眼就隨便給出權限的使用者,所以今天拿它來開刀。

找出顯示中的 Activity

首先用 adb shell dumpsys activity 瞭解一下目前是哪一個 activity :

ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
  Stack #1:
    Task id #7379
    * TaskRecord{b876752 #7379 A=com.machipopo.media17 U=0 sz=1}
      userId=0 effectiveUid=u0a204 mCallingUid=u0a204 mCallingPackage=com.machipopo.media17
      affinity=com.machipopo.media17
      intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.machipopo
.media17/.StartActivity bnds=[48,156][126,234]}
      realActivity=com.machipopo.media17/.StartActivity
      autoRemoveRecents=false isPersistable=true numFullscreen=1 taskType=0 mTaskToReturnTo=1
      rootWasReset=true mNeverRelinquishIdentity=true mReuseTask=false mLockTaskAuth=LOCK_TASK_AUTH_PINNABLE
      Activities=[ActivityRecord{822c1b8 u0 com.machipopo.media17/.activity.MenuActivity_V2 t7379}]
      askedCompatMode=false inRecents=true isAvailable=true
      lastThumbnail=android.graphics.Bitmap@80a2cec lastThumbnailFile=/data/system/recent_images/7379_task_thumbnail
.png
      stackId=1
      hasBeenVisible=true mResizeable=false firstActiveTime=1519539613444 lastActiveTime=1519539613444 (inactive for
 26s)

主要是看 Activities= 那一行,就知道目前的 activity 是什麼。

反編譯取得原始碼

我已經裝好了 Mobile Security Framework ,直接上傳 APK 檔案就可以反組譯,還會順便做一些其他分析。

到安裝 MobSF 的目錄底下有個 uploads 資料夾, APK 的 checksum 就是資料夾名稱,進去之後會看到幾個資料夾:

接下來的重點就是看 java_sourcesmali_source 這兩個資料夾了。

閱讀 Java 原始碼

打開 MenuActivity_V2.java ,往下捲一下很快就看到:

顯然就是檢查有沒有寫入外部儲存空間權限的函式,我猜測它的行為如下:

  1. 呼叫 android.support.v4 API ,檢查使用者是否已經授權存取外部儲存空間(269 行)
  2. 如果沒有授權的話,把字串 android.permission.WRITE_EXTERNAL_STORAGE 加進 arrayList 。也就是說,arrayList 裡面的字串代表應用程式還需要的權限。
  3. arrayList 的長度,設為 n2 ,然後回傳。

再找一下 F() 它的 caller 有誰,搜尋了一下,只有這個函式:

在 1410 行的 if 檢查了 this.F() 是不是回傳零,也就是「應用程式所需要的權限數量是不是零」,如果不是零的話,執行 1411~1414 this.n 的相關操作。

看到它在 1411 行檢查了 this.n 是否為空,如果不為空的話呼叫 this.n.dismiss() ,初步猜測 this.n 就是上面截圖中對話框的控制 class ,而 1411~1412 則是在檢查目前有沒有已經開啟的對話框,如果有的話,把它關閉 (dismiss) 。

在檔案開始處 MenuActivity_V2 class 的屬性型別宣告證實了我的猜想:

從 207 行,我們得知 this.n 的型別是 com.machipopo.media17.fragment.dialog.e ,看起來就是個對話框 (dialog) 。

至於對話框 class 要怎麼操作,現階段不是很重要,畢竟我們只需要把對話關掉(應該說是完全不要呼叫顯示對話框的函式)就好了。

關掉對話框有很多種方法,但由於我們實際上要修改應用程式的行為光是改 java code 是沒用的,需要修改 smali code ,因此我想還是先看看它的 smali code 長什麼樣子,再思考要怎麼修改比較方便。

閱讀 smali 原始碼

一樣在 uploads 資料夾裡面,可以找到 smali_source/com/machipopo/media17/activity/MenuActivity_V2.smali 這個檔案,也就是對應 MenuActivity_V2.java 的 smali 原始碼。

首先我們需要先找到 F() 的 caller ,也就是 onStart()

雖然說我根本就沒學過 smali (應該只有研究 Java VM 的人才需要瞭解 smali 吧),但稍微看了整個 smali 原始碼,還是可以看到一些規律,加上一些背景知識就可以看懂一段 smali code 的行為。

smali 看來還是蠻高階的東西,簡單搜尋了一下 onStart 字串,就找到這一段:

什麼 Singleton , AppLogic 的,跟 Java code 裡面的名字一樣,因此確定從 5621 行就是 onStart() 沒錯。

往下到 5648 行,就看到呼叫 F() 的指令:

5649 行的行為,猜測大概就是呼叫 F() ,而 F 會回傳一個整數 (I) ,這個回傳值會放在 {p0} 裡面。

5651 行把 {p0} 裡面的結果移動到 v0

5653 行是重點,判斷 v0 是否為零 (equal to zero, eqz) ,如果是的話 jump 到 :cond_81 ,如果不是的話繼續執行

以上 5649~5653 這段 smali code ,對應的 Java code 是 1410 行 if (this.F() != 0 && Build.VERSION.SDK_INT >= 23) { && 前面的東西。

瞭解了以上行為之後,看得出來,如果在使用者有給出所有權限的狀況下:

  1. F() 會回傳零
  2. 在 5653 的判斷會成功,直接跳到 :cond_81

因此 :cond_81 之後就是我們想要的行為(回歸正常操作),而 5653 ~ :cond_81 這段則會跳出對話框,不斷阻擋我們操作。

看一下 :cond_81 會做什麼:

5768~5772 對應到 Java code 1416 的 com.machipopo.media17.business.AppLogic.a().o((Context)this);

最後的 goto 則是 1418 行 if (com.machipopo.media17.business.AppLogic.a().b(.......

綜合以上判斷,我們需要修改的地方,就是把 5648 ~ :cond_81 這段行為,完全刪除,如此一來,無論 F() 的回傳值為何, 1416 行檢查權限成功之後的行為都會照常執行。

重新組裝 APK

關於整個反編譯和重新 build apk 的流程,可以參考 Huli 的這篇《人人都會的 apk 反編譯》。

用 apktool 反編譯原始碼(其實直接用 MobSF 反編譯出來的應該也可以用,但為了避免 apktool 沒辦法把 MobSF 反編譯的原始碼包回去,省點 debug 的力氣,還是就另外用一次 apktool),然後照著剛剛的規劃,刪掉 5648 ~ :cond_81 的東西。需要特別注意的是,用 apktool 反編譯出來的原始碼會跟 MobSF 的有點不一樣,主要是 symbol 名稱不同,像是 cond_81 可能會是其他數字,但程式碼的結構會是一樣的,修改的時候注意一下就可以。

修改過 smali code 之後,用 apktool b xxx -o new_17.apk 它包回 apk 檔案,

然後它就發生了錯誤…:

I: Using Apktool 2.2.3
I: Checking whether sources has changed...
I: Smaling smali folder into classes.dex...
base/smali/com/machipopo/media17/activity/MenuActivity_V2.smali[5659,0] Cannot get the location of a label that hasn't been placed yet.
Exception in thread "main" brut.androlib.AndrolibException: Could not smali file: com/machipopo/media17/activity/MenuActivity_V2.smali

不確定是什麼原因(畢竟我不是很懂 smali),我猜大概是中間刪掉的 code 有什麼重要意義,所以導致後面呼叫錯誤,但沒關係,還有其他方法可以修改行為:

直接在 5651 行它把結果存入 v0 之後,把 v0 覆蓋為零(5653 行是我加入的),至於問我我怎麼知道 const/4 v0, 0x0 這種寫法?我是往上稍微捲一下, 5637 行就有這種指令,我直接改了寫上 5653 行。

解決這問題之後,使用 apktool b 的時候還有遇到另一個錯誤,照著連結中的解法,成功封裝起來了。

接下來的步驟是 zipalign 打包好的 apk,然後用 apksigner 簽章(產生 keystore 的方法見這裡),方法可以直接參考 Google 的文件

安裝問題

接下來我就把 apk 放進手機裡,然後點開安裝,它卻只會跳出「套件剖析錯誤」的提示,從 adb logcat 看詳細錯誤看到: Parse error when parsing manifest. Discontinuing installation

查了一下,照著這篇改用 adb install 安裝就可以了。

成功

https://dl2.pushbulletusercontent.com/BxEUwLrfTKMNj8HUojseWOQgRLD2rRh9/Screenshot_20180225-225223.png

再也不會有惱人的提示框啦!

參考

學習 smali 語法: http://blog.csdn.net/wdaming1986/article/details/8299996

來龍去脈

其實 17 的 app 在更早以前的版本不只會要外部儲存空間的權限,應用程式一打開,就會向你要求麥克風、相機和簡訊的存取權限,這個是 2017 年 7 月左右版本的原始碼:

當時看這個 app 我就更不爽了,應用程式一打開什麼功能都還沒開始用就跟你要一大堆權限,不給權限的話還整個不給你用,真的是非常不負責任的開發者行為。

Android Island work profile apps and AFWall

Island is an app that allows you to isolate, clone, freeze, hide, archive apps.

Unfortunately when you use it with iptables firewall app like AFWall the apps placed in the work profile have no access to the network. (because there aren’t rules allowing them in the firewall)

AFWall doesn’t support multiple profiles – there’s an experimental option called “multi-user support" but only works in mode where selected items are blocked:

 

AFWall doesn’t show work profile apps. So I need a way to allow work profile app traffic.

With a bit of digging, I found the rule for work profile (UNIX) uid : user_id*100000+app_uid , the primary user has user id of 0, the work profile Island created has user id 10.

So you can go look for app uid, add user_id*100000 , and it becomes the (UNIX) uid of the app in that work profile.

A quick way to know app_uid is go to AFWall preferences > User interface > “Show UID for apps".

Then you need to write your own rules that allows traffic to pass through, the number after --uid-owner is the UID you want to change, in this example I allowed 3 apps:

Place it in /data/local/, chmod +x it, add its path to AFWall custom script, then apply rules in AFWall. You should now get internet in work profile apps.

Linux kernel function / system call hijacking

想要 hijack 某個 Linux kernel 的 function ,所以開始了這次的搜尋。

Linux kernel 各部分之間的 function 呼叫有2種,一種是直接把要呼叫的 function header file include 進來,如果是在同一個檔案當然也不需要 include。另一種是呼叫 exported function ,允許外部呼叫的 function 在定義之後會加上 EXPORT_SYMBOL(function_name) ,這個 function 的記憶體位址就可以從 kernel symbol table 找到(symbol table 裏面不只有 function ,也有變數)。外部呼叫是指 kernel 的其他部分還有 kernel module ,外部呼叫時會去 symbol table 找位址再進行呼叫(應該吧)。

所以只要修改 symbol table 的內容,就可以 hijack 特定的 function,讓他去找我們自己定義的位址的 function 去呼叫了。不對

但是一般 kernel symbol table 是唯讀的,看一下 /proc/kallsyms 的內容,第一欄是記憶體位址,第二欄是屬性(是否可讀寫等等,標籤代表的意義請 man nm),第三欄是變數或函式名稱。

system call table 裏面存了所有 system call 的記憶體位址,它的 symbol 叫做 sys_call_table ,在大部分的 Linux 上面應該是 R (唯讀)需要先解除 page write protection 才能修改。(其他參考)HTC Android kernel 的 sys_call_table 標籤居然是 T ,意思好像是可讀寫?

syscall table 裏面存了所有 syscall 的進入點(記憶體位址),用 sys_call_table[CONSTANT] 的方式可以找到特定 syscall 的進入點,像是如果要找 fork() 的進入點,就這樣寫:sys_call_table[__NR_fork]__NR_fork 這些 constant 定義於 unistd.h ,依處理器架構會有不同。上面連結的那兩篇有如何 hijack syscall 的教學。

後來發現我要的不是 syscall hijacking ,而是 kernel function hijacking ,我搞混了這兩個。syscall 是 userspace 程式和 kernel 溝通的界面,一般來說都是由應用程式呼叫;kernel function 是 kernel 內部的函式,提供給其他 kernelspace (可能在 kernel 或是 kernel module )的函式調用,又有是否 export 之分,有 export 的 function 在宣告後會加上 EXPORT_SYMBOL(function_name) ,它的位址就在 symbol table 找得到。

要 hijack kernel function 似乎要牽涉到一些 shellcode… 而且是 platform-specific ,還有待研究。(順帶一看

搜尋的過程當中找到這幾個問答:

發現若不是要 hijack 的話,基本的 debug 可以透過 kprobe

 

編譯 HTC Android kernel 和自製 kernel module

Kernel

這部分參考 HTC kernel source code 隨附的 Readme

首先把 compiler 抓下來

git clone https://android.googlesource.com/platform/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6

指定環境變數

export TOP= [where you installed the toolchain or top of android AOSP code base]
export PATH=$TOP/prebuilts/gcc/linux-x86/arm/arm-eabi-4.6/bin:$PATH (use corresponding arm-eabi bin path)
export ARCH=arm
export SUBARCH=arm
export CROSS_COMPILE=arm-eabi-

套用 defconfig 設定檔

make k2_u_defconfig

真正編譯,可以調整平行編譯數

make -j8

Kernel Module

kernel module 需要已經編譯好的 kernel 才能編譯。Makefile 參考這邊

obj-m 指定編譯出的 object 檔名,應該要與 .c 原始檔同名(如果要編 feng.c ,object 檔就要寫 feng.o)
KERNELDIR 是已經編譯好的 kernel 的目錄

CROSS_COMPILE 指定 toolchain 的目錄

然後 make 就可以了。

初探 Android kernel 漏洞

爲了想要取得 HTC One SV 的 root 權限,進行了這次研究。

通常要 root HTC 的手機必須先到 HTCDev 網站解鎖 bootloader ,安裝第三方 bootloader ,然後注入 su 執行檔。如此一來 HTC 就知道那一臺裝置解鎖了,未來維修的時候或許會拒絕。(雖然網路上爬文的結果大部分人都說只要有將 bootloader 重新鎖定,維修的工程師就不會去特別檢查解鎖記錄。非軟體問題在保固期間內就算有解鎖記錄,只要有重新鎖定,維修的價格還是有保固的優惠。這是臺灣的情形)這種官方解鎖也會將手機上的資料完全清除,因爲一旦解鎖就有可能取得 root 權限,可以取得 /data 分割區內的各種個人資料、帳號密碼。

我就在想,有沒有辦法不跟 HTC 註冊就取得 root 權限?

如果不找官方解鎖,取得 root 權限的方法還有一種,就是利用 kernel 本身的提權 (privilege escalation) 漏洞。開啓 USB 偵錯模式,使用 adb shell ,我們就有了本地一般使用者的權限,再上傳利用這種漏洞的執行檔,執行之後如果利用成功,就可以取得 root 權限。

這種漏洞很多,android_run_root_shell 工具整合了多種漏洞來取得 root 權限。

這種漏洞可以藉由分析 kernel 原始碼來取得,很多都是 overflow 和 pointer 的問題(但我不會分析啦)。就算不親自去分析,也有很多已經公開,甚至已經有 exploit 程式的漏洞,大部分手機的 kernel 都沒有修補。Android kernel 很多漏洞都是從 Linux kernel 移植過來的。出自 Linux kernel 的漏洞影響的裝置幾乎是全部(所有的裝置都可以利用這個漏洞);出自 driver 的漏洞就只影響有使用此 driver 的裝置。

研究 Android kernel 漏洞的人好像蠻多的,而且很多已經有 Proof of Concept 或是實際的攻擊程式,大概是因爲 Android 裝置很多,而且這類核心漏洞修補困難吧。(製造商客製化的 Android kernel 要套用 patch 比較麻煩吧,而且每一款裝置的 kernel 都略有不同)

CVE-2013-2094

又稱 perf_swevent_init exploit 。

這個漏洞的破解程式 android_run_root_shell 裏面有,有漏洞的裝置清單在這

這個漏洞有點麻煩,要能夠利用它得取得幾個 kernel symbol 的位址,可以藉由反組譯 kernel 來取得,爲了要反組譯 kernel 得先取得 kernel image (zImage) ,使用工具可以把 zImage 從 boot.img 分離出來。

boot.img 是從 /dev/block/mmcblk0p16 之類的裝置(每一款手機裝置節點不同)當中 dd 出來的映像檔,裏面包含 zImage 和 init ramdisk ,boot.img 格式的細節

再利用 arm-linux-androideabi-objdump (ndk 裏面有)反組譯 kernel ,取得 kernel symbol 位址

$ arm-linux-androideabi-objdump --disassemble-all -b binary -m arm --adjust-vma=0xc0008000 kernel.zImage > kernel.dasm

這步驟我不是很懂。後來找到一個 script 把分離 boot.img 和反組譯、找 symbol 全包了。總之就是要找到這幾個位址(別照抄這幾個位址,這不是臺灣版 One SV 的):

prepare_kernel_cred=0xc00b3cc4
commit_creds=0xc00b3814
remap_pfn_range=0xc0132584
ptmx_fops=0xc0ede13c
perf_swevent_enabled=0xc0ec7784

利用找到的 kernel symbol 位址去 android_run_root_shell 加上 One SV 的支援,改改 C 的 code ,加上裝置和位址的定義這樣,這還是在資工系學 C 以來第一次有實際用途…… 不過這個工具裏面的另一個 fb_mem exploit 會造成 segmentation fault ,執行不到這個漏洞就結束,所以要註解掉 fb_mem 的 code 。如何編譯 README 有寫了。

但……我最大的麻煩是,連 boot.img 都沒有辦法取得。不同版本不同地區的 kernel 略有不同,kernel symbol 的位址也不一樣,我在網路上找不到臺灣版 One SV 原廠 Android 4.1.2 的 boot.img

另一個取得 symbol 位址的方法是讀取 /proc/kallsyms ,但是從 2011 年初 Android 就引進了 patch ,預設在 /proc/kallsyms 的地址只顯示全零,必須要將 /proc/sys/kernel/kptr_restrict 設爲1,才會顯示真的地址。但 /proc/sys/kernel/kptr_restrict 限定只有 root 才能更改……

這就要靠下一個漏洞來解決了……

CVE-2013-2596

又稱 fb_mem exploit ,我在 One SV 實測確實可以取得 root 權限的漏洞。kernelchopper 是比較低階利用此漏洞的程式,可以任讀寫記憶體,我實測是可以讀,但我不敢亂寫記憶體所以沒試,所以這個漏洞在 One SV 是存在的,它寫的手動改記憶體取得 root 權限的方式我看不太懂,不會用。motochopper 是高階工具 ,原本是專爲 Galaxy S4 做的,我稍微修改一下執行用 script 讓它破解完不要重開機就可以給 One SV 用了。HTC 的手機都有額外的防寫機制,執行檔放進 /system 之後重開機會消失,這應該就是所謂的 S-ON / S-OFF (我還沒深入研究),所以破解程式送進去執行之後不能重開機。

取得了 root 之後,就可以改 /proc/sys/kernel/kptr_restrict 然後讀 /proc/kallsyms 了。不過都已經有 root 了,要那些 kernel symbol 地址只是要驗證 perf_swevent_init 漏洞而已。

android_run_root_shell 工具包裏面還有其他漏洞,不過我要先去研究 s-off 了。