vysor原理以及Android同屏方案

轉帖:javascript

vysor是一個免root實現電腦控制手機的chrome插件,目前也有幾款相似的經過電腦控制手機的軟件,不過都須要root權限,而且流暢度並不高。vysor沒有多餘的功能,流暢度也很高,剛接觸到這款插件時我驚訝於它的流暢度以及免root,就一直對它的實現原理很感興趣。這款插件我用了大半年,最近在升級後我發現它竟然開始收費了,終生版須要39.99美圓,不過通過簡單的分析後我很輕鬆的破解了它的pro版,在分析的過程當中發現它的原理並不複雜,因此就打算本身也實現一個相似的軟件。java

截屏常見的方案

在介紹vysor的原理前我先簡單介紹一下目前公開的截屏方案。linux

  • View.getDrawingCache()

這是最多見的應用內截屏方法,這個函數的原理就是經過view的Cache來獲取一個bitmap對象,而後保存成圖片文件,這種截屏方式很是的簡單,可是侷限行也很明顯,首先它只能截取應用內部的界面,甚至連狀態欄都不能截取到。其次是對某些view的兼容性也很差,好比webview內的內容也沒法截取。android

  • 讀取/dev/graphics/fb0

由於Android是基於linux內核,因此咱們也能在android中找到framebuffer這個設備,咱們能夠經過讀取/dev/graphics/fb0這個幀緩存文件中的數據來獲取屏幕上的內容,可是這個文件是system權限的,因此只有經過root才能讀取到其中的內容,而且直接經過framebuffer讀取出來的畫面還須要轉換成rgb才能正常顯示。下面是經過adb讀取這個文件內容的效果。web

  • 反射調用SurfaceControl.screenshot()/Surface.screenshot()

SurfaceControl.screenshot()(低版本是Surface.screenshot())是系統內部提供的截屏函數,可是這個函數是@hide的,因此沒法直接調用,須要反射調用。我嘗試反射調用這個函數,可是函數返回的是null,後面發現SurfaceControl這個類也是隱藏的,因此從用戶代碼中沒法獲取這個類。也有一些方法可以調用到這個函數,好比從新編譯一套sdk,或者在源碼環境下編譯apk,可是這種方案兼容性太差,只能在特定ROM下成功運行。chrome

  • screencap -p xxx.png/screenshot xxx.png

這兩個是在shell下調用的命令,經過adb shell能夠直接截圖,可是在代碼裏調用則須要系統權限,因此沒法調用。能夠看到要實現相似vysor的同步操做,可使用這兩個命令來截取屏幕而後傳到電腦顯示,可是我本身實現後發現這種方式很是的卡,由於這兩個命令不能壓縮圖片,因此致使獲取和生成圖片的時間很是長。shell

  • MediaProjection,VirtualDisplay (>=5.0)

在5.0之後,google開放了截屏的接口,能夠經過」虛擬屏幕」來錄製和截取屏幕,不過由於這種方式會彈出確認對話框,而且只在5.0上有效,因此我沒有對這種方案作深刻的研究。json

能夠看到,上述方案中並無解決方案可以作到兼容性和效率都很是完美,可是我在接觸到vysor後發現它不但畫面清晰,流暢,並且不須要root。那麼它是用了什麼黑科技呢?下面咱們反編譯它的代碼來研究一下它的實現機制。windows

vysor原理

反編譯vysor的apk後能夠發現它的代碼並很少,經過分析後我發現它的核心代碼在Main這個類中。瀏覽器

