Bài 9: Bảo mật Ứng dụng Android


1. Mô hình an toàn Android (Android Security Model)

Android xây dựng bảo mật dựa trên 4 trụ cột chính:

  • Application Sandboxing — cô lập ứng dụng ở cấp tiến trình và tập tin
  • Permission — cơ chế kiểm soát quyền truy cập tài nguyên
  • IPC (Inter-Process Communication) — cơ chế giao tiếp an toàn giữa các tiến trình
  • Code Signing & Platform Key — xác thực nguồn gốc và tính toàn vẹn ứng dụng

2. Phân loại ứng dụng Android

Android phân biệt hai loại ứng dụng:

LoạiVị tríĐặc điểm
System app/system/Cài sẵn với OS, read-only, không thể xóa/sửa
User-installed app/data/Phân vùng read-write, độc lập với nhau

3. Application Sandboxing

3.1 Cơ chế cô lập bằng UID

Khi một ứng dụng được cài đặt, Android cấp cho nó một UID (User ID) duy nhất — còn gọi là app ID. Mỗi ứng dụng chạy như một Linux user riêng biệt, được cô lập ở hai cấp độ:

  • Cấp tiến trình (process): mỗi app chạy trong tiến trình riêng với UID riêng
  • Cấp tập tin (file): mỗi app có thư mục riêng tại /data/data/<package>, chỉ UID của app đó mới có quyền đọc/ghi

3.2 Phân vùng UID

UID 0                   → root
UID 1000 (AID_SYSTEM)  → các dịch vụ hệ thống
UID >= 10000 (AID_APP) → ứng dụng người dùng

Quy tắc đặt username cho app:

  • app_XXX — trong đó XXX là offset tính từ AID_APP
  • uY_aXXX — trong môi trường multi-user, Y là Android user ID

Ví dụ: UID = 10037 → username = u0_a37 (Y=0, XXX=37)

# Kiểm tra UID của một package cụ thể
pm list packages
dumpsys | grep -A18 "Package \[com.example.app1\]"

# Xem các tiến trình đang chạy với UID tương ứng
ps

3.3 Quản lý UID trong hệ thống

UID của ứng dụng được lưu tại hai file:

/data/system/packages.xml   → thông tin chi tiết (XML)
/data/system/packages.list  → thông tin dạng phẳng (flat)

Cấu trúc một dòng trong packages.list:

<package_name> <UID> <debug_flag> <data_dir> <seinfo_label> <GID_list>

Ví dụ:

com.google.android.email 10037 0 /data/data/com.google.android.email default 3003,1028,1015

Giải thích từng trường:

Thứ tựÝ nghĩa
1Package name
2UID của ứng dụng
3Debug flag (1 = debug mode)
4Đường dẫn thư mục data của app
5SEInfo label (dùng bởi SELinux)
6Danh sách GID bổ sung (tương ứng với các permission được cấp)

3.4 Shared User ID

Hai hoặc nhiều ứng dụng có thể chia sẻ cùng một UID — gọi là shared user ID. Khi đó chúng:

  • Chạy trong cùng một tiến trình
  • Có thể chia sẻ dữ liệu với nhau

Điều kiện bắt buộc: các ứng dụng phải được ký bởi cùng một code signing key.

Ví dụ: com.android.keyguardcom.android.systemui cùng UID 10012:

com.android.keyguard  10012 0 /data/data/com.android.keyguard  platform 1028,1015,1035,3002,3001
com.android.systemui  10012 0 /data/data/com.android.systemui platform 1028,1015,1035,3002,3001

3.5 Quyền truy cập file giữa các ứng dụng

Thông thường, thư mục của app hoàn toàn private. Nếu muốn chia sẻ file, app có thể tạo file với flag:

  • MODE_WORLD_READABLE → tương ứng bit S_IROTH — cho phép đọc
  • MODE_WORLD_WRITEABLE → tương ứng bit S_IWOTH — cho phép ghi
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_WORLD_READABLE);
fos.write(string.getBytes());
fos.close();

4. Permission (Quyền hạn)

4.1 Khái niệm

