Android是基於linux內核的操作系統,根據語言環境可以簡單的划分為java層、native C層、linux內核層。java層通過jni與native層交互,使用linux提供的底層函數功能。
因此,類似linux系統,我們可以在Android下實現對另一個進程的掛鈎和代碼注入。在這簡單介紹下掛鈎和代碼注入的方法和兩個庫,以及針對《刀塔傳奇》實現的代碼注入。
利用libinject實現so注入和API Hook
一. so注入
Linux上有一個強大的系統調用ptrace,它提供了父進程觀察和控制子進程的能力,並允許父進程檢查和替換子進程寄存器的值(大名鼎鼎的gdb也是基於ptrace的),當使用ptrace后,發送給子進程的信號會轉發給父進程,而子進程會被阻塞,父進程收到信號后,就可以對子進程進行檢查和修改。其原型為:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
1). enum __ptrace_request request:ptrace要執行的命令。
2). pid_t pid: 指示ptrace要跟蹤的進程。
3). void *addr: 指示要監控的內存地址。
4). void *data: 存放讀取出的或者要寫入的數據。
在獲得root權限的情況下,我們可以再Android上ptrace另一個進程,讀取和修改該進程的內存數據。看雪論壇上有大神實現了Android下的so注入libinject,代碼的大致原理:
-
ptrace attack到目標進程,保持寄存器數據,接管程序的運行。
-
找到目標進程的mmap函數地址,調用mmap分配一段內存空間。
-
找到目標進程的dlopen、dlclose、dlsym的地址,調用dlopen載入.so文件,調用dlsym獲取.so文件的地址。
-
找到so中” hook_entry”函數的地址並執行。
-
收尾,調用dlclose,還原寄存器,執行ptrace_detach,把控制權還給目標程序。
libinject中有兩個主要功能(尋找函數地址和調用函數),函數分別為get_remote_addr
和ptrace_call_wrapper
,代碼如下:
void* get_remote_addr(pid_t target_pid, const char* module_name, void* local_addr) { void* local_handle, *remote_handle; local_handle = get_module_base(-1, module_name); remote_handle = get_module_base(target_pid, module_name); DEBUG_PRINT("[+] get_remote_addr: local[%x], remote[%x]\n", local_handle, remote_handle); void * ret_addr = (void *)((uint32_t)local_addr + (uint32_t)remote_handle - (uint32_t)local_handle); #if defined(__i386__) if (!strcmp(module_name, libc_path)) { ret_addr += 2; } #endif return ret_addr; } int ptrace_call_wrapper(pid_t target_pid, const char * func_name, void * func_addr, long * parameters, int param_num, struct ptREGS
* REGS
) { DEBUG_PRINT("[+] Calling %s in target process.\n", func_name); if (ptrace_call(target_pid, (uint32_t)func_addr, parameters, param_num, REGS
) == -1) return -1; if (ptrace_getregs(target_pid, regs) == -1) return -1; DEBUG_PRINT("[+] Target process returned from %s, return value=%x, pc=%x \n", func_name, ptrace_retval(regs), ptrace_ip(regs)); return 0; }
so的地址是通過分析/proc/pid/maps文件得到的。 Maps文件如下:
在arm處理器下,執行函數的代碼如下:
#elif defined(__i386__) long ptrace_call(pid_t pid, uint32_t addr, long *params, uint32_t num_params, struct userREGS
_struct * REGS
) { REGS
->esp -= (num_params) * sizeof(long) ; ptrace_writedata(pid, (void *)regs->esp, (uint8_t *)params, (num_params) * sizeof(long)); long tmp_addr = 0x00; regs->esp -= sizeof(long); ptrace_writedata(pid, regs->esp, (char *)&tmp_addr, sizeof(tmp_addr)); regs->eip = addr; if (ptrace_setregs(pid, regs) == -1 || ptrace_continue( pid) == -1) { printf("error\n"); return -1; } int stat = 0; waitpid(pid, &stat, WUNTRACED); while (stat != 0xb7f) { if (ptrace_continue(pid) == -1) { printf("error\n"); return -1; } waitpid(pid, &stat, WUNTRACED); } return 0; }
把返回地址設置為0的目的是讓目標進程繼續執行完后找不到返回地址出錯,進程會被掛起,控制權又回到了父進程也就是libinject手上。
二. API Hook
api hook技術有2種elf hook 和inline hook。Elf hook 通過修改動態連接庫的PLT/GOT表,從而達到函數調用的重定向目的,這種方法只能hook模塊的外部調用,例如hook打開文件的系統函數檢測程序打開文件的情況,hook系統時間相關的函數,達到加速的目的(市面上的加速外掛基本都是采取這種方法)。但是這種方法不能hook模塊的內部調用,因為模塊內部調用不需要查GOT表。而游戲引擎的功能都封裝在一個動態連接庫里,基本都是內部調用,ELF HOOK無法生效。本文所采用的是另外一個方法:INLINE HOOK。
INLINE HOOK的思路大致是這樣:首先找到目標函數在內存中的地址,然后把該地址塊設置為可寫,修改目標函數地址的內容,讓游戲調用目標函數時跳轉到我們自己的函數地址,我們的函數執行完后再跳轉回來。這樣不論是模塊內部調用或外部調用,INLINE HOOK都能生效。具體步驟如下:
-
找到目標函數在內存中的地址,跟上文提到的尋找函數地址的方法不一樣,注入so后,我們的代碼是在目標進程空間中執行的,無法通過so基址的偏移計算函數在內存的地址,因為目標so在內存中只有一份。通過另外一種方式尋找函數地址,在linux下,可執行文件和動態連接庫都是使用ELF文件格式的,ELF結構體中包含了所有符號的信息,通過解析ELF,可以獲取到目標函數在內存中的地址。ELF文件格式這里就不介紹,感興趣的同學可以查看ELF文件介紹。
-
代碼段加載進內存后,一般不需要修改,所以代碼段是沒有寫屬性的,需要調用mprotect()把內存塊加上PROT_WRITE屬性。
-
接下來就可以把匯編指令寫入指定地址了,以Arm指令集為例,把目標函數的頭12個字節(ARM每條指令32位,即4個字節)先備份下來,然后替換成如下內容:
0xe59ff000, ldr pc, [pc, #0] ; 跳轉到hook_func處開始執行 (ARM模式下,由於采用多級流水線結構,PC實際值為當前指令地址+8)
0xe1a08008, 同 nop。
hook_func, hook函數的地址。
當執行到我們的目標函數時,會被跳轉到目標函數地址+2的位置,也就是我們的hook函數。在我們的hook函數里先把目標函數的頭12個字節還原,然后再調用目標函數,調用完后再把頭12個字節修改回來。關鍵代碼如下:
unsigned int orig=0; unsigned int store[3] = {0,0,0}; int jump_code[3] = {0xe59ff000, 0xe1a08008, 0}; //addr:目標函數地址,hookf:hook函數。 int hook_direct(unsigned int addr, void *hookf) { //設置hook函數 jump_code[2] = (unsigned int)hookf; orig = addr;//保持好目標函數的地址 //備份前3個int for (i = 0; i < 3; i++) store[i] = ((int*)addr)[i]; for (i = 0; i < 3; i++) //修改為我們的跳轉指令 ((int*)orig)[i] = jump_code[i] return 1; } void hook_func()//hook函數 { printf(“hello\n”); //還原目標函數的頭12個字節 for (i = 0; i < 3; i++) ((int*)orig)[i] = store[i] void(*orig_func)() = (void*)orig; orig_func();//執行目標函數 //重新修改目標函數的頭12個字節。 for (i = 0; i < 3; i++) ((int*)orig)[i] = jump_code[i]; }
這種方式沒有處理函數的返回值,如果目標函數有返回值的話就會有問題。嚴謹的做法如下:
int jump_pre_code[12] = { 0xe92d0008, 0xe1a0300f, 0xe283301c, 0xe583e000, 0xe89d0008, 0xe28dd004, 0xe1a0e00f, 0xe59ff008, 0xe59fe000, 0xe1a0f00e,0,0 }; Jump_pre_code[11]= (unsigned int)hookf; for(int i = 0; i<12; i++) jump_code[i+2] = jump_pre_code[i];
jump_pre_code所對應的匯編指令如下:
stmdb sp!, {r3} ; 將r3壓入棧中,保存r3因為后邊要修改r3,棧頂指針-1;
mov r3, pc ; 將4指令所在地址賦給r3
add r3, r3, #0x1C ; 將r3加上0x1c即從4指令所在地址往下7條指令,即就是jump_pre_code[10]賦給r3
str lr, [r3] ; 將調用原函數的返回地址存入jump_pre_cod[10]中
ldmia sp, {r3} ; 從棧中取出之前保存的r3
add sp, sp, #4 ;還原調用棧
mov lr, pc ; 將返回地址存入lr中
ldr pc, [pc, #0x8] ; 將10指令所在地址往下2條指令處的內容賦給pc,即PC跳到hookf我們自定義的hook函數
ldr lr, [pc, #0] ; 執行完我們自定的hook函數后就會返回到這了,此處將當前指令往下2條指令出的內容即jump_pre_cod[10]取出賦給lr,即還原原先調用者的返回地址
mov pc, lr ; 跳到原調用者的返回地址去
addr_ret ;存放原函數的返回值
hookf ;hook函數
利用adbi實現對《刀塔傳奇》so注入和API Hook
對於Android下的inject和hook,github上有個adbi框架。這個框架所使用的方法和上面介紹的差不多,采用inline hook但是沒有處理返回值的情況,根據剛才介紹的方法,可以把返回值處理的功能加上,Thumb指令集的hook有個BUG,沒有正確的跳轉到hook函數的地址,跳轉地址少了算2個字節,應該是jumpt[18]到jumpt[21]這4個字節存放hook_func的地址。
該框架包括2個模塊:hijack和instruments,hijack編譯出來后是一個可執行文件,用來注入動態鏈接庫的。Instruments里包括base和example,example是一個hook的例子,編譯出來是一個so文件。
《刀塔傳奇》是利用cocos2d-x+lua編寫的游戲,我們可以利用adbi實現對cocos2dx+lua中常用的函數如lua_pushstring
的inject和hook從而來執行我們自己的lua腳本。lua_pushstring
的聲明如下:
LUA_API void (lua_pushstring) (lua_State *L, const char* str);
基本思路是拿到lua 虛擬機的地址,然后調用luaL_loadstring和lua_call就可以在游戲進程中執行我們自己的lua腳本。主要代碼如下:
int (*my_lua_call)(lua_State *, int, int) = NULL; int (*my_luaL_loadstring)(lua_State*, const char *) =NULL; //目標進程lua_pushstring的hook函數,調用lua_pushstring時會先調用它 void my_lua_pushstring (lua_State *L, const char* str); { int (*orig_lua_isuserdata)(lua_State *L, int idx); //得到原函數 orig_lua_pushstring = (void*)eph.orig; log("start hook func..precall...\n"); hook_precall(&eph); //還原原函數首地址 orig_lua_pushstring (L, str);//調用原函數。 if (counter) { log("lua_pushstring() called\n"); counter--; if (!counter) log("removing hook for lua_pushstring ()\n"); } //如果找到了lua_call和luaL_loadstring的話就執行自己的lua代碼。 if(my_lua_call!=NULL&&my_luaL_loadstring!=NULL) { my_luaL_loadstring(L, LUACODE); my_lua_call(L,0,0); } hook_postcall(&eph);//備份 } void my_init(void) //so注入成功后會調用這個函數 { counter = 3; log("%s my_init started\n", __FILE__); set_logfunction(my_log); unsigned long int lua_call_addr; unsigned long int lua_loadstring_addr; //在進程中找lua_call函數地址 find_name(getpid(),"lua_call", soname, &lua_call_addr); //在目標進程中找luaL_loadstring函數地址. find_name(getpid(), "luaL_loadstring",soname, &lua_loadstring_addr); my_lua_call = lua_call_addr; my_luaL_loadstring = lua_loadstring_addr; log("lua_call addr:%p, luaL_loadstring addr:%p\n", lua_call_addr, lua_loadstring_addr); //執行hook。 hook(&eph, getpid(), soname, "lua_pushstring", my_lua_pushstring_arm, my_lua_pushstring); }
(1)adbi主要包括hijack和libbase。Hijack提供了代碼注入的功能,libbase提供了掛鈎和卸載鈎子的功能。
(2)編譯adbi。主要是利用Android的NDK分別編譯hijack和libbase,編譯過后會生成一個libexample.so文件,並保存到/data/local/tmp/目錄下,賦予執行權限,如下圖:
(3)查找所需掛鈎游戲的so文件,可以通過兩種方式,一種是解壓apk文件查看lib目錄下的so文件,另一種是使用命令“cat /proc/PID/maps”查看。如圖顯示了部分so文件:
(4)保存執行hijack命令,如下圖所示,其中776是《刀塔傳奇》的進程ID。
(5)運行《刀塔傳奇》,在游戲調用lua_pushstring函數時,會跳轉執行自己的函數,主要函數功能如下:
local function Run20()
local label = CCLabelTTF:create("hello cocos2.x", "Arial", 60)
local MenuItem = CCMenuItemLabel:create(label)
MenuItem:registerScriptTapHandler(MainMenuCallback)
local s = CCDirector:sharedDirector():getWinSize()
local Menu = CCMenu:create()
Menu:addChild(MenuItem)
Menu:setPosition(100, 100)
MenuItem:setPosition(250, 25)
local scene = CCDirector:sharedDirector():getRunningScene()
scene:addChild(Menu)
Menu:setZOrder(99999999)
log("crate menuitem......")
end
(6)執行上述代碼的效果如下圖(使用的是MTL的HTC手機)
參考: