最近在為distri.lua實現一個lua調試系統,有一個簡單的需求,lua導入一個文件的時候,將這個文件的文件名記錄下來,
以方便調試器在設置斷點的時候判斷是否一個合法的文件.
lua導入文件是通過luaL_loadfilex實現的,一個簡單的思路就是修改luaL_loadfilex,在luaL_loadfilex中調用一個外部定義的函數將導入的文件名傳給那個外部函數,由它記錄下來.
但這種侵入式的方案,除非在逼不得已的情況下不應該使用.
另一個思路是hook luaL_loadfilex,在運行時用另外一個函數替換luaL_loadfilex,由這個替換函數去記錄下需要的信息
然后在跳轉回原luaL_loadfilex的執行流程上.
與是我從decode中提取除了Hook.h,Hook.c稍加調整以適應Linux系統.
hook的原理很簡單:
* 首先,使用mprotect將luaL_loadfilex所在代碼段設置為可讀/可寫/可執行,以避免在修改代碼時出現段訪問異常
* 之后需要把luaL_loadfilex最前面的一段指令替換成一個跳轉指令,跳轉到替換函數中去執行.為了在替換函數執行
完之后可以正確的回到luaL_loadfilex的正常執行路徑上,需要把luaL_loadfilex中被替換部分的指令保存下來,然后
在后面再添加一條跳轉指令,調到luaL_loadfilex后面的執行路徑去.
被替換掉的指令布局和執行流程如下圖:
luaL_loadfilex:
jmp hook
-------------
其余指令 <-------------------------------------------------|
|
hook: |
執行必要的記錄 |
--保存的代碼---- |
luaL_loadfilex中被替換的指令 |
jmp其余指令--------------------------------------------------|
HookFunction實現如下:
void* HookFunction(void* function, void* hook)
{
// Don't allow rehooking of the same function since that screws things up.
assert(!GetIsHooked(function, hook));
if (GetIsHooked(function, hook))
{
return NULL;
}
// Missing from windows.h
//#define HEAP_CREATE_ENABLE_EXECUTE 0x00040000
// Jump instruction is 5 bytes.
const int jumpSize = 5;
// Compute the instruction boundary so we don't override half an instruction.
int boundary = GetInstructionBoundary(function, jumpSize);
size_t pagesize = sysconf(_SC_PAGE_SIZE);
unsigned char* trampoline = NULL;
trampoline = (unsigned char*)/*aligned_alloc*/memalign(pagesize,pagesize);
if(mprotect(trampoline, pagesize, PROT_WRITE|PROT_READ|PROT_EXEC)){
free(trampoline);
return NULL;
}
// Copy the original bytes to the trampoline area and append a jump back
// to the original function (after our jump).
memcpy(trampoline, function, boundary);
AdjustRelativeJumps(trampoline, boundary, ((unsigned char*)function) - trampoline);
WriteJump(trampoline + boundary, ((unsigned char*)function) + boundary);
void *ptr = (void*)(((size_t)function/pagesize)*pagesize);
// Give ourself write access to the region.
if(!mprotect(ptr, pagesize, PROT_WRITE|PROT_READ|PROT_EXEC))
{
// Fill the area with nops and write the jump instruction to our
// hook function.
memset(function, 0x90, boundary);
WriteJump(function, hook);
// Restore the original protections.
//VirtualProtect(function, boundary, protection, &protection);
mprotect(ptr, pagesize, PROT_READ|PROT_EXEC);
// Flush the cache so we know that our new code gets executed.
//FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
return trampoline;
//return 0;
}
free(trampoline);
return NULL;
//return -1;
}
本以為一切就這樣結束了,運行程序的時候,正確的進入了替換函數,但在執行完記錄操作要回到luaL_loadfilex后續執行流程的時候程序
掛了,報段訪問異常.
為啥呢,我們看下WriteJump的實現:
/**
* Writes a relative jmp instruction.
*/
void WriteJump(void* dst, void* address)
{
unsigned char* jump = (unsigned char*)(dst);
// Setup a jump instruction.
jump[0] = 0xE9;
*((unsigned long*)(&jump[1])) = (unsigned long)(address) - (unsigned long)(dst) - 5;
}
使用的是E9跳轉指令+4字節的立即數做相對rip計數器的跳轉.這在32位程序下這是沒問題的,因為trampoline和luaL_loadfilex的位移差必定
在4字節的范圍內.但我程序的運行環境是64位的,這個時候程序就出問題了, trampoline和luaL_loadfilex的位移差已經超過4個字節.這就導致
[jmp其余指令]跳轉到到錯誤的地址上了.
如何解決這個問題:
* 用FF指令做跳轉,但這個方案要修改的地方就多了,除了` WriteJump`,還有`AdjustRelativeJumps`並且還會導致指令長度變長.
* 用static數據區保存luaL_loadfilex中被替換的指令,使得位移差被控制在4字節以內.
針對我的需求,我選擇了方案2,下面是修改過的HookFunction和WriteJump以及使用示例:
/**
* Writes a relative jmp instruction.
*/
void WriteJump(void* dst, void* address)
{
unsigned char* jump = (unsigned char*)(dst);
// Setup a jump instruction.
jump[0] = 0xE9;
*((unsigned int*)(&jump[1])) = (unsigned int)((unsigned long)(address) - (unsigned long)(dst) - 5);
}
void* HookFunction(void* function, void* hook,void *saveaddr,size_t saveaddr_size)
{
// Don't allow rehooking of the same function since that screws things up.
assert(!GetIsHooked(function, hook));
if (GetIsHooked(function, hook))
{
return NULL;
}
// Missing from windows.h
//#define HEAP_CREATE_ENABLE_EXECUTE 0x00040000
// Jump instruction is 5 bytes.
const int jumpSize = 5;
// Compute the instruction boundary so we don't override half an instruction.
int boundary = GetInstructionBoundary(function, jumpSize);
if(saveaddr_size < (size_t)boundary) return NULL;
size_t pagesize = sysconf(_SC_PAGE_SIZE);
if(mprotect(saveaddr, boundary, PROT_WRITE|PROT_READ|PROT_EXEC)){
//free(trampoline);
return NULL;
}
// Copy the original bytes to the trampoline area and append a jump back
// to the original function (after our jump).
memcpy(saveaddr, function, boundary);
AdjustRelativeJumps(saveaddr, boundary, ((unsigned char*)function) - (unsigned char*)saveaddr);
WriteJump(saveaddr + boundary, ((unsigned char*)function) + boundary);
void *ptr = (void*)(((size_t)function/pagesize)*pagesize);
// Give ourself write access to the region.
if(!mprotect(ptr, pagesize, PROT_WRITE|PROT_READ|PROT_EXEC))
{
// Fill the area with nops and write the jump instruction to our
// hook function.
memset(function, 0x90, boundary);
WriteJump(function, hook);
// Restore the original protections.
//VirtualProtect(function, boundary, protection, &protection);
mprotect(ptr, pagesize, PROT_READ|PROT_EXEC);
// Flush the cache so we know that our new code gets executed.
//FlushInstructionCache(GetCurrentProcess(), NULL, NULL);
return saveaddr;
//return 0;
}
return NULL;
//return -1;
}
////使用示例
int (*ori_luaL_loadfilex)(lua_State *L, const char *filename,const char *mode) = NULL;
int my_luaL_loadfilex(lua_State *L, const char *filename,const char *mode){
printf("%s\n",filename);//記錄導入的lua文件,供調試器使用
return ori_luaL_loadfilex(L,filename,mode);
}
static char luaL_loadfilex_buf[4096] __attribute__((aligned(4096)));
int debug_init(){
ori_luaL_loadfilex = HookFunction(luaL_loadfilex,my_luaL_loadfilex,luaL_loadfilex_buf,4096);
if(!ori_luaL_loadfilex){
return -1;
}
return 0;
}