聲明:本篇文章已受權微信公衆號 guolin_blog (郭霖)獨家發佈java
最近有業務上的要求,要求app在本地進行諸如軟件多開、hook框架、模擬器等安全檢測,防止做弊行爲。git
防做弊一直是老生常談的問題,而軟件多開檢測每每是防做弊中的重要一環,在查找資料的過程當中發現多開軟件公司對防多開手段進行了針對性的升級,即便很是新的資料也沒法作到通殺。github
因此站在前人的肩膀上,繼續研究。安全
借鑑方案來自如下兩個帖子微信
《Android多開/分身檢測》blog.darkness463.top/2018/05/04/…/網絡
《Android虛擬機多開檢測》www.jianshu.com/p/216d65d99…併發
文中的方案簡單總結起來是4點 1.私有文件路徑檢測; 2.應用列表檢測; 3.maps檢測; 4.ps檢測;app
代碼此處不貼了,這四種方案測試結果以下框架
測試機器/多開軟件* | 多開分身6.9 | 平行空間4.0.8389 | 雙開助手3.8.4 | 分身大師2.5.1 | VirtualXP0.11.2 | Virtual App * |
---|---|---|---|---|---|---|
紅米3S/Android6.0/原生eng | XXXO | OXOO | OXOO | XOOO | XXXO | XXXO |
華爲P9/Android7.0/EUI 5.0 root | XXXX | OXOX | OXOX | XOOX | XXXX | XXXO |
小米MIX2/Android8.0/MIUI穩定版9.5 | XXXX | OXOX | OXOX | XOOX | XXXX | XXXO |
一加5T/Android8.1/氫OS 5.1 穩定版 | XXXX | OXOX | OXOX | XOOX | XXXX | XXXO |
*測試方案順序1234,測試結果X表明未能檢測O成功檢測多開; *virtual app測試版本是git開源版,商用版已經修復uid的問題;dom
能夠看到的是,檢測效果不是很理想,沒有哪種方法能夠作到通殺市面排名靠前的這些多開軟件,甚至在高版本機器上,多開軟件完美避開了檢測。
爲了不歧義,咱們接下來所說的app都是指的同一款軟件,並定義普通運行的app叫作本體,運行在多開軟件上的app叫克隆體。並提出如下兩個概念
狹義多開:只要app是經過多開軟件打開的,則認爲多開,即便同一時間內只運行了一個app
廣義多開:不管app是否運行在多開軟件上,只要app在運行期間,有其他的『本身』在運行,則認爲多開 (有點《第六日》的意思,克隆人覺得本身是真人,發現跟本身如出一轍的人,都認爲對方是克隆人)
咱們前面所借鑑的四種方案,都是去針對狹義多開進行檢測,經過判斷運行在多開軟件時的特徵進行反制,多開軟件也會針對這些檢測方案進行研究,提出相應措施。
那麼咱們退一步,順着檢測廣義多開的方向進行思考,咱們容許app運行在多開軟件上,可是在一臺機器上同一時間有且只能運行一個app(不管本體or克隆體),只要app能發現有一個一樣的本身,而後幹掉對方或自殺,就達到防止廣義多開的目的。
那麼咱們怎樣讓這兩個app見面呢?
微信同一帳號不能同時登陸在不一樣的手機上,靠的是網絡請求,限定登陸設備。
那在本地如何處理這種狀況呢?是否是也能夠靠網絡通訊的方式完成見面? 答案固然是確定的啊,否則我寫這篇幹嗎,利用socket,本身既當客戶端又當服務端就能完成咱們的需求。
1.app運行後,先作發送端,在合適的時候去鏈接本地端口併發送一段密文消息,若是有端口鏈接且密文匹配,則認爲以前已經有app在運行了(廣義多開),接收端進行處理; 2.app再成爲接收端,接收可能到來鏈接; 3.後續如有app啓動(不管本體or克隆體),則重複1&2步驟,達到『同一時間只有一個app在運行』的目的,解決廣義多開的問題。
思路有了,接下來就是實現,完整代碼地址見文章底部。
想固然利用netstat指令來掃描已經開啓的本地端口
可是這個方法有3個坑 1.netstat在部分機器上用不了; 410063005.iteye.com/blog/192354…
2.busybox 在部分機器用不了;
3.netstat的輸出從源碼上看,實際是純打印; blog.csdn.net/earbao/arti…
既然有這些坑,乾脆直接手動處理,由於netstat的本質上仍是去讀取/proc/net/tcp等文件再格式化處理,tcp文件格式也是很標準化的,經過研究源碼,找出端口之間的關係。 0100007F:8CA7 其實就是 127.0.0.1:36007
最終掃描tcp文件並格式化端口的關鍵代碼
String tcp6 = CommandUtil.getSingleInstance().exec("cat /proc/net/tcp6");
if (TextUtils.isEmpty(tcp6)) return;
String[] lines = tcp6.split("\n");
ArrayList<Integer> portList = new ArrayList<>();
for (int i = 0, len = lines.length; i < len; i++) {
int localHost = lines[i].indexOf("0100007F:");
//127.0.0.1:的位置
if (localHost < 0) continue;
String singlePort = lines[i].substring(localHost + 9, localHost + 13);
//截取端口
Integer port = Integer.parseInt(singlePort, 16);
//16進制轉成10進制
portList.add(port);
}
複製代碼
接下來向每一個端口都發起一個線程進行鏈接,併發送自定義消息,該段消息用app的包名就好了(多開軟件很大程度會hook getPackageName方法,乾脆就順着多開軟件作)
try {
//發起鏈接,併發送消息
Socket socket = new Socket("127.0.0.1", port);
socket.setSoTimeout(2000);
OutputStream outputStream = socket.getOutputStream();
outputStream.write((secret + "\n").getBytes("utf-8"));
outputStream.flush();
socket.shutdownOutput();
//獲取輸入流,這裏沒作處理,純打印
InputStream inputStream = socket.getInputStream();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String info = null;
while ((info = bufferedReader.readLine()) != null) {
Log.i(TAG, "ClientThread: " + info);
}
bufferedReader.close();
inputStream.close();
socket.close();
} catch (ConnectException e) {
Log.i(TAG, port + "port refused");
}
複製代碼
主動鏈接的過程完成,先於本身啓動的app(多是本體or克隆體)接收到消息並進行處理。
接下來就是成爲接收端,監聽某端口,等待可能到來的app鏈接(多是本體or克隆體)。
private void startServer(String secret) {
Random random = new Random();
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket();
serverSocket.bind(new InetSocketAddress("127.0.0.1",
random.nextInt(55534) + 10000));
//開一個10000~65535之間的端口
while (true) {
Socket socket = serverSocket.accept();
ReadThread readThread = new ReadThread(secret, socket);
//假如這個方案不少app都在用,仍是每一個鏈接都開線程處理一些
readThread.start();
// serverSocket.close();
}
} catch (BindException e) {
startServer(secret);//may be loop forever
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
開啓端口時爲了不開一個已經開啓的端口,主動捕獲BindExecption,並迭代調用,可能會所以無限循環,若是怕死循環的話,能夠加一個相似ConcurrentHashMap最壞嘗試次數的計數值。不過實際測試沒那麼衰,隨機端口範圍10000~65535,最多嘗試兩次就行了。
每個處理線程,作的事情就是匹配密文,對應上了就是某個克隆體or本體發送的密文,這裏是接收端主動運行一個空指針異常,殺死本身。處理方式有點像《三體》的黑暗森林法則,誰先暴露誰先死。
private class ReadThread extends Thread {
private ReadThread(String secret, Socket socket) {
InputStream inputStream = null;
try {
inputStream = socket.getInputStream();
byte buffer[] = new byte[1024 * 4];
int temp = 0;
while ((temp = inputStream.read(buffer)) != -1) {
String result = new String(buffer, 0, temp);
if (result.contains(secret)) {
checkCallback.findSuspect();//提供回調,開發者自行處理
checkCallback = null;
}
}
inputStream.close();
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
複製代碼
*由於端口通訊須要Internet權限,本庫不會經過網絡上傳任何隱私
以以前提到的那些機型和多開軟件作測試樣本,目前測試效果基本作到通殺。 因安卓機型太廣,真機覆蓋測試不徹底,有空你們去git提issue;
在application的mainProcess裏調用一次便可。 模擬器由於會搶localhost,demo裏作了模擬器判斷。
本文方案已經集成到EasyProtectorLib
github地址: github.com/lamster2018…
中文文檔見:www.jianshu.com/p/c37b1bdb4…
使用方法 VirtualApkCheckUtil.getSingleInstance().checkByPortListening(String secret);
1.檢測到多開應該提供回調給開發者自行處理;--v1.0.4 support
2.一樣的思路,利用ContentProvider也應該能夠完成
*感謝同事大龍提供的思路