//weibo: @少仲
0x0 前言
在Windows平臺,各種Hook技術已經被玩的天花亂墜.軟件爲了優先獲得系統的控制權,都使用了各種Hook技術.常見的SSDT Hook/Inline Hook/IRP Hook/IDT Hook/IAT Hook等等,而Linux平臺由於架構不同,Hook實現起來比較困難,但是經過研究,也出現了一些可行的Hook方案.
0x1 LD_PRELOAD原理
和Windows一樣,Linux平臺的程序也分爲靜態鏈接和動態鏈接兩種.靜態鏈接是把所有函數都打包編譯到程序當中,而動態鏈接沒有把函數編譯到可執行文件當中去,而是在程序運行時動態的載入動態庫.程序需要調用的時候,會查找動態鏈接庫的位置,以便從動態庫中查找函數的地址給程序使用.系統提供動態鏈接庫可以使多個程序公用相同的代碼,節省了磁盤空間,更節省了程序員的效率.同時可以不再修正程序的情況下,升級動態庫裏的函數就可以完成升級.然而如果動態庫不是開發人員所寫,而是別人所寫的代碼,那麼程序的流程有可能會被修改.或者說如果我們想要調用別人的動態庫,也可以使用這樣的方法完成Hook.LD_PRELOAD的利用就這樣應運而生,它可以直接影響程序運行時的動態鏈接.只要通過它搶先加載想要加載的動態鏈接庫,程序在運行時就會優先加載這個動態鏈接庫,從而覆蓋正常的動態鏈接庫.那麼在Android系統下也是如此嗎?下面看看代碼分析.
0x2 代碼分析
摘自AOSP\bionic\linker\linker.cpp (節選了關鍵代碼)//初始化環境變量
linker_env_init(args);
//如果執行了setuid()函數,則LD_PRELOAD無效
if (get_AT_SECURE())
{
nullify_closed_stdio();
}
debuggerd_init();
const char* LD_DEBUG = linker_env_get("LD_DEBUG");
if (LD_DEBUG != NULL)
{
gLdDebugVerbosity = atoi(LD_DEBUG);
}
const char* ldpath_env = NULL;
const char* ldpreload_env = NULL;
//如果沒有program_is_setuid爲假,LD_PRELOAD就生效了
if (!get_AT_SECURE())
{
ldpath_env = linker_env_get("LD_LIBRARY_PATH");
//LD_PRELOAD賦值給了ldpreload_env
ldpreload_env = linker_env_get("LD_PRELOAD");
}
//...中間部分代碼省略
parse_LD_LIBRARY_PATH(ldpath_env);
parse_LD_PRELOAD(ldpreload_env);
--------------------------------------------------------------------------
#define LDPRELOAD_BUFSIZE 512
#define LDPRELOAD_MAX 8
static char gLdPreloadsBuffer[LDPRELOAD_BUFSIZE];
static void parse_path(const char* path, const char* delimiters, const char** array, char* buf, size_t buf_size, size_t max_count)
{
if (path == NULL)
{
return;
}
size_t len = strlcpy(buf, path, buf_size);
size_t i = 0;
char* buf_p = buf;
while (i < max_count && (array[i] = strsep(&buf_p, delimiters)))
{
if (*array[i] != '\0')
{
++i;
}
}
if (i > 0 && len >= buf_size && buf[buf_size - 2] != '\0')
{
array[i - 1] = NULL;
}
else
{
array[i] = NULL;
}
}
static void parse_LD_PRELOAD(const char* path)
{
parse_path(path, " :", gLdPreloadNames,gLdPreloadsBuffer, sizeof(gLdPreloadsBuffer), LDPRELOAD_MAX);
}
LD_PRELOAD中所有的庫文件路徑是以冒號":"分隔的,在parse_LD_PRELOAD()函數中它們被分隔並保存到了一個全局的緩衝區gLdPreloadsBuffer中,這個緩衝區被設定爲512個字節,LD_PRELOAD環境變量中指定的庫的個數最多爲8個.看到這裏也已經很清楚了,在Android系統裏面LD_PRELOAD也是確實存在的.
0x3 編碼測試
準備編寫一個校驗的密碼的小程序,通過strcmp來比較字符串,如果正確則打印registion,如果錯誤就返回invalid.之後通過hook strcmp函數,輸入任何字符串都會返回成功.//test.c
#include <stdio.h>
#include <string.h>
int check_func(const char* str)
{
char key[] = "checkkey";
return (!strcmp(key,str));
}
int main(int argc,char* argv[])
{
int ret = -1;
if(argc < 2)
{
printf("please input key.\n");
return ret;
}
if (!check_func(argv[1]))
{
printf("the key is invalid.\n");
return ret;
}
else
{
printf("thanks for registion.\n");
return 0;
}
}
//hook.c
#include <stdio.h>
#include <dlfcn.h>
void* get_real_addr(const char* s1,const char* s2)
{
void* lib = dlopen("libc.so",RTLD_NOW | RTLD_GLOBAL);
void* symbol = NULL;
if (lib == NULL)
{
fprintf(stderr, "dlopen failed...\n");
return NULL;
}
symbol = dlsym(lib,"strcmp");
fprintf(stderr, "the real strcmp addr : %p\n", symbol);
dlclose(lib);
return symbol;
}
int strcmp(const char* str1,const char* str2)
{
fprintf(stderr,"str1 = %s, str2 = %s\n",str1,str2);
get_real_addr(str1,str2);
//永久返回0
return 0;
}
//Android.mk
LOCAL_PATH := $(call my-dir)
# module hook
include $(CLEAR_VARS)
LOCAL_MODULE := hook
LOCAL_SRC_FILES := hook.c
include $(BUILD_SHARED_LIBRARY)
# module test
include $(CLEAR_VARS)
LOCAL_MODULE := test
LOCAL_SRC_FILES := test.c
include $(BUILD_EXECUTABLE)
0x4 測試
測試環境:MI 4
4.4.4
msm8974
Linux version 3.4.0-gbb551a9-01034-g7a5f473 (builder@taishan) (gcc version 4.7 (GCC) )
#1 SMP PREEMPT Tue Aug 12 21:37:08 CST 2014
將test程序和libhook.so同時push到手機當中,先正常執行test程序,如果字符串不是checkkey就會返回invalid,執行export LD_PRELOAD="/data/local/tmp/libhook.so"之後再運行test程序,輸入任何字符串都會返回成功.
0x5 如何避免被Hook
(1).靜態鏈接,把代碼都靜態鏈接入可執行程序.
(2).通過設置執行文件的setgid / setuid標誌.在有SUID權限的執行文件,系統會忽略LD_PRELOAD環境變量.
也就是說,如果你有以root方式運行的程序,最好設置上SUID權限.
(3).在程序的開始檢測LD_PRELOAD
int verify_ld_preload(void)
{
int ret = 0;
char env_name[] = "ld_preload";
char *ld_preload = getenv(env_name);
if (ld_preload != NULL)
{
ret = -1;
}
return ret;
}