Esta nota foi escrita em 2014, mas eu acabei caindo sob repressões no Habr e ela não viu a luz do dia. Durante o tempo de banimento, eu esqueci sobre ela, mas agora a encontrei nos rascunhos. Pensei em apagar, mas talvez possa ser útil para alguém.

Em suma, uma pequena leitura administrativa de sexta-feira sobre como encontrar o 'ativado'. LD_PRELOAD.
1. Uma pequena introdução para aqueles que não estão familiarizados com a substituição de funções.
Os outros podem ir diretamente para p.2.
Começamos com um 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 ("%dn", rand()%100);
}
}
Compilamos sem nenhuma flag:
$ 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 vamos imaginar que não temos o código fonte do programa, mas precisamos mudar seu comportamento.
Criamos 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 primeiro exportarmos nossa biblioteca através
$ export LD_PRELOAD=$PWD/ld_rand.so
ou executarmos previamente
# echo "$PWD/ld_rand.so" > /etc/ld.so.preload
e em seguida executarmos o programa normalmente. Não alteramos uma linha do código do próprio programa, mas seu comportamento agora depende de uma pequena função em nossa biblioteca. Além disso, no momento da escrita, o falso rand nem existia.
O que fez nosso programa usar o falso rand? Разберем по шагам.
Quando o aplicativo é iniciado, são carregadas certas bibliotecas que contêm funções necessárias para o programa. Podemos visualizá-las usando 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 SO, mas deve haver obrigatoriamente o arquivo libc.so. É essa biblioteca que fornece chamadas de sistema e funções básicas, como open, malloc, printf etc. Nossa rand também está entre elas. Vamos verificar isso:
# nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " rand$"
000000000003aef0 T rand
Vejamos se o conjunto de bibliotecas muda 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)
Parece que a variável instalada LD_PRELOAD faz com que nossa ld_rand.so seja carregada mesmo que o programa não a exige. E, como nossa função 'rand' é carregada antes de rand de libc.so, ela e que domina a situação.
Ok, conseguimos substituir a função nativa, mas como fazer para que sua funcionalidade seja mantida e algumas ações sejam adicionadas. 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 coden");
orig_rand_f_type orig_rand;
orig_rand = (orig_rand_f_type)dlsym(RTLD_NEXT,"rand");
return orig_rand();
}
Aqui, como nosso 'acréscimo', apenas exibimos uma linha de texto, após a qual criamos um ponteiro para a função original. randPara obter o endereço dessa função, precisaremos dlsym — é uma função da biblioteca libdl, que encontrará nosso rand no stack de bibliotecas dinâmicas. Em seguida, chamaremos essa função e retornaremos seu valor. Assim, precisaremos adicionar "-ldl" na compilação:
$ gcc -ldl -shared -fPIC ./o_rand_evil.c -o ld_rand_evil.so
$ LD_PRELOAD=$PWD/ld_rand_evil.so ./ld_rand
Código injetado malicioso
66
Código injetado malicioso
28
Código injetado malicioso
93
Código injetado malicioso
93
Código injetado malicioso
95
E nosso programa usa "nativo" rand, realizando previamente algumas ações inadequadas.
2. As dificuldades da busca
Sabendo da ameaça potencial, queremos detectar o que preload foi executado. É claro que a melhor maneira de detecção é integrá-lo no kernel, mas eu estava interessado em opções de detecção no espaço do usuário.
A seguir, por pares, apresentarei soluções para detecção e suas refutações.
2.1. Começaremos com algo simples
Como mencionado anteriormente, a biblioteca carregada pode ser especificada usando a variável LD_PRELOAD ou especificando-a no arquivo /etc/ld.so.preload. Vamos criar dois detectores simples.
O primeiro — para 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 — para verificar a abertura de 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");
}
Carregaremos as 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) [+]
Aqui e adiante [+] indica uma detecção bem-sucedida.
Assim, [-] indica a evasão da detecção.
Quão eficaz é tal detector? Primeiro, lidemos com a 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, eliminamos a verificação 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) [-]
Sim, aqui podem ser utilizadas outras maneiras de acessar o arquivo, como, open64, stat etc., mas, essencialmente, para enganá-los são necessárias as mesmas 5-10 linhas de código.
2.2. Avançando
Acima, usamos getenv() para obter o valor LD_PRELOAD, mas há um método mais "baixo nível" de chegar até as ENV-variáveis. Não usaremos funções intermediárias, mas acessaremos o array **environ, onde está armazenada uma cópia do ambiente:
#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 já não impede a determinação da invasão.
$ LD_PRELOAD=./ld_undetect_getenv.so ./detect_environ
LD_PRELOAD (**environ) [+]
Parece que assim o problema está resolvido? Isso ainda está apenas começando.
Depois que o programa é iniciado, o valor da variável LD_PRELOAD na memória já não é necessário para os hackers, ou seja, pode-se considerá-lo e excluí-lo antes da execução de quaisquer instruções. Claro, modificar um array na memória é, no mínimo, uma má prática de programação, mas será que isso pode deter alguém que já não deseja nosso bem?
Para isso, precisamos criar nossa função falsa init(), onde interceptaremos o LD_PRELOAD e a passaremos para o nosso compilador:
#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;
}
Executamos, verificamos:
$ 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 lugar onde se pode detectar uma substituição LD_PRELOAD, ainda há /proc/. Vamos começar pelo óbvio /proc/{PID}/environ.
Na verdade, existe uma solução universal para o undetect. **environ e /proc/self/environO problema está no comportamento "errado" unsetenv(env).
a opção correta
void evil_init()
{
\/\/ Primeiro, salvamos o valor atual de 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) {
\/\/ Removemos o atual LD_PRELOAD
\/\/unsetenv(env);
\/\/ Em vez de unset, apenas zeramos nossa variável
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
$
Mas vamos imaginar que não a encontramos e /proc/self/environ contém dados "problemáticos".
Primeiro, tentaremos com nossa "mascarada" anterior:
$ (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 usa para abrir o arquivo o mesmo open(), 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 valores da memória real sem as 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 != ' '; 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);
}
E esta etapa foi concluída:
$ (LD_PRELOAD=.\/ld_undetect_proc_environ.so cat \/proc\/self\/environ; echo) | tr " 00" "n" | grep -F LD_PRELOAD
$
O próximo lugar óbvio é /proc/self/maps. Não vale a pena se demorar. A solução é absolutamente idêntica à anterior: copiamos os dados do arquivo, excluindo as linhas entre libc.so e ld.so.
2.4. Opção do Chokepoint
Esta solução me agradou especialmente pela sua simplicidade. Comparamos os endereços das funções carregadas diretamente de libc, e os endereços "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;
}
Carregamos a biblioteca com a interceptação "open()" e verificamos:
$ export LD_PRELOAD=$PWD/ld_undetect_open.so
$ ./detect_chokepoint
LD_PRELOAD (syscall - open) [+]
Endereço da Libc: 0x7fa86893b160
Próximo endereço: 0x7fa868a26135
A negação foi 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. Chamadas do sistema
Pode parecer que isso é tudo, mas vamos explorar um pouco mais. Se direcionarmos a chamada do sistema diretamente ao núcleo, isso permitirá contornar todo o processo de interceptação. A solução abaixo, claro, depende da arquitetura (x86_64). Vamos tentar implementar para detectar a abertura 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 (chamada do syscall open) [+]
E esta tarefa tem solução. Extraído de man‘a:
ptrace — é um mecanismo que permite que um processo pai observe e controle a execução de outro processo, examinando e alterando seus dados e registradores. Geralmente, essa função é usada para criar pontos de interrupção durante a depuração e rastrear chamadas do sistema.
O processo pai pode iniciar o rastreamento chamando a função fork(2), em seguida, o processo filho gerado pode executar PTRACE_TRACEME, após o que (geralmente) segue a execução de exec(3). Por outro lado, o processo pai pode iniciar a depuração de um processo existente usando PTRACE_ATTACH.
Durante o rastreamento, o processo filho é interrompido toda vez que recebe um sinal, mesmo que esse sinal seja ignorado. (A exceção é o SIGKILL, que funciona normalmente.) O processo pai será notificado sobre isso ao chamar wait(2), após o que ele pode examinar e alterar o conteúdo do processo filho antes de ele ser iniciado. Depois disso, o processo pai permite que o filho continue, em alguns casos ignorando o sinal enviado a ele ou enviando outro sinal no lugar).
Portanto, a solução é rastrear o processo, interrompendo-o antes de cada chamada do sistema e, se necessário, redirecionando o fluxo para uma função de armadilha.
#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 (chamada do syscall open) [+]
$ LD_PRELOAD=./ld_undetect_syscall.so ./detect_syscall
LD_PRELOAD (chamada do syscall open) [-]
+0-0=5
Muito obrigado
, cujos artigos, códigos e comentários contribuíram muito mais do que eu para que esta nota fosse publicada aqui.
Fonte: habr.com