Permission là một chuỗi (string) định nghĩa khả năng thực hiện một tác vụ, ví dụ:

  • Truy cập tài nguyên vật lý (camera, microphone, thẻ nhớ SD)
  • Chia sẻ dữ liệu (danh sách liên lạc, lịch sử SMS)
  • Cho phép ứng dụng thứ 3 khởi động/truy cập vào các component của app

Quyền hạn được khai báo trong file AndroidManifest.xml:

<manifest>
    <uses-permission android:name="android.permission.CAMERA" />
    <application>
        ...
    </application>
</manifest>

4.2 Các mức thực thi Permission

Permission có thể được kiểm tra ở nhiều cấp độ khác nhau trong hệ thống:

graph TD A[Permission Request] --> B{Mức kiểm tra} B --> C[Kernel Level - Linux UID/GID check] B --> D[Android OS Level - Component check] B --> E[Cả hai cấp độ]

4.3 Permission Protection Level

Đây là thuộc tính quan trọng nhất của một permission, xác định mức độ rủi rocách hệ thống quyết định có cấp hay không:

Rủi ro thấp — Permission tự động được cấp khi cài đặt, không cần xác nhận từ người dùng.

Ví dụ: ACCESS_NETWORK_STATE, GET_ACCOUNTS, INTERNET

Rủi ro cao — Có thể truy cập dữ liệu nhạy cảm hoặc kiểm soát thiết bị. Android hiển thị dialog yêu cầu người dùng chấp thuận tường minh (runtime permission từ Android 6.0+).

Ví dụ: READ_SMS, CAMERA, READ_CONTACTS, ACCESS_FINE_LOCATION

Chỉ cấp cho app được ký cùng key với app khai báo permission đó.

Ví dụ: NET_ADMIN, ACCESS_ALL_EXTERNAL_STORAGE

Đây là protection level mạnh nhất — chỉ các app trong cùng “nhà phát triển” mới được cấp.

Cấp cho app thuộc system image hoặc được ký cùng key với app khai báo permission.

  • Giúp vendor không cần chia sẻ key với nhau trên các app cài sẵn
  • Từ Android 4.4 trở đi: app cần được cài tại /system/priv-app/ thay vì /system/app/

4.4 Custom Permission

Ứng dụng bên thứ ba có thể tự định nghĩa permission với protection level tùy chọn:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

    <permission
        android:name="com.example.app.permission.PERMISSION1"
        android:label="@string/permission1_label"
        android:description="@string/permission1_desc"
        android:permissionGroup="com.example.app.permission-group.TEST_GROUP"
        android:protectionLevel="signature" />

</manifest>

4.5 Yêu cầu Permission (uses-permission)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.app">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application android:name="SampleApp">
        ...
    </application>
</manifest>

4.6 Ánh xạ Permission → GID

Một điểm rất quan trọng trong cơ chế thực thi permission trên Android: permission được ánh xạ sang GID và gán vào danh sách GID bổ sung của tiến trình app. Tệp cấu hình ánh xạ này là:

/etc/permissions/platform.xml

Ví dụ nội dung platform.xml:

<?xml version="1.0" encoding="utf-8"?>
<permissions>
    <permission name="android.permission.INTERNET">
        <group gid="inet" />
    </permission>

    <permission name="android.permission.WRITE_EXTERNAL_STORAGE">
        <group gid="sdcard" />
        <group gid="sdcard_rw" />
    </permission>

    <assign-permission name="android.permission.MODIFY_AUDIO_SETTINGS" uid="media" />
    <assign-permission name="android.permission.ACCESS_SURFACE_FLINGER"  uid="media" />
</permissions>

Ánh xạ GID name → số được định nghĩa trong android_filesystem_config.h:

#define AID_INET       3003  // can create AF_INET and AF_INET6 sockets
#define AID_SDCARD_RW  1015  // external storage write access
#define AID_SDCARD_R   1028  // external storage read access

Nghĩa là: một app được cấp INTERNET → kernel cho phép app tạo socket vì tiến trình app có GID=3003 (inet).