首先來看Main函數的main方法,這個方法比較長,這裏直接貼出源碼。

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
65
66
67
public static void main(String[] args) throws Exception {
        if (args.length > 0) {
            commandLinePassword = args[0];
            Log.i(LOGTAG, "Received command line password: " + commandLinePassword);
        }
        Looper.prepare();
        looper = Looper.myLooper();
        AsyncServer server = new AsyncServer();
        AsyncHttpServer httpServer = new AsyncHttpServer() {
            protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
                Log.i(Main.LOGTAG, request.getHeaders().toString());
                return super.onRequest(request, response);
            }
        };
        String str = "getInstance";
        Object[] objArr = new Object[0];
        InputManager im = (InputManager) InputManager.class.getDeclaredMethod(r20, new Class[0]).invoke(null, objArr);
        str = "obtain";
        MotionEvent.class.getDeclaredMethod(r20, new Class[0]).setAccessible(true);
        str = "injectInputEvent";
        Method injectInputEventMethod = InputManager.class.getMethod(r20, new Class[]{InputEvent.class, Integer.TYPE});
        KeyCharacterMap kcm = KeyCharacterMap.load(-1);
        Class cls = Class.forName("android.os.ServiceManager");
        Method getServiceMethod = cls.getDeclaredMethod("getService", new Class[]{String.class});
        IClipboard clipboard = IClipboard.Stub.asInterface((IBinder) getServiceMethod.invoke(null, new Object[]{"clipboard"}));
        clipboard.addPrimaryClipChangedListener(new AnonymousClass3(clipboard), null);
        IPowerManager pm = IPowerManager.Stub.asInterface((IBinder) getServiceMethod.invoke(null, new Object[]{"power"}));
        IWindowManager wm = IWindowManager.Stub.asInterface((IBinder) getServiceMethod.invoke(null, new Object[]{"window"}));
        IRotationWatcher watcher = new Stub() {
            public void onRotationChanged(int rotation) throws RemoteException {
                if (Main.webSocket != null) {
                    Point displaySize = SurfaceControlVirtualDisplayFactory.getCurrentDisplaySize();
                    JSONObject json = new JSONObject();
                    try {
                        json.put("type", "displaySize");
                        json.put("screenWidth", displaySize.x);
                        json.put("screenHeight", displaySize.y);
                        json.put("nav", Main.hasNavBar());
                        Main.webSocket.send(json.toString());
                    } catch (JSONException e) {
                    }
                }
            }
        };
        wm.watchRotation(watcher);
        httpServer.get("/screenshot.jpg", new AnonymousClass5(wm));
        httpServer.websocket("/input", "mirror-protocol", new AnonymousClass6(watcher, im, injectInputEventMethod, pm, wm, kcm));
        httpServer.get("/h264", new AnonymousClass7(im, injectInputEventMethod, pm, wm));
        Log.i(LOGTAG, "Server starting");
        AsyncServerSocket rawSocket = server.listen(null, 53517, new AnonymousClass8(wm));
        if (httpServer.listen(server, 53516) == null || rawSocket == null) {
            System.out.println("No server socket?");
            Log.e(LOGTAG, "No server socket?");
            throw new AssertionError("No server socket?");
        }
        System.out.println("Started");
        Log.i(LOGTAG, "Waiting for exit");
        Looper.loop();
        Log.i(LOGTAG, "Looper done");
        server.stop();
        if (current != null) {
            current.stop();
            current = null;
        }
        Log.i(LOGTAG, "Done!");
        System.exit(0);
    }

 

這個軟件koushikdutta是由開發的,這個團隊之前發佈過一個很是流行的開源網絡庫:async。在這個項目中也用到了這個開源庫。main函數主要是新建了一個httpserver而後開放了幾個接口,經過screenshot.jpg獲取截圖,經過socket input接口來發送點擊信息,經過h264這個接口來獲取實時的屏幕視頻流。每個接口都有對應的響應函數,這裏咱們主要研究截圖,因此就看screenshot這個接口。h264這個接口傳輸的是實時的視頻流,因此就流暢性來講應該會更好,它也是經過virtualdisplay來實現的有興趣的讀者能夠自行研究。

