檢測Android虛擬機的方法和代碼實現(xiàn)
剛剛看了一些關于Detect Android Emulator的開源項目/文章/論文,我看的這些其實都是13年14年提出的方法,方法里大多是檢測一些環(huán)境屬性,檢查一些文件這樣,但實際上檢測的思路并不局限于此。有的是很直接了當去檢測qemu,而其它的方法則是旁敲側擊比如檢測adb,檢測ptrace之類的。思路也很靈活。
***看到有提出通過利用QEMU這樣的模擬CPU與物理CPU之間的實際差異(任務調(diào)度差異), 模擬傳感器和物理傳感器的差異,緩存的差異等方法來檢測。相比檢測環(huán)境屬性,檢測效果會提升很多。
下面我就列出各個資料中所提出的一些方法/思路/代碼供大家交流學習。
QEMU Properties
- public class Property {
- public String name;
- public String seek_value;
- public Property(String name, String seek_value) {
- this.name = name;
- this.seek_value = seek_value;
- }
- }
- /**
- * 已知屬性, 格式為 [屬性名, 屬性值], 用于判定當前是否為QEMU環(huán)境
- */
- private static Property[] known_props = {new Property("init.svc.qemud", null),
- new Property("init.svc.qemu-props", null), new Property("qemu.hw.mainkeys", null),
- new Property("qemu.sf.fake_camera", null), new Property("qemu.sf.lcd_density", null),
- new Property("ro.bootloader", "unknown"), new Property("ro.bootmode", "unknown"),
- new Property("ro.hardware", "goldfish"), new Property("ro.kernel.android.qemud", null),
- new Property("ro.kernel.qemu.gles", null), new Property("ro.kernel.qemu", "1"),
- new Property("ro.product.device", "generic"), new Property("ro.product.model", "sdk"),
- new Property("ro.product.name", "sdk"),
- new Property("ro.serialno", null)};
- /**
- * 一個閾值, 因為所謂"已知"的模擬器屬性并不完全準確, 有可能出現(xiàn)假陽性結果, 因此保持一定的閾值能讓檢測效果更好
- */
- private static int MIN_PROPERTIES_THRESHOLD = 0x5;
- /**
- * 嘗試通過查詢指定的系統(tǒng)屬性來檢測QEMU環(huán)境, ***跟閾值比較得出檢測結果.
- *
- * @param context A {link Context} object for the Android application.
- * @return {@code true} if enough properties where found to exist or {@code false} if not.
- */
- public boolean hasQEmuProps(Context context) {
- int found_props = 0;
- for (Property property : known_props) {
- String property_value = Utilities.getProp(context, property.name);
- // See if we expected just a non-null
- if ((property.seek_value == null) && (property_value != null)) {
- found_props++;
- }
- // See if we expected a value to seek
- if ((property.seek_value != null) && (property_value.indexOf(property.seek_value) != -1)) {
- found_props++;
- }
- }
- if (found_props >= MIN_PROPERTIES_THRESHOLD) {
- return true;
- }
- return false;
- }
這些都是基于一些經(jīng)驗和特征來比對的屬性, 這里的屬性以及之后的一些文件呀屬性啊之類的我就不再多作解釋。
Device ID
- private static String[] known_device_ids = {"000000000000000", // Default emulator id
- "e21833235b6eef10", // VirusTotal id
- "012345678912345"};
- public static boolean hasKnownDeviceId(Context context) {
- TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- String deviceId = telephonyManager.getDeviceId();
- for (String known_deviceId : known_device_ids) {
- if (known_deviceId.equalsIgnoreCase(deviceId)) {
- return true;
- }
- }
- return false;
- }
Default Number
- private static String[] known_numbers = {
- "15555215554", // 模擬器默認電話號碼 + VirusTotal
- "15555215556", "15555215558", "15555215560", "15555215562", "15555215564", "15555215566",
- "15555215568", "15555215570", "15555215572", "15555215574", "15555215576", "15555215578",
- "15555215580", "15555215582", "15555215584",};
- public static boolean hasKnownPhoneNumber(Context context) {
- TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- String phoneNumber = telephonyManager.getLine1Number();
- for (String number : known_numbers) {
- if (number.equalsIgnoreCase(phoneNumber)) {
- return true;
- }
- }
- return false;
- }
IMSI
- private static String[] known_imsi_ids = {"310260000000000" // 默認IMSI編號
- };
- public static boolean hasKnownImsi(Context context) {
- TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
- String imsi = telephonyManager.getSubscriberId();
- for (String known_imsi : known_imsi_ids) {
- if (known_imsi.equalsIgnoreCase(imsi)) {
- return true;
- }
- }
- return false;
- }
Build類
- public static boolean hasEmulatorBuild(Context context) {
- String BOARD = android.os.Build.BOARD; // The name of the underlying board, like "unknown".
- // This appears to occur often on real hardware... that's sad
- // String BOOTLOADER = android.os.Build.BOOTLOADER; // The system bootloader version number.
- String BRAND = android.os.Build.BRAND; // The brand (e.g., carrier) the software is customized for, if any.
- // "generic"
- String DEVICE = android.os.Build.DEVICE; // The name of the industrial design. "generic"
- String HARDWARE = android.os.Build.HARDWARE; // The name of the hardware (from the kernel command line or
- // /proc). "goldfish"
- String MODEL = android.os.Build.MODEL; // The end-user-visible name for the end product. "sdk"
- String PRODUCT = android.os.Build.PRODUCT; // The name of the overall product.
- if ((BOARD.compareTo("unknown") == 0) /* || (BOOTLOADER.compareTo("unknown") == 0) */
- || (BRAND.compareTo("generic") == 0) || (DEVICE.compareTo("generic") == 0)
- || (MODEL.compareTo("sdk") == 0) || (PRODUCT.compareTo("sdk") == 0)
- || (HARDWARE.compareTo("goldfish") == 0)) {
- return true;
- }
- return false;
- }
運營商名
- public static boolean isOperatorNameAndroid(Context paramContext) {
- String szOperatorName = ((TelephonyManager) paramContext.getSystemService(Context.TELEPHONY_SERVICE)).getNetworkOperatorName();
- boolean isAndroid = szOperatorName.equalsIgnoreCase("android");
- return isAndroid;
- }
QEMU驅(qū)動
- private static String[] known_qemu_drivers = {"goldfish"};
- /**
- * 讀取驅(qū)動文件, 檢查是否包含已知的qemu驅(qū)動
- *
- * @return {@code true} if any known drivers where found to exist or {@code false} if not.
- */
- public static boolean hasQEmuDrivers() {
- for (File drivers_file : new File[]{new File("/proc/tty/drivers"), new File("/proc/cpuinfo")}) {
- if (drivers_file.exists() && drivers_file.canRead()) {
- // We don't care to read much past things since info we care about should be inside here
- byte[] data = new byte[1024];
- try {
- InputStream is = new FileInputStream(drivers_file);
- is.read(data);
- is.close();
- } catch (Exception exception) {
- exception.printStackTrace();
- }
- String driver_data = new String(data);
- for (String known_qemu_driver : FindEmulator.known_qemu_drivers) {
- if (driver_data.indexOf(known_qemu_driver) != -1) {
- return true;
- }
- }
- }
- }
- return false;
- }
QEMU文件
- private static String[] known_files = {"/system/lib/libc_malloc_debug_qemu.so", "/sys/qemu_trace",
- "/system/bin/qemu-props"};
- /**
- * 檢查是否存在已知的QEMU環(huán)境文件
- *
- * @return {@code true} if any files where found to exist or {@code false} if not.
- */
- public static boolean hasQEmuFiles() {
- for (String pipe : known_files) {
- File qemu_file = new File(pipe);
- if (qemu_file.exists()) {
- return true;
- }
- }
- return false;
- }
Genymotion文件
- private static String[] known_geny_files = {"/dev/socket/genyd", "/dev/socket/baseband_genyd"};
- /**
- * 檢查是否存在已知的Genemytion環(huán)境文件
- *
- * @return {@code true} if any files where found to exist or {@code false} if not.
- */
- public static boolean hasGenyFiles() {
- for (String file : known_geny_files) {
- File geny_file = new File(file);
- if (geny_file.exists()) {
- return true;
- }
- }
- return false;
- }
QEMU管道
- private static String[] known_pipes = {"/dev/socket/qemud", "/dev/qemu_pipe"};
- /**
- * 檢查是否存在已知的QEMU使用的管道
- *
- * @return {@code true} if any pipes where found to exist or {@code false} if not.
- */
- public static boolean hasPipes() {
- for (String pipe : known_pipes) {
- File qemu_socket = new File(pipe);
- if (qemu_socket.exists()) {
- return true;
- }
- }
- return false;
- }
設置斷點
- static {
- // This is only valid for arm
- System.loadLibrary("anti");
- }
- public native static int qemuBkpt();
- public static boolean checkQemuBreakpoint() {
- boolean hit_breakpoint = false;
- // Potentially you may want to see if this is a specific value
- int result = qemuBkpt();
- if (result > 0) {
- hit_breakpoint = true;
- }
- return hit_breakpoint;
- }
以下是對應的c++代碼
- void handler_sigtrap(int signo) {
- exit(-1);
- }
- void handler_sigbus(int signo) {
- exit(-1);
- }
- int setupSigTrap() {
- // BKPT throws SIGTRAP on nexus 5 / oneplus one (and most devices)
- signal(SIGTRAP, handler_sigtrap);
- // BKPT throws SIGBUS on nexus 4
- signal(SIGBUS, handler_sigbus);
- }
- // This will cause a SIGSEGV on some QEMU or be properly respected
- int tryBKPT() {
- __asm__ __volatile__ ("bkpt 255");
- }
- jint Java_diff_strazzere_anti_emulator_FindEmulator_qemuBkpt(JNIEnv* env, jobject jObject) {
- pid_t child = fork();
- int child_status, status = 0;
- if(child == 0) {
- setupSigTrap();
- tryBKPT();
- } else if(child == -1) {
- status = -1;
- } else {
- int timeout = 0;
- int i = 0;
- while ( waitpid(child, &child_status, WNOHANG) == 0 ) {
- sleep(1);
- // Time could be adjusted here, though in my experience if the child has not returned instantly
- // then something has gone wrong and it is an emulated device
- if(i++ == 1) {
- timeout = 1;
- break;
- }
- }
- if(timeout == 1) {
- // Process timed out - likely an emulated device and child is frozen
- status = 1;
- }
- if ( WIFEXITED(child_status) ) {
- // 子進程正常退出
- status = 0;
- } else {
- // Didn't exit properly - very likely an emulator
- status = 2;
- }
- // Ensure child is dead
- kill(child, SIGKILL);
- }
- return status;
- }
這里我的描述可能并不準確, 因為并沒有找到相關的資料. 我只能以自己的理解來解釋一下:
SIGTRAP是調(diào)試器設置斷點時發(fā)生的信號, 在nexus5或一加手機等大多數(shù)手機都可以觸發(fā). SIGBUS則是在一個總線錯誤, 指針也許訪問了一個有效地址, 但總線會因為數(shù)據(jù)未對齊等原因無法使用, 在nexus4手機上可以觸發(fā). 而bkpt則是arm的斷點指令, 這是曾經(jīng)qemu被提出來的一個issue, qemu會因為SIGSEGV信號而崩潰, 作者想利用這個崩潰來檢測qemu. 如果程序沒有正常退出或被凍結, 那么就可以認定很可能是在模擬器里.
ADB
- public static boolean hasEmulatorAdb() {
- try {
- return FindDebugger.hasAdbInEmulator();
- } catch (Exception exception) {
- exception.printStackTrace();
- return false;
- }
- }
isUserAMonkey()
- public static boolean hasEmulatorAdb() {
- try {
- return FindDebugger.hasAdbInEmulator();
- } catch (Exception exception) {
- exception.printStackTrace();
- return false;
- }
- }
這個其實是用于檢測當前操作到底是用戶還是腳本在要求應用執(zhí)行。
isDebuggerConnected()
- /**
- * 你信或不信, 還真有許多加固程序使用這個方法...
- */
- public static boolean isBeingDebugged() {
- return Debug.isDebuggerConnected();
- }
這個方法是用來檢測調(diào)試,判斷是否有調(diào)試器連接。
ptrace
- private static String tracerpid = "TracerPid";
- /**
- * 阿里巴巴用于檢測是否在跟蹤應用進程
- *
- * 容易規(guī)避, 用法是創(chuàng)建一個線程每3秒檢測一次, 如果檢測到則程序崩潰
- *
- * @return
- * @throws IOException
- */
- public static boolean hasTracerPid() throws IOException {
- BufferedReader reader = null;
- try {
- reader = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/self/status")), 1000);
- String line;
- while ((line = reader.readLine()) != null) {
- if (line.length() > tracerpid.length()) {
- if (line.substring(0, tracerpid.length()).equalsIgnoreCase(tracerpid)) {
- if (Integer.decode(line.substring(tracerpid.length() + 1).trim()) > 0) {
- return true;
- }
- break;
- }
- }
- }
- } catch (Exception exception) {
- exception.printStackTrace();
- } finally {
- reader.close();
- }
- return false;
- }
這個方法是通過檢查 /proc/self/status 的TracerPid項,這個項在沒有跟蹤的時候默認為0,當有程序在跟蹤時會修改為對應的pid。因此如果TracerPid不等于0,那么就可以認為是在模擬器環(huán)境。
TCP連接
- public static boolean hasAdbInEmulator() throws IOException {
- boolean adbInEmulator = false;
- BufferedReader reader = null;
- try {
- reader = new BufferedReader(new InputStreamReader(new FileInputStream("/proc/net/tcp")), 1000);
- String line;
- // Skip column names
- reader.readLine();
- ArrayList<tcp> tcpList = new ArrayList<tcp>();
- while ((line = reader.readLine()) != null) {
- tcpList.add(tcp.create(line.split("\\W+")));
- }
- reader.close();
- // Adb is always bounce to 0.0.0.0 - though the port can change
- // real devices should be != 127.0.0.1
- int adbPort = -1;
- for (tcp tcpItem : tcpList) {
- if (tcpItem.localIp == 0) {
- adbPort = tcpItem.localPort;
- break;
- }
- }
- if (adbPort != -1) {
- for (tcp tcpItem : tcpList) {
- if ((tcpItem.localIp != 0) && (tcpItem.localPort == adbPort)) {
- adbInEmulator = true;
- }
- }
- }
- } catch (Exception exception) {
- exception.printStackTrace();
- } finally {
- reader.close();
- }
- return adbInEmulator;
- }
- public static class tcp {
- public int id;
- public long localIp;
- public int localPort;
- public int remoteIp;
- public int remotePort;
- static tcp create(String[] params) {
- return new tcp(params[1], params[2], params[3], params[4], params[5], params[6], params[7], params[8],
- params[9], params[10], params[11], params[12], params[13], params[14]);
- }
- public tcp(String id, String localIp, String localPort, String remoteIp, String remotePort, String state,
- String tx_queue, String rx_queue, String tr, String tm_when, String retrnsmt, String uid,
- String timeout, String inode) {
- this.id = Integer.parseInt(id, 16);
- this.localIp = Long.parseLong(localIp, 16);
- this.localPort = Integer.parseInt(localPort, 16);
- }
- }
這個方法是通過讀取/proc/net/tcp的信息來判斷是否存在adb,比如真機的的信息為0: 4604D20A:B512 A3D13AD8...,而模擬器上的對應信息就是 0: 00000000:0016 00000000:0000,因為adb通常是反射到0.0.0.0這個ip上,雖然端口有可能改變,但確實是可行的。
TaintDroid
- public static boolean hasPackageNameInstalled(Context context, String packageName) {
- PackageManager packageManager = context.getPackageManager();
- // In theory, if the package installer does not throw an exception, package exists
- try {
- packageManager.getInstallerPackageName(packageName);
- return true;
- } catch (IllegalArgumentException exception) {
- return false;
- }
- }
- public static boolean hasAppAnalysisPackage(Context context) {
- return Utilities.hasPackageNameInstalled(context, "org.appanalysis");
- }
- public static boolean hasTaintClass() {
- try {
- Class.forName("dalvik.system.Taint");
- return true;
- }
- catch (ClassNotFoundException exception) {
- return false;
- }
- }
這個比較單純了。就是通過檢測包名,檢測Taint類來判斷是否安裝有TaintDroid這個污點分析工具。另外也還可以檢測TaintDroid的一些成員變量。
eth0
- private static boolean hasEth0Interface() {
- try {
- for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
- NetworkInterface intf = en.nextElement();
- if (intf.getName().equals("eth0"))
- return true;
- }
- } catch (SocketException ex) {
- }
- return false;
- }
檢測是否存在eth0網(wǎng)卡。
傳感器
手機上配備了各式各樣的傳感器, 但它們實質(zhì)上都是基于從環(huán)境收集的信息輸出值, 因此想要模擬傳感器是非常具有挑戰(zhàn)性的. 這些傳感器為識別手機和模擬器提供了新的機會。
比如在論文 Rage Against the Virtual Machine: Hindering Dynamic Analysis of Android Malware 中,作者對Android模擬器的加速器進行測試,作者發(fā)現(xiàn)Android模擬器上的傳感器會在相同的時間間隔內(nèi)(觀測結果是0.8s, 標準偏差為0.003043)產(chǎn)生相同的值。顯然對于現(xiàn)實世界的傳感器,這是不可能的。
于是我們可以先注冊一個傳感器監(jiān)聽器,如果注冊失敗,就可能是在模擬器中(排除實際設備不支持傳感器的可能性)。如果注冊成功,那么檢查onSensorChanged回調(diào)方法,如果在連續(xù)調(diào)用這個方法的過程所觀察到的傳感器值或時間間隔相同,那么就可以認定是在模擬器環(huán)境中。
QEMU任務調(diào)度
出于性能優(yōu)化的原因, QEMU在每次執(zhí)行指令時都不會主動更新程序計數(shù)器(PC), 由于翻譯指令在本地執(zhí)行, 而增加PC需要額外的指令帶來開銷. 所以QEMU只在執(zhí)行那些從線性執(zhí)行過程里中斷的指令(例如分支指令)時才會更新程序計數(shù)器。
這也就導致在執(zhí)行一些基本塊的期間如果發(fā)生了調(diào)度事件, 那么也沒有辦法恢復調(diào)度前的PC,也是出于這個原因,QEMU僅在執(zhí)行基本塊后才發(fā)生調(diào)度事件,絕不會執(zhí)行的過程中發(fā)生。
如上圖,因為調(diào)度可能在任意時間發(fā)生,所以在非模擬器環(huán)境下,會觀察到大量的調(diào)度點. 而在模擬器環(huán)境中,只能看到特定的調(diào)度點。
SMC識別
因為QEMU會跟蹤代碼頁的改動,于是存在一種新穎的方法來檢測QEMU--使用自修改代碼(Self-Modifying Code, SMC)引起模擬器和實際設備之間的執(zhí)行流變化。
ARM處理器包含有兩個不同的緩沖Cache, 一個用于指令訪問(I-Cache),而另一個用于數(shù)據(jù)訪問(D-Cache)。但如ARM這樣的哈佛架構并不能保證I-Cache和D-Cache之間的一致性。因此CPU有可能在新代碼片已經(jīng)寫入主存后執(zhí)行舊的代碼片(也許是無效的)。
這個問題可以通過強迫兩個緩存一致得到解決, 這有兩步:
- 清理主存, 以便將D-Cache中新寫入的代碼移入主存
- 使I-Cache無效, 以便它可以用主存的新內(nèi)容重新填充
在原生Android代碼中,可以使用cacheflush函數(shù),該函數(shù)通過系統(tǒng)調(diào)用完成上述操作。
識別代碼,使用一個具有讀寫權限的內(nèi)存, 其中包含兩個不同函數(shù)f1和f2的代碼,這兩個函數(shù)其實很簡單,只是單純在一個全局字符串變量的末尾附加各自的函數(shù)名稱, 這兩個函數(shù)會在循環(huán)里交錯執(zhí)行,這樣就可以通過結果的字符串推斷出函數(shù)調(diào)用序列。
如前所述,我們調(diào)用cacheflush來同步緩存. 在實際設備和模擬器上運行代碼得到的結果是相同的--每次執(zhí)行都會產(chǎn)生一致的函數(shù)調(diào)用序列。
接下來我們移除調(diào)用cacheflush,執(zhí)行相同的操作。那么在實際設備中, 我們每次運行都會觀察到一個隨機的函數(shù)調(diào)用序列,這也如前所述的那樣,因為I-Cache可能包含一些舊指令,每次調(diào)用的時候緩存都不同步所導致的。
而模擬器環(huán)境卻不會發(fā)生這樣的情況, 而且函數(shù)調(diào)用序列會跟之前沒有移除cacheflush時完全相同, 也就是每次函數(shù)調(diào)用前緩存都是一致的. 這是因為QEMU會跟蹤代碼頁上的修改,并確保生成的代碼始終與內(nèi)存中的目標指令匹配, 因此QEMU會放棄之前版本的代碼翻譯并重新生成新代碼。
結語
看到這里會不會已經(jīng)覺得檢測方法夠多了,可是我還只是看了13年14年的資料。有關近幾年的資料還未涉及。
***我就把這些檢測方法整合在一張思維導圖(見附件)里供大家一覽,歡迎大家和我交流帶帶我
參考鏈接
strazzere/anti-emulator:***發(fā)表于2013年HitCon, 提出了檢測虛擬機的一些方法和思路, 應該是Android模擬器檢測的開山之作了, 本文也主要基于該倉庫進行講解.
Rage Against the Virtual Machine: Hindering Dynamic Analysis of Android Malware:通過任務調(diào)度檢測和使用SMC識別都是參考于這篇論文. 這篇論文和下面這篇論文十分有參考價值, 值得一讀.
Evading Android Runtime Analysis via Sandbox Detection:論文中提出了大量的檢測Android運行環(huán)境的方法和思路, 內(nèi)容豐富且十分全面, 也值得一讀.
CalebFenton/AndroidEmulatorDetect:這個倉庫其實是整合了一些文章和倉庫中的檢測方法和代碼, 而且并不全面, 不過倒是給出了很多參考鏈接, 我順藤摸瓜.
How can I detect when an Android application is running in the emulator? 網(wǎng)友給出了很多解決方法. 但實際上并不全面, 也只是模擬器檢測中的冰山一角罷了. 畢竟可以檢測的地方多了去了。
利用任務調(diào)度特性檢測Android模擬器