一个例子
#prog.c
#include <stdio.h>
int main(void) {
printf("Calling the fopen() function...\n");
FILE *fd = fopen("test.txt","r");
if (!fd) {
printf("fopen() returned NULL\n");
return 1;
}
printf("fopen() succeeded\n");
return 0;
}
编译
gcc prog.c -o prog
现在创建一个myfopen.c
#include <stdio.h>
FILE *fopen(const char *path, const char *mode) {
printf("Always failing fopen\n");
return NULL;
}
编译
gcc -Wall -fPIC -shared -o myfopen.so myfopen.c
运行
$ LD_PRELOAD=./myfopen.so ./prog
Calling the fopen() function...
Always failing fopen
fopen() returned NULL
或者
export LD_PRELOAD=myfopen.so
后者一个绝对地址
export LD_PRELOAD=/root/2119/myfopen.so
还有一种方法就是将so写入到指定的文件中
sudo sh -c 'echo #{path_to_shared_library} > /etc/ld.so.preload'
在环境变量或者/etc/ld.so.preload中查找可用这个方法
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
int main()
{
if(getenv("LD_PRELOAD"))
printf("LD_PRELOAD detected through getenv()\n");
else
printf("Environment is clean\n");
if(open("/etc/ld.so.preload", O_RDONLY) > 0)
printf("/etc/ld.so.preload detected through open()\n");
else
printf("/etc/ld.so.preload is not present\n");
}
% sudo touch /etc/ld.so.preload
% gcc -o detect detect.c
% LD_PRELOAD= ./detect
LD_PRELOAD detected through getenv()
/etc/ld.so.preload detected through open()
%
既然可以在用户层hook函数,那么我们可以hook open和getenv函数
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <limits.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
// We will store the real function pointer in here
int (*o_open)(const char*, int oflag) = NULL;
char* (*o_getenv)(const char *) = NULL;
char* getenv(const char *name)
{
if(!o_getenv)
// Find the real function pointer
o_getenv = dlsym(RTLD_NEXT, "getenv");
if(strcmp(name, "LD_PRELOAD") == 0)
// This environment variable does not exist, I swear
return NULL;
// Everything is ok, call the real getenv
return o_getenv(name);
}
int open(const char *path, int oflag, ...)
{
char real_path[PATH_MAX];
if(!o_open)
// Find the real function pointer
o_open = dlsym(RTLD_NEXT, "open");
// Resolve symbolic links and dot notation fu
realpath(path, real_path);
if(strcmp(real_path, "/etc/ld.so.preload") == 0)
{
// This file does not exist, I swear.
errno = ENOENT;
return -1;
}
// Everything is ok, call the real open
return o_open(path, oflag);
}
// Still many other functions to hook, like fopen, open64, stat, readdir,
// rename, unlink, etc.
这样我们在用之前的方式查找,就不灵了
% gcc -shared -fpic -ldl -o stealth_preload.so stealth_preload.c
% LD_PRELOAD=./stealth_preload.so ./detect
Environment is clean
/etc/ld.so.preload is not present
%
这样ldd查看任何elf,都会看到so的加载
[root@localhost 2119]# ldd ./prog
linux-vdso.so.1 => (0x00007fff1dbee000)
./myfopen.so (0x00007f1b563b3000)
libc.so.6 => /lib64/libc.so.6 (0x00007f1b55fe5000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1b565b5000)
[root@localhost 1119]# ldd /usr/bin/python
linux-vdso.so.1 => (0x00007ffe56f25000)
/root/2119/myfopen.so (0x00007f13a21ca000)
libpython2.7.so.1.0 => /lib64/libpython2.7.so.1.0 (0x00007f13a1dfe000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f13a1be2000)
libdl.so.2 => /lib64/libdl.so.2 (0x00007f13a19de000)
libutil.so.1 => /lib64/libutil.so.1 (0x00007f13a17db000)
libm.so.6 => /lib64/libm.so.6 (0x00007f13a14d9000)
libc.so.6 => /lib64/libc.so.6 (0x00007f13a110b000)
/lib64/ld-linux-x86-64.so.2 (0x00007f13a23cc000)
既然so已经加载到虚拟地址中,那么也可以通过伪文件系统查看
[root@localhost ~]# more /proc/25426/maps | grep myfopen
7f7e0cda2000-7f7e0cda3000 r-xp 00000000 fd:01 132036 /root/2119/myfopen.so
7f7e0cda3000-7f7e0cfa2000 ---p 00001000 fd:01 132036 /root/2119/myfopen.so
7f7e0cfa2000-7f7e0cfa3000 r--p 00000000 fd:01 132036 /root/2119/myfopen.so
7f7e0cfa3000-7f7e0cfa4000 rw-p 00001000 fd:01 132036 /root/2119/myfopen.so
$ LD_PRELOAD=/tmp/noenviron_preload.so cat /proc/self/maps
00400000-0040c000 r-xp 00000000 fe:01 400301 /usr/bin/cat
0060b000-0060c000 r--p 0000b000 fe:01 400301 /usr/bin/cat
0060c000-0060d000 rw-p 0000c000 fe:01 400301 /usr/bin/cat
00ef7000-00f18000 rw-p 00000000 00:00 0 [heap]
7fce2e877000-7fce2e87a000 r-xp 00000000 fe:01 411128 /usr/lib/libdl-2.20.so
7fce2e87a000-7fce2ea79000 ---p 00003000 fe:01 411128 /usr/lib/libdl-2.20.so
7fce2ea79000-7fce2ea7a000 r--p 00002000 fe:01 411128 /usr/lib/libdl-2.20.so
7fce2ea7a000-7fce2ea7b000 rw-p 00003000 fe:01 411128 /usr/lib/libdl-2.20.so
7fce2ea7b000-7fce2ec14000 r-xp 00000000 fe:01 418477 /usr/lib/libc-2.20.so
7fce2ec14000-7fce2ee14000 ---p 00199000 fe:01 418477 /usr/lib/libc-2.20.so
7fce2ee14000-7fce2ee18000 r--p 00199000 fe:01 418477 /usr/lib/libc-2.20.so
7fce2ee18000-7fce2ee1a000 rw-p 0019d000 fe:01 418477 /usr/lib/libc-2.20.so
7fce2ee1a000-7fce2ee1e000 rw-p 00000000 00:00 0
7fce2ee1e000-7fce2ee1f000 r-xp 00000000 00:1e 20903 /tmp/noenviron_preload.so
7fce2ee1f000-7fce2f01f000 ---p 00001000 00:1e 20903 /tmp/noenviron_preload.so
7fce2f01f000-7fce2f020000 rw-p 00001000 00:1e 20903 /tmp/noenviron_preload.so
7fce2f020000-7fce2f042000 r-xp 00000000 fe:01 412638 /usr/lib/ld-2.20.so
7fce2f076000-7fce2f200000 r--p 00000000 fe:01 453088 /usr/lib/locale/locale-archive
7fce2f200000-7fce2f203000 rw-p 00000000 00:00 0
7fce2f21e000-7fce2f241000 rw-p 00000000 00:00 0
7fce2f241000-7fce2f242000 r--p 00021000 fe:01 412638 /usr/lib/ld-2.20.so
7fce2f242000-7fce2f243000 rw-p 00022000 fe:01 412638 /usr/lib/ld-2.20.so
7fce2f243000-7fce2f244000 rw-p 00000000 00:00 0
7fff3d885000-7fff3d8a6000 rw-p 00000000 00:00 0 [stack]
7fff3d8f4000-7fff3d8f6000 r--p 00000000 00:00 0 [vvar]
7fff3d8f6000-7fff3d8f8000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0 [vsyscall]
通过这种方法的检测脚本如下
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#define BUFFER_SIZE 256
// Avoid to use libc strstr
// Return a pointer after the first location of sub in str
char* afterSubstr(char *str, const char *sub)
{
int i, found;
char *ptr;
found = 0;
for(ptr = str; *ptr != '\0'; ptr++)
{
found = 1;
for(i = 0; found == 1 && sub[i] != '\0'; i++)
if(sub[i] != ptr[i])
found = 0;
if(found == 1)
break;
}
if(found == 0)
return NULL;
return ptr + i;
}
// Try to match the following regexp: libname-[0-9]+\.[0-9]+\.so$
// Not using any libc function makes that code awful, I know
int isLib(char *str, const char *lib)
{
int i, found;
static const char *end = ".so\n";
char *ptr;
// Trying to find lib in str
ptr = afterSubstr(str, lib);
if(ptr == NULL)
return 0;
// Should be followed by a '-'
if(*ptr != '-')
return 0;
// Checking the first [0-9]+\.
found = 0;
for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
found = 1;
if(found == 0 || *ptr != '.')
return 0;
// Checking the second [0-9]+
found = 0;
for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
found = 1;
if(found == 0)
return 0;
// Checking if it ends with ".so\n"
for(i = 0; end[i] != '\0'; i++)
if(end[i] != ptr[i])
return 0;
return 1;
}
int main()
{
FILE *memory_map;
char buffer[BUFFER_SIZE];
int after_libc = 0;
memory_map = fopen("/proc/self/maps", "r");
if(memory_map == NULL)
{
printf("/proc/self/maps is unaccessible, probably a LD_PRELOAD attempt\n");
return 1;
}
// Read the memory map line by line
// Try to look for a library loaded in between the libc and ld
while(fgets(buffer, BUFFER_SIZE, memory_map) != NULL)
{
// Look for a libc entry
if(isLib(buffer, "libc"))
after_libc = 1;
else if(after_libc)
{
// Look for a ld entry
if(isLib(buffer, "ld"))
{
// If we got this far then everythin is fine
printf("Memory maps are clean\n");
break;
}
// If it's not an anonymous memory map
else if(afterSubstr(buffer, "00000000 00:00 0") == NULL)
{
// Something has been preloaded by ld.so
printf("LD_PRELOAD detected through memory maps\n");
break;
}
}
}
}
$ gcc -o memory_detect memory_detect.c
$ LD_PRELOAD=./noenviron_preload.so ./memory_detect
LD_PRELOAD detected through memory maps
用这个方法进行查看,也存在hook方式继续进行隐藏的情况
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <limits.h>
#include <errno.h>
FILE* (*o_fopen)(const char*, const char*) = NULL;
char *soname = "fakememory_preload.so";
void fakeMaps(char *original_path, char *fake_path, char *pattern)
{
FILE *original, *fake;
char buffer[PATH_MAX];
original = o_fopen(original_path, "r");
fake = o_fopen(fake_path, "w");
// Copy original in fake but discard the lines containing pattern
while(fgets(buffer, PATH_MAX, original))
if(strstr(buffer, pattern) == NULL)
fputs(buffer, fake);
fclose(fake);
fclose(original);
}
FILE* fopen(const char *path, const char *mode)
{
char real_path[PATH_MAX], maps_path[PATH_MAX];
pid_t pid = getpid();
if(!o_fopen)
// Find the real function pointer
o_fopen = dlsym(RTLD_NEXT, "fopen");
// Resolve symbolic links and dot notation fu
realpath(path, real_path);
snprintf(maps_path, PATH_MAX, "/proc/%d/maps", pid);
if(strcmp(real_path, maps_path) == 0)
{
snprintf(maps_path, PATH_MAX, "/tmp/%d.fakemaps", pid);
// Create a file in tmp containing our fake map
fakeMaps(real_path, maps_path, soname);
return o_fopen(maps_path, mode);
}
// Everything is ok, call the real open
return o_fopen(path, mode);
}
$ gcc -o fakememory_preload.so -shared -fpic -ldl fakememory_preload.c
$ LD_PRELOAD=./fakememory_preload.so ./memory_detect
Memory maps are clean
既然LD_PRELOAD是在用户层进行hook,我们可以直接调用syscall来进行
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#define BUFFER_SIZE 256
int syscall_open(char *path, long oflag)
{
int fd = -1;
#ifdef __i386__
__asm__ (
"mov $5, %%eax;" // Open syscall number
"mov %1, %%ebx;" // Address of our string
"mov %2, %%ecx;" // Open mode
"mov $0, %%edx;" // No create mode
"int $0x80;" // Straight to ring0
"mov %%eax, %0;" // Returned file descriptor
:"=r" (fd)
:"m" (path), "m" (oflag)
:"eax", "ebx", "ecx", "edx"
);
#elif __amd64__
__asm__ (
"mov $2, %%rax;" // Open syscall number
"mov %1, %%rdi;" // Address of our string
"mov %2, %%rsi;" // Open mode
"mov $0, %%rdx;" // No create mode
"syscall;" // Straight to ring0
"mov %%eax, %0;" // Returned file descriptor
:"=r" (fd)
:"m" (path), "m" (oflag)
:"rax", "rdi", "rsi", "rdx"
);
#endif
return fd;
}
size_t syscall_gets(char *buffer, size_t buffer_size, int fd)
{
size_t i;
for(i = 0; i < buffer_size-1; i++)
{
size_t nbytes;
#ifdef __i386__
__asm__ (
"mov $3, %%eax;" // Read syscall number
"mov %1, %%ebx;" // File descriptor
"mov %2, %%ecx;" // Address of our buffer
"mov $1, %%edx;" // Read 1 byte
"int $0x80;" // Straight to ring0
"mov %%eax, %0;" // Returned read byte number
:"=r" (nbytes)
:"m" (fd), "r" (&(buffer[i]))
:"eax", "ebx", "ecx", "edx"
);
#elif __amd64__
__asm__ (
"mov $0, %%rax;" // Read syscall number
"mov %1, %%rdi;" // File descriptor
"mov %2, %%rsi;" // Address of our buffer
"mov $1, %%rdx;" // Read 1 byte
"syscall;" // Straight to ring0
"mov %%rax, %0;" // Returned read byte number
:"=r" (nbytes)
:"m" (fd), "r" (&(buffer[i]))
:"rax", "rdi", "rsi", "rdx"
);
#endif
if(nbytes != 1)
break;
if(buffer[i] == '\n')
{
i++;
break;
}
}
buffer[i] = '\0';
return i;
}
// Avoid to use libc strstr
char* afterSubstr(char *str, const char *sub)
{
int i, found;
char *ptr;
found = 0;
for(ptr = str; *ptr != '\0'; ptr++)
{
found = 1;
for(i = 0; found == 1 && sub[i] != '\0'; i++)
if(sub[i] != ptr[i])
found = 0;
if(found == 1)
break;
}
if(found == 0)
return NULL;
return ptr + i;
}
// Try to match the following regexp: libname-[0-9]+\.[0-9]+\.so$
// Not using any libc function makes that code awful, I know
int isLib(char *str, const char *lib)
{
int i, found;
static const char *end = ".so\n";
char *ptr;
// Trying to find lib in str
ptr = afterSubstr(str, lib);
if(ptr == NULL)
return 0;
// Should be followed by a '-'
if(*ptr != '-')
return 0;
// Checking the first [0-9]+\.
found = 0;
for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
found = 1;
if(found == 0 || *ptr != '.')
return 0;
// Checking the second [0-9]+
found = 0;
for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
found = 1;
if(found == 0)
return 0;
// Checking if it ends with ".so\n"
for(i = 0; end[i] != '\0'; i++)
if(end[i] != ptr[i])
return 0;
return 1;
}
int main()
{
int memory_map;
char buffer[BUFFER_SIZE];
int after_libc = 0;
// If the file was succesfully opened
if(syscall_open("/etc/ld.so.preload", O_RDONLY) > 0)
printf("/etc/ld.so.preload detected through open syscall\n");
else
printf("/etc/ld.so.preload is not present\n");
// Open the memory map through a syscall this time
memory_map = syscall_open("/proc/self/maps", O_RDONLY);
if(memory_map == -1)
{
printf("/proc/self/maps is unaccessible, probably a LD_PRELOAD attempt\n");
return 1;
}
// Read the memory map line by line
// Try to look for a library loaded in between the libc and ld
while(syscall_gets(buffer, BUFFER_SIZE, memory_map) != 0)
{
// Look for a libc entry
if(isLib(buffer, "libc"))
after_libc = 1;
else if(after_libc)
{
// Look for a ld entry
if(isLib(buffer, "ld"))
{
// If we got this far then everythin is fine
printf("Memory maps are clean\n");
break;
}
// If it's not an anonymous memory map
else if(afterSubstr(buffer, "00000000 00:00 0") == NULL)
{
// Something has been preloaded by ld.so
printf("LD_PRELOAD detected through memory maps\n");
break;
}
}
}
}
$ gcc -o syscall_detect syscall_detect.c
$ LD_PRELOAD=./fakememory_preload.so ./syscall_detect
/etc/ld.so.preload detected through open syscall
LD_PRELOAD detected through memory maps
注意:export设置的环境变量在重新开启shell会失效,如需持久化,最好的方法是在.bashrc
中添加
取消ld_preload
unset LD_PRELOAD
普通的检查方式,通过环境变量和文件位置进行检测
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
int main()
{
if(getenv("LD_PRELOAD"))
printf("LD_PRELOAD detected through getenv()\n");
else
printf("Environment is clean\n");
if(open("/etc/ld.so.preload", O_RDONLY) > 0)
printf("/etc/ld.so.preload detected through open()\n");
else
printf("/etc/ld.so.preload is not present\n");
}
结果
% sudo touch /etc/ld.so.preload
% gcc -o detect detect.c
% LD_PRELOAD= ./detect
LD_PRELOAD detected through getenv()
/etc/ld.so.preload detected through open()
然后就是简单的hook open函数和getenv函数
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <limits.h>
#include <errno.h>
#include <sys/stat.h>
#include <fcntl.h>
// We will store the real function pointer in here
int (*o_open)(const char*, int oflag) = NULL;
char* (*o_getenv)(const char *) = NULL;
char* getenv(const char *name)
{
if(!o_getenv)
// Find the real function pointer
o_getenv = dlsym(RTLD_NEXT, "getenv");
if(strcmp(name, "LD_PRELOAD") == 0)
// This environment variable does not exist, I swear
return NULL;
// Everything is ok, call the real getenv
return o_getenv(name);
}
int open(const char *path, int oflag, ...)
{
char real_path[PATH_MAX];
if(!o_open)
// Find the real function pointer
o_open = dlsym(RTLD_NEXT, "open");
// Resolve symbolic links and dot notation fu
realpath(path, real_path);
if(strcmp(real_path, "/etc/ld.so.preload") == 0)
{
// This file does not exist, I swear.
errno = ENOENT;
return -1;
}
// Everything is ok, call the real open
return o_open(path, oflag);
}
// Still many other functions to hook, like fopen, open64, stat, readdir,
// rename, unlink, etc.
此时已无法查找到
% gcc -shared -fpic -ldl -o stealth_preload.so stealth_preload.c
% LD_PRELOAD=./stealth_preload.so ./detect
Environment is clean
/etc/ld.so.preload is not present
既然preload可以hook任何函数,那么我们就找一个不使用任何函数调用的方法来检查环境变量:通过读取内存来检查,其中Linux C中environ 变量是一个char** 类型,存储着系统的环境变量
#include <stdio.h>
// This will resolve at linking time
extern char **environ;
int main()
{
long i, j;
char env[] = "LD_PRELOAD";
// Go through all environment strings, the end of the array
// is marked by a null pointer.
for(i = 0; environ[i]; i++)
{
// Check is the string begins by LD_PRELOAD
// I said NO CALL not even to strstr
for(j = 0; env[j] != '\0' && environ[i][j] != '\0'; j++)
if(env[j] != environ[i][j])
break;
// If the complete chain was found
if(env[j] == '\0')
{
printf("LD_PRELOAD detected through environ\n");
return;
}
}
printf("Environment is clean\n");
}
运行结果
% gcc -o nocall_detect nocall_detect.c
% LD_PRELOAD=./stealth_preload.so ./nocall_detect
LD_PRELOAD detected through environ
这个也有一个绕过方法,就是在hook函数之后,再把内存中的字段删除掉,我们可以在init()中进行相关操作,而如果这个进程产生了子进程,那么他将不再继承LD_PRELOAD环境变量(因为已经删掉了),所以我们要在execve之前再加上这个环境变量
下面这个方法就是hook了init函数和execve函数,添加删除内存特定内容的功能
#define _GNU_SOURCE
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
extern char **environ;
int (*o_execve)(const char *path, char *const argv[], char *const envp[]) = NULL;
char *sopath;
// Called as soon as the library is loaded, the program has not executed any
// instructions yet.
void init()
{
int i, j;
static const char *ldpreload = "LD_PRELOAD";
// First save the value of LD_PRELOAD
int len = strlen(getenv(ldpreload));
sopath = (char*) malloc(len+1);
strcpy(sopath, getenv(ldpreload));
// unsetenv() has a weird behavior, this is a custom implementation
// Look for LD_PRELOAD variable
for(i = 0; environ[i]; i++)
{
int found = 1;
for(j = 0; ldpreload[j] != '\0' && environ[i][j] != '\0'; j++)
if(ldpreload[j] != environ[i][j])
{
found = 0;
break;
}
if(found)
{
// Set to zero the variable
for(j = 0; environ[i][j] != '\0'; j++)
environ[i][j] = '\0';
break;
// Free that memory
free((void*)environ[i]);
}
}
// Remove the string pointer from environ
for(j = i; environ[j]; j++)
environ[j] = environ[j+1];
}
int execve(const char *path, char *const argv[], char *const envp[])
{
int i, j, ldi = -1, r;
char** new_env;
if(!o_execve)
o_execve = dlsym(RTLD_NEXT,"execve");
// Look if the provided environment already contains LD_PRELOAD
for(i = 0; envp[i]; i++)
{
if(strstr(envp[i], "LD_PRELOAD"))
ldi = i;
}
// If it doesn't, add it at the end
if(ldi == -1)
{
ldi = i;
i++;
}
// Create a new environment
new_env = (char**) malloc((i+1)*sizeof(char*));
// Copy the old environment in the new one, except for LD_PRELOAD
for(j = 0; j < i; j++)
{
// Overwrite or create the LD_PRELOAD variable
if(j == ldi)
{
new_env[j] = (char*) malloc(256);
strcpy(new_env[j], "LD_PRELOAD=");
strcat(new_env[j], sopath);
}
else
new_env[j] = (char*) envp[j];
}
// That string array is NULL terminated
new_env[i] = NULL;
r = o_execve(path, argv, new_env);
free(new_env[ldi]);
free(new_env);
return r;
}
// You also have to patch all the other variants of exec
结果显示,通过内存查找的方法已经无法找到
$ gcc -o noenviron_preload.so -shared -fpic -ldl -Wl,-init,init noenviron_preload.c
$ LD_PRELOAD=./noenviron_preload.so ./nocall_detect
Environment is clean
基于内存的排查
除了查看ld.so.preload文件和环境变量,还有其他的方法也可以检测,比如读取proc中的map文件
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#define BUFFER_SIZE 256
// Avoid to use libc strstr
// Return a pointer after the first location of sub in str
char* afterSubstr(char *str, const char *sub)
{
int i, found;
char *ptr;
found = 0;
for(ptr = str; *ptr != '\0'; ptr++)
{
found = 1;
for(i = 0; found == 1 && sub[i] != '\0'; i++)
if(sub[i] != ptr[i])
found = 0;
if(found == 1)
break;
}
if(found == 0)
return NULL;
return ptr + i;
}
// Try to match the following regexp: libname-[0-9]+\.[0-9]+\.so$
// Not using any libc function makes that code awful, I know
int isLib(char *str, const char *lib)
{
int i, found;
static const char *end = ".so\n";
char *ptr;
// Trying to find lib in str
ptr = afterSubstr(str, lib);
if(ptr == NULL)
return 0;
// Should be followed by a '-'
if(*ptr != '-')
return 0;
// Checking the first [0-9]+\.
found = 0;
for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
found = 1;
if(found == 0 || *ptr != '.')
return 0;
// Checking the second [0-9]+
found = 0;
for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
found = 1;
if(found == 0)
return 0;
// Checking if it ends with ".so\n"
for(i = 0; end[i] != '\0'; i++)
if(end[i] != ptr[i])
return 0;
return 1;
}
int main()
{
FILE *memory_map;
char buffer[BUFFER_SIZE];
int after_libc = 0;
memory_map = fopen("/proc/self/maps", "r");
if(memory_map == NULL)
{
printf("/proc/self/maps is unaccessible, probably a LD_PRELOAD attempt\n");
return 1;
}
// Read the memory map line by line
// Try to look for a library loaded in between the libc and ld
while(fgets(buffer, BUFFER_SIZE, memory_map) != NULL)
{
// Look for a libc entry
if(isLib(buffer, "libc"))
after_libc = 1;
else if(after_libc)
{
// Look for a ld entry
if(isLib(buffer, "ld"))
{
// If we got this far then everythin is fine
printf("Memory maps are clean\n");
break;
}
// If it's not an anonymous memory map
else if(afterSubstr(buffer, "00000000 00:00 0") == NULL)
{
// Something has been preloaded by ld.so
printf("LD_PRELOAD detected through memory maps\n");
break;
}
}
}
}
结果可以看出,通过查看maps检测到了ld_preload
$ gcc -o memory_detect memory_detect.c
$ LD_PRELOAD=./noenviron_preload.so ./memory_detect
LD_PRELOAD detected through memory maps
同样是由于可以hook任意函数,我们可以hook open(), open64(), openat64(), freopen() 等函数,伪造一个虚假的maps
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <limits.h>
#include <errno.h>
FILE* (*o_fopen)(const char*, const char*) = NULL;
char *soname = "fakememory_preload.so";
void fakeMaps(char *original_path, char *fake_path, char *pattern)
{
FILE *original, *fake;
char buffer[PATH_MAX];
original = o_fopen(original_path, "r");
fake = o_fopen(fake_path, "w");
// Copy original in fake but discard the lines containing pattern
while(fgets(buffer, PATH_MAX, original))
if(strstr(buffer, pattern) == NULL)
fputs(buffer, fake);
fclose(fake);
fclose(original);
}
FILE* fopen(const char *path, const char *mode)
{
char real_path[PATH_MAX], maps_path[PATH_MAX];
pid_t pid = getpid();
if(!o_fopen)
// Find the real function pointer
o_fopen = dlsym(RTLD_NEXT, "fopen");
// Resolve symbolic links and dot notation fu
realpath(path, real_path);
snprintf(maps_path, PATH_MAX, "/proc/%d/maps", pid);
if(strcmp(real_path, maps_path) == 0)
{
snprintf(maps_path, PATH_MAX, "/tmp/%d.fakemaps", pid);
// Create a file in tmp containing our fake map
fakeMaps(real_path, maps_path, soname);
return o_fopen(maps_path, mode);
}
// Everything is ok, call the real open
return o_fopen(path, mode);
}
结果如下,我们hook了fopen等函数,制造了虚假的maps
$ gcc -o fakememory_preload.so -shared -fpic -ldl fakememory_preload.c
$ LD_PRELOAD=./fakememory_preload.so ./memory_detect
Memory maps are clean
现在我们直接调用syscall来绕过,通过直接调用syscall来实现读取ld.so.preload和读取maps
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#define BUFFER_SIZE 256
int syscall_open(char *path, long oflag)
{
int fd = -1;
#ifdef __i386__
__asm__ (
"mov $5, %%eax;" // Open syscall number
"mov %1, %%ebx;" // Address of our string
"mov %2, %%ecx;" // Open mode
"mov $0, %%edx;" // No create mode
"int $0x80;" // Straight to ring0
"mov %%eax, %0;" // Returned file descriptor
:"=r" (fd)
:"m" (path), "m" (oflag)
:"eax", "ebx", "ecx", "edx"
);
#elif __amd64__
__asm__ (
"mov $2, %%rax;" // Open syscall number
"mov %1, %%rdi;" // Address of our string
"mov %2, %%rsi;" // Open mode
"mov $0, %%rdx;" // No create mode
"syscall;" // Straight to ring0
"mov %%eax, %0;" // Returned file descriptor
:"=r" (fd)
:"m" (path), "m" (oflag)
:"rax", "rdi", "rsi", "rdx"
);
#endif
return fd;
}
size_t syscall_gets(char *buffer, size_t buffer_size, int fd)
{
size_t i;
for(i = 0; i < buffer_size-1; i++)
{
size_t nbytes;
#ifdef __i386__
__asm__ (
"mov $3, %%eax;" // Read syscall number
"mov %1, %%ebx;" // File descriptor
"mov %2, %%ecx;" // Address of our buffer
"mov $1, %%edx;" // Read 1 byte
"int $0x80;" // Straight to ring0
"mov %%eax, %0;" // Returned read byte number
:"=r" (nbytes)
:"m" (fd), "r" (&(buffer[i]))
:"eax", "ebx", "ecx", "edx"
);
#elif __amd64__
__asm__ (
"mov $0, %%rax;" // Read syscall number
"mov %1, %%rdi;" // File descriptor
"mov %2, %%rsi;" // Address of our buffer
"mov $1, %%rdx;" // Read 1 byte
"syscall;" // Straight to ring0
"mov %%rax, %0;" // Returned read byte number
:"=r" (nbytes)
:"m" (fd), "r" (&(buffer[i]))
:"rax", "rdi", "rsi", "rdx"
);
#endif
if(nbytes != 1)
break;
if(buffer[i] == '\n')
{
i++;
break;
}
}
buffer[i] = '\0';
return i;
}
// Avoid to use libc strstr
char* afterSubstr(char *str, const char *sub)
{
int i, found;
char *ptr;
found = 0;
for(ptr = str; *ptr != '\0'; ptr++)
{
found = 1;
for(i = 0; found == 1 && sub[i] != '\0'; i++)
if(sub[i] != ptr[i])
found = 0;
if(found == 1)
break;
}
if(found == 0)
return NULL;
return ptr + i;
}
// Try to match the following regexp: libname-[0-9]+\.[0-9]+\.so$
// Not using any libc function makes that code awful, I know
int isLib(char *str, const char *lib)
{
int i, found;
static const char *end = ".so\n";
char *ptr;
// Trying to find lib in str
ptr = afterSubstr(str, lib);
if(ptr == NULL)
return 0;
// Should be followed by a '-'
if(*ptr != '-')
return 0;
// Checking the first [0-9]+\.
found = 0;
for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
found = 1;
if(found == 0 || *ptr != '.')
return 0;
// Checking the second [0-9]+
found = 0;
for(ptr += 1; *ptr >= '0' && *ptr <= '9'; ptr++)
found = 1;
if(found == 0)
return 0;
// Checking if it ends with ".so\n"
for(i = 0; end[i] != '\0'; i++)
if(end[i] != ptr[i])
return 0;
return 1;
}
int main()
{
int memory_map;
char buffer[BUFFER_SIZE];
int after_libc = 0;
// If the file was succesfully opened
if(syscall_open("/etc/ld.so.preload", O_RDONLY) > 0)
printf("/etc/ld.so.preload detected through open syscall\n");
else
printf("/etc/ld.so.preload is not present\n");
// Open the memory map through a syscall this time
memory_map = syscall_open("/proc/self/maps", O_RDONLY);
if(memory_map == -1)
{
printf("/proc/self/maps is unaccessible, probably a LD_PRELOAD attempt\n");
return 1;
}
// Read the memory map line by line
// Try to look for a library loaded in between the libc and ld
while(syscall_gets(buffer, BUFFER_SIZE, memory_map) != 0)
{
// Look for a libc entry
if(isLib(buffer, "libc"))
after_libc = 1;
else if(after_libc)
{
// Look for a ld entry
if(isLib(buffer, "ld"))
{
// If we got this far then everythin is fine
printf("Memory maps are clean\n");
break;
}
// If it's not an anonymous memory map
else if(afterSubstr(buffer, "00000000 00:00 0") == NULL)
{
// Something has been preloaded by ld.so
printf("LD_PRELOAD detected through memory maps\n");
break;
}
}
}
}
结果如下:我们使用syscall绕过了关于hook的隐藏姿势
$ gcc -o syscall_detect syscall_detect.c
$ LD_PRELOAD=./fakememory_preload.so ./syscall_detect
/etc/ld.so.preload detected through open syscall
LD_PRELOAD detected through memory maps
然而这也并不是万能的方法,现在有两种方式可以来改变syscall的行为
- SECCOMP可以限制syscall的调用,这个功能本是用来做sandbox的
- Ptrace用来debug进程,并且允许在每次调用syscall之前或者之后停止进程的运行
我们就用Ptrace这个特性,如果是open syscall,我们就将执行流程交给hook函数,而hook函数的内容就是我们用汇编语言模拟的函数调用,代码如下
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <errno.h>
#include <limits.h>
#include <sys/prctl.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <asm/unistd.h>
// Some useful defines to make the code architecture independent
#if defined(__i386__)
#define REG_SYSCALL ORIG_EAX
#define REG_SP esp
#define REG_IP eip
#elif defined(__x86_64__)
#define REG_SYSCALL ORIG_RAX
#define REG_SP rsp
#define REG_IP rip
#endif
long NOHOOK = 0;
char *soname = "nosyscall_preload.so";
void fakeMaps(char *original_path, char *fake_path, char *pattern)
{
FILE *original, *fake;
char buffer[PATH_MAX];
original = fopen(original_path, "r");
fake = fopen(fake_path, "w");
// Copy original in fake but discard the lines containing pattern
while(fgets(buffer, PATH_MAX, original))
if(strstr(buffer, pattern) == NULL)
fputs(buffer, fake);
fclose(fake);
fclose(original);
}
long open_gate(const char *path, long oflag, long cflag)
{
char real_path[PATH_MAX], maps_path[PATH_MAX];
long ret;
pid_t pid;
pid = getpid();
// Resolve symbolic links and dot notation fu
realpath(path, real_path);
snprintf(maps_path, PATH_MAX, "/proc/%d/maps", pid);
if(strcmp(real_path, "/etc/ld.so.preload") == 0)
{
// This file does not exist, I swear.
errno = ENOENT;
ret = -1;
}
else if(strcmp(real_path, maps_path) == 0)
{
snprintf(maps_path, PATH_MAX, "/tmp/%d.fakemaps", pid);
// Create a file in tmp containing our fake map
NOHOOK = 1; // Entering NOHOOK section
fakeMaps(real_path, maps_path, soname);
ret = open(maps_path, oflag);
}
else
{
// Everything is ok, call the real open
NOHOOK = 1; // Entering NOHOOK section
ret = open(path, oflag, cflag);
}
// Exiting NOHOOK section
NOHOOK = 0;
#ifdef __i386__
// Tricky stack cleaning and return in the x86 case
// We need to clean the 3 arguments (12 bytes) that were pushed on the stack
__asm__ __volatile__ ("mov %0, %%eax;" // set the return value
"mov (%%ebp), %%ecx;" // move saved ebp 12 bytes up
"mov %%ecx, 0xc(%%ebp);"
"mov 0x4(%%ebp), %%ecx;" // move saved eip 12 bytes up
"mov %%ecx, 0x10(%%ebp);"
"add $0xc, %%ebp;" //move stack base 12 bytes up
"leave;" // normal leave and return
"ret;"
:
:"m" (ret)
:
);
#endif
return ret;
}
void init()
{
pid_t program;
// Forking a child process
program = fork();
if(program != 0)
{
// Parent process which will debug the program in the child process
int status;
long syscall_nr;
struct user_regs_struct regs;
// We attach to the child
if(ptrace(PTRACE_ATTACH, program) != 0)
{
printf("Failed to attach to the program.\n");
exit(1);
}
waitpid(program, &status, 0);
// We are only interested in tracing SYSCALLs
ptrace(PTRACE_SETOPTIONS, program, 0, PTRACE_O_TRACESYSGOOD);
while(1)
{
ptrace(PTRACE_SYSCALL, program, 0, 0);
waitpid(program, &status, 0);
if(WIFEXITED(status) || WIFSIGNALED(status))
break; // Stop tracing if the parent process terminates
else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP|0x80)
{
// Getting the syscall number
syscall_nr = ptrace(PTRACE_PEEKUSER, program, sizeof(long)*REG_SYSCALL);
// Is it an open syscall ?
if(syscall_nr == __NR_open)
{
// Getting the value of NOHOOK in the child process
NOHOOK = ptrace(PTRACE_PEEKDATA, program, (void*)&NOHOOK);
// Only hook the syscall if it's not in a NOHOOK section
if(!NOHOOK)
{
// Now we are going to simulate a call
// First get the register state
ptrace(PTRACE_GETREGS, program, 0, ®s);
// Under x86 we need to push the arguments on the stack
#ifdef __i386__
regs.REG_SP -= sizeof(long);
ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.edx);
regs.REG_SP -= sizeof(long);
ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.ecx);
regs.REG_SP -= sizeof(long);
ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.ebx);
#endif
// Push return address on the stack
regs.REG_SP -= sizeof(long);
ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.REG_IP);
// Set RIP to open_gate address
regs.REG_IP = (unsigned long) open_gate;
// Finnally set the register
ptrace(PTRACE_SETREGS, program, 0, ®s);
}
}
//We always get a second signal after the syscall
ptrace(PTRACE_SYSCALL, program, 0, 0);
waitpid(program, &status, 0);
}
}
exit(0);
}
else
{
// Child process
// Sleep a bit to give the parent process enough time to attach
sleep(0);
}
}
结果如下
gcc -o nosyscall_preload.so -shared -fpic -Wl,-init,init nosyscall_preload.c
LD_PRELOAD=./nosyscall_preload.so ./syscall_detect
/etc/ld.so.preload is not present
Memory maps are clean
虽然我们对syscall进行了hook,但是对于静态编译的文件,由于不会加载动态链接库,所以还是无法对静态文件中的syscall进行hook
参考: https://haxelion.eu/article/LD_NOT_PRELOADED_FOR_REAL/