Android Docker Cho Pentest API Mobile: Tại Sao Không?

Published By: RED TEAM

Published On: 27/02/2026

Published in:

Làm Web Pentester thi thoảng mình cũng được nhận mấy dự án pentest API cho Android App. Mà để pentest API Android App thì bắt buộc phải dựng giả lập Android để thao tác còn lấy request, chứ không thể trông chờ 100% vào file Postman khách hàng đưa cho. Và thế là hành trình chật vật setup giả lập với Genymotion hoặc Android Studio cứ thế lặp lại mỗi khi có task. Cứ mỗi lần chuẩn bị vào việc là lại phải đối mặt với một combo quen thuộc: Genymotion đôi khi lại dở chứng lỗi VirtualBox, lỗi card mạng, chưa kể công đoạn root máy, cài certificate để bắt gói tin HTTPS cũng đủ làm mình bay màu hết cả buổi sáng, đôi khi là cả ngày. Máy không khỏe để gánh 2 3 con giả lập cùng lúc, công ty cũng không phải lúc nào cũng thừa điện thoại để mình mượn pentest. Mình đã tự hỏi “Chẳng lẽ không có cách nào nhẹ nhàng và tinh tế hơn sao?”. Thì có anh đưa cho mình giải pháp dựng Android trên Docker.

Chúng ta đã quá quen thuộc với Docker trong việc đóng gói Microservices, chạy Web Server hay Database nhờ tính cách ly và khả năng di động tuyệt vời. Thế nhưng, tận dụng những đặc tính đó để dựng một Android Pentest Lab lại là một hướng đi cực kỳ thú vị mà không phải ai cũng nghĩ tới. Hãy tưởng tượng bạn có một bộ setup chuẩn chỉ gồm: Android đã root, cài sẵn Frida-server, kèm theo một đống script bypass SSL Pinning, đóng gói tất cả vào một Image, khi nào cần chỉ việc docker-compose up. Khả năng tùy biến và mở rộng thì hoàn toàn theo cách anh em viết Dockerfile.

Cách dựng Docker này được tham khảo từ đây, mình sẽ giải thích một số điều cơ bản khi build cũng như các lưu ý và tối ưu dựa trên nguồn này

Build Dockerfile

Android SDK command-line tools được viết bằng Java, nên việc chọn một image có sẵn JDK (Java Development Kit) là bắt buộc. Bài viết gốc đang sử dụng openjdk:21-jdk-slim, tuy nhiên image này đã không còn tồn tại trên Dockerhub nữa. Vì thế mình dùng lựa chọn thay thế là eclipse-temurin:21-jdk-jammy cho ổn định về sau.

Ở đây chúng ta cần quan tâm tới một số biến quan trọng, liên quan trực tiếp tới cấu hình android mà chúng ta build như:

  • API_LEVEL: Đây là số hiệu phiên bản Android

  • IMG_TYPE: Loại image hệ thống. Thường có các loại như default (AOSP thuần), google_apis (có Google Play Services), hoặc google_apis_playstore (có cả CH Play).

  • ARCHITECTURE: Kiến trúc CPU

  • DEVICE_ID: Profile phần cứng, quy định độ phân giải màn hình, mật độ điểm ảnh…

Anh em cứ hình dung thế này cho dễ:

  • API_LEVEL là linh hồn (Android 11, 12, 13...).

  • ARCHITECTUREDEVICE_ID là thể xác (CPU x86_64 cho nhanh, dáng hình Pixel).

Để tìm chuẩn chỉ những tham số này phù hợp với nhu cầu, cũng như để không bị lỗi khi build, ta có thể tìm API Level ở đây

Mỗi API level sẽ ứng với một version Android khác nhau