Thẻ <assign-permission> làm ngược lại: gán quyền hạn cho tiến trình đang chạy với UID tương ứng (ví dụ: tiến trình media được tự động có MODIFY_AUDIO_SETTINGS).

4.7 Quản lý Permission qua Package Manager

Permission được gán lúc cài đặt bởi Package Manager Service, lưu trong:

/data/system/packages.xml

Ví dụ entry trong packages.xml:

<package name="com.google.android.apps.translate"
    codePath="/data/app/com.google.android.apps.translate-2.apk"
    userId="10204"
    installer="com.android.vending">
    <sigs count="1">
        <cert index="7" />
    </sigs>
    <perms>
        <item name="android.permission.READ_EXTERNAL_STORAGE" />
        <item name="android.permission.CAMERA" />
        <item name="android.permission.INTERNET" />
        <item name="android.permission.RECORD_AUDIO" />
        ...
    </perms>
</package>

4.8 Zygote và việc gán thuộc tính tiến trình

Tất cả ứng dụng Android đều có tiến trình cha là Zygote. Khi một app được khởi chạy, hệ thống fork từ Zygote và gán:

  • UID tương ứng với app
  • GID chính
  • Danh sách GID bổ sung (từ ánh xạ permission → GID)

Điều này giúp khởi động nhanh vì Zygote đã pre-load toàn bộ thư viện Android runtime.


5. IPC — Inter-Process Communication

5.1 Vấn đề

Android chạy mỗi app trong sandbox riêng, nhưng đôi khi các tiến trình cần giao tiếp. Các cơ chế IPC truyền thống của Linux như file, socket, pipe, semaphore, shared memory, message queue đều có thể dùng, nhưng Android cần một cơ chế an toàn và tích hợp với mô hình permission hơn.

5.2 Binder — IPC mới của Android

Binder là cơ chế IPC đặc thù của Android, phát triển dựa trên OpenBinder.

graph LR A[Process A\nBinder Client] -->|transact| C[Linux Kernel\nBinder Driver /dev/binder] C -->|onTransact| B[Process B\nBinder Server]

Binder dựa trên kiến trúc distributed component, tương tự Windows COM hay CORBA trên Unix — nhưng chỉ hoạt động trên một thiết bị (không hỗ trợ RPC qua mạng).

5.3 Binder Security — Identity duy nhất

Các đối tượng Binder duy trì một định danh duy nhất xuyên suốt các tiến trình. Nếu:

  • Tiến trình A tạo một Binder object
  • Truyền cho tiến trình B
  • B truyền tiếp cho tiến trình C

Thì tất cả lời gọi từ A, B, C đều được thực hiện trên cùng một object trong không gian bộ nhớ của A. Binder object có thể hoạt động như một security token — nền tảng của capability-based security trong Android.

5.4 Capability-based Security

Binder token = capability:

  • Sở hữu Binder token → có toàn quyền truy cập Binder đó
  • Không thể “đoán” hay giả mạo token của tiến trình khác

5.5 Service Manager và Service Discovery

Binder sử dụng context manager (còn gọi là service manager) để cho phép xác định dịch vụ:

  1. Service manager khởi chạy khi boot
  2. Các dịch vụ đăng ký với service manager (tên + Binder reference)
  3. App muốn dùng dịch vụ → tra cứu theo tên → nhận Binder reference → gọi trực tiếp

Chỉ một số tiến trình hệ thống được đăng ký dịch vụ. Ví dụ: chỉ tiến trình có UID=1002 (AID_BLUETOOTH) mới được đăng ký Bluetooth service.

# Liệt kê tất cả Binder services đã đăng ký
service list

Output ví dụ:

Found 79 services
  sip: [android.net.sip.ISipService]
  phone: [com.android.internal.telephony.ITelephony]
  nfc: [android.nfc.INfcAdapter]
  ...

5.6 Binder đảm bảo UID/PID không thể giả mạo

Binder đảm bảo rằng UID và PID của caller không thể bị spoofed. Điều này cho phép các dịch vụ hệ thống kiểm soát quyền truy cập:

