參考:http://zeng9t.com/tech/2019/04/30/Android%E6%A8%A1%E6%8B%9F%E5%99%A8%E6%A3%80%E6%B5%8B%E5%8F%8A%E5%AF%B9%E6%8A%97%E6%96%B9%E6%B3%95.html
前言
因為做項目的原因,對目前檢測模擬器環境的方法大概有了一個了解,有業界方法也有學術界的方法。如何進行對抗的方法,在后續進行說明。 做項目就是邊學邊做的過程中不斷學習,知識不可能學完,並且計算機相關的知識和技術變化迭代很快,需要用到的時候再學再擴展,保持一顆學習的心及學習方法很重要。
檢測模擬器環境的不同用途
關於emulator環境的檢測,業界的應用比如阿里等公司,防止一些利益集團進行淘寶惡意刷單、支付寶注冊得紅包等褥羊毛操作;還有比如手游防止玩家使用模擬器進行操作造成了游戲的不公平性。而惡意應用會利用模擬器檢測技術來防止在emulator環境下被動態分析,因此會檢測emulator環境,然后在emulator環境下觸發不同的代碼路徑(通常是非惡意的代碼邏輯)。
檢測核心思想
利用emulator和真機的區別,當然這種區別是可檢測的,相對易於實現的,根據具體需求來衡量使用什么樣的方法是足夠的,並且可擴展、可維護、對程序運行效率影響小。
常用的實用方法
TelephonyManager類
- getLine1Number
- getDeviceId
- getSubscriberId
- getVoiceMailNumber
- getSimSerialNumber
Build信息
- BRAND == generic
- DEVICE == generic
- HARDWARE == goldfish
- PRODUCT == sdk
- HOST == android-test
- TAGS == test-keys
特征文件
- /dev/socket/qemud
- /dev/qemu_pipe
- /system/lib/libc_malloc_debug_qemu.so
- /sys/qemu_trace
- /system/bin/qemu-prop
系統屬性
- ro.hardware == goldfish
- ro.product.device == generic
- ro.product.model == sdk
- ro.product.name == sdk
基於差異化信息
- /proc/cpuinfo真機CPU一般都是基於ARM,模擬器一般為Intel或AMD
- 模擬器通話記錄、聯系人、短信等通常為空
- 檢查是否存在Dev Tools等模擬器上特有的應用程序
基於硬件數據
- 可以多次檢測單個傳感器數據,但部分傳感器模擬器也可以模擬實現。檢測傳感器數量,模擬器傳感器數量一般無法超過10,而一般手機傳感器數量大於20;多次對傳感器數據取值觀察是否變化等方法來判斷。常見傳感器有:陀螺儀、加速度計、[心率傳感器]、[光線傳感器]、[壓力傳感器]、[計步器]、重力傳感器、旋轉矢量傳感器等,其中括號中的傳感器數據一般模擬器較難進行模擬
- 檢查電池的電壓、電量、溫度等是否實時變化
- 觸摸面積:真機變化且不為0,模擬器為0
基於應用層行為數據
- 例如安裝應用數量,是否具備常見應用,是否有聯系人等。對設備的行為模式進行統計分析,作為風險設備畫像的參考維度。
基於cache行為
- ARM采用的哈弗架構將指令存儲跟數據存儲分開,ARM的一級緩存分為I-Cache(指令緩存)與D-Cache(數據緩存),而Simpled X86只有一塊緩存,而模擬器可以看做是Simpled-x86架構。如果將一段代碼可執行代碼動態映射到內存,在執行的時候,Simpled-X86架構上動態修改這部分代碼后,指令cache會被同步修改,而ARM修改的卻是D-Cache中的內容,此時I-Cache中的指令並不一定被更新,因此程序就會在ARM與Simpled-x86上有不同的表現,根據計算結果便可以知道究竟是x86還是在ARM平台上運行。
- 無論是x86還是ARM,只要是靜態編譯的程序,都沒有修改代碼段的權限,所以,首先需要將上面的匯編代碼翻譯成可執行文件,再需要申請一塊內存,將可執行代碼段映射過去,執行。 以下實現代碼是測試代碼的核心,主要就是將地址e2844001的指令add r4, r4, #1,在運行中動態替換為e2877001的指令add r7, r7, #1,這里目標是ARM-V7架構的,要注意它采用的是三級流水,PC值=當前程序執行位置+8。通過arm交叉編譯鏈編譯出的可執行代碼如下:
8410: e92d41f0 push {r4, r5, r6, r7, r8, lr}
8414: e3a07000 mov r7, #0
8418: e1a0800f mov r8, pc // 本平台針對ARM7,三級流水 PC值=當前程序執行位置+8
841c: e3a04000 mov r4, #0
8420: e2877001 add r7, r7, #1
....
842c: e1a0800f mov r8, pc
8430: e248800c sub r8, r8, #12 // PC值=當前程序執行位置+8
8434: e5885000 str r5, [r8]
8438: e354000a cmp r4, #10
843c: aa000002 bge 844c <out>
.....
如果是在ARM上運行,e2844001處指令無法被覆蓋,最終執行的是add r4,#1 ,而在x86平台上,執行的是add r7,#1 ,代碼執行完畢, r0的值在模擬器上是1,而在真機上是10。之后,將上述可執行代碼通過mmap,映射到內存並執行即可,具體做法如下,將可執行的二進制代碼直接拷貝可執行代碼區,去執行:
void (*asmcheck)(void);
int emulator_detect() {
//可執行二進制代碼
char code[] =
"\xF0\x41\x2D\xE9"
"\x00\x70\xA0\xE3"
"\x0F\x80\xA0\xE1"
"\x00\x40\xA0\xE3"
"\x01\x70\x87\xE2"
"\x00\x50\x98\xE5"
"\x01\x40\x84\xE2"
....
// 映射一塊可執行內存 PROT_EXEC
void *exec = mmap(NULL, (size_t) getpagesize(), PROT_EXEC|PROT_WRITE|PROT_READ, MAP_ANONYMOUS | MAP_SHARED, -1, (off_t) 0);
memcpy(exec, code, sizeof(code) + 1);
//強制賦值到函數
asmcheck = (void *) exec;
//執行函數
asmcheck();
__asm __volatile (
"mov %0,r0 \n"
:"=r"(a)
);
munmap(exec, getpagesize());
return a;
}
防止在真機上出現崩潰,最好單獨開一個進程服務,利用Binder實現模擬器鑒別的查詢。再結合其他檢測方法做綜合度量。
基於指令執行行為
- 為了效率上的考慮,qemu在翻譯執行ARM指令時並沒有實時更新模擬的pc寄存器值,只會在一段代碼翻譯執行完之后再更新,而真機中pc寄存器是一直在更新的。可以設計CPU任務調度程序以檢測模擬器。
對抗
針對大部分Java層檢測方法和部分native層檢測方法都可以被hook掉,可以考慮在native code中進行檢測,並使用自定義API替換相應的系統API,防止被hook,面對一些特殊處理后的模擬器具有相對較好的檢測效果。
說明
檢測方法有很多,例如還有基於mac信息和藍牙信息等,可以利用多種檢測手段進行綜合分析,但也要考慮到具體需求,不要過度檢測以免影響應用運行效率。
檢測示例
public static boolean isEmulatorAbsoluly() {
if (Build.PRODUCT.contains("sdk") ||
Build.PRODUCT.contains("sdk_x86") ||
Build.PRODUCT.contains("sdk_google") ||
Build.PRODUCT.contains("Andy") ||
Build.PRODUCT.contains("Droid4X") ||
Build.PRODUCT.contains("nox") ||
Build.PRODUCT.contains("vbox86p")) {
return true;
}
if (Build.MANUFACTURER.equals("Genymotion") ||
Build.MANUFACTURER.contains("Andy") ||
Build.MANUFACTURER.contains("nox") ||
Build.MANUFACTURER.contains("TiantianVM")) {
return true;
}
if (Build.BRAND.contains("Andy")) {
return true;
}
if (Build.DEVICE.contains("Andy") ||
Build.DEVICE.contains("Droid4X") ||
Build.DEVICE.contains("nox") ||
Build.DEVICE.contains("vbox86p")) {
return true;
}
if (Build.MODEL.contains("Emulator") ||
Build.MODEL.equals("google_sdk") ||
Build.MODEL.contains("Droid4X") ||
Build.MODEL.contains("TiantianVM") ||
Build.MODEL.contains("Andy") ||
Build.MODEL.equals("Android SDK built for x86_64") ||
Build.MODEL.equals("Android SDK built for x86")) {
return true;
}
if (Build.HARDWARE.equals("vbox86") ||
Build.HARDWARE.contains("nox") ||
Build.HARDWARE.contains("ttVM_x86")) {
return true;
}
if (Build.FINGERPRINT.contains("generic/sdk/generic") ||
Build.FINGERPRINT.contains("generic_x86/sdk_x86/generic_x86") ||
Build.FINGERPRINT.contains("Andy") ||
Build.FINGERPRINT.contains("ttVM_Hdragon") ||
Build.FINGERPRINT.contains("generic/google_sdk/generic") ||
Build.FINGERPRINT.contains("vbox86p") ||
Build.FINGERPRINT.contains("generic/vbox86p/vbox86p")) {
return true;
}
return false;
}
反檢測
既然有檢測方法,那么最常見的反檢測方法可以自然地想到是針對這些檢測方法,做hook,因為大部分檢測方法在調用鏈上最終都是需要通過相應的API來獲取相關的數據。有就Java層的hook也有native層的hook,Android上常見的Hook框架有Xposed和Frida等,Xposed對x86的兼容性較差,Frida的跨平台兼容性較好。部分字段是系統鏡像自帶屬性,因此可通過修改AOSP源碼自編譯系統鏡像再裝入emulator。
Some Paper Resources
Evading Android Runtime Analysis via Sandbox Detection
Rethinking anti-emulation techniques for large-scale software deployment