Về IMG_TYPE có 3 loại chính anh em cần phân biệt rõ:

  1. default:

    • Đặc điểm: Android gốc (AOSP), không có chợ ứng dụng, không có Google Maps/Services.

    • Ưu điểm: Cực nhẹ, root siêu dễ.

    • Nhược điểm: App nào gọi Google API (Map, Login Gmail...) sẽ crash.

  2. google_apis (Khuyên dùng cho Pentest):

    • Đặc điểm: Có sẵn Google Play Services (GMS) nhưng KHÔNG có CH Play (Store).

    • Ưu điểm: Chạy được 99% các app, dễ root (vì bootloader không bị khóa chặt như bản Store).

  3. google_apis_playstore:

    • Đặc điểm: Có cả CH Play để tải app.

    • Nhược điểm: Đây là bản "User", bảo mật rất cao. Việc root bản này cực khó (thường phải dùng Magisk + Zygisk phức tạp) và cài Certificate vào hệ thống cũng gian nan hơn.

Vậy nên với nhu cầu cơ bản thì cứ cài google_apis cho dễ

Một tham số nữa là CMD_LINE_VERSION, đây là phiên bản (Build ID) của bộ công cụ dòng lệnh Android mà Google phát hành. Mã số này có thể được lấy từ https://developer.android.com/studio

Tuy nhiên google chỉ hiển thị các phiên bản mới nhất

Nên để tìm kiếm các phiên bản cũ hơn, anh em có thể dùng sdkmanager để tra cứu. Hoặc anh em có thể tìm trong repo của Google

https://dl.google.com/android/repository/repository2-1.xml

Một cách ăn gian hơn là anh em hỏi mấy con AI, và đây là những gì nó trả lời.

Phiên bản ToolsMã số (CMD_LINE_VERSION)Yêu cầu Java (JDK)Tương thích tốt nhất
Latest (2024)11076708Java 17+Android 14, 15
Tools 11.010406996Java 17Android 13, 14
Tools 9.09477386Java 11/17Android 12, 13
Tools 8.09123335Java 11Android 11, 12
Tools 7.08512546Java 8/11Android 10, 11
Tools 6.08092744Java 8/11Android 9, 10
Tools 5.06858069Java 8Android 8, 9
Tools 2.1 (Cũ)6609375Java 8Android 7, 8

Build install-sdk.sh

Nhiệm vụ của install-sdk.sh ở đây là để tải SDK, Emulator, Platform-tools từ Google về image.

Mấy dòng lệnh này dùng để tải bộ công cụ dòng lệnh (Command Line Tools mình nói ở trên) về và sắp xếp lại cấu trúc thư mục

