小心環境變量-淺談LD_PRELOAD

//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;
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章