軟件大廠,環(huán)境檢測(cè)思路和規(guī)避思路,安卓改機(jī)應(yīng)該改什么數(shù)據(jù)和參數(shù),安卓boot內(nèi)核修改
前言:
現(xiàn)在大廠的設(shè)備指紋層出不窮,但是想要確保穩(wěn)定性和唯一性高精準(zhǔn)其實(shí)也挺難的一件事,有的是通過設(shè)備信息比重進(jìn)行的設(shè)備ID唯一值確認(rèn)。比如A設(shè)備信息占比10%,B設(shè)備信息占比20%,當(dāng)比重超過60%以上,設(shè)備指紋才會(huì)發(fā)生變化。這樣的好處就是當(dāng)你只修改某一個(gè)字段的時(shí)候,設(shè)備指紋不發(fā)生變化。還有的干脆找一個(gè)隱蔽的并且唯一的設(shè)備信息,作為緩存,每次讀取緩存的方式去判斷,設(shè)備信息是唯一,比如常見的有Native獲取DRM,popen cat? /sys/devices/soc0/serial_number? ,svc讀取bootid并且保存到文件,netlinker獲取網(wǎng)卡。都是很常見并且隱蔽的的設(shè)備指紋。一個(gè)設(shè)備指紋大廠會(huì)使用多種方式去獲取,那么我們應(yīng)該如何進(jìn)行對(duì)抗,我也會(huì)在文章里面說一下我自己的見解和方案,如何在一個(gè)“最佳”點(diǎn)去解決問題
設(shè)備指紋:
設(shè)備指紋主要分為三部分,Java層設(shè)備指紋,Native設(shè)備指紋,popen執(zhí)行一些命令獲取設(shè)備信息,包括一些核心的設(shè)備指紋。
Android Id
聊到設(shè)備指紋最經(jīng)典的一個(gè)字段就是Android id,就我目前所知,他的獲取方式不下5種,分別介紹一下。
方法1:
最基礎(chǔ)的Android id獲取方式,這個(gè)不多說,直接Hook就行
//原始獲取android id
String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
CLog.i(String.format("android_id -> 2222 %s", androidId));
方法2:
第一種獲取以后,系統(tǒng)會(huì)把Android id 保存起來,保存到一個(gè)HashMap里面,防止多次IPC初始化,所以為了驗(yàn)證第一種方法的準(zhǔn)確性,可以二次獲取cache。
和上面的Android id進(jìn)行對(duì)比,9.0以上需要繞過Android id 的反射限制
方法3:
方法3也是很基礎(chǔ)的Api,主要通過ContentResolver 進(jìn)行間接獲取,很多大廠也都在使用。
方法4:
通過query命令去查詢,獲取Android id,這種方式底層走的也是ContentResolver。
硬盤字節(jié)總大?。?/p>
在設(shè)備指紋里面,如果想回復(fù)出廠設(shè)置也能保證原有的設(shè)備信息,這個(gè)字段可以在服務(wù)端的相似度算法里面占比很重,可以以型號(hào)進(jìn)行分類。我之前測(cè)試過,回復(fù)出廠設(shè)置指紋也不發(fā)生變化的設(shè)備指紋核心的設(shè)備指紋就幾個(gè)。
比如硬盤大小,ipv6,還有一個(gè)就是MAC地址,這幾個(gè)設(shè)備指紋也是很核心的設(shè)備指紋。首先先介紹硬盤字節(jié)大小。也是三種獲取方法,但是方法底層都是一條系統(tǒng)調(diào)用。所以如果要進(jìn)行對(duì)抗的話,只需要在SVC層進(jìn)行處理即可。獲取三種方法如下,不建議分別進(jìn)行處理,可能會(huì)導(dǎo)致有地方泄漏,特別是直接開啟一條進(jìn)程通過execve去執(zhí)行,然后管道傳過來,導(dǎo)致很容易Hook不全。
jclass pJclass = env->FindClass("android/os/StatFs");
jmethodID id = env->GetMethodID(pJclass, "<init>", "(Ljava/lang/String;)V");
jobject pJobject =
? ? ? ? env->NewObject(pJclass, id, env->NewStringUTF("/storage/emulated/0"));
jlong i = env->CallLongMethod(pJobject, env->GetMethodID(pJclass, "getTotalBytes", "()J"));
LOG(ERROR) << "Java獲取getTotalBytes "<<i;
char buffer[1024];
FILE *fp = popen("stat -f /storage/emulated/0", "r");
if (fp != nullptr) {
? ? while (fgets(buffer, sizeof(buffer), fp) != nullptr) {
? ? ? ? //LOGI("ps -ef %s",buffer)
? ? ? ? LOG(INFO) << "stat -f /storage/emulated/0" << buffer;
? ? }
? ? pclose(fp);
}
struct statfs64 buf={};
if (statfs64("/storage/emulated/0", &buf) == -1) {
? ? LOG(ERROR) << "statfs64系統(tǒng)信息失敗";
? ? return;
}
LOG(INFO) << "f_type (文件系統(tǒng)類型): " << buf.f_type;
LOG(INFO) << "f_bsize (塊大小): " << buf.f_bsize;
LOG(INFO) << "f_blocks (總數(shù)據(jù)塊): " << buf.f_blocks;
LOG(INFO) << "f_bfree (空閑塊): " << buf.f_bfree;
LOG(INFO) << "f_bavail (非特權(quán)用戶可用的空閑塊): " << buf.f_bavail;
LOG(INFO) << "f_files (總文件節(jié)點(diǎn)數(shù)): " << buf.f_files;
LOG(INFO) << "f_ffree (空閑文件節(jié)點(diǎn)數(shù)): " << buf.f_ffree;
LOG(INFO) << "f_fsid (文件系統(tǒng) ID): " << buf.f_fsid.__val[0] << ", " << buf.f_fsid.__val[1];
LOG(INFO) << "f_namelen (最大文件名長(zhǎng)度): " << buf.f_namelen;
,Hook的Java方法,但是發(fā)現(xiàn)Native層并不能全量攔截,
上面這種方式只適合Java獲取,
Mac地址:
這個(gè)沒啥好說的,基礎(chǔ)字段,Java層獲取,netlink獲取,命令行獲取。讀文件獲取,四種獲取方法,和上面類似,直接在svc的 recvmsg,recv,recvfrom的after進(jìn)行數(shù)據(jù)包替換即可
如果判斷是netlink的消息,并且是獲取網(wǎng)卡類型直接對(duì)里面的數(shù)據(jù)包解析和替換即可。
case SC_recvmsg: {
? ? //LOGI("start handle SC_recvmsg systexit after")
? ? if (isMockFingerptint()) {
? ? ? ? NetlinkMacHandler::netlinkHandler_recmsg(tracee);
? ? }
? ? break;
}
case SC_recv:
case SC_recvfrom: {
? ? //LOGE("start handle SC_recvfrom systexit after")
? ? //recv底層走的recvfrom,所以不需要處理recvfrom
? ? if (isMockFingerptint()) {
? ? ? ? NetlinkMacHandler::netlinkHandler_recv(tracee);
? ? }
? ? break;
}
在讀文件獲取這塊因?yàn)榫W(wǎng)卡信息已經(jīng)在內(nèi)存里面,所以直接IO重定向過去即可。
常用的獲取網(wǎng)卡信息的文件,以wlan0為例子,場(chǎng)景的獲取目錄如下:可以cat獲取,也可以直接讀文件。
/sys/class/net/wlan0/address
/sys/devices/virtual/net/wlan0/address
...
附近網(wǎng)卡信息:
這個(gè)字段主要是監(jiān)控群控的一些信息的,主要作用是獲取當(dāng)前wifi 附近的人MAC信息的。
比如大廠一般檢測(cè)群控的手段就是獲取附近的網(wǎng)卡,如果有聚集性就可以認(rèn)為是群控。獲取的方式也也跟上面一樣,五種獲取方法。
獲取方法底層也是和MAC獲取方法一樣,底層都是netlink,比如可以直接執(zhí)行 popen獲取。
popen("ip neigh show", "r");
也可以直接直接讀文件,路徑如下:
/proc/net/arp
還可以直接netlink獲取,在收到消息以后判斷消息類型是 hdr->nlmsg_type == RTM_NEWNEIGH 直接進(jìn)行替換即可。
IPV6:
設(shè)個(gè)設(shè)備指紋也是很核心的設(shè)備指紋,這個(gè)玩意底層獲取也是netlink,但是netlink獲取,但是這塊處理很不好處理,我暫時(shí)也沒進(jìn)行處理。
常用的獲取方式比如,Java獲取,命令獲取。如果需要進(jìn)行替換的話,只需要處理命令行和Java的Hook即可。
命令行可以在對(duì)方執(zhí)行命令之前,將命令換成cat命令,去cat自己提前Mock好的文件,效果是一樣的。
當(dāng)然,還有另一種思路,其實(shí)這個(gè)字段可以服務(wù)端獲取,客戶端二次上報(bào),進(jìn)行匹配。
try {
? ? NetworkInterface networkInterface;
? ? InetAddress inetAddress;
? ? for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements(); ) {
? ? ? ? networkInterface = en.nextElement();
? ? ? ? for (Enumeration<InetAddress> enumIpAddr = networkInterface.getInetAddresses(); enumIpAddr.hasMoreElements(); ) {
? ? ? ? ? ? inetAddress = enumIpAddr.nextElement();
? ? ? ? ? ? if (inetAddress instanceof Inet6Address) {
? ? ? ? ? ? ? ? CLog.e("Java 獲取 ipv6 " + inetAddress.getHostAddress());
? ? ? ? ? ? }
? ? ? ? }
? ? }
} catch (Throwable ex) {
? ? CLog.e("printf ipv6 info error " + ex);
}
命令行獲取如下,ip命令獲取如下:
ip -6 addr show
打印的內(nèi)容如下:
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
? ? inet6 ::1/128 scope host
? ? ? ?valid_lft forever preferred_lft forever
3: dummy0: <BROADCAST,NOARP,UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 1000
? ? inet6 fe80::b86c:79ff:fe96:4945/64 scope link
? ? ? ?valid_lft forever preferred_lft forever
10: rmnet_data0@rmnet_ipa0: <UP,LOWER_UP> mtu 1500 state UNKNOWN qlen 1000
? ? inet6 fe80::2ad1:b5a0:792b:9ec4/64 scope link
? ? ? ?valid_lft forever preferred_lft forever
30: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 3000
? ? inet6 fe80::8670:a04c:b8cf:467c/64 scope link stable-privacy
? ? ? ?valid_lft forever preferred_lft forever
系統(tǒng)內(nèi)核信息:
這玩意底層走的都是uname函數(shù),直接對(duì)uname系統(tǒng)調(diào)用處理即可。獲取方法比如,可以直接svc調(diào)用uname函數(shù),也可以直接根據(jù)命令行。
修改的話也很簡(jiǎn)單,直接在uname的after里面直接對(duì)數(shù)據(jù)進(jìn)行替換即可。
uname -a
包名隨機(jī)路徑:
這個(gè)是一個(gè)非常非常核心的字段,就是/data/app/隨機(jī)Base64路徑/base.apk。
這個(gè)隨機(jī)路徑就是設(shè)備指紋,比如一些大廠會(huì)玩,讀取你微信的隨機(jī)路徑,獲取微信的包信息,然后獲取里面的隨機(jī)路徑。
比如微信,快手,京東,淘寶這種隨機(jī)路徑,作為核心的唯一設(shè)備指紋,只要你不卸載微信,或者其他大廠apk,你得設(shè)備指紋永遠(yuǎn)不發(fā)生變化,無論你如何修改他自己Apk里面的信息,跟他都不產(chǎn)生任何影響。
系統(tǒng)賬號(hào):
一般嘗試比如小米之類的,登入了指定賬號(hào),可以得到一個(gè)賬號(hào)的id信息,這個(gè)也需要處理一下,最好的辦法是不登入賬號(hào)。
環(huán)境檢測(cè):
檢測(cè)環(huán)境大多數(shù)圍繞Hunter的源碼檢測(cè)思路去復(fù)現(xiàn),很多都是Hunter的源碼,很多也都是行業(yè)內(nèi)沒有公開的一些檢測(cè)思路,現(xiàn)在市面上檢測(cè)已經(jīng)很多沒更新了,加速行業(yè)內(nèi)卷,我輩刻不容緩。?
Apk簽名:
提到環(huán)境檢測(cè)不得不說的就是Apk重打包檢測(cè),現(xiàn)在檢測(cè)方法千奇百怪,
模擬器檢測(cè):
Java層基礎(chǔ)的獲取api架構(gòu)啥的,這塊就不一一敘述了。
檢測(cè)溫度掛載文件:
int thermal_check() {
? ? DIR *dir_ptr;
? ? int count = 0;
? ? struct dirent *entry;
? ? if ((dir_ptr = opendir("/sys/class/thermal/")) != nullptr) {
? ? ? ? while ((entry = readdir(dir_ptr))) {
? ? ? ? ? ? if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) {
? ? ? ? ? ? ? ? continue;
? ? ? ? ? ? }
? ? ? ? ? ? char *tmp = entry->d_name;
? ? ? ? ? ? if (strstr(tmp, "thermal_zone") != nullptr) {
? ? ? ? ? ? ? ? count++;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? closedir(dir_ptr);
? ? } else {
? ? ? ? count = -1;
? ? }
? ? return count;
}
模擬器特征文件:
string simulator_files_check() {
? ? if (file_exist("/system/bin/androVM-prop")) {//檢測(cè)androidVM
? ? ? ? return "/system/bin/androVM-prop";
? ? } else if (file_exist("/system/bin/microvirt-prop")) {//檢測(cè)逍遙模擬器--新版本找不到特征
? ? ? ? return "/system/bin/microvirt-prop";
? ? } else if (file_exist("/system/lib/libdroid4x.so")) {//檢測(cè)海馬模擬器
? ? ? ? return "/system/lib/libdroid4x.so";
? ? } else if (file_exist("/system/bin/windroyed")) {//檢測(cè)文卓爺模擬器
? ? ? ? return "/system/bin/windroyed";
? ? } else if (file_exist("/system/bin/nox-prop")) {//檢測(cè)夜神模擬器--某些版本找不到特征
? ? ? ? return "/system/bin/nox-prop";
? ? } else if (file_exist("system/lib/libnoxspeedup.so")) {//檢測(cè)夜神模擬器
? ? ? ? return "system/lib/libnoxspeedup.so";
? ? } else if (file_exist("/system/bin/ttVM-prop")) {//檢測(cè)天天模擬器
? ? ? ? return "/system/bin/ttVM-prop";
? ? } else if (file_exist("/data/.bluestacks.prop")) {//檢測(cè)bluestacks模擬器? 51模擬器
? ? ? ? return "/data/.bluestacks.prop";
? ? } else if (file_exist("/system/bin/duosconfig")) {//檢測(cè)AMIDuOS模擬器
? ? ? ? return "/system/bin/duosconfig";
? ? } else if (file_exist("/system/etc/xxzs_prop.sh")) {//檢測(cè)星星模擬器
? ? ? ? return "/system/etc/xxzs_prop.sh";
? ? } else if (file_exist("/system/etc/mumu-configs/device-prop-configs/mumu.config")) {//網(wǎng)易MuMu模擬器
? ? ? ? return "/system/etc/mumu-configs/device-prop-configs/mumu.config";
? ? } else if (file_exist("/system/priv-app/ldAppStore")) {//雷電模擬器
? ? ? ? return "/system/priv-app/ldAppStore";
? ? } else if (file_exist("system/bin/ldinit") && file_exist("system/bin/ldmountsf")) {//雷電模擬器
? ? ? ? return "system/bin/ldinit";
? ? } else if (file_exist("/system/app/AntStore") && file_exist("/system/app/AntLauncher")) {//小蟻模擬器
? ? ? ? return "/system/app/AntStore";
? ? } else if (file_exist("vmos.prop")) {//vmos虛擬機(jī)
? ? ? ? return "vmos.prop";
? ? } else if (file_exist("fstab.titan") && file_exist("init.titan.rc")) {//光速虛擬機(jī)
? ? ? ? return "fstab.titan";
? ? } else if (file_exist("x8.prop")) {//x8沙箱和51虛擬機(jī)
? ? ? ? return "x8.prop";
? ? } else if (file_exist("/system/lib/libc_malloc_debug_qemu.so")) {//AVD QEMU
? ? ? ? return "/system/lib/libc_malloc_debug_qemu.so";
? ? }
? ? LOGD("simulator file check info not find? ");
? ? return "";
}
模擬器基礎(chǔ)特征:
檢測(cè)云手機(jī):
這塊思路還是很多的,不同的云手機(jī)檢測(cè)的思路也不一樣。大部分云手機(jī)做的還是很好的,很多都可以過掉Hunter的檢測(cè)。
檢測(cè)電流&電壓:
private final BroadcastReceiver batteryInfoReceiver = new BroadcastReceiver() {
? ? @Override
? ? public void onReceive(Context context, Intent intent) {
? ? ? ? // 電池狀態(tài)
? ? ? ? int plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
? ? ? ? // 電壓(以毫伏為單位)
? ? ? ? int voltage = intent.getIntExtra(BatteryManager.EXTRA_VOLTAGE, -1);
? ? ? ? // 獲取電池電流(毫安)
? ? ? ? int currentNow = -1;
? ? ? ? if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
? ? ? ? ? ? BatteryManager batteryManager = (BatteryManager) context.getSystemService(Context.BATTERY_SERVICE);
? ? ? ? ? ? currentNow = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CURRENT_NOW);
? ? ? ? }
? ? ? ? // 判斷是否在充電
? ? ? ? if (plugged == BatteryManager.BATTERY_PLUGGED_AC || plugged == BatteryManager.BATTERY_PLUGGED_USB || plugged == BatteryManager.BATTERY_PLUGGED_WIRELESS) {
? ? ? ? ? ? // 在充電
? ? ? ? ? ? if (voltage != -1 && currentNow != -1) {
? ? ? ? ? ? ? ? float voltageInVolts = voltage / 1000f; // 將電壓轉(zhuǎn)換為伏特
? ? ? ? ? ? ? ? float currentInAmperes = currentNow / 1000000f; // 將電流轉(zhuǎn)換為安培
? ? ? ? ? ? ? ? float chargingPower = voltageInVolts * currentInAmperes; // 計(jì)算充電功率(瓦特)
? ? ? ? ? ? ? ? CLog.i(String.format("充電功率: %.2fW", chargingPower));
? ? ? ? ? ? ? ? if (Math.abs(chargingPower) > 300) {
? ? ? ? ? ? ? ? ? ? CLog.e("充電功率過高");
? ? ? ? ? ? ? ? ? ? handlerItemData(new ListItemBean(
? ? ? ? ? ? ? ? ? ? ? ? ? ? "電池異常:充電功率過高(可能是云手機(jī))",
? ? ? ? ? ? ? ? ? ? ? ? ? ? ListItemBean.RiskLeave.Deadly,
? ? ? ? ? ? ? ? ? ? ? ? ? ? "檢測(cè)到過大的充電功率 -> " + String.format("%.2fW", Math.abs(chargingPower))
? ? ? ? ? ? ? ? ? ? ));
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
};
檢測(cè)攝像頭&傳感器相關(guān):
判斷攝像頭有個(gè)數(shù)。
try {
? ? CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
? ? String[] cameraIds = manager.getCameraIdList();
? ? //攝像頭個(gè)數(shù)
? ? CLog.i("cameraIds -> "+ Arrays.toString(cameraIds));
? ? if(cameraIds.length < CAMERA_MINIMUM_QUANTITY_LIMIT){
? ? ? ? items.add(
? ? ? ? ? ? ? ? new ListItemBean(
? ? ? ? ? ? ? ? "當(dāng)前手機(jī)可能是模擬器&云手機(jī)",
? ? ? ? ? ? ? ? ListItemBean.RiskLeave.Warn,
? ? ? ? ? ? ? ? "camera size -> "+cameraIds.length
? ? ? ? ));
? ? }
} catch (Throwable ignored) {
}
檢測(cè)傳感器個(gè)數(shù):
這塊思路就是直接獲取個(gè)數(shù),少于10個(gè)可以直接認(rèn)定為黑產(chǎn)。我目前沒發(fā)現(xiàn)那個(gè)手機(jī)少于10個(gè)傳感器,這塊如果可能的話可以嘗試調(diào)用一下傳感器,保證傳感器是否可用,防止云手機(jī)以假亂真。
try {
? ? //3,檢測(cè)傳感器類型,支持的全部類型傳感器
? ? SensorManager sm = (SensorManager) context.getSystemService(SENSOR_SERVICE);
? ? List<Sensor> sensorlist = sm.getSensorList(Sensor.TYPE_ALL);
? ? ArrayList<Integer> sensorTypeS = new ArrayList<>();
? ? for (Sensor sensor : sensorlist) {
? ? ? ? //獲取傳感器類型
? ? ? ? int type = sensor.getType();
? ? ? ? if (!sensorTypeS.contains(type)) {
? ? ? ? ? ? //發(fā)現(xiàn)一種類型則添加一種類型
? ? ? ? ? ? sensorTypeS.add(type);
? ? ? ? }
? ? }
? ? //小米k40 51個(gè)傳感器類型
? ? //普通的pix 27個(gè)
? ? //華為榮耀20 18個(gè)傳感器
? ? CLog.e("sensor types size -> " + sensorlist.size());
? ? //我們認(rèn)為傳感器少于20個(gè)則認(rèn)為是風(fēng)險(xiǎn)設(shè)備
? ? if (sensorlist.size() < SENSOR_MINIMUM_QUANTITY_LIMIT) {
? ? ? ? items.add(new ListItemBean(
? ? ? ? ? ? ? ? "當(dāng)前手機(jī)可能是模擬器&云手機(jī)",
? ? ? ? ? ? ? ? ListItemBean.RiskLeave.Warn,
? ? ? ? ? ? ? ? "sensor size -> ("+ sensorlist.size()+") \n" +
? ? ? ? ? ? ? ? "sensor type size -> ("+sensorTypeS.size()+") \n"
? ? ? ? ? ? ? ? //+ "sensor info -> \n"+ Sensorlist? ?//打印全部傳感器信息
? ? ? ? ));
? ? }
檢測(cè)傳感器名稱:
這塊檢測(cè)思路主要是檢測(cè)傳感器的名稱,正常小米之類的手機(jī)他是不可能存在叫什么 AOSP的傳感器的。
這種AOSP基本都是自己編譯的ROM,所以這塊也可以作為監(jiān)測(cè)點(diǎn)??梢陨蠄?bào)傳感器的一些名稱信息,也是環(huán)境檢測(cè)一個(gè)很重要的抓手。
一般小白肯定不會(huì)說去改傳感器名稱。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
? ? ArrayList<Sensor> aospSensor = new ArrayList<>();
? ? for(Sensor sensor:sensorlist){
? ? ? ? if(sensor.getVendor().contains("AOSP")){
? ? ? ? ? ? aospSensor.add(sensor);
? ? ? ? }
? ? }
? ? if (aospSensor.size()>3) {
? ? ? ? CLog.e("傳感器參數(shù)是否異常(生產(chǎn)廠商為AOSP)");
? ? ? ? items.add(new ListItemBean(
? ? ? ? ? ? ? ? "當(dāng)前手機(jī)可能是模擬器&云手機(jī)",
? ? ? ? ? ? ? ? ListItemBean.RiskLeave.Warn,
? ? ? ? ? ? ? ? aospSensor.size()
? ? ? ? ? ? ? ? ? ? ? ? +"/"+sensorlist.size()+"傳感器參數(shù)異常 -> "+ aospSensor
? ? ? ? ));
? ? }
}
檢測(cè)掛載文件:
這塊就是去遍歷mounts 下面這幾個(gè)文件,檢測(cè)里面是否包含docker關(guān)鍵字,防止一些云手機(jī)搞虛擬化,通過使用docker進(jìn)行掛載。
這塊也是很好的監(jiān)測(cè)點(diǎn)
String[] marks = {"docker"};
//檢測(cè)proc/mounts是否包含docker關(guān)鍵字
String mark = NativeEngine.getZhenxiInfoK("/proc/mounts",marks );
if(mark == null){
? ? mark = NativeEngine.getZhenxiInfoK("/proc/self/mountstats", marks);
? ? if(mark == null){
? ? ? ? mark = NativeEngine.getZhenxiInfoK("/proc/self/mountinfo", marks);
? ? }
}
if(mark!=null){
? ? items.add(new ListItemBean(
? ? ? ? ? ? "當(dāng)前手機(jī)可能是模擬器&云手機(jī)",
? ? ? ? ? ? ListItemBean.RiskLeave.Warn,
? ? ? ? ? ? "(mounts異常)\n"+mark
? ? ));
}
檢測(cè)ROM是否Match:
檢測(cè)環(huán)境信息:
這塊思路主要好多種,主要是為了防止一些自定義ROM,通過修改機(jī)型的方法,繞過自定義ROM檢測(cè)逃逸。
可以直接執(zhí)行g(shù)etprop 把所有的環(huán)境信息都拿到手,如果是小米手機(jī),里面環(huán)境信息里面,肯定是有MIUI關(guān)鍵字。
比如小米的手機(jī),我會(huì)去檢測(cè)是否包含這幾個(gè)關(guān)鍵環(huán)境信息。
private static final String KEY_MIUI_VERSION_NAME = "ro.miui.ui.version.name";
private static final String KEY_MIUI_VERSION_CODE = "ro.miui.ui.version.code";
private static final String KEY_MIUI_INTERNAL_STORAGE = "ro.miui.internal.storage";
這塊可以采集以后服務(wù)端進(jìn)行判斷,防止自定義ROM 機(jī)型偽造。
檢測(cè)服務(wù)列表:
這塊還是執(zhí)行 service list,一般小米手機(jī)之類的,都會(huì)有小米的系統(tǒng)服務(wù),這種東西很難去偽造,如果他偽造了假的,你就嘗試調(diào)用即可。
這塊還是建議上傳到服務(wù)端,由服務(wù)端算法同學(xué)去根據(jù)相似度算法去推斷,不要再本地進(jìn)行判斷,因?yàn)镠unter是非聯(lián)網(wǎng)Apk,所以只是在客戶端打了個(gè)樣子。
檢測(cè)當(dāng)前環(huán)境是否被Hook:
這塊檢測(cè)方法千奇百怪,首先最基本maps去檢測(cè)frida或者根據(jù)調(diào)用棧檢測(cè)lsp特征,基礎(chǔ)的檢測(cè)方案不說了。因?yàn)槲矣X得并不是一個(gè)很好的方案。改個(gè)名就繞過了。
比如frida特征三件套,檢測(cè)思路主要:
static const char *FRIDA_THREAD_GUM_JS_LOOP = "gum-js-loop";
static const char *FRIDA_THREAD_GMAIN = "gmain";
static const char *FRIDA_NAMEDPIPE_LINJECTOR = "linjector";
Hook檢測(cè),我們其實(shí)只需要檢測(cè)內(nèi)存沒有被修改即可。
檢測(cè)沙箱:
這塊檢測(cè)核心邏輯全部放在ISO線程檢測(cè)。可以配置一個(gè)服務(wù),然后服務(wù)里使用如下變量即可。
<service
? ? android:name=".ZhenxiServer"
? ? android:isolatedProcess="true"
? ? android:useAppZygote="true"
? ? />
這個(gè)服務(wù)非常惡心,一般沙箱對(duì)這個(gè)線程都會(huì)進(jìn)行跳過。
這塊有人可能會(huì)問什么是iso線程?可以理解成一個(gè)獨(dú)立的安全的線程,只能通過和外部IPC交互的方式進(jìn)行通訊。useAppZygote 相當(dāng)于讓這個(gè)進(jìn)程運(yùn)行在Zygote中。這個(gè)時(shí)候時(shí)機(jī)特別早,早到什么程度呢?就連libart.so 都沒加載,所以這個(gè)檢測(cè)進(jìn)程只能調(diào)用一些原始的libc方法,不能調(diào)用任何Art相關(guān)的函數(shù)。
檢測(cè)多余線程PID:
主要實(shí)現(xiàn)思路就是去檢測(cè)proc下面是否有除了main進(jìn)程以外的其他pid,因?yàn)檎?dòng)的話,肯定是只有一個(gè)main進(jìn)程。
但是沙箱的話會(huì)在啟動(dòng)之前去啟動(dòng)別的進(jìn)程,所以這塊可以進(jìn)行bypass。后面我會(huì)統(tǒng)一說這塊應(yīng)該如何對(duì)抗,包括如何繞過。
這塊先介紹檢測(cè)思路,和檢測(cè)原理。這塊里面有一個(gè)replaceSecInsns是我自己封裝的一個(gè)函數(shù),我擔(dān)心 opendir 被Hook了,所以每次執(zhí)行都去把指令替換成本地文件的指令,而不是去執(zhí)行內(nèi)存里面的指令。
內(nèi)核文件相關(guān)(重要):
內(nèi)核文件指的是系統(tǒng)的相關(guān)文件,很多大廠會(huì)直接通過popen cat或者直接fopen只讀的方式去讀取文件內(nèi)容。核心的也就那幾個(gè)。
一般讀取的時(shí)候都是直接svc openat 底層需要用到svc的IO重定向,如果這塊不處理的話,基本沒辦法進(jìn)行mock和修改 。
build.prop相關(guān)? (MIUI系統(tǒng)所在路徑不同)
"/system/build.prop"
"/odm/etc/build.prop"
"/product/build.prop"
"/vendor/build.prop"
/proc/sys/kernel/random/boot_id
這個(gè)ID重啟或者刷機(jī)以后發(fā)生變化,很多大廠會(huì)讀取這個(gè)值,這個(gè)值類似一個(gè)UUID,SVC讀取這個(gè)值,然后將這個(gè)值保存到私有目錄。
跟DRM ID 相比,好處就是不同App讀取的值是一樣的。一個(gè)設(shè)備指紋占比很重的值。
/proc/sys/kernel/random/uuid
同上
/sys/block/mmcblk0/device/cid
同上
/sys/devices/soc0/serial_number
同上
/proc/misc
同上
/proc/version
這個(gè)是一個(gè)linux系統(tǒng)內(nèi)核文件,里面記錄了當(dāng)前Linux系統(tǒng)版本的相關(guān)信息。里面的值類似如下
eg. Linux version 3.18.31-perf-g9b0888a(builder@c3-miui-ota-bd96.bj)
這個(gè)文件在android 11以上基本讀不到了 ,但是在android 9是可以讀到的 。但是android 11有沒有什么代替方案呢?答案是有的,svc 調(diào)用uname 。使用方式類似如下,uname也是一個(gè)命令行,還可以通過popen uname -a的方式去獲取 (popen部分會(huì)介紹到)。這個(gè)函數(shù)在IOS上面也比較實(shí)用。
struct utsname buff;
? ? int i = uname(&buff);
? ? LOGE("uname sysname %s ", buff.sysname)
? ? LOGE("uname nodename %s ", buff.nodename)
? ? LOGE("uname release %s ", buff.release)
? ? LOGE("uname version %s ", buff.version)
? ? LOGE("uname machine %s ", buff.machine)
? ? LOGE("uname domainname %s ", buff.domainname)
通過這幾項(xiàng)就可以拿到/proc/version 里面的所有信息,
很多大廠會(huì)用/ popen uname -a / svc uname函數(shù) / 和svc openat去讀/proc/version以此判斷獲取的值是否準(zhǔn)確,如果有一個(gè)對(duì)不上都會(huì)認(rèn)為當(dāng)前設(shè)備被修改。
getprop
這個(gè)執(zhí)行的內(nèi)容返回的值和,adb shell 以后執(zhí)行g(shù)etprop 結(jié)果是一樣的。輸出的是當(dāng)前手機(jī)全部的Build相關(guān)配置。獲取代碼具體如下 。
pfile = popen("getprop", "r");
pfile = popen("getprop | grep dalvik", "r");
pfile = popen("getprop ro.odm.build.id", "r");
?
while (fgets(buf, sizeof(buf), pfile)) {
? ? LOGE("getprop -> %s", buf);
}
返回結(jié)果就不展示了,自己用手機(jī) adb shell 在執(zhí)行g(shù)etprop 即可 。
ip a(重要)
這個(gè)也是很核心的設(shè)備指紋,里面會(huì)獲取當(dāng)前手機(jī)的網(wǎng)卡信息,whan0 wlan1 p2p0 這些信息。這個(gè)底層走的也是netlinker
所以在netlinker層直接修改攔截,他哪怕執(zhí)行的命令行也是生效的 。返回的東西很多,可以自己嘗試打印一下。很多大廠也會(huì)用這種方式去掃描你得網(wǎng)卡Mac地址 。
ls -al /sdcard/Android/data
掃描私有目錄,返回私有目錄的一些信息 ??梢耘袛喈?dāng)前App是否存在其他App目錄下,主要用于檢測(cè)沙箱。
其實(shí)檢測(cè)沙箱還有一個(gè)很好的辦法,就是檢測(cè)手機(jī)的進(jìn)程信息 。如果當(dāng)前App在自己正常情況啟動(dòng),只會(huì)有一條線程。
但是如果放在VA沙盒內(nèi)部的話,VA沙盒本身會(huì)啟動(dòng)一條線程,自己的App本身也會(huì)啟動(dòng)一條線程。所以線程數(shù)量就對(duì)不上。也可以認(rèn)為作弊
popen掃描Magisk
這些命令都可以進(jìn)行magisk的列表的掃描,判斷當(dāng)前線程是否存在magisk等關(guān)鍵字,都是很好的辦法
popen("df | grep /sbin/.magisk", "r");
popen("mount? | grep /sbin/.magisk", "r");
popen("ps | grep magisk", "r");
修改的話也很簡(jiǎn)單,如果是ps 或者 df 直接生成一份不存在magisk關(guān)鍵字的文件,(還有一些痕跡關(guān)鍵字,比如xposed,edxp,riru這些都是常用的檢測(cè)關(guān)鍵字)
mout直接 svc IO重定向繞過即可 。
popen logcat
有很多大廠,他當(dāng)發(fā)現(xiàn)你設(shè)備信息異常的時(shí)候,會(huì)直接執(zhí)行popen logcat 直接掃描你當(dāng)前手機(jī)的日志系統(tǒng) 。
把異常的log都進(jìn)行上報(bào),用于石錘當(dāng)前用戶是否作弊 。所以這個(gè)也需要處理?
Native獲取DRM ID(重要)
這個(gè)指紋也是很多大廠用作唯一ID的核心指紋。處理的話也需要注意,很核心的一個(gè)設(shè)備指紋ID。
Java層DRM相關(guān)(重要字段):
這個(gè)DRM是水印相關(guān),主要為了處理不同手機(jī)加水印的唯一ID 核心的是一個(gè)叫deviceUniqueId 的東西,這玩意是一個(gè)隨機(jī)的32位字節(jié)數(shù)組。很多大廠用這個(gè)作為核心的設(shè)備指紋,不僅在Java層進(jìn)行獲取,還有在Native層進(jìn)行獲取
配置相關(guān):
常見的配置如下,這些字段其實(shí)修改不修改不重要,因?yàn)楹芏啻髲S如果手機(jī)開了開發(fā)者選項(xiàng)或者debug模式之類的。
會(huì)增加當(dāng)前手機(jī)的風(fēng)險(xiǎn)值。
PUT_MOCK_AND_SAVE_ORG("sys.usb.config", "none", null, true);
PUT_MOCK_AND_SAVE_ORG("sys.usb.state", "none", null, true);
PUT_MOCK_AND_SAVE_ORG("persist.sys.usb.config", "none", null, true);
PUT_MOCK_AND_SAVE_ORG("persist.sys.usb.qmmi.func", "none", null, true);
//這兩個(gè)config可能會(huì)拿不到,拿不到則不進(jìn)行mock
PUT_MOCK_AND_SAVE_ORG("vendor.usb.mimode", "none", null, true);
PUT_MOCK_AND_SAVE_ORG("persist.vendor.usb.config", "none", null, true);
PUT_MOCK_AND_SAVE_ORG("ro.debuggable", "0", null, true);
PUT_MOCK_AND_SAVE_ORG("init.svc.adbd", "stopped", null, true);
PUT_MOCK_AND_SAVE_ORG("ro.secure", "1", null, true);
//手機(jī)解鎖狀態(tài)
PUT_MOCK_AND_SAVE_ORG("ro.boot.flash.locked", "1", null, true);
PUT_MOCK_AND_SAVE_ORG("sys.oem_unlock_allowed", "1", null, true);
Build相關(guān):
Build里面還是有很多有用的東西,比如手機(jī)是否開啟adb ,usb接口的狀態(tài)之類
的。
IMEI , IMSI ,ICCID,Line1Number:
這些基礎(chǔ)的Java設(shè)備指紋字段沒啥好說的,百度一下就能找到具體的獲取方法,但是修改的時(shí)候需要注意,不要直接Hook,嘗試優(yōu)先Hook ipc即可 。
藍(lán)牙網(wǎng)卡MAC:
藍(lán)牙的網(wǎng)卡不是普通的網(wǎng)卡
Setting相關(guān)
其實(shí)Setting里面還有很多別的功能東西,常見的就是Settings.Secure 和 Settings.Global
在Settings.Global 里面其實(shí)還有一些別的字段,具體API如下。這些都是一些比較隱蔽的設(shè)備指紋。
Settings.Global.getString(context.getContentResolver(),"mi_health_id")
Settings.Global.getString(context.getContentResolver(),"mi_health_id")
Settings.Global.getString(context.getContentResolver(),"gcbooster_uuid")
Settings.Global.getString(context.getContentResolver(),"key_mqs_uuid")
Settings.Global.getString(context.getContentResolver(),"ad_aaid")
————————————————
版權(quán)聲明:
原文鏈接:https://mp.weixin.qq.com/s/P0fq3EVGPYyIvDI96VtukQ