1. 程式人生 > >檢測Android模擬器的方法和程式碼實現

檢測Android模擬器的方法和程式碼實現

專自:https://bbs.pediy.com/thread-225717.htm

剛剛看了一些關於Detect Android Emulator的開源專案/文章/論文, 我看的這些其實都是13年14年提出的方法, 方法裡大多是檢測一些環境屬性, 檢查一些檔案這樣, 但實際上檢測的思路並不侷限於此. 有的是很直接了當去檢測qemu, 而其它的方法則是旁敲側擊比如檢測adb, 檢測ptrace之類的. 思路也很靈活. 最後看到有提出通過利用QEMU這樣的模擬CPU與物理CPU之間的實際差異(任務排程差異), 模擬感測器和物理感測器的差異, 快取的差異等方法來檢測. 相比檢測環境屬性, 檢測效果會提升很多.

 

下面我就列出各個資料中所提出的一些方法/思路/程式碼供大家交流學習.

QEMU Properties

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

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環境

 */

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)};

/**

 * 一個閾值, 因為所謂"已知"的模擬器屬性並不完全準確, 有可能出現假陽性結果, 因此保持一定的閾值能讓檢測效果更好

 */

private static int MIN_PROPERTIES_THRESHOLD = 0x5;

/**

 * 嘗試通過查詢指定的系統屬性來檢測QEMU環境, 最後跟閾值比較得出檢測結果.

 *

 * @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;

}

這些都是基於一些經驗和特徵來比對的屬性, 這裡的屬性以及之後的一些檔案呀屬性啊之類的我就不再多作解釋.

Device ID

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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

1

2

3

4

5

6

7

8

9

10

11

12

13

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類

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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 forif 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;

}

運營商名

1

2

3

4

5

public static boolean isOperatorNameAndroid(Context paramContext) {

    String szOperatorName = ((TelephonyManager) paramContext.getSystemService(Context.TELEPHONY_SERVICE)).getNetworkOperatorName();

    boolean isAndroid = szOperatorName.equalsIgnoreCase("android");

    return isAndroid;

}

QEMU驅動

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

private static String[] known_qemu_drivers = {"goldfish"};

/**

 * 讀取驅動檔案, 檢查是否包含已知的qemu驅動

 *

 * @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檔案

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

private static String[] known_files = {"/system/lib/libc_malloc_debug_qemu.so""/sys/qemu_trace",

        "/system/bin/qemu-props"};

/**

 * 檢查是否存在已知的QEMU環境檔案

 *

 * @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檔案

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

private static String[] known_geny_files = {"/dev/socket/genyd""/dev/socket/baseband_genyd"};

/**

 * 檢查是否存在已知的Genemytion環境檔案

 *

 * @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管道

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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;

}

設定斷點

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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++程式碼

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

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 = 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是偵錯程式設定斷點時發生的訊號, 在nexus5或一加手機等大多數手機都可以觸發. SIGBUS則是在一個匯流排錯誤, 指標也許訪問了一個有效地址, 但匯流排會因為資料未對齊等原因無法使用, 在nexus4手機上可以觸發. 而bkpt則是arm的斷點指令, 這是曾經qemu被提出來的一個issue, qemu會因為SIGSEGV訊號而崩潰, 作者想利用這個崩潰來檢測qemu. 如果程式沒有正常退出或被凍結, 那麼就可以認定很可能是在模擬器裡.

ADB

1

2

3

4

5

6

7

8

public static boolean hasEmulatorAdb() {

    try {

        return FindDebugger.hasAdbInEmulator();

    } catch (Exception exception) {

        exception.printStackTrace();

        return false;

    }

}

isUserAMonkey()

1

2

3

public static boolean isUserAMonkey() {

    return ActivityManager.isUserAMonkey();

}

這個其實是用於檢測當前操作到底是使用者還是指令碼在要求應用執行.

isDebuggerConnected()

1

2

3

4

5

6

/**

 * 你信或不信, 還真有許多加固程式使用這個方法...

 */

public static boolean isBeingDebugged() {

    return Debug.isDebuggerConnected();

}

這個方法是用來檢測除錯, 判斷是否有偵錯程式連線.

ptrace

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

private static String tracerpid = "TracerPid";

