Android 破解之道 (二)
前言
在這篇文章,我們來討論一下基于Android系統多緩存文件方式截屏的一些事。《 破解之道(一)》開篇介紹了基于Root環境截屏的技術,使用這種方式獲取屏幕數據是快捷而便捷的。然而,大家先不要開心太早,此中卻有兩個系統級問題,很少有文章涉獵討論,在此向大家詳細解說一下。
SurfaceFlinger 簡述
下面這張截屏圖片包含了較多信息,大家在往下閱讀前,請稍微思考一下。
從截屏中讀取的信息大概歸納如下,歡迎大家友情補充:
第一個問題解答:
首先,請大家查閱源碼:
frameworks/base/services/surfaceflinger/DisplayHardware/DisplayHardware.cpp
截取其中關鍵的兩段:
渲染方式聲明:
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;具體渲染操作:
void DisplayHardware::flip(const Region& dirty) const { checkGLErrors(); EGLDisplay dpy = mDisplay; EGLSurface surface = mSurface; 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()); } 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
以上聲明中,成員函數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內的信號量。
第四個問題解答:
使用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 pipeclose(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
原文地址:?https://yq.aliyun.com/articles/3033
總結
以上是生活随笔為你收集整理的Android 破解之道 (二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Andriod 破解之道(一)
- 下一篇: 如何通过数据包套接字攻击Linux内核