wget [<https://dl.google.com/android/repository/commandlinetools-linux-${CMD_LINE_VERSION}.zip>](<https://dl.google.com/android/repository/commandlinetools-linux-$%7BCMD_LINE_VERSION%7D.zip>) -P /tmp && \\
mkdir -p $ANDROID_SDK_ROOT/cmdline-tools/ && \\
unzip -d $ANDROID_SDK_ROOT/cmdline-tools/ /tmp/commandlinetools-linux-${CMD_LINE_VERSION}.zip && \\
mv $ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools/ $ANDROID_SDK_ROOT/cmdline-tools/tools/

Khi bạn giải nén file zip của Google, mặc định nó sẽ tạo ra một thư mục tên là cmdline-tools. Nếu cứ để nguyên như vậy, đường dẫn sẽ là: $ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools/bin/sdkmanager

Tuy nhiên, công cụ sdkmanager lại được lập trình để tìm kiếm file cấu hình theo cấu trúc cố định là folder tools nằm trong cmdline-tools. Nếu không đúng cấu trúc này, khi chạy lệnh nó sẽ báo lỗi:

"Could not determine SDK root"

Vì vậy, dòng lệnh mv giúp chúng ta sửa lại đường dẫn thành: $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager cho đúng chuẩn.

Đoạn command cuối chỉ dùng để chạy sdkmanager tải những thứ cần thiết nhất về:

sdkmanager --install "$PACKAGE_PATH" "$ANDROID_PLATFORM_VERSION" platform-tools emulator
  • "$PACKAGE_PATH": Chính là cái biến loằng ngoằng system-images;... mà chúng ta đã định nghĩa trong Dockerfile. Nó sẽ tải đúng cái ROM (System Image) mà anh em cần (ví dụ Android 14, Google APIs).

  • "$ANDROID_PLATFORM_VERSION": Tải bộ SDK Platform tương ứng để biên dịch.

  • platform-tools: Chứa các công cụ như ADB, Fastboot.

  • emulator: Phần mềm giả lập để chạy cái ROM kia lên.

Giờ thì làm sao để bật máy lên, thông mạng từ bên ngoài vào giả lập, Và quan trọng nhất là làm sao để bypass luôn cái màn hình xác thực ADB?

Build start-emulator.sh

Script này giải quyết 3 bài toán hóc búa nhất khi chạy Android trong Docker: Kết nối mạng ra ngoài, Xử lý đồ họa không màn hình (Headless)Bypass xác thực.

Vấn đề đầu tiên trước: Làm sao để thông mạng ra ngoài.Mặc định, Android Emulator chỉ lắng nghe (bind) ở địa chỉ 127.0.0.1 (localhost) bên trong container để bảo mật. Điều này có nghĩa là dù bạn có mở port trong Docker (-p 5555:5555), máy tính bên ngoài cũng không thể kết nối vào được vì Docker chỉ forward traffic vào IP của card mạng eth0 chứ không phải loopback 127.0.0.1.

Để giải quyết vấn đề này, tác giả gốc đã sử dụng socat

LOCAL_IP=$(ip addr list eth0 | grep "inet " | cut -d' ' -f6 | cut -d/ -f1)
socat tcp-listen:"$EMULATOR_CONSOLE_PORT",bind="$LOCAL_IP",fork tcp:127.0.0.1:"$EMULATOR_CONSOLE_PORT" &
socat tcp-listen:"$ADB_PORT",bind="$LOCAL_IP",fork tcp:127.0.0.1:"$ADB_PORT" &

Dòng đầu tiên sử dụng chuỗi lệnh ip addr, grepcut để bóc tách và lấy chính xác địa chỉ IP hiện tại của card mạng eth0 trong container gán vào biến LOCAL_IP

Hai lệnh socat tiếp theo yêu cầu socat luôn lắng nghe trên IP đó tại các cổng Console (5554) và ADB (5555); khi có tín hiệu kết nối từ máy host gửi vào, tham số fork cho phép socat nhân bản tiến trình để chuyển tiếp luồng dữ liệu đó thẳng vào địa chỉ 127.0.0.1 nơi Emulator đang chờ sẵn, đảm bảo kết nối ADB luôn thông suốt và ổn định.

Nếu không muốn dùng socat, anh em có thể sử dụng các tool khác như ncat với công dụng tương tự.

Tiếp đến là tạo máy ảo:

if [ "$TEST_AVD" == "1" ]; then
  echo "Use the exists Android Virtual Emulator ..."
else
  echo no | avdmanager create avd \\
    --name android \\
    --package "$PACKAGE_PATH" \\
    --device "$DEVICE_ID"
fi

Đoạn này kiểm tra xem đã có máy ảo nào tên là android chưa. Nếu chưa (lần đầu chạy), nó sẽ tạo mới dựa trên các biến môi trường chúng ta đã setup trong Dockerfile ($PACKAGE_PATH, $DEVICE_ID). Khi tạo máy ảo, Google hay hỏi "Do you want to create a custom hardware profile? [no]". Lệnh echo no giúp tự động trả lời "Không" để script chạy tiếp.

Giờ là vấn đề tiếp theo, xử lý đồ họa không màn hình (Headless).

if [ "$GPU_ACCELERATED" == "true" ]; then
  export DISPLAY=":0.0"
  Xvfb "$DISPLAY" -screen 0 1920x1080x16 -nolisten tcp &
else
  export GPU_MODE="swiftshader_indirect"
fi

Docker là môi trường dòng lệnh (CLI), không có màn hình vật lý. Android thì cần màn hình để vẽ giao diện. Vậy nên có 2 hướng cho anh em ở đây:

  • Nếu bật GPU (true): Dùng Xvfb (X Virtual Framebuffer). Nó tạo ra một cái "màn hình ảo" trong RAM để Android vẽ lên đó.

  • Nếu tắt GPU: Dùng chế độ swiftshader_indirect. Đây là công nghệ của Google giúp CPU "gánh" việc render đồ họa. Nó sẽ chậm hơn nhưng tương thích tốt với mọi loại VPS/Server không có card màn hình rời.

Phần GPU_ACCELERATED mình sẽ nói thêm ở phần sau, vì nếu chạy mặc định với Dockerfile phía trên, sẽ chỉ sử dụng swiftshader_indirect.

Tiếp theo là đoạn khởi động Android:

emulator \\
  -avd android \\
  -gpu "$GPU_MODE" \\
  -memory $OPT_MEMORY \\
  -no-boot-anim \\
  -cores $OPT_CORES \\
  -ranchu \\
  $AUTH_FLAG \\
  -no-window \\
  -no-snapshot  || update_state "ANDROID_STOPPED"
  • no-window: Cờ quan trọng nhất với Docker. Nó bảo Emulator: "Tao biết mày không có màn hình đâu, cứ chạy đi đừng báo lỗi".

  • $AUTH_FLAG: Biến này chứa giá trị skip-adb-auth (nếu biến môi trường SKIP_AUTH=true), giúp bỏ qua bước xác thực RSA Key.

Ngoài ra ở đầu start-emulator.sh còn gọi tới emulator-monitoring.sh và lệnh wait_for_boot &. Đây là một script phụ (thường dùng để check xem khi nào Android boot xong thì báo ra log). Nếu bạn không có file này, bạn có thể comment dòng đó đi, script vẫn chạy được nhưng sẽ không in ra thông báo "Boot completed" thôi.

Build docker-compose.yml

Đây là chỗ để anh em thiết lập những tham số cần thiết như API_LEVEL, CMD_LINE_VERSION, IMG_TYPE

args:
        - API_LEVEL=34
        - CMD_LINE_VERSION=11076708_latest
        - IMG_TYPE=google_apis

Các tham số này sẽ ghi đè tham số trong Dockerfile. Một phần quan trọng nữa là cấu hình phần cứng

environment:
  - DISABLE_ANIMATION=false
  - SKIP_AUTH=false
  - MEMORY=8192
  - CORES=4

Lời khuyên: Anh em nên hạ xuống mức thực tế hơn, ví dụ MEMORY=4096 (4GB) và CORES=4 là quá đủ để test App mượt mà. Để 16GB coi chừng tràn RAM máy thật gây treo máy.

Tự động tải và cài đặt Frida-server

Làm việc với Android App thì hiếm khi nào không cần bypass SSL pinning, bypass Rootcheck… Vì thế nên tại sao không cài sẵn Frida-server lên để tiết kiệm thời gian, nhất là khi mình có thể customize cái Docker của mình nhỉ.

Trước hết cần sửa Dockerfile một chút:

# ... (các đoạn code cũ)

# ==========================================
# CUSTOM: Cài đặt Frida Server
# ==========================================
# Cài xz-utils để giải nén file .xz
RUN apt-get update && apt-get install -y xz-utils

# Định nghĩa version Frida muốn dùng
ARG FRIDA_VERSION=16.1.4

# Tải về, giải nén và đổi tên thành 'frida-server' nằm ở /opt/
RUN wget -q --show-progress <https://github.com/frida/frida/releases/download/${FRIDA_VERSION}/frida-server-${FRIDA_VERSION}-android-x86_64.xz> -P /opt/ && \\
    unxz /opt/frida-server-${FRIDA_VERSION}-android-x86_64.xz && \\
    mv /opt/frida-server-${FRIDA_VERSION}-android-x86_64 /opt/frida-server && \\
    chmod +x /opt/frida-server

# ... (ENTRYPOINT giữ nguyên)

Sau đó phải sửa start-emulator.sh để push frida-server vào android

# ... (đoạn code socat ở trên giữ nguyên)

# ==========================================
# CUSTOM: Auto Install Frida function
# ==========================================
install_frida_background() {
    echo "Creating Installation Service..."
    
    # Vòng lặp chờ cho đến khi Android boot xong (sys.boot_completed = 1)
    echo "Waiting for Android to boot..."
    /opt/android/platform-tools/adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done;'
    
    echo "Android Booted! Starting Frida installation..."
    
    # 1. Đẩy file từ /opt/frida-server vào máy ảo
    echo "Pushing frida-server to /data/local/tmp/..."
    /opt/android/platform-tools/adb push /opt/frida-server /data/local/tmp/
    
    # 2. Cấp quyền thực thi 
    echo "Setting permissions..."
    /opt/android/platform-tools/adb shell "chmod 755 /data/local/tmp/frida-server"
    
    echo "Frida Server 16.1.4 installed and ready!"
   
}

# CHẠY HÀM NÀY DƯỚI NỀN (&) ĐỂ KHÔNG BLOCK LỆNH EMULATOR BÊN DƯỚI
install_frida_background &

# ==========================================

# Start the emulator... (đoạn code cũ giữ nguyên)
echo "Starting the emulator ..."
emulator \\
  -avd android \\
  # ...

Test Run

Sau khi đã chỉnh sửa một chút so với file gốc, chạy lệnh sau:

docker compose up android-emulator

Android đã lên và cài sẵn frida-server. Giờ chỉ cần vào và test xem có bypass với script và bắt được request của các app không thôi.

Issues

Nếu anh em đọc kỹ file start-emulator.sh ở trên, anh em sẽ thấy một đoạn if-else xử lý biến GPU_ACCELERATED

if [ "$GPU_ACCELERATED" == "true" ]; then
  export DISPLAY=":0.0"
  Xvfb "$DISPLAY" -screen 0 1920x1080x16 -nolisten tcp &
else
  export GPU_MODE="swiftshader_indirect"
fi

Như đã nói, đây là logic để yêu cầu sử dụng GPU vào xử lý tính toán, giúp tăng hiệu năng cho máy ảo. Về mặt lý thuyết, khi thiết lập GPU_ACCELERATED=true, đoạn script sẽ cấu hình biến GPU_MODE="host". Thiết lập này chỉ thị cho Android Emulator bỏ qua lớp mô phỏng đồ họa bằng phần mềm và thực hiện quá trình Hardware Passthrough (Chuyển tiếp phần cứng). Cụ thể, hệ điều hành Android ảo sẽ gọi trực tiếp các tập lệnh đồ họa (thông qua API như Vulkan hoặc OpenGL) xuống bộ vi xử lý đồ họa vật lý của máy chủ (Host). Quá trình này giúp giảm tải hoàn toàn cho CPU, mang lại FPS cao và độ trễ thao tác thấp nhất.

Tuy nhiên khi build trên Windows, mình gặp lỗi sau:

android-emulator-cuda-1  | ERROR        | emuglConfig_get_vulkan_hardware_gpu_support_info: Failed to create vulkan instance. Error: [VK_ERROR_INCOMPATIBLE_DRIVER] -9
android-emulator-cuda-1  | ERROR        | Failed to create Vulkan instance. Error VK_ERROR_INCOMPATIBLE_DRIVER.
android-emulator-cuda-1  | /opt/start-emulator.sh: line 78:   118 Segmentation fault      (core dumped) emulator -avd android -gpu "$GPU_MODE" ...

Dựa vào đoạn log này, ta có thể thấy những vấn đề sau: Emulator của Google hiện nay sử dụng Vulkan (hoặc OpenGL) làm Engine đồ họa chính (Backend: gfxstream). Khi chạy với cờ -gpu host, Emulator bên trong Linux Container cố gắng tìm kiếm driver đồ họa của hệ thống để khởi tạo Vulkan. Tuy nhiên, nó lại trả về mã lỗi -9 (Không tìm thấy driver tương thích). Khi không tìm thấy GPU vật lý mà code vẫn cố tình truy cập vào vùng nhớ của GPU sẽ gây ra Segmentation fault.

Để giải thích cho vấn đề này, thì cần quay lại kiến trúc của Docker Destop trên Windows. Sơ đồ kiến trúc của nó là Windows OS -> WSL2 Kernel -> Docker Engine -> Container. Docker Desktop trên Windows không hoạt động trực tiếp mà phải vận hành thông qua một lớp ảo hóa là WSL2. Lớp ảo hóa này cô lập môi trường Linux bên trong container khỏi hệ thống driver đồ họa gốc của Windows. Thêm vào đó, do base image eclipse-temurin:21-jdk-jammy là một môi trường tối giản, thiếu đi các bộ driver đồ họa chuyên sâu có khả năng phiên dịch lệnh xuyên qua WSL2, dẫn đến việc API từ chối khởi tạo với mã lỗi VK_ERROR_INCOMPATIBLE_DRIVER (-9).

Mình cũng đã thử chày cối fix bằng vài đường cơ bản. Ví dụ như thêm devices: - /dev/dri:/dev/dri vào docker-compose.yml để ép Container nhận diện card đồ họa, nhưng WSL2 lại báo lỗi Permission Denied hoặc không tìm thấy device. Giải pháp khác được đề xuất là dùng NVIDIA Container Toolkit. Tuy nhiên xét lại thì với mục đích là Pentest API Mobile, cố đấm ăn xôi bật GPU acceleration vừa không cần thiết, lại vừa rước thêm rắc rối khi phải nhồi nhét cả mớ driver làm phình to dung lượng image, nên mình không cố chấp làm thêm nữa.

 

Tổng kết

Trong quá trình mày mò setup và chắp bút cho bài viết này, mình đã tiện tay bê luôn bộ Docker Android vừa build vào các dự án pentest thực tế. Kết quả là nó giúp mình tiết kiệm được một mớ thời gian so với việc phải vật lộn với các phần mềm ảo hóa khác. Các thao tác cốt lõi như hứng traffic qua Burp Suite, ép chạy các script chặn bắt API hay bypass SSL Pinning đều diễn ra cực kỳ mượt mà. Điểm ăn tiền nhất là môi trường này rất "sạch" và hoàn toàn có thể đập đi xây lại chỉ bằng một câu lệnh.

Tuy nhiên, bộ Docker này mang trong mình một điểm yếu khá khó chịu (mà thực ra phần mềm ảo hóa nào trên Windows cũng dính): Rào cản kiến trúc ARM. Image giả lập chúng ta đang build mặc định sử dụng kiến trúc x86_64 (vì đa số máy cá nhân hay server Windows/Linux của anh em đều chạy chip Intel hoặc AMD). Do đó, khi đụng phải những ứng dụng mobile được build thuần ARM, anh em sẽ không thể chạy trực tiếp được. Giải pháp chữa cháy lúc này là bắt buộc phải cài thêm bộ giả lập kiến trúc QEMU vào trong Docker để ép nó chạy dịch ngược. Thế nhưng, cái giá phải trả là hiệu năng máy ảo sẽ bị bóp nghẹt, chạy siêu chậm và delay. Để xử lý triệt để các app thuần ARM, anh em đành phải dùng điện thoại thật, hoặc đầu tư chạy Docker trên các dòng máy sử dụng chip ARM gốc (như Macbook chip M-series) mà thôi.

41 lượt xem