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() {
$ 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, ®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); } }
Verificamos:
$ ./detect_syscall LD_PRELOAD (open syscall) [+] $ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall LD_PRELOAD (open syscall) [-]
+ 0-0 = 5Muito obrigado
Charles HubainChokepointValdikssPhilippe teuwenderhasscujos artigos, códigos-fonte e comentários fizeram muito mais do que eu para fazer este artigo aparecer aqui.