public boolean enable() {
    if ((Binder.getCallingUid() != Process.SYSTEM_UID)
            && (!checkIfCallerIsForegroundUser())) {
        Log.w(TAG, "enable() not allowed for non-active and non-system user");
        return false;
    }
    // ...
}

Quy trình kiểm tra permission khi một component bị gọi:

sequenceDiagram participant Caller as App A (caller) participant Binder participant System as Android System participant Callee as App B component (callee) Caller->>Binder: transact() Binder->>System: getCallingUID() System->>System: Tra cứu packages.xml → lấy permissions của caller System->>System: Kiểm tra callee yêu cầu permission gì alt Permission hợp lệ System->>Callee: Cho phép truy cập else Không hợp lệ System-->>Caller: throw SecurityException end

6. Code Signing & Platform Keys

6.1 Tại sao cần ký APK?

Android sử dụng chữ ký trên APK để đảm bảo:

  • Same origin policy: update ứng dụng phải đến từ cùng một developer (cùng key)
  • Trust relationship: thiết lập mối quan hệ tin cậy giữa các ứng dụng (shared UID)
  • Integrity check: so sánh phiên bản hiện tại và phiên bản update

6.2 Platform Keys

Ứng dụng hệ thống (system app) được ký bởi platform key. Platform key được quản lý bởi:

  • Nhà sản xuất thiết bị (OEM)
  • Nhà mạng (carrier)
  • Google (cho thiết bị Nexus/Pixel)
  • Cộng đồng custom ROM (AOSP build)

6.3 Cấu trúc APK

APK thực chất là một file ZIP với cấu trúc mở rộng từ JAR:

apk/
├── AndroidManifest.xml
├── classes.dex
├── resources.arsc
├── assets/
├── lib/
│   ├── armeabi/
│   │   └── libapp.so
│   └── armeabi-v7a/
│       └── libapp.so
├── META-INF/
│   ├── CERT.RSA    ← Chứng chỉ + chữ ký
│   ├── CERT.SF     ← Signature file
│   └── MANIFEST.MF ← Digest của từng file
└── res/
    ├── anim/
    ├── drawable/
    ├── layout/
    └── ...

6.4 Quá trình ký — Java JAR Signing

Bước 1: MANIFEST.MF — chứa digest của mỗi file:

Manifest-Version: 1.0
Created-By: 1.0 (Android)

Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: K/ORd/ltoqSlgDD/9DY7aCNIBvU=

Name: res/menu/main.xml
SHA1-Digest: kC8WDilgurof+F2AxgcSSKDhjno=

Bước 2: CERT.SF — chứa digest của toàn bộ MANIFEST.MF và từng entry:

Signature-Version: 1.0
SHA1-Digest-Manifest: zboXjEhVBxEOz2ZC+B4OW2SWBxo=

Name: res/drawable-xhdpi/ic_launcher.png
SHA1-Digest: jTeE2YSL3uBd...

Name: res/menu/main.xml
SHA1-Digest: kSQDLtTE07cL...

Bước 3: CERT.RSA — chứa chứng chỉ X.509 và chữ ký số lên file .SF

Kiểm tra thủ công bằng OpenSSL:

# Tính hash của MANIFEST.MF
openssl sha1 -binary MANIFEST.MF | openssl base64

# Ký APK bằng jarsigner
jarsigner -keystore debug.keystore \
    -sigalg SHA1withRSA \
    test.apk androiddebugkey

# Verify chữ ký
jarsigner -keystore debug.keystore -verify -verbose -certs test.apk

# Xem thông tin certificate
keytool -list -printcert -jarfile test.apk

# Trích xuất certificate từ APK
unzip test.apk META-INF/CERT.RSA
openssl pkcs7 -inform DER -print_certs -out cert.pem < META-INF/CERT.RSA

6.5 Android Code Signing vs JAR Signing

Đặc điểmJAR SigningAndroid APK Signing
Chuẩn cơ sởJARJAR (mở rộng)
KhóaX.509X.509 (có thể self-signed)
Entry không kýCho phépKhông cho phép — tất cả entry phải được ký
Nhiều signerCho phépTất cả entry phải cùng một signer
Tooljarsignersignapk
# Ký APK bằng signapk
java -jar signapk.jar cert.cer key.pk8 test.apk test-signed.apk