接下來咱們來看screenshot對應的響應函數AnonymousClass5的實現代碼。

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
* renamed from: com.koushikdutta.vysor.Main.5 */
    static class AnonymousClass5 implements HttpServerRequestCallback {
        final /* synthetic */ IWindowManager val$wm;

        AnonymousClass5(IWindowManager iWindowManager) {
            this.val$wm = iWindowManager;
        }

        public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
            if (Main.checkPassword(request.getQuery().getString("password"))) {
                Log.i(Main.LOGTAG, "screenshot authentication success");
                try {
                    Bitmap bitmap = EncoderFeeder.screenshot(this.val$wm);
                    ByteArrayOutputStream bout = new ByteArrayOutputStream();
                    bitmap.compress(CompressFormat.JPEG, 100, bout);
                    bout.flush();
                    response.send("image/jpeg", bout.toByteArray());
                    return;
                } catch (Exception e) {
                    response.code(500);
                    response.send(e.toString());
                    return;
                }
            }
            Log.i(Main.LOGTAG, "screenshot authentication failed");
            response.code(401);
            response.send("Not Authorized.");
        }
    }

 

這個類傳入了一個wm類,這個類是用來監聽屏幕旋轉的,這裏不用管它。另外在vysor開始運行時,會隨機生成一個驗證碼,只有驗證經過才能進行鏈接,因此這裏有一個驗證的過程,這裏也不過管。能夠看到這個類定義的響應函數的代碼很是簡單,就是經過EncoderFeeder.screenshot()函數來過去截圖的bitmap,而後返回給請求端。那麼EncoderFeeder.screenshot這個函數是怎樣實現截圖的呢?

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 Bitmap screenshot(IWindowManager wm) throws Exception {
        String surfaceClassName;
        Point size = SurfaceControlVirtualDisplayFactory.getCurrentDisplaySize(false);
        if (VERSION.SDK_INT <= 17) {
            surfaceClassName = "android.view.Surface";
        } else {
            surfaceClassName = "android.view.SurfaceControl";
        }
        Bitmap b = (Bitmap) Class.forName(surfaceClassName).getDeclaredMethod("screenshot", new Class[]{Integer.TYPE, Integer.TYPE}).invoke(null, new Object[]{Integer.valueOf(size.x), Integer.valueOf(size.y)});
        int rotation = wm.getRotation();
        if (rotation == 0) {
            return b;
        }
        Matrix m = new Matrix();
        if (rotation == 1) {
            m.postRotate(-90.0f);
        } else if (rotation == 2) {
            m.postRotate(-180.0f);
        } else if (rotation == 3) {
            m.postRotate(-270.0f);
        }
        return Bitmap.createBitmap(b, 0, 0, size.x, size.y, m, false);
    }

這裏的截圖的核心代碼也是反射調用Surface/SurfaceControl的screenshot方法。可是咱們前面已經瞭解到,這個類只有在系統權限下才能獲取到,那麼vysor又是怎麼調用到這個函數的呢?咱們能夠確認的是vysor不是經過重編譯sdk和使用系統簽名來完成的,由於那樣只能對特定的rom適用。

當時看到這裏的代碼後我也很是困惑,vysor是怎麼調用到這個類的。我注意到了vysor的核心代碼不是在某個Activity或者Service中而是在一個Main類中,按照通常的邏輯來講,這種實時傳屏應該是放在Service中不斷截屏而後發給服務端,因此我決定再看下它的服務端的代碼。

vysor的服務端是一個chrome插件,用javascript寫成的,因此找到源碼比java更加簡單。雖然js通過混淆,可是很容易的能夠經過一些工具來解密。而後就是分析它的代碼了,終於被我找到了關鍵的代碼。

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
function y(e, t, n) {
        m(e, "Connecting...");

        function o(o) {
            var i = Math.round(Math.random() * (1 << 30)).toString(16);
            var r = "echo -n " + i + " > /data/local/tmp/vysor.pwd ; chmod 600 /data/local/tmp/vysor.pwd";
            Adb.shell({
                command: "ls -l /system/bin/app_process*",
                serialno: e
            }, function(s) {
                var c = "/system/bin/app_process";
                if (s && s.indexOf("app_process32") != -1) {
                    c += "32"
                }
                Adb.sendClientCommand({
                    command: 'shell:sh -c "CLASSPATH=' + o + " " + c + " /system/bin com.koushikdutta.vysor.Main " + i + '"',
                    serialno: e
                }, function(o) {
                    Adb.shell({
                        serialno: e,
                        command: 'sh -c "' + r + '"'
                    }, function(e) {
                        Socket.eat(o);
                        n(t, i)
                    })
                })
            })
        }

能夠看到上面的代碼是調用了adb shell命令來啓動com.koushikdutta.vysor.Main類,而且上面獲取了app_process這個程序。相信對android熟悉讀者已經明白它的原理了。我簡單解釋一下。咱們已經知道Surface/SurfaceControl這兩個類是須要具備相應權限的程序才能調用到,用戶進程沒法獲取到。adb shell能夠調用screencap或者screenshot來截取屏幕,那就說明adb shell具備截屏的權限。Surface/SurfaceControl和screenshot/screencap它們內部的實現機制應該是相同的,因此也就是說adb shell是具備截屏權限的也就是可以調用到Surface/SurfaceControl。那麼咱們怎麼經過adb shell來調用到這兩個類呢,答案就是app_process。app_process能夠直接運行一個普通的java類,詳細的資料你們能夠在網上找到。也就是說咱們經過adb shell運行app_process,而後經過app_process來運行一個java類,在java類中就能夠訪問到Surface/SurfaceControl這兩個類,是否是很巧妙?

理論有了,下面咱們來經過代碼驗證。這裏咱們能夠直接使用vysor的代碼。由於是測試用因此我沒有添加其餘功能。

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
public class Main {


    static Looper looper;

    public static void main(String[] args) {

        AsyncHttpServer httpServer = new AsyncHttpServer() {
            protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
                return super.onRequest(request, response);
            }
        };

        Looper.prepare();
        looper = Looper.myLooper();
        System.out.println("Andcast Main Entry!");
        AsyncServer server = new AsyncServer();
        httpServer.get("/screenshot.jpg", new AnonymousClass5());
        httpServer.listen(server, 53516);

        Looper.loop();

    }

    /* renamed from: com.koushikdutta.vysor.Main.5 */
    static class AnonymousClass5 implements HttpServerRequestCallback {

        public void onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
                try {
                    Bitmap bitmap = ScreenShotFb.screenshot();
                    ByteArrayOutputStream bout = new ByteArrayOutputStream();
                    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bout);
                    bout.flush();
                    response.send("image/jpeg", bout.toByteArray());
                    return;
                } catch (Exception e) {
                    response.code(500);
                    response.send(e.toString());
                    return;
                }
        }
    }
}

 

編譯成apk而後安裝後,咱們使用adb shell來運行這個類,主要方法以下,首先導出classpath,不然會提示找不到類。

1
export CLASSPATH=/data/app/com.zke1e.andcast-1/base.apk

 

而後調用app_process來啓動這個類。

1
exec app_process /system/bin com.zke1e.andcast.Main '$@'

 

能夠看到類已經成功運行了,正在監聽請求。

而後使用adb forward轉發端口。

1
adb forward tcp:53516 tcp:53516

最後在瀏覽器裏訪問,就能夠獲取截圖了。

固然只有簡單的截圖功能是不夠,咱們須要可以流暢實時的傳輸android的屏幕,而且可以在電腦上控制,通過兩天的編寫,我使用java實現了相似vysor的功能。從流暢度和清晰度上都和vysor差很少,後續還會考慮加入文件傳輸和聲音傳輸等功能。最近計劃編寫一個java版的android反編譯集成環境,相似android killer。由於android killer只能在windows上使用,而linux下沒有相似的方面的軟件。到時這個同步軟件能夠做爲插件和反編譯套件集成。最後放一張截圖。

更新

通過一段時間的研究,最後實現了將傳輸的截圖改爲了h264碼流,提升的流暢度和穩定性,而後將接受端放在了瀏覽器中,實現了能夠在瀏覽器中對android手機進行控制,下面是截圖。

相關文章
相關標籤/搜索