Android逆向工程簡單分析

Android逆向工程
在Root前提下,我們可以使用Hooker方式綁定so庫,通過逆向方式篡改數(shù)值,從而達(dá)到所謂破解目的。然而,目前無論是軟件加固方式,或是數(shù)據(jù)處理能力后臺(tái)化,還是客戶端數(shù)據(jù)真實(shí)性驗(yàn)證,都有了一定積累和發(fā)展,讓此“懶技術(shù)”不再是破解修改的萬金油。再者,閱讀匯編指令,函數(shù)指針替換,壓棧出棧等技術(shù)需要一定技術(shù)沉淀,不利于開發(fā)同學(xué)上手。
兩年前,也是因?yàn)閼?,很懶,非常懶,堆積了足夠的動(dòng)力,寫了一個(gè)基于人工模擬方式,對(duì)一個(gè)特定規(guī)則的游戲進(jìn)行暴力破解。我們都知道,人工模擬方式,繞過了大量防破解技術(shù),只要還是人機(jī)交互模式,并且滿足一定的游戲規(guī)則,基本是無法防御的。
技術(shù)實(shí)現(xiàn)原理
因涉及到安全方面的考量,本文主要圍繞技術(shù)實(shí)現(xiàn)原理和關(guān)鍵技術(shù)點(diǎn)進(jìn)行闡述。
技術(shù)要求:
支持多分辨率
支持多點(diǎn)觸摸
支持輸入速率動(dòng)態(tài)變更
處理能力峰值需要達(dá)到30fps
實(shí)現(xiàn)方式分三步:
劫持屏幕
分析數(shù)據(jù)
模擬輸出
1.劫持屏幕
先說說劫持屏幕,做過截屏功能的同學(xué)應(yīng)該清楚,Root了之后能訪問設(shè)備“dev/graphic”文件夾,里面有fb0, fb1, fb2三個(gè)screen buffer文件。這里用到的是fb0文件。
拋出一個(gè)問題,當(dāng)前主流屏幕分辨率都在1920*1080區(qū)間,一張圖片的緩存能去到2M左右,要達(dá)到30fps的性能指標(biāo),光是屏幕數(shù)據(jù)的讀寫耗時(shí),就滿足不了要求。怎么做呢?
一般在做圖像處理的時(shí)候都會(huì)想到parallel programming。然而,這里的圖片是時(shí)間相關(guān)的,不適宜采用多線程任務(wù)派發(fā)。
懶人一番思量后,發(fā)現(xiàn)一條捷徑,共享內(nèi)存讀取,請(qǐng)看以下代碼。
mapbase = mmap(0, **mapsize, PROT_READ, MAP_SHARED, fd, offset);
這行代碼廣泛存在于各個(gè)截屏代碼片段中,精髓在于PROT_READ 和 MAP_SHARED上。先科普一下mmap參數(shù)中這兩個(gè)參數(shù)吧。
prot : 映射區(qū)域的保護(hù)方式??梢詾橐韵聨追N方式的組合:
PROT_EXEC 映射區(qū)域可被執(zhí)行
PROT_READ 映射區(qū)域可被讀取
PROT_WRITE 映射區(qū)域可被寫入
PROT_NONE 映射區(qū)域不能存取
flags : 影響映射區(qū)域的各種特性。在調(diào)用mmap()時(shí)必須要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果參數(shù)start所指的地址無法成功建立映射時(shí),則放棄映射,不對(duì)地址做修正。通常不鼓勵(lì)用此旗標(biāo)。
MAP_SHARED 對(duì)映射區(qū)域的寫入數(shù)據(jù)會(huì)復(fù)制回文件內(nèi),而且允許其他映射該文件的進(jìn)程共享。
MAP_PRIVATE 對(duì)映射區(qū)域的寫入操作會(huì)產(chǎn)生一個(gè)映射文件的復(fù)制,即私人的“寫入時(shí)復(fù)制”(copy on write)對(duì)此區(qū)域作的任何修改都不會(huì)寫回原來的文件內(nèi)容。
MAP_ANONYMOUS建立匿名映射。此時(shí)會(huì)忽略參數(shù)fd,不涉及文件,而且映射區(qū)域無法和其他進(jìn)程共享。
MAP_DENYWRITE只允許對(duì)映射區(qū)域的寫入操作,其他對(duì)文件直接寫入的操作將會(huì)被拒絕。
MAP_LOCKED 將映射區(qū)域鎖定住,這表示該區(qū)域不會(huì)被置換(swap)。
因?yàn)槲覀儾恍枰獙懫?,所以prot只需要采用PORT_READ;而我們期望避免屏幕數(shù)據(jù)的多次創(chuàng)建,flags就需要用到MAP_SHARED,這樣文件句柄fd指向的內(nèi)存塊數(shù)據(jù)就會(huì)實(shí)時(shí)變更,無需多次創(chuàng)建,拷貝,釋放數(shù)據(jù)。
2.分析數(shù)據(jù)
截取到屏幕數(shù)據(jù)就好辦了,對(duì)每一幀進(jìn)行數(shù)據(jù)處理,這里完全就是算法問題了。懶人都用搓算法,大概的思路就是:7*7宮格,對(duì)于所有相連的兩個(gè)同色item做了橫向映射表和縱向映射表,然后輪尋處理5連,4連和3連。里面還有一些涉及到實(shí)現(xiàn)細(xì)節(jié)的映射表重置與預(yù)判,因?yàn)椴皇潜疚闹攸c(diǎn),就帶過了。
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(); }
下面是程序運(yùn)行時(shí)的Log信息片段,以供大家參考。

3. 模擬輸出
算法會(huì)輸出當(dāng)前屏幕的一個(gè)模擬手勢(shì)操作隊(duì)列,最精彩的當(dāng)然放到最后,也是此工程的技術(shù)點(diǎn),怎么模擬輸出手勢(shì)的問題。
Android所給予的截屏和模擬操作分別為 adb screenshot 和 adb shell sendevent (根據(jù)android版本,有些機(jī)型用的是input event,記得沒錯(cuò)的話~)
所有需要adb處理的指令,都不能采用高并發(fā)方式調(diào)用,要不然要么機(jī)器重啟,要么指令堵塞。所以adb這條路不通。
怎么辦呢?
懶人又一番思量后,linux系統(tǒng)大都采用文件buffer,直接將指令寫文件吧。其實(shí)adb也是寫文件,不過adb做了一層轉(zhuǎn)譯,這里涉及到設(shè)備層指令代碼,不同機(jī)型定義的指令代碼不盡相同。
要完成此任務(wù),首先要弄清楚幾件事情:
一個(gè)點(diǎn)擊事件的構(gòu)成是怎樣的
一個(gè)滑動(dòng)事件的構(gòu)成多了什么
事件的指令代碼分別代表什么
萬能的adb給了我一些思路,adb shell getevent,會(huì)打印出當(dāng)前event的指令。再科普一下,event有很多,包括compass_sensor,light_sensor,pressure_sensor,accelerometer_sensor等等。
我們這里監(jiān)聽的是,touchscreen_sensor。

有了上面的指導(dǎo)信息,要構(gòu)建一個(gè)模擬操作函數(shù)就很容易了。操作屏幕打印出想要的模擬的手勢(shì),然后寫下來就好了。一共會(huì)有這么幾個(gè)模擬操作函數(shù)需要?jiǎng)?chuàng)建:
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);
下面給出一個(gè)我寫好的范例出來,大家可以依葫蘆畫瓢,把剩下的寫好。
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 // 此項(xiàng)目非必要,因?yàn)闆]有用到多點(diǎn)觸摸,是另一個(gè)項(xiàng)目使用到了 event.type = 0x3; event.code = 0x2f; event.value = touch; write(global.fd_event, &event, sizeof(event)); // 1. ABS_MT_TRACKING_ID: // 理論上必要,因?yàn)锳ndroid事件輸入是批量處理的,需要用到輸入id, // 但是這里偷懶使用了同步鎖,并且沒有多點(diǎn)觸摸需求,所以不會(huì)有Tracking_ID串?dāng)_問題,也就不需要記數(shù)了 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: // 觸摸點(diǎn)x,y坐標(biāo) 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 // 數(shù)據(jù)同步到設(shè)備 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: // 結(jié)束符 event.type = 0x0; event.code = 0x0; event.value = 0x0; write(global.fd_event, &event, sizeof(event)); pthread_mutex_unlock(&global.writeEventLock); }
為了大家對(duì)Android逆向有一個(gè)簡單的理解,我們看下面幾個(gè)問題。
首先,請(qǐng)大家查閱源碼:
frameworks/base/services/surfaceflinger/DisplayHardware/DisplayHardware.cpp
截取其中關(guān)鍵的兩段:
渲染方式聲明:
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); ? }
這段代碼主要用來檢查系統(tǒng)的主繪圖表面是否支持EGL_ANDROID_swap_rectangle擴(kuò)展屬性。如果支持的話,那么每次在調(diào)用函數(shù)eglSwapBuffers來渲染UI時(shí),都會(huì)使用軟件的方式來支持部分更新區(qū)域功能,即:先得到不在新臟區(qū)域里面的那部分舊臟區(qū)域的內(nèi)容,然后再將得到的這部分舊臟區(qū)域的內(nèi)容拷貝回到要渲染的新圖形緩沖區(qū)中去,這要求每次在渲染UI時(shí),都要將被渲染的圖形緩沖區(qū)以及對(duì)應(yīng)的臟區(qū)域保存下來。注意,如果系統(tǒng)的主繪圖表面同時(shí)支持EGL_ANDROID_swap_rectangle擴(kuò)展屬性以及部分更新屬性,那么將會(huì)優(yōu)先使用部分更新屬性,因?yàn)楹笳呤侵苯釉谟布现С植糠指?,因而性能?huì)更好。
在Android源碼中有以下對(duì)framebuffer的結(jié)構(gòu)定義:
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;
以上聲明中,成員函數(shù)compositionComplete用來通知fb設(shè)備device,圖形緩沖區(qū)的組合工作已經(jīng)完成。引用參考[2]的文章說明,此函數(shù)指針并沒有被使用到。那么,我們就要找到在哪里能夠獲取得到屏幕渲染完成的信號(hào)量了。
這個(gè)問題建議大家先行閱讀所有引用參考文章。然后因?yàn)閼?,這里就直接給出大家結(jié)論,過程需參考surfaceflinger的所有源碼。
我們都知道Android在渲染屏幕的時(shí)候,一開始用到了double buffer技術(shù),而后的4.0以上版本升級(jí)到triple buffer。buffer的緩存是以文件內(nèi)存映射的方式存儲(chǔ)在dev\graphics\fb0路徑。每塊buffer置換的時(shí)候,會(huì)有唯一的,一個(gè),信號(hào)量(注意修飾語)拋給應(yīng)用層,接收方是我們經(jīng)常用到的SurfaceView控件。SurfaceView內(nèi)的OnSurfaceChanged() API 即是當(dāng)前屏幕更新的信號(hào)量,除此之外,程序無從通過任何其他官方API形式獲取屏幕切換的時(shí)間點(diǎn)。這也是Android應(yīng)用商場(chǎng)為何沒有顯示當(dāng)前任意屏幕的FPS數(shù)值的軟件(補(bǔ)充一下,有,需要Root,用到的就是本文后續(xù)介紹的技術(shù)。準(zhǔn)確來說,是本文實(shí)現(xiàn)了一遍他們的技術(shù))。
本文將在稍后的獨(dú)立章節(jié)說明如何實(shí)現(xiàn)強(qiáng)行暴力獲取埋在系統(tǒng)底層surfaceflinger service內(nèi)的信號(hào)量。
Hooker 代碼注入
系統(tǒng)屏幕切換所用到的函數(shù)是在surfaceflinger內(nèi)的elfswapbuffer()函數(shù),要獲取得系統(tǒng)屏幕切換的信號(hào)量,需要劫持surfaceflinger service內(nèi)的elfswapbuffer()函數(shù),替換成我們自己的newelfswapbuffer()函數(shù),并在系統(tǒng)每次調(diào)用newelfswapbuffer()函數(shù)時(shí),此向JNI層拋出一個(gè)信號(hào)量,這樣就能強(qiáng)行獲得屏幕切換狀態(tài)量。
而,這樣做,需要用到hooker技能,向系統(tǒng)服務(wù)注入一段代碼,勾住elfswapbuffer()函數(shù)的ELF表地址,然后把自己的newelfswapbuffer()函數(shù)地址替換入ELF表內(nèi)。在程序結(jié)束后,需要逆向?qū)崿F(xiàn)一遍以上操作,還原ELF表。
程序用到了以下兩個(gè)核心文件:

一個(gè)文件負(fù)責(zé)注入系統(tǒng)服務(wù),另一個(gè)負(fù)責(zé)感染系統(tǒng)程序。
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管道通訊,那是因?yàn)樽⑷氲氖且欢味M(jìn)制可執(zhí)行代碼,而我們?cè)谕顺龀绦驎r(shí)需要與此二進(jìn)制代碼通訊,以便正常退出。