# Chuyển đổi key sang định dạng pk8
echo "keypwd" | openssl pkcs8 -in mykey.pem \
    -topk8 -outform DER -out key.pk8 -passout stdin

7. Component Security & Permission

Android app có 4 loại component, mỗi loại có cách bảo vệ riêng.

7.1 Activity

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.testapps.test1">

    <activity
        android:name=".Activity1"
        android:permission="com.example.testapps.test1.permission.START_ACTIVITY1">
        <intent-filter>
            ...
        </intent-filter>
    </activity>
</manifest>

Để app X gọi Activity1, app X phải khai báo:

<uses-permission android:name="com.example.testapps.test1.permission.START_ACTIVITY1" />

7.2 Service

<service
    android:name=".MailListenerService"
    android:permission="com.example.testapps.test1.permission.BIND_TO_MAIL_LISTENER"
    android:enabled="true"
    android:exported="true">
    <intent-filter>...</intent-filter>
</service>

7.3 Content Provider

Content Provider hỗ trợ phân tách quyền đọc và ghi:

<provider
    android:name=".MailProvider"
    android:authorities="com.example.testapps.test1.mailprovider"
    android:readPermission="com.example.testapps.test1.permission.DB_READ"
    android:writePermission="com.example.testapps.test1.permission.DB_WRITE">
</provider>

URI Permission (Fine-grained Access)

Thay vì cấp quyền truy cập toàn bộ Provider, có thể cấp quyền theo URI cụ thể:

<provider
    ...
    android:grantUriPermissions="true">
</provider>

Cho phép grant quyền truy cập vào bất kỳ URI nào trong Provider.

<provider ...>
    <grant-uri-permission android:path="/attachments/" />
</provider>

Chỉ cho phép truy cập URI /attachments/không bao gồm các URI con.

<provider ...>
    <grant-uri-permission android:pathPrefix="/messages/" />
</provider>

Cho phép truy cập tất cả URI bắt đầu bằng /messages/.

<provider ...>
    <grant-uri-permission android:pathPattern=".*public.*" />
</provider>

Cho phép truy cập tất cả URI chứa chuỗi public.

7.4 Broadcast Receiver

Giới hạn ai được gửi broadcast đến Receiver:

<!-- Trong AndroidManifest.xml -->
<receiver
    android:name=".UIMailBroadcastReceiver"
    android:permission="com.example.testapps.test1.permission.MSG_NOTIFY_SEND">
    <intent-filter>
        <action android:name="com.example.testapps.test1.action.MESSAGE_RECEIVED" />
    </intent-filter>
</receiver>

Hoặc đăng ký động trong code:

IntentFilter intentFilter = new IntentFilter(MESSAGE_RECEIVED);
UIMailBroadcastReceiver rcv = new UIMailBroadcastReceiver();
myContext.registerReceiver(rcv, intentFilter,
    "com.example.testapps.test1.permission.MSG_NOTIFY_SEND", null);

Giới hạn Receiver nào được nhận broadcast từ app:

// Chỉ broadcast đến receiver có quyền SEND_SMS
sendBroadcast(new Intent("com.example.NOTIFY"), Manifest.permission.SEND_SMS);

Receiver muốn nhận phải khai báo:

<uses-permission android:name="android.permission.SEND_SMS" />

8. Multi-User Support

  • Android ban đầu thiết kế cho single-user → UID gắn với app, không phải user
  • Từ Android 4.2: hỗ trợ multi-user (chỉ tablet)
  • Từ Android 5.0: hỗ trợ multi-user trên nhiều loại thiết bị hơn
  • Mỗi user có ID duy nhất, bắt đầu từ 0, thư mục riêng tại /data/system/users/<user_ID>
  • UID của app trong môi trường multi-user: uY_aXXX (Y = Android user ID)

9. SEAndroid (SELinux for Android)

