前言
在这篇文章,我们来讨论一下基于Android系统多缓存文件方式截屏的一些事。《 破解之道(一)》开篇介绍了基于Root环境截屏的技术,使用这种方式获取屏幕数据是快捷而便捷的。然而,大家先不要开心太早,此中却有两个系统级问题,很少有文章涉猎讨论,在此向大家详细解说一下。
SurfaceFlinger 简述
下面这张截屏图片包含了较多信息,大家在往下阅读前,请稍微思考一下。
从截屏中读取的信息大概归纳如下,欢迎大家友情补充:
- 系统应该是分屏刷新的,能看到切分了三块区域
- 系统应该有个一刷新完成的标记
- 系统应该会派发刷新完成的状态量
- 这张图片是怎么捕获的
- 我是不是走火入魔了,研究这玩意
第一个问题解答:
首先,请大家查阅源码:
frameworks/base/services/surfaceflinger/DisplayHardware/DisplayHardware.cpp
截取其中关键的两段:
渲染方式声明:
#ifdef EGL_ANDROID_swap_rectangle
if (extensions.hasExtension("EGL_ANDROID_swap_rectangle")) {
if (eglSetSwapRectangleANDROID(display, surface,
0, 0, mWidth, mHeight) == EGL_TRUE) {
// This could fail if this extension is not supported by this
// specific surface (of config)
mFlags |= SWAP_RECTANGLE;
}
}
// when we have the choice between PARTIAL_UPDATES and SWAP_RECTANGLE
// choose PARTIAL_UPDATES, which should be more efficient
if (mFlags & PARTIAL_UPDATES)
mFlags &= ~SWAP_RECTANGLE;
#endif
具体渲染操作:
void DisplayHardware::flip(const Region& dirty) const
{
checkGLErrors();
EGLDisplay dpy = mDisplay;
EGLSurface surface = mSurface;
#ifdef EGL_ANDROID_swap_rectangle
if (mFlags & SWAP_RECTANGLE) {
const Region newDirty(dirty.intersect(bounds()));
const Rect b(newDirty.getBounds());
eglSetSwapRectangleANDROID(dpy, surface,
b.left, b.top, b.width(), b.height());
}
#endif
if (mFlags & PARTIAL_UPDATES) {
mNativeWindow->setUpdateRectangle(dirty.getBounds());
}
mPageFlipCount++;
eglSwapBuffers(dpy, surface);
checkEGLErrors("eglSwapBuffers");
// for debugging
//glClearColor(1,0,0,0);
//glClear(GL_COLOR_BUFFER_BIT);
}
这段代码主要用来检查系统的主绘图表面是否支持EGL_ANDROID_swap_rectangle扩展属性。如果支持的话,那么每次在调用函数eglSwapBuffers来渲染UI时,都会使用软件的方式来支持部分更新区域功能,即:先得到不在新脏区域里面的那部分旧脏区域的内容,然后再将得到的这部分旧脏区域的内容拷贝回到要渲染的新图形缓冲区中去,这要求每次在渲染UI时,都要将被渲染的图形缓冲区以及对应的脏区域保存下来。注意,如果系统的主绘图表面同时支持EGL_ANDROID_swap_rectangle扩展属性以及部分更新属性,那么将会优先使用部分更新属性,因为后者是直接在硬件上支持部分更新,因而性能会更好。
第二个问题解答:
在Android源码中有以下对framebuffer的结构定义:
hardware/libhardware/include/hardware/gralloc.h
typedef struct framebuffer_device_t {
struct hw_device_t common;
/* flags describing some attributes of the framebuffer */
const uint32_t flags;
/* dimensions of the framebuffer in pixels */
const uint32_t width;
const uint32_t height;
/* frambuffer stride in pixels */
const int stride;
/* framebuffer pixel format */
const int format;
/* resolution of the framebuffer's display panel in pixel per inch*/
const float xdpi;
const float ydpi;
/* framebuffer's display panel refresh rate in frames per second */
const float fps;
/* min swap interval supported by this framebuffer */
const int minSwapInterval;
/* max swap interval supported by this framebuffer */
const int maxSwapInterval;
int reserved[8];
int (*setSwapInterval)(struct framebuffer_device_t* window,
int interval);
int (*setUpdateRect)(struct framebuffer_device_t* window,
int left, int top, int width, int height);
int (*post)(struct framebuffer_device_t* dev, buffer_handle_t buffer);
int (*compositionComplete)(struct framebuffer_device_t* dev);
void* reserved_proc[8];
} framebuffer_device_t;
以上声明中,成员函数compositionComplete用来通知fb设备device,图形缓冲区的组合工作已经完成。引用参考[2]的文章说明,此函数指针并没有被使用到。那么,我们就要找到在哪里能够获取得到屏幕渲染完成的信号量了。
第三个问题解答:
这个问题建议大家先行阅读所有引用参考文章。然后因为懒,这里就直接给出大家结论,过程需参考surfaceflinger的所有源码。
我们都知道Android在渲染屏幕的时候,一开始用到了double buffer技术,而后的4.0以上版本升级到triple buffer。buffer的缓存是以文件内存映射的方式存储在devgraphicsfb0路径。每块buffer置换的时候,会有唯一的,一个,信号量(注意修饰语)抛给应用层,接收方是我们经常用到的SurfaceView控件。SurfaceView内的OnSurfaceChanged() API 即是当前屏幕更新的信号量,除此之外,程序无从通过任何其他官方API形式获取屏幕切换的时间点。这也是Android应用商场为何没有显示当前任意屏幕的FPS数值的软件(补充一下,有,需要Root,用到的就是本文后续介绍的技术。准确来说,是本文实现了一遍他们的技术)。
本文将在稍后的独立章节说明如何实现强行暴力获取埋在系统底层surfaceflinger service内的信号量。
第四个问题解答:
使用mmap MAP_SHARED方式读屏,就有可能出现此问题。因为屏幕是持续变换的,也就是fd指针指向的内存地址是持续变换的。那有同学就会问了,为什么在《 破解之道(一)》一文中所展示的截屏图片上没有此问题?答案很简单,其实是有的,只要同学细心分析里面的8张截屏图片,会发现有色差现象出现。只是在图像特征选取和识别上面规避了此影响。
第五个问题解答:
详见下一章节的问题。
Hooker 代码注入
考虑到文章已经很长,Hooker又不是什么善良的东西,具体实现方式的介绍会较为简单。大家感兴趣可以去看雪论坛逛逛。
系统屏幕切换所用到的函数是在surfaceflinger内的elfswapbuffer()函数,要获取得系统屏幕切换的信号量,需要劫持surfaceflinger service内的elfswapbuffer()函数,替换成我们自己的newelfswapbuffer()函数,并在系统每次调用newelfswapbuffer()函数时,此向JNI层抛出一个信号量,这样就能强行获得屏幕切换状态量。
而,这样做,需要用到hooker技能,向系统服务注入一段代码,勾住elfswapbuffer()函数的ELF表地址,然后把自己的newelfswapbuffer()函数地址替换入ELF表内。在程序结束后,需要逆向实现一遍以上操作,还原ELF表。
程序用到了以下两个核心文件:
一个文件负责注入系统服务,另一个负责感染系统程序。
Inject surfaceflinger
int main(int argc, char** argv) {
pid_t target_pid;
target_pid = find_pid_of("/system/bin/surfaceflinger");
if (-1 == target_pid) {
printf("Can't find the process\n");
return -1;
}
//target_pid = find_pid_of("/data/test");
inject_remote_process(target_pid, argv[1], "hook_entry", argv[2], strlen(argv[2]));
return 0;
}
Infect surfaceflinger
int hook_entry(char * argv) {
LOGD("Hook success\n");
LOGD("pipe path:%s", argv);
if(mkfifo(argv, 0777) != 0 && errno != EEXIST) {
LOGD("pipe create failed:%d",errno);
return -1;
} else {
LOGD("pipe create successfully");
}
LOGD("Start injecting\n");
elfHook(LIB_PATH, "eglSwapBuffers", (void *)new_eglSwapBuffers, (void **)&old_eglSwapBuffers);
while(1){
int fPipe = open(argv, O_TRUNC, O_RDWR);
if (fPipe == -1) {
LOGD("pipe open failed");
break;
} else {
LOGD("pipe open successfully");
}
char command[10];
memset(command, 0x0, 10);
int ret = read(fPipe, &command, 10);
if(ret > 0 && strcmp(command, "done") == 0) {
LOGD("ptrace detach successfully with %s", command);
break;
} else {
LOGD("ret:%d received command: %s", ret, command);
}
// close the pipe
close(fPipe);
usleep(100);
}
elfHook(LIB_PATH, "eglSwapBuffers", (void *)old_eglSwapBuffers, (void **)&new_eglSwapBuffers);
}
我们能看到以上代码还用到了pipe管道通讯,那是因为注入的是一段二进制可执行代码,而我们在退出程序时需要与此二进制代码通讯,以便正常退出。
详细的Log信息和具体细节因为安全原因,不便具体描述(其实已经有很多蛛丝马迹了),还是那句话,莫犯错。
技术验证
以下是基于一个游戏所做的技术验证:
图片是有序的:
| 1 | 5 |
| 2 | 6 |
| 3 | 7 |
| 4 | 8 |
这是没有使用Hooker之前的效果:
--------------------------------------------
使用Hooker之后的效果:
我们可以看到,使用了Hooker之后,截屏图片不再存在断层。剩下的坑,有机会再介绍。
后记
破解技术,是矛与盾的结合。这两篇文章,已有许多可延伸之处,再深入下去,到了汇编层,会越发枯燥了。后续,要不说说怎么防御吧,这也是一个很有趣的话题。
引用参考
Android系统Surface机制的SurfaceFlinger服务对帧缓冲区(Frame Buffer)的管理分析
http://www.cnblogs.com/mfryf/archive/2013/05/22/3092063.html
Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析
http://blog.csdn.net/luoshengyang/article/details/7747932
android surfaceflinger研究----Surface机制
http://blog.csdn.net/windskier/article/details/7041610