Android 破解之道 (二)

简介: Android,高效劫持屏幕时,遇到错屏渲染的解决方式

前言

在这篇文章,我们来讨论一下基于Android系统多缓存文件方式截屏的一些事。《 破解之道(一)》开篇介绍了基于Root环境截屏的技术,使用这种方式获取屏幕数据是快捷而便捷的。然而,大家先不要开心太早,此中却有两个系统级问题,很少有文章涉猎讨论,在此向大家详细解说一下。

SurfaceFlinger 简述

下面这张截屏图片包含了较多信息,大家在往下阅读前,请稍微思考一下。

screen021

从截屏中读取的信息大概归纳如下,欢迎大家友情补充:

  1. 系统应该是分屏刷新的,能看到切分了三块区域
  2. 系统应该有个一刷新完成的标记
  3. 系统应该会派发刷新完成的状态量
  4. 这张图片是怎么捕获的
  5. 我是不是走火入魔了,研究这玩意


第一个问题解答:

首先,请大家查阅源码:
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张截屏图片,会发现有色差现象出现。只是在图像特征选取和识别上面规避了此影响。

01299DA8_7C43_44DA_A442_3E4BF03218DE


第五个问题解答:

详见下一章节的问题。


Hooker 代码注入

考虑到文章已经很长,Hooker又不是什么善良的东西,具体实现方式的介绍会较为简单。大家感兴趣可以去看雪论坛逛逛。

系统屏幕切换所用到的函数是在surfaceflinger内的elfswapbuffer()函数,要获取得系统屏幕切换的信号量,需要劫持surfaceflinger service内的elfswapbuffer()函数,替换成我们自己的newelfswapbuffer()函数,并在系统每次调用newelfswapbuffer()函数时,此向JNI层抛出一个信号量,这样就能强行获得屏幕切换状态量。

而,这样做,需要用到hooker技能,向系统服务注入一段代码,勾住elfswapbuffer()函数的ELF表地址,然后把自己的newelfswapbuffer()函数地址替换入ELF表内。在程序结束后,需要逆向实现一遍以上操作,还原ELF表。

程序用到了以下两个核心文件:

07359000_C2FC_4B0E_AEB4_4ACA49BF97D6

一个文件负责注入系统服务,另一个负责感染系统程序。

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之前的效果:

screen530_jpeg screen534_jpeg
screen531_jpeg screen535_jpeg
screen532_jpeg screen536_jpeg
screen533_jpeg screen537_jpeg



--------------------------------------------

使用Hooker之后的效果:

screen034_jpeg screen042_jpeg
screen036_jpeg screen044_jpeg
screen038_jpeg screen046_jpeg
screen040_jpeg screen047_jpeg

我们可以看到,使用了Hooker之后,截屏图片不再存在断层。剩下的坑,有机会再介绍。


后记

破解技术,是矛与盾的结合。这两篇文章,已有许多可延伸之处,再深入下去,到了汇编层,会越发枯燥了。后续,要不说说怎么防御吧,这也是一个很有趣的话题。


引用参考

  1. Android系统Surface机制的SurfaceFlinger服务对帧缓冲区(Frame Buffer)的管理分析

    http://www.cnblogs.com/mfryf/archive/2013/05/22/3092063.html
    
  2. Android帧缓冲区(Frame Buffer)硬件抽象层(HAL)模块Gralloc的实现原理分析

    http://blog.csdn.net/luoshengyang/article/details/7747932
    
  3. android surfaceflinger研究----Surface机制

    http://blog.csdn.net/windskier/article/details/7041610
目录
相关文章
|
7月前
|
Shell Android开发
安卓逆向 -- 防抓包破解(JustTrustMe)
安卓逆向 -- 防抓包破解(JustTrustMe)
364 1
|
Java 数据安全/隐私保护 Android开发
app逆向实战强化篇——破解某安卓APP请求加密参数
app逆向实战强化篇——破解某安卓APP请求加密参数
|
Java 开发工具 Android开发
安卓逆向系列篇:Dalvik概念&破解实例(二)
安卓逆向系列篇:Dalvik概念&破解实例
181 0
|
Java API Android开发
安卓逆向系列篇:Dalvik概念&破解实例(一)
安卓逆向系列篇:Dalvik概念&破解实例
186 0
|
存储 安全 Java
分析Android程序之破解第一个程序
分析Android程序之破解第一个程序
176 0
|
JavaScript 前端开发 Java
安卓APP破解利器之FRIDA
本文讲的是安卓APP破解利器之FRIDA,在我去年参加RadareCon大会的时候,我了解到了一个动态的二进制插桩框架——Frida。起初我觉得它似乎只有一丁点趣味,后来经过实践才发现它原来是如此的有趣。
3530 0
|
安全 Java Android开发
Android安全与逆向之简单破解APK方法
Android安全与逆向之简单破解APK方法
494 0
|
Java API Android开发
安卓逆向系列教程 4.3 登山赛车内购破解
4.3 登山赛车内购破解 作者:飞龙 首先在这里下载游戏:http://g.10086.cn/game/760000032287?spm=www.pdindex.android.addjgame.1 我们要破解的东西是这个,获得金币: 点击之后会有个弹出框,我们随便输入一些东西,然后点击“确认支付”: 出现了“短信验证码验证失败”的Toast。
1044 1
|
存储 Android开发
Android热更新插件JRebel For Android的安装,破解,使用
它是一个AndroidStudio的插件,安装后我们会在原本运行程序的工具栏中出现三个新的图标。 全部图标.png 后面我们就可以使用它来运行我们的程序,它可以帮助我们节约程序的运行时间,让我们不用再苦苦等待。
2646 0
|
移动开发 Java 开发工具