Đặc điểmDACMAC
Tên đầy đủDiscretionary Access ControlMandatory Access Control
Ai kiểm soátUser (chủ sở hữu file)Policy do admin định nghĩa
Linh hoạtCao (user chuyển quyền tùy ý)Thấp (policy cứng)
Bảo mậtThấp hơnCao hơn

SELinux là phiên bản MAC cho Linux kernel. SEAndroid là bản điều chỉnh của SELinux dành riêng cho Android, được tích hợp từ Android 4.3.

SEAndroid bổ sung một lớp kiểm soát bên trên cơ chế UID/GID truyền thống — ngay cả khi root, một tiến trình cũng có thể bị từ chối truy cập nếu SELinux policy không cho phép.


10. System Updates & Verified Boot

10.1 Cơ chế cập nhật hệ thống

Over-the-Air (OTA):

  1. Tải file ZIP có chữ ký code
  2. Recovery OS biên dịch và áp dụng update
  3. Recovery chỉ chấp nhận update được ký bởi nhà sản xuất thiết bị
  4. Chữ ký được nhúng vào phần Comment của file ZIP

Các hình thức cập nhật khác:

  • Boot loader unlocking: thay thế recovery + system image, cài update từ bên thứ 3 — dữ liệu user sẽ bị xóa
  • ADB (Android Debug Bridge): kết nối PC và cập nhật trực tiếp

10.2 Verified Boot

Từ Android 4.4, Android hỗ trợ Verified Boot dựa trên dm-verity (Linux Device-Mapper):

Cơ chế sử dụng Hash Tree (Merkle Tree):

  • Nút lá: hash của từng data block
  • Nút trung gian: hash của các nút con
  • Nút gốc (root hash): hash tổng hợp toàn bộ cây
graph TD R[Root Hash\nXác thực bằng RSA public key] --> I1[Intermediate Hash 1] R --> I2[Intermediate Hash 2] I1 --> L1[Hash Block 1] I1 --> L2[Hash Block 2] I2 --> L3[Hash Block 3] I2 --> L4[Hash Block 4] L1 --> D1[Data Block 1] L2 --> D2[Data Block 2] L3 --> D3[Data Block 3] L4 --> D4[Data Block 4]
  • Chỉ cần tin tưởng root hash → có thể xác thực toàn bộ phân vùng
  • Mỗi block được kiểm tra runtime khi đọc
  • Vì kernel thực hiện kiểm tra → quá trình boot cần xác thực kernel trước

11. Package Management

11.1 Quá trình cài đặt APK

sequenceDiagram participant User as User/PackageInstaller participant PM as PackageManagerService participant Installd as installd daemon participant Vold as vold daemon User->>PM: Install APK request PM->>PM: Parse AndroidManifest.xml PM->>PM: Verify signature PM->>PM: Check permissions PM->>Installd: /dev/socket/installd Installd->>Installd: Copy APK to /data/app/ Installd->>Installd: Extract native libs to /data/app-lib/ Installd->>Installd: dex2oat (compile .dex → .oat) PM->>PM: Update packages.xml & packages.list PM->>User: Install complete

11.2 Package Verification

Android hỗ trợ Package Verification — một app có thể tự đăng ký làm verifier để kiểm tra APK trước khi cài:

<receiver
    android:name=".MyPackageVerificationReceiver"
    android:permission="android.permission.BIND_PACKAGE_VERIFIER">
    <intent-filter>
        <action android:name="android.intent.action.PACKAGE_NEEDS_VERIFICATION" />
        <action android:name="android.intent.action.PACKAGE_VERIFIED" />
        <data android:mimeType="application/vnd.android.package-archive" />
    </intent-filter>
</receiver>

Điều kiện để trở thành verifier:

  1. Khai báo permission PACKAGE_VERIFICATION_AGENT
  2. Thêm thẻ <package-verifier> với tên package và public key
  3. Hệ thống phải có Settings.Global.PACKAGE_VERIFIER_ENABLE = true

Google Play Protect là triển khai thực tế của cơ chế này — kiểm tra APK với Google’s cloud backend trước và sau khi cài đặt.


12. Câu hỏi ôn tập


Tài liệu tham khảo & Mở rộng