Tìm hiểu lỗi ứng dụng Android qua Mobisec
Gần đây mình có tìm hiểu các lỗi trên ứng dụng Android qua các challenge trên trang Mobisec (https://mobisec.reyammer.io). Giới thiệu qua trang Mobisec này của tác giả @reyammer sẽ có phần slides để hướng dẫn về các lỗi mobile (cụ thể là android), phần challenges có các challenge như các bài CTF, và hệ thống phân tích, chạy các file apk chứa payload được gửi lên để giải các challenge tương ứng và flag được trả lại qua log với tag “MOBISEC”. Trong phần challenges có 3 mảng là:
- App dev: cần lập trình cơ bản các app theo yêu cầu (như là bindservice, tạo cái gì đó tương tác (ghi lại sau nhớ nha), lắng nghe các broadcast,… ). Các kĩ năng này sẽ cần để xây dựng payload sau này.
- Reversing: cần khả năng reverse apk đề cho để đọc code và lấy flag trong file
- Exploitation: Là kết hợp của 2 mảng trên, cần reverse app để xác định lỗi và tạo app khai thác file apk lỗi đang chạy trên server. Trong loại này có một vài bài được tác giả dựng lại từ những case đã xảy ra thực tế.
Dưới đây là write up của các challenge cuối của mảng exploitation. Link tải các file apk: https://drive.google.com/drive/folders/15FT1dEtI-lf7V3CvUWYrTR04rsIaJG5P?usp=sharing
1. Fortnite
Challenge này lấy ý tưởng từ lỗi đã xảy ra trên app Fortnite(https://thehackernews.com/2018/08/fortnite-android-app-apk.html). Lỗi này hay xảy ra ở các chức năng update của các app android. Cụ thể nguyên nhân lỗi này là do lưu file update trong thư mục chung mà app khác có thể truy cập và sửa đổi được (bộ nhớ ngoài) do đó file update sẽ bị thay thế bằng file của attacker từ đó attacker có thể thực thi code tùy ý hoặc cài đặt ứng dụng mới. Trong quá trình pentest mình cũng đã gặp case như vậy.
Decompile file apk bằng JADX, xem qua về chức năng của app.
Trong hàm onCreate() sẽ liên tục thực hiện việc tải file cập nhật và signature của nó. Sau đó trong hàm verifyAndRunCode() thực hiện việc kiểm tra tính toàn vẹn của file tải về bằng file signature và thực hiện chạy code trong đó.
Trong đó flag chứa ở field flag trong class MainActivityvà được set bằng intent gửi từ server khi chạy chương trình qua hàm setFlag()
Chương trình bị lỗi ở hàm downloadFile() khi cả file update và file sign đều lưu ở bộ nhớ ngoài dẫn đến ứng dụng thứ 3 chỉ cần có quyền WRITE_EXTERNAL_STORAGE thì có thể ghi đè thay thế bằng 1 file update khác và file sign khác.
Như vậy mục tiêu của tác giả là mình sẽ phải thay thế file update và sign hợp lệ để chương trình sẽ load code của mình, từ đó lấy được giá trị của field flag. Thời gian vòng lặp giữa tải file và update tác giả để tương đối dư giả, trong thực tế thời gian sẽ nhanh hơn nhiều nên cần race lúc file vừa tải về và thực hiện mở file.
Để khai thác lỗi này mình tạo 1 file apk chứa payload để chạy code. Trong trường hợp bài này là phải truy cập được field flag của class cha (MainActivity) để đọc flag, nên mình sẽ dùng getContextClassLoader() để lấy được context hiện tại của class cha và dùng hàm setAccessible() để có thể lấy được giá trị của field flag trong class cha vì field này là private.
Lưu ý: Có vài cách để có thể lấy giá trị field flag trong class MainActivity nhưng nếu không lấy được context hiện tại thì flag trả về sẽ là giá trị khởi tạo ban đầu là “dummyflag” chứ không phải là flag hiện tại.
Sau khi tạo file payload mình sẽ tạo chương trình đóng vai trò là chương trình thứ 3 liên tục thực hiện việc ghi file payload và file sign đè lên file update và file sign của chương trình.
Để biết được thuật toán để sign là gì thì để ý lúc chương trình check valid của file sẽ thực hiện sign file update và so sánh với file sign. Dò ngược theo hàm đó sẽ ra được thuật toán để sign (cụ thể ở đây là SHA-256).
Mình sẽ lưu file apk chứa payload trong thư mục raw của app thứ 3. Sau đó liên tục ghi đè lên bộ nhớ ngoài.
2. Keyboard
Theo tác giả thì bài này dựa trên lỗi có thật của một ứng dụng bàn phím. Tiến hành decompile và phân tích code.
Qua hàm setFlag có thể thấy flag sau khi được nhận từ intent gửi từ server khi chạy chương trình sẽ được lưu trong file InfoKeyboardPrefs.xml trong thư mục /data/data/com.mobisec.keyboard/shared_prefs. Đây là thư mục trong của app và app khác không thể đọc hay thay đổi được.
Giá trị của các file trong /data/data/com.mobisec.keyboard/shared_prefs khi mới khởi tạo và khi nhận được intent chứa flag. Đồng thời chương trình cũng chạy liên tục hàm checkForUpdates().
Lại một lần nữa giống bài trước, chương trình sẽ lưu file update dạng zip trong bộ nhớ ngoài. Và thực hiện giải nén file update.zip, nếu không giải nén được sẽ kiểm tra debugmode ở 2 file KeyboardPrefs.xml và GlobalKeyboardPrefs.xml xem debugmode có đang bật không, nếu một trong 2 bật thì sẽ hiện nội dung của các file xml trong /data/data/com.mobisec.keyboard/shared_prefs ra log.
Xem qua hàm extractFolder(), hàm này chỉ extract file update.zip ra bộ nhớ ngoài thôi.
Vậy theo mình hướng làm là:
1) Ghi đè file update.zip ở bộ nhớ ngoài bằng file zip chứa payload của mình
2) Lợi dụng hàm extractFolder() tạo file GlobalKeyboardPrefs.xml trong /data/data/com.mobisec.keyboard/shared_prefs để bật debug mode lên
3) Thay thế file update.zip thành 1 file zip khác bị lỗi để nhảy vào phần catch khi extractFolderlần sau để in mọi thông tin trong file xml ra log, trong đó có flag.
Khi thực hiện extractFolder hay ghi file thì tiến trình sẽ chạy dưới quyền của user của app com.mobisec.keyboard. Để ghi vào thư mục /data/data/com.mobisec.keyboard/shared_prefs mình sẽ dùng “../” ở tên thư mục trong zip.
Nội dung file GlobalKeyboardPrefs.xml của mình chỉ là set debugmode thành true thôi.
Cuối cùng là xây dựng chương trình hoàn chỉnh ghi file zip payload vào bộ nhớ ngoài và ghi file zip lỗi đè lên file ban đầu sau đó vài giây.
3. File browser
Challenge này thì có nhiều file hơn những challenge trước. Mình sẽ tóm tắt tổng quan chương trình như sau.
1) Chương trình nhận input từ người dùng và gửi intent tới QueryActivityvới 2 extra là:
- oper: chứa lệnh (ls, du,cat)
- arg: chứa tham số của lệnh
2) Trong QueryActivity sẽ thực hiện command nhận từ intent ngoại trừ lệnh “cat” và trả về output của lệnh trong extra result, đồng thời cũng kiểm tra nếu intent có extra “debug=true” thì sẽ ghi kết quả ra log ở /sdcard/browser.log
3) Các lệnh sau khi được chạy sẽ được mã hóa AES và lưu vào database ở /data/data/com.mobisec.filebrowser/databases/LogDb. Flag cũng được lưu giống vậy và nằm ở hàng đầu tiên.
4) Class PluginActivity tạo 1 PendingIntent tới QueryActivity nhưng chưa sử dụng đến và trả về PendingIntent qua Intent
5) Xem qua AndroidManifest.xml, các class đều set exported=false (để app khác không gọi trực tiếp activity được) và PluginActivity có intent-filter với action com.mobisec.browser.action.START_PLUGINvà authorities là com.mobisec.provider.Log. Có thể tìm hiểu thêm về Intent-filter ở đây: https://developer.android.com/guide/topics/manifest/intent-filter-element
Bình thường với exported=false thì app ngoài không thể gọi activity của app này lên và gửi Intent vào được. Nhưng nếu có PendingIntent (https://developer.android.com/reference/android/app/PendingIntent) chứa Intent gọi đến Activity bên trong app victim thì có thể gọi được. https://stackoverflow.com/questions/2808796/what-is-an-android-pendingintent
Như vậy với app này lỗi sẽ nằm ở việc ta có thể lấy được PendingIntent gọi tới QueryActivity qua việc gửi Inent tới PluginAcitivty với action và authorities thích hợp.
Khi đã gửi được Intent tới PluginActivity ta nhận PendingIntent ở onActivityResult()
Với PendingIntent nhận được, ta có thể gọi vào QueryActivity dễ dàng. Vấn đề ở đây là mình cần thêm extra vào Intent với payload để chạy lệnh và set extra debug=true. Để thêm extra vào PendingIntent mình sẽ tạo Intent rỗng và thêm extra vào, sau đó dùng hàm send() của Pending Intent đã nhận được để gửi.
Kịch bản khai thác sẽ như sau:
- Gửi Intent với command injection payload đọc file /data/data/com.mobisec.filebrowser/shared_prefs/keys.xml để lấy key lát decrypt data trong database, ghi output ra /sdcard/browser.log, đọc file /sdcard/browser.logvà ghi ra log.
- Thực hiện command lấy giá trị của db ghi vào file
- Dùng key decrypt ra flag
Author: Vũ Tô Thanh Hoài