vysor原理以及Android同屏方案
vysor原理以及Android同屏⽅案
vysor是⼀个免root实现电脑控制⼿机的chrome插件,⽬前也有⼏款类似的通过电脑控制⼿机的软件,不过都需要root权限,并且流畅度并不⾼。vysor没有多余的功能,流畅度也很⾼,刚接触到这款插件时我惊讶于它的流畅度以及免root,就⼀直对它的实现原理很感兴趣。这款插件我⽤了⼤半年,最近在升级后我发现它居然开始收费了,终⽣版需要39.99美元,不过经过简单的分析后我很轻松的破解了它的pro版,在分析的过程中发现它的原理并不复杂,所以就打算⾃⼰也实现⼀个类似的软件。
截屏常见的⽅案
在介绍vysor的原理前我先简单介绍⼀下⽬前公开的截屏⽅案。
这是最常见的应⽤内截屏⽅法,这个函数的原理就是通过view的Cache来获取⼀个bitmap对象,然后保存成图⽚⽂件,这种截屏⽅式⾮常的简单,但是局限⾏也很明显,⾸先它只能截取应⽤内部的界⾯,甚⾄连状态栏都不能截取到。其次是对某些view的兼容性也不好,⽐如webview内的内容也⽆法截取。
读取/dev/graphics/fb0
因为Android是基于linux内核,所以我们也能在android中到framebuffer这个设备,我们可以通过读取/dev/graphics/fb0这个帧缓存⽂件中的数据来获取屏幕上的内容,但是这个⽂件是system权限的,所以只有通过root才能读取到其中的内容,并且直接通过framebuffer读取出来的画⾯还需要转换成rgb才能正常显⽰。下⾯是通过adb读取这个⽂件内容的效果。
反射调⽤SurfaceControl.screenshot()/Surface.screenshot()
SurfaceControl.screenshot()(低版本是Surface.screenshot())是系统内部提供的截屏函数,但是这个函数是@hide的,所以⽆法直接调⽤,需要反射调⽤。我尝试反射调⽤这个函数,但是函数返回的是null,后⾯发现SurfaceControl这个类也是隐藏的,所以从⽤户代码中⽆法获取这个类。也有⼀些⽅法能够调⽤到这个函数,⽐如重新编译⼀套sdk,或者在源码环境下编译apk,但是这种⽅案兼容性太差,只能在特定ROM下成功运⾏。
screencap -p xxx.png/screenshot xxx.png
这两个是在shell下调⽤的命令,通过adb shell可以直接截图,但是在代码⾥调⽤则需要系统权限,所以⽆法调⽤。可以看到要实现类似vysor的同步操作,可以使⽤这两个命令来截取屏幕然后传到电脑显⽰,但是我⾃⼰实现后发现这种⽅式⾮常的卡,因为这两个命令不能压缩图⽚,所以导致获取和⽣成图⽚的时间⾮常长。
MediaProjection,VirtualDisplay (>=5.0)
在5.0以后,google开放了截屏的接⼝,可以通过”虚拟屏幕”来录制和截取屏幕,不过因为这种⽅式会弹出确认对话框,并且只在5.0上有效,所以我没有对这种⽅案做深⼊的研究。
可以看到,上述⽅案中并没有解决⽅案能够做到兼容性和效率都⾮常完美,但是我在接触到vysor后发现它不但画⾯清晰,流畅,⽽且不需要root。那么它是⽤了什么⿊科技呢?下⾯我们反编译它的代码来研究⼀下它的实现机制。
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 52public 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();
AsyncServer server = new AsyncServer();
AsyncHttpServer httpServer = new AsyncHttpServer() {
protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {
Log.i(Main.LOGTAG, Headers().toString());
Request(request, response);
}
};
String str = "getInstance";
Object[] objArr = new Object[0];
InputManager im = (InputManager) DeclaredMethod(r20, new Class[0]).invoke(null, objArr);
str = "obtain";
DeclaredMethod(r20, new Class[0]).setAccessible(true);
str = "injectInputEvent";
Method injectInputEventMethod = Method(r20, new Class[]{InputEvent.class, Integer.TYPE});
KeyCharacterMap kcm = KeyCharacterMap.load(-1);
Class cls = Class.forName("android.os.ServiceManager");
Method getServiceMethod = DeclaredMethod("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 = CurrentDisplaySize();
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.String());
} catch (JSONException e) {
}
}
}
};
wm.watchRotation(watcher);
<("/screenshot.jpg", new AnonymousClass5(wm));
httpServer.websocket("/input", "mirror-protocol", new AnonymousClass6(watcher, im, injectInputEventMethod, pm, wm, kcm));        ("/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?");
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67            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!");
}
这个软件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.Query().getString("password"))) {
Log.i(Main.LOGTAG, "screenshot authentication success");
try {
Bitmap bitmap = EncoderFeeder.screenshot(this.val$wm);
ByteArrayOutputStream bout = new ByteArrayOutputStream();
bitmappress(CompressFormat.JPEG, 100, bout);
bout.flush();
response.send("image/jpeg", ByteArray());
return;
} catch (Exception e) {
response.String());
return;
}
}
Log.i(Main.LOGTAG, "screenshot authentication failed");
response.send("Not Authorized.");
}
}
这个类传⼊了⼀个wm类,这个类是⽤来监听屏幕旋转的,这⾥不⽤管它。另外在vysor开始运⾏时,会随机⽣成⼀个验证码,只有验证通过才能进⾏连接,所以这⾥有⼀个验证的过程,这⾥也不过管。可以看到这个类定义的响应函数的代码⾮常简单,就是通过EncoderFeeder.screenshot()函数来过去截图的bi
tmap,然后返回给请求端。那么EncoderFeeder.screenshot这个函数是怎样实现截图的呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23public static Bitmap screenshot(IWindowManager wm) throws Exception {
String surfaceClassName;
Point size = CurrentDisplaySize(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);
}
ateBitmap(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 9function y(e, t, n) {
m(e, "");
function o(o) {
var i = und(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
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28            }, 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 43public class Main {
static Looper looper;
public static void main(String[] args) {
AsyncHttpServer httpServer = new AsyncHttpServer() {
protected boolean onRequest(AsyncHttpServerRequest request, AsyncHttpServerResponse response) {                Request(request, response);
}
};
Looper.prepare();
looper = Looper();
System.out.println("Andcast Main Entry!");
AsyncServer server = new AsyncServer();
<("/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();
bitmappress(Bitmap.CompressFormat.JPEG, 100, bout);
bout.flush();
response.send("image/jpeg", ByteArray());
return;
} catch (Exception e) {
response.String());
return;
}
}
}
}
编译成apk然后安装后,我们使⽤adb shell来运⾏这个类,主要⽅法如下,⾸先导出classpath,否则会提⽰不到类。
1export CLASSPATH=/data/app/com.zke1e.andcast-1/base.apk
然后调⽤app_process来启动这个类。
1exec app_process /system/bin com.zke1e.andcast.Main '$@'
可以看到类已经成功运⾏了,正在监听请求。
然后使⽤adb forward转发端⼝。
1adb forward tcp:53516 tcp:53516
最后在浏览器⾥访问,就可以获取截图了。
当然只有简单的截图功能是不够,我们需要能够流畅实时的传输android的屏幕,并且能够在电脑上控制,经过两天的编写,我使⽤java实现了类似vysor的功能。从流畅度和清晰度上都和vysor差不多,后续还会考虑加⼊⽂件传输和声⾳传输等功能。最近计划编写⼀个java版的android反编译集成环境,类似android killer。因为android killer只能在windows上使⽤,⽽linux下没有类似的⽅⾯的软件。到时这个同步软件可以作为插件和反编译套件集成。最后放⼀张截图。
更新
经过⼀段时间的研究,最后实现了将传输的截图改成了h264码流,提⾼的流畅度和稳定性,然后将接受端放在了浏览器中,实现了可以在浏览器中对android⼿机进⾏控制,下⾯是截图。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。