/**

 * 阿里巴巴用於檢測是否在跟蹤應用程序

 *

 * 容易規避, 用法是建立一個執行緒每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/statusTracerPid項, 這個項在沒有跟蹤的時候預設為0, 當有程式在跟蹤時會修改為對應的pid. 因此如果TracerPid不等於0, 那麼就可以認為是在模擬器環境.

TCP連線

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

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(id16);

        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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

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

1

2

3

4

5

6

7

8

9

10

11

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網絡卡.

感測器

手機上配備了各式各樣的感測器, 但它們實質上都是基於從環境收集的資訊輸出值, 因此想要模擬感測器是非常具有挑戰性的. 這些感測器為識別手機和模擬器提供了新的機會.

 

比如在論文Rage Against the Virtual Machine: Hindering Dynamic Analysis of Android Malware中, 作者對Android模擬器的加速器進行測試, 作者發現Android模擬器上的感測器會在相同的時間間隔內(觀測結果是0.8s, 標準偏差為0.003043)產生相同的值. 顯然對於現實世界的感測器, 這是不可能的.

 

acc-cdf.png

 

於是我們可以先註冊一個感測器監聽器, 如果註冊失敗, 就可能是在模擬器中(排除實際裝置不支援感測器的可能性). 如果註冊成功, 那麼檢查onSensorChanged回撥方法, 如果在連續呼叫這個方法的過程所觀察到的感測器值或時間間隔相同, 那麼就可以認定是在模擬器環境中.

QEMU任務排程

出於效能優化的原因, QEMU在每次執行指令時都不會主動更新程式計數器(PC), 由於翻譯指令在本地執行, 而增加PC需要額外的指令帶來開銷. 所以QEMU只在執行那些從線性執行過程裡中斷的指令(例如分支指令)時才會更新程式計數器. 這也就導致在執行一些基本塊的期間如果發生了排程事件, 那麼也沒有辦法恢復排程前的PC, 也是出於這個原因, QEMU僅在執行基本塊後才發生排程事件, 絕不會執行的過程中發生.

 

sche-point.png

 

如上圖, 因為排程可能在任意時間發生, 所以在非模擬器環境下, 會觀察到大量的排程點. 而在模擬器環境中, 只能看到特定的排程點.

SMC識別

因為QEMU會跟蹤內碼表的改動, 於是存在一種新穎的方法來檢測QEMU--使用自修改程式碼(Self-Modifying Code, SMC)引起模擬器和實際裝置之間的執行流變化.

 

memory.png

 

ARM處理器包含有兩個不同的緩衝Cache, 一個用於指令訪問(I-Cache), 而另一個用於資料訪問(D-Cache). 但如ARM這樣的哈佛架構並不能保證I-Cache和D-Cache之間的一致性. 因此CPU有可能在新程式碼片已經寫入主存後執行舊的程式碼片(也許是無效的).

 

這個問題可以通過強迫兩個快取一致得到解決, 這有兩步:

  1. 清理主存, 以便將D-Cache中新寫入的程式碼移入主存
  2. 使I-Cache無效, 以便它可以用主存的新內容重新填充.

在原生Android程式碼中, 可以使用cacheflush函式, 該函式通過系統呼叫完成上述操作.

 

diff.png

 

識別程式碼, 使用一個具有讀寫許可權的記憶體, 其中包含兩個不同函式f1和f2的程式碼, 這兩個函式其實很簡單, 只是單純在一個全域性字串變數的末尾附加各自的函式名稱, 這兩個函式會在迴圈裡交錯執行, 這樣就可以通過結果的字串推斷出函式呼叫序列.

 

如前所述, 我們呼叫cacheflush來同步快取. 在實際裝置和模擬器上執行程式碼得到的結果是相同的--每次執行都會產生一致的函式呼叫序列.

 

接下來我們移除呼叫cacheflush, 執行相同的操作. 那麼在實際裝置中, 我們每次執行都會觀察到一個隨機的函式呼叫序列, 這也如前所述的那樣, 因為I-Cache可能包含一些舊指令, 每次呼叫的時候快取都不同步所導致的.

 

而模擬器環境卻不會發生這樣的情況, 而且函式呼叫序列會跟之前沒有移除cacheflush時完全相同, 也就是每次函式呼叫前快取都是一致的. 這是因為QE