在 Root 前提下,我们可以使用 Hooker 方式绑定 so 库,通过逆向方式篡改数值,从而达到所谓破解目的。然而,目前无论是软件加固方式,或是数据处理能力后台化,还是客户端数据真实性验证,都有了一定积累和发展,让此 "懒技术" 不再是破解修改的万金油。再者,阅读汇编指令,函数指针替换,压栈出栈等技术需要一定技术沉淀,不利于开发同学上手。
两年前,也是因为懒,很懒,非常懒,堆积了足够的动力,写了一个基于人工模拟方式,对一个特定规则的游戏进行暴力破解。我们都知道,人工模拟方式,绕过了大量防破解技术,只要还是人机交互模式,并且满足一定的游戏规则,基本是无法防御的。
因涉及到安全方面的考量,本文主要围绕技术实现原理和关键技术点进行阐述。
技术要求:
实现方式分三步:
先说说劫持屏幕,做过截屏功能的同学应该清楚,Root 了之后能访问设备 "dev/graphic" 文件夹,里面有 fb0, fb1, fb2 三个 screen buffer 文件。这里用到的是 fb0 文件。
抛出一个问题,当前主流屏幕分辨率都在 1920*1080 区间,一张图片的缓存能去到 2M 左右,要达到 30fps 的性能指标,光是屏幕数据的读写耗时,就满足不了要求。怎么做呢?
一般在做图像处理的时候都会想到 parallel programming。然而,这里的图片是时间相关的,不适宜采用多线程任务派发。
懒人一番思量后,发现一条捷径,共享内存读取,请看以下代码。
- mapbase = mmap(0, **mapsize, PROT_READ, MAP_SHARED, fd, offset);
这行代码广泛存在于各个截屏代码片段中,精髓在于 PROT_READ 和 MAP_SHARED 上。先科普一下 mmap 参数中这两个参数吧。
prot : 映射区域的保护方式。可以为以下几种方式的组合:
flags : 影响映射区域的各种特性。在调用 mmap() 时必须要指定 MAP_SHARED 或 MAP_PRIVATE。
因为我们不需要写屏,所以 prot 只需要采用 PORT_READ;而我们期望避免屏幕数据的多次创建,flags 就需要用到 MAP_SHARED,这样文件句柄 fd 指向的内存块数据就会实时变更,无需多次创建,拷贝,释放数据。
截取到屏幕数据就好办了,对每一帧进行数据处理,这里完全就是算法问题了。懒人都用搓算法,大概的思路就是:7*7 宫格,对于所有相连的两个同色 item 做了横向映射表和纵向映射表,然后轮寻处理 5 连,4 连和 3 连。里面还有一些涉及到实现细节的映射表重置与预判,因为不是本文重点,就带过了。
- void Handle_X_Combination() {
- LOGE("Handle_X_Combination");
- gen_Horizontal_Matrix(6);
- get_Horizontal_X_Match();
- gen_Vertical_Matrix(0, 6);
- get_Vertical_X_Match();
- }
下面是程序运行时的 Log 信息片段,以供大家参考。
算法会输出当前屏幕的一个模拟手势操作队列,最精彩的当然放到最后,也是此工程的技术点,怎么模拟输出手势的问题。
Android 所给予的截屏和模拟操作分别为 adb screenshot 和 adb shell sendevent (根据 android 版本,有些机型用的是 input event,记得没错的话~)
所有需要 adb 处理的指令,都不能采用高并发方式调用,要不然要么机器重启,要么指令堵塞。所以 adb 这条路不通。
怎么办呢?
懒人又一番思量后,linux 系统大都采用文件 buffer,直接将指令写文件吧。其实 adb 也是写文件,不过 adb 做了一层转译,这里涉及到设备层指令代码,不同机型定义的指令代码不尽相同。
要完成此任务,首先要弄清楚几件事情:
万能的 adb 给了我一些思路,adb shell getevent,会打印出当前 event 的指令。再科普一下,event 有很多,包括 compass_sensor,light_sensor,pressure_sensor,accelerometer_sensor 等等。
我们这里监听的是,touchscreen_sensor。
有了上面的指导信息,要构建一个模拟操作函数就很容易了。操作屏幕打印出想要的模拟的手势,然后写下来就好了。一共会有这么几个模拟操作函数需要创建:
下面给出一个我写好的范例出来,大家可以依葫芦画瓢,把剩下的写好。
- void simulate_long_press_start_event(int touch, int fromX, int fromY);
- void simulate_long_press_hold_event(int touch, int fromX, int fromY);
- void simulate_long_press_end_event(int touch);
- void simulate_press_event(int touch, int fromX, int fromY);
- void simulate_move_event(int touch, int fromX, int fromY, int toX, int toY);
- void simulate_press_event(int touch, int fromX, int fromY) {
- pthread_mutex_lock( & global.writeEventLock);
- LOGE("simulate_press_event");
- INPUT_EVENT event;
- // 0. Multi-Touch
- // 此项目非必要,因为没有用到多点触摸,是另一个项目使用到了
- event.type = 0x3;
- event.code = 0x2f;
- event.value = touch;
- write(global.fd_event, &event, sizeof(event));
- // 1. ABS_MT_TRACKING_ID:
- // 理论上必要,因为Android事件输入是批量处理的,需要用到输入id,
- // 但是这里偷懒使用了同步锁,并且没有多点触摸需求,所以不会有Tracking_ID串扰问题,也就不需要记数了
- event.type = 0x3;
- event.code = 0x39;
- event.value = global.event_id > 60000 ? 10 : global.event_id++;
- write(global.fd_event, &event, sizeof(event));
- // 2. At screen coordinates:
- // 触摸点x,y坐标
- event.type = 0x3;
- event.code = 0x35;
- event.value = fromX;
- write(global.fd_event, &event, sizeof(event));
- event.type = 0x3;
- event.code = 0x36;
- event.value = fromY;
- write(global.fd_event, &event, sizeof(event));
- // 4. Sync
- // 数据同步到设备
- event.type = 0x0;
- event.code = 0x0;
- event.value = 0x0;
- write(global.fd_event, &event, sizeof(event));
- event.type = 0x3;
- event.code = 0x39;
- event.value = 0xffffffff;
- write(global.fd_event, &event, sizeof(event));
- // 4. Pure event separator:
- // 结束符
- event.type = 0x0;
- event.code = 0x0;
- event.value = 0x0;
- write(global.fd_event, &event, sizeof(event));
- pthread_mutex_unlock( & global.writeEventLock);
- }
为了大家对 Android 逆向有一个简单的理解,我们看下面几个问题。
首先,请大家查阅源码:
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 的缓存是以文件内存映射的方式存储在 dev\graphics\fb0 路径。每块 buffer 置换的时候,会有唯一的,一个,信号量(注意修饰语)抛给应用层,接收方是我们经常用到的 SurfaceView 控件。SurfaceView 内的 OnSurfaceChanged() API 即是当前屏幕更新的信号量,除此之外,程序无从通过任何其他官方 API 形式获取屏幕切换的时间点。这也是 Android 应用商场为何没有显示当前任意屏幕的 FPS 数值的软件(补充一下,有,需要 Root,用到的就是本文后续介绍的技术。准确来说,是本文实现了一遍他们的技术)。
本文将在稍后的独立章节说明如何实现强行暴力获取埋在系统底层 surfaceflinger service 内的信号量。
系统屏幕切换所用到的函数是在 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 管道通讯,那是因为注入的是一段二进制可执行代码,而我们在退出程序时需要与此二进制代码通讯,以便正常退出。
关于代码的 hook,大家可以参考之前的文章:
来源: http://www.bubuko.com/infodetail-1861818.html