Тази бележка беше написана през 2014 година, но по време на репресиите в Хабр не можа да види бял свят. През времето на забраната забравих за нея, а сега я намерих в черновите. Помислих да я изтрия, но евентуално на някого ще му е от полза.

Общо взето, малко петъчно админско четиво на тема търсене на "включен" LD_PRELOAD.
1. Небольшо отклонение за тези, които не са запознати с заместването на функции
Другите могат веднага да преминат към п.2.
Нека започнем с класическия пример:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
srand (time(NULL));
for(int i=0; i<5; i++){
printf ("%dn", rand()%100);
}
}
Компилираме без никакви флагове:
$ gcc ./ld_rand.c -o ld_rand
И, очаквано, получаваме 5 случайни числа по-малки от 100:
$ ./ld_rand
53
93
48
57
20
Но да предположим, че нямаме изходния код на програмата, а поведението трябва да бъде променено.
Нека създадем своя библиотека със собствен прототип на функция, например:
int rand(){
return 42;
}
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
И сега нашият случаен избор е напълно предсказуем:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand
42
42
42
42
42
Трикът изглежда още по-впечатляващ, ако първо експортираме нашата библиотека чрез
$ export LD_PRELOAD=$PWD/ld_rand.so
или предварително изпълним
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
след което стартираме програмата в обикновен режим. Ние не променихме нито ред в кода на самата програма, но нейното поведение сега зависи от малката функция в нашата библиотека. Още повече, в момента на написването на програмата фалшивият rand още не е съществувал.
Какво накара нашата програма да използва фалшивия rand? Разберем по шагам.
Когато приложението се стартира, се зареждат определени библиотеки, които съдържат функции, необходими на програмата. Можем да ги видим, използвайки ldd:
# ldd ./ld_rand
linux-vdso.so.1 (0x00007ffc8b1f3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fe3da8af000)
/lib64/ld-linux-x86-64.so.2 (0x00007fe3daa7e000)
Този списък може да бъде различен в зависимост от версията на ОС, но там задължително трябва да бъде файлът libc.so. Именно тази библиотека осигурява системни повици и основни функции, като open, malloc, printf и т.н. Нашият rand също влиза в тях. Нека се уверим в това:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand
Да видим, ще се промени ли комплектът библиотеки при използване на LD_PRELOAD
# LD_PRELOAD=$PWD/ld_rand.so ldd ./ld_rand
linux-vdso.so.1 (0x00007ffea52ae000)
/scripts/c/ldpreload/ld_rand.so (0x00007f690d3f9000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f690d230000)
/lib64/ld-linux-x86-64.so.2 (0x00007f690d405000)
Оказва се, че зададената променлива LD_PRELOAD налага да се зареди нашата ld_rand.so дори въпреки че самата програма не я изисква. И, тъй като нашата функция "rand" се зарежда преди rand от libc.so, тя и управлява всичко.
Добре, успяхме да заменим родната функция, но как да направим така, че нейният функционал да остане същият и да добавим определени действия. Модифицираме нашия рандом:
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
typedef int (*orig_rand_f_type)(void);
int rand()
{
/* Выполняем некий код */
printf("Evil injected coden");
orig_rand_f_type orig_rand;
orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand");
return orig_rand();
}
Тук в качеството на нашата "добавка" просто отпечатваме една строка текст, след което създаваме указател на изходната функция rand. За да получим адреса на тази функция, ще ни трябва dlsym — това е функция от библиотеката libdl, която ще намери нашата rand в стека на динамичните библиотеки. След което ще извикаме тази функция и ще върнем нейното значение. Съответно, ще трябва да добавим «-ldl» при сборката:
$ gcc -ldl -shared -fPIC ./o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD/ld_rand_evil.so ./ld_rand
Evil injected code
66
Evil injected code
28
Evil injected code
93
Evil injected code
93
Evil injected code
95
И нашата програма използва «родния» rand, предварително изпълнявайки някои нежелани действия.
2. Мъките на търсенето
Знаейки за потенциалната заплаха, искаме да открием, че preload е била изпълнена. Ясно е, че най-добрият начин за детекция е да я вкараме в ядрото, но ме интересуват именно вариантите за определяне в потребителското пространство.
Следват решения за откриване и техните опровержения.
2.1. Започваме с простото
Както беше споменато по-рано, указването на зарежданата библиотека може да стане чрез променлива LD_PRELOAD или записвайки я в файл /etc/ld.so.preload. Нека създадем два най-прости детектора.
Първи — за проверка на зададената променлива на средата:
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
char* pGetenv = getenv("LD_PRELOAD");
pGetenv != NULL ?
printf("LD_PRELOAD (getenv) [+]n"):
printf("LD_PRELOAD (getenv) [-]n");
}
Втори — за проверка на отварянето на файла:
#include <stdio.h>
#include <fcntl.h>
int main()
{
open("/etc/ld.so.preload", O_RDONLY) != -1 ?
printf("LD_PRELOAD (open) [+]n"):
printf("LD_PRELOAD (open) [-]n");
}
Подгрузваме библиотеките:
$ export LD_PRELOAD=$PWD/ld_rand.so
$ echo "$PWD/ld_rand.so" > /etc/ld.so.preload
$ ./detect_base_getenv
LD_PRELOAD (getenv) [+]
$ ./detect_base_open
LD_PRELOAD (open) [+]
Тук и нататък [+] указва на успешно откритие.
Съответно, [-] означава заобиколка на детекцията.
Колко ефективен е такъв откривател? Първо ще се заемем с променливата на средата:
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <dlfcn.h>
char* (*orig_getenv)(const char *) = NULL;
char* getenv(const char *name)
{
if(!orig_getenv) orig_getenv = dlsym(RTLD_NEXT, "getenv");
if(strcmp(name, "LD_PRELOAD") == 0) return NULL;
return orig_getenv(name);
}
$ gcc -shared -fpic -ldl ./ld_undetect_getenv.c -o ./ld_undetect_getenv.so
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_base_getenv
LD_PRELOAD (getenv) [-]
Аналогично се освобождаваме и от проверката open:
#define _GNU_SOURCE
#include <string.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <errno.h>
int (*orig_open)(const char*, int oflag) = NULL;
int open(const char *path, int oflag, ...)
{
char real_path[256];
if(!orig_open) orig_open = dlsym(RTLD_NEXT, "open");
realpath(path, real_path);
if(strcmp(real_path, "/etc/ld.so.preload") == 0){
errno = ENOENT;
return -1;
}
return orig_open(path, oflag);
}
$ gcc -shared -fpic -ldl ./ld_undetect_open.c -o ./ld_undetect_open.so
$ LD_PRELOAD=./ld_undetect_open.so ./detect_base_open
LD_PRELOAD (open) [-]
Да, тук могат да се използват и други начини за достъп до файла, като open64, stat и т.н., но по същество, за да ги измамим, са необходими същите 5-10 реда код.
2.2. Движа се напред
По-горе използвахме getenv() за получаване на стойността LD_PRELOAD, но има и по «ниско ниво» начин да достигнем до ENV-променливите. Няма да използваме междинни функции, а ще се обърнем към масива **environ, в който се съхранява копие на средата:
#include <stdio.h>
#include <string.h>
extern char **environ;
int main(int argc, char **argv) {
int i;
char env[] = "LD_PRELOAD";
if (environ != NULL)
for (i = 0; environ[i] != NULL; i++)
{
char * pch;
pch = strstr(environ[i],env);
if(pch != NULL)
{
printf("LD_PRELOAD (**environ) [+]n");
return 0;
}
}
printf("LD_PRELOAD (**environ) [-]n");
return 0;
}
Тъй като тук четем данните директно от паметта, такъв извикване не може да бъде прихванат, и нашият undetect_getenv вече не пречи на определянето на инвазия.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ
LD_PRELOAD (**environ) [+]
От гледна точка на това, проблемът изглежда решен? Все пак, всичко тепърва започва.
След стартиране на програмата, стойността на променливата LD_PRELOAD в паметта вече не е необходима на хакерите, тоест може да се счита за излишна и да се премахне преди изпълнението на каквито и да било инструкции. Разбира се, промяната на масив в паметта е поне лош стил на програмиране, но дали това може да спре някого, който не желае да ни е полезен?
За целта ще трябва да създадем собствена фалшива функция init(), в която ще перехванем зададения LD_PRELOAD и да я предадем на нашия компилиращ процесор:
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <dlfcn.h>
#include <stdlib.h>
extern char **environ;
char *evil_env;
int (*orig_execve)(const char *path, char *const argv[], char *const envp[]) = NULL;
// Создаём фейковую версию init
// которая будет вызвана при загрузке программы
// до выполнения каких-либо инструкций
void evil_init()
{
// Сначала сохраним текущее значение LD_PRELOAD
static const char *ldpreload = "LD_PRELOAD";
int len = strlen(getenv(ldpreload));
evil_env = (char*) malloc(len+1);
strcpy(evil_env, getenv(ldpreload));
int i;
char env[] = "LD_PRELOAD";
if (environ != NULL)
for (i = 0; environ[i] != NULL; i++) {
char * pch;
pch = strstr(environ[i],env);
if(pch != NULL) {
// Избавляемся от текущего LD_PRELOAD
unsetenv(env);
break;
}
}
}
int execve(const char *path, char *const argv[], char *const envp[])
{
int i = 0, j = 0, k = -1, ret = 0;
char** new_env;
if(!orig_execve) orig_execve = dlsym(RTLD_NEXT,"execve");
// Проверям не существует ли других установленных LD_PRELOAD
for(i = 0; envp[i]; i++){
if(strstr(envp[i], "LD_PRELOAD")) k = i;
}
// Если LD_PRELOAD не было установлено до нас, то добавим его
if(k == -1){
k = i;
i++;
}
// Создаём новое окружение
new_env = (char**) malloc((i+1)*sizeof(char*));
// Копируем старое окружение, за исключением LD_PRELOAD
for(j = 0; j < i; j++) {
// перезаписываем или создаём LD_PRELOAD
if(j == k) {
new_env[j] = (char*) malloc(256);
strcpy(new_env[j], "LD_PRELOAD=");
strcat(new_env[j], evil_env);
}
else new_env[j] = (char*) envp[j];
}
new_env[i] = NULL;
ret = orig_execve(path, argv, new_env);
free(new_env[k]);
free(new_env);
return ret;
}
Изпълняваме, проверяваме:
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ.c -o ./ld_undetect_environ.so
$ LD_PRELOAD=./ld_undetect_environ.so ./detect_environ
LD_PRELOAD (**environ) [-]
2.3. ./proc/self/
Въпреки това, паметта не е последното място, където можем да открием подмяна LD_PRELOAD, има и /proc/. Нека започнем с очевидното /proc/{PID}/environ.
Всъщност има универсално решение за undetect’а **environ и /proc/self/environ. Проблемът е в "неправилното" поведение unsetenv(env).
правилния вариант
void evil_init()
{
// Първо запазваме текущата стойност на LD_PRELOAD
static const char *ldpreload = "LD_PRELOAD";
int len = strlen(getenv(ldpreload));
evil_env = (char*) malloc(len+1);
strcpy(evil_env, getenv(ldpreload));
int i;
char env[] = "LD_PRELOAD";
if (environ != NULL)
for (i = 0; environ[i] != NULL; i++) {
char * pch;
pch = strstr(environ[i],env);
if(pch != NULL) {
// Избавяме се от текущия LD_PRELOAD
//unsetenv(env);
// Вместо unset просто зануляваме нашата променлива
for(int j = 0; environ[i][j] != ' '; j++) environ[i][j] = ' ';
break;
}
}
}
$ gcc -shared -fpic -ldl -Wl,-init,evil_init ./ld_undetect_environ_2.c -o ./ld_undetect_environ_2.so
$ (LD_PRELOAD=./ld_undetect_environ_2.so cat ./proc/self/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
Но да предположим, че не сме го намерили и /proc/self/environ съдържа "проблемни" данни.
Първо, ще опитаме с предишната ни "маскировка":
$ (LD_PRELOAD=./ld_undetect_environ.so cat ./proc/self/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
LD_PRELOAD=./ld_undetect_environ.so
cat използва за отваряне на файла същия open(), затова решението е аналогично на това, което вече беше направено в п.2.1, но сега създаваме временен файл, в който копираме стойностите на истинската памет без редовете, съдържащи LD_PRELOAD.
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <limits.h>
#include <errno.h>
#define BUFFER_SIZE 256
int (*orig_open)(const char*, int oflag) = NULL;
char *soname = "fakememory_preload.so";
char *sstrstr(char *str, const char *sub)
{
int i, found;
char *ptr;
found = 0;
for(ptr = str; *ptr != ' '; ptr++) {
found = 1;
for(i = 0; found == 1 && sub[i] != ' '; i++){
if(sub[i] != ptr[i]) found = 0;
}
if(found == 1)
break;
}
if(found == 0)
return NULL;
return ptr + i;
}
void fakeMaps(char *original_path, char *fake_path, char *pattern)
{
int fd;
char buffer[BUFFER_SIZE];
int bytes = -1;
int wbytes = -1;
int k = 0;
pid_t pid = getpid();
int fh;
if ((fh=orig_open(fake_path,O_CREAT|O_WRONLY))==-1) {
printf("LD: Cannot open write-file [%s] (%d) (%s)n", fake_path, errno, strerror(errno));
exit (42);
}
if((fd=orig_open(original_path, O_RDONLY))==-1) {
printf("LD: Cannot open read-file.n");
exit(42);
}
do
{
char t = 0;
bytes = read(fd, &t, 1);
buffer[k++] = t;
//printf("%c", t);
if(t == ' ') {
//printf("n");
if(!sstrstr(buffer, "LD_PRELOAD")) {
if((wbytes = write(fh,buffer,k))==-1) {
//printf("write errorn");
}
else {
//printf("writed %dn", wbytes);
}
}
k = 0;
}
}
while(bytes != 0);
close(fd);
close(fh);
}
int open(const char *path, int oflag, ...)
{
char real_path[PATH_MAX], proc_path[PATH_MAX], proc_path_0[PATH_MAX];
pid_t pid = getpid();
if(!orig_open)
orig_open = dlsym(RTLD_NEXT, "open");
realpath(path, real_path);
snprintf(proc_path, PATH_MAX, "/proc/%d/environ", pid);
if(strcmp(real_path, proc_path) == 0) {
snprintf(proc_path, PATH_MAX, "/tmp/%d.fakemaps", pid);
realpath(proc_path_0, proc_path);
fakeMaps(real_path, proc_path, soname);
return orig_open(proc_path, oflag);
}
return orig_open(path, oflag);
}
И този етап е преминат:
$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat ./proc/self/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
Следващото очевидно място — /proc/self/maps. Няма смисъл да се задържаме на него. Решението е абсолютно идентично на предишното: копираме данните от файла, изключвайки редовете между libc.so и ld.so.
2.4. Вариант от Chokepoint
Това решение ми харесва особено много заради своята простота. Сравняваме адресите на функциите, зареждани директно от libc, и "NEXT"-адресите.
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#define LIBC "/lib/x86_64-linux-gnu/libc.so.6"
int main(int argc, char *argv[]) {
void *libc = dlopen(LIBC, RTLD_LAZY); // Open up libc directly
char *syscall_open = "open";
int i;
void *(*libc_func)();
void *(*next_func)();
libc_func = dlsym(libc, syscall_open);
next_func = dlsym(RTLD_NEXT, syscall_open);
if (libc_func != next_func) {
printf("LD_PRELOAD (syscall - %s) [+]n", syscall_open);
printf("Libc address: %pn", libc_func);
printf("Next address: %pn", next_func);
}
else {
printf("LD_PRELOAD (syscall - %s) [-]n", syscall_open);
}
return 0;
}
Зареждаме библиотека с перехват на "open()" и проверяваме:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (системен повик - open) [+]
Адрес на Libc: 0x7fa86893b160
Следващ адрес: 0x7fa868a26135
Опровержението се оказа още по-просто:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h>
extern void * _dl_sym (void *, const char *, void *);
void * dlsym (void * handle, const char * symbol)
{
return _dl_sym (handle, symbol, dlsym);
}
# LD_PRELOAD=./ld_undetect_chokepoint.so ./detect_chokepoint
LD_PRELOAD (syscall - open) [-]
2.5. Системни повикове
Изглежда, че това е всичко, но ще поразвалим още. Ако насочим системния повик директно към ядрото, това ще позволи да заобиколим целия процес на прихващане. Решението по-долу, разбира се, е зависимо от архитектурата (x86_64). Нека опитаме да реализираме за откриването на отваряне на ld.so.preload.
#include <stdio.h>
#include <sys/stat.h>
#include <fcntl.h>
#define BUFFER_SIZE 256
int syscall_open(char *path, long oflag)
{
int fd = -1;
__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"
);
return fd;
}
int main()
{
syscall_open("/etc/ld.so.preload", O_RDONLY) > 0 ?
printf("LD_PRELOAD (open syscall) [+]n"):
printf("LD_PRELOAD (open syscall) [-]n");
}
$ ./detect_syscall
LD_PRELOAD (системен повик - open) [+]
И тази задача има решение. Извадка от man‘а:
ptrace — това е средство, което позволява на родителския процес да наблюдава и контролира протичането на друг процес, да прегледа и променя неговите данни и регистри. Обикновено тази функция се използва за поставяне на точки на прекъсване в програмата за отстраняване на неизправности и за следене на системни повикове.
Родителският процес може да започне проследяване, като първо извика функцията fork(2), а след това полученото дъщерно процес може да извърши PTRACE_TRACEME, след което (обикновено) следва изпълнение exec(3). От друга страна, родителският процес може да започне отстраняването на съществуващ процес с помощта на PTRACE_ATTACH.
При проследяване дъщерният процес спира всеки път при получаване на сигнал, дори ако този сигнал се игнорира. (Изключение е SIGKILL, който работи по нормалния начин.) Родителят ще бъде уведомен за това при извикване на wait(2), след което може да прегледа и променя съдържанието на дъщерния процес преди неговото стартиране. След това родителят позволява на дъщерния да продължи, в някои случаи игнорирайки изпратения му сигнал или изпращайки вместо това друг сигнал).
Така че решението е да се проследява процесът, спирайки го преди всеки системен повик и, ако е необходимо, пренасочвайки потока към функцията режим на работа.
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <limits.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/user.h>
#include <asm/unistd.h>
#if defined(__x86_64__)
#define REG_SYSCALL ORIG_RAX
#define REG_SP rsp
#define REG_IP rip
#endif
long NOHOOK = 0;
long evil_open(const char *path, long oflag, long cflag)
{
char real_path[PATH_MAX], maps_path[PATH_MAX];
long ret;
pid_t pid;
pid = getpid();
realpath(path, real_path);
if(strcmp(real_path, "/etc/ld.so.preload") == 0)
{
errno = ENOENT;
ret = -1;
}
else
{
NOHOOK = 1; // Entering NOHOOK section
ret = open(path, oflag, cflag);
}
// Exiting NOHOOK section
NOHOOK = 0;
return ret;
}
void init()
{
pid_t program;
// Форкаем дочерний процесс
program = fork();
if(program != 0) {
int status;
long syscall_nr;
struct user_regs_struct regs;
// Подключаемся к дочернему процессу
if(ptrace(PTRACE_ATTACH, program) != 0) {
printf("Failed to attach to the program.n");
exit(1);
}
waitpid(program, &status, 0);
// Отслеживаем только 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;
else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP|0x80) {
// Получаем номер системного вызова
syscall_nr = ptrace(PTRACE_PEEKUSER, program, sizeof(long)*REG_SYSCALL);
if(syscall_nr == __NR_open) {
// Читаем слово из памяти дочернего процесса
NOHOOK = ptrace(PTRACE_PEEKDATA, program, (void*)&NOHOOK);
// Перехватываем вызов
if(!NOHOOK) {
// Копируем регистры дочернего процесса
// в переменную regs родительского
ptrace(PTRACE_GETREGS, program, 0, ®s);
// Push return address on the stack
regs.REG_SP -= sizeof(long);
// Копируем слово в память дочернего процесса
ptrace(PTRACE_POKEDATA, program, (void*)regs.REG_SP, regs.REG_IP);
// Устанавливаем RIP по адресу evil_open
regs.REG_IP = (unsigned long) evil_open;
// Записываем состояние регистров процесса
ptrace(PTRACE_SETREGS, program, 0, ®s);
}
}
ptrace(PTRACE_SYSCALL, program, 0, 0);
waitpid(program, &status, 0);
}
}
exit(0);
}
else {
sleep(0);
}
}
Проверяваме:
$ ./detect_syscall
LD_PRELOAD (системен повик - open) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (системен повик - open) [-]
+0-0=5
Огромно благодаря
, чиито статии, изходни кодове и коментари направиха много повече от мен, за да се появи тази бележка тук.
Източник: habr.com
