Sobrescrevendo Funções de Bibliotecas com LD_PRELOAD em Assembly NASM
Uma das características mais notáveis da linkagem dinâmica em Linux é a capacidade de sobrescrever (override) funções de uma biblioteca compartilhada com a sua própria implementação. O loader dinâmico suporta uma variável de ambiente especial, a LD_PRELOAD, que permite pré-carregar uma biblioteca antes de todas as outras.
Quando a LD_PRELOAD é definida com o caminho para uma biblioteca .so, o loader carrega essa biblioteca primeiro. Se o programa principal ou qualquer outra biblioteca tentar usar uma função que já foi definida na biblioteca pré-carregada, a versão da LD_PRELOAD terá prioridade e será executada em vez da original.
Essa técnica nos permite modificar o comportamento de um programa mesmo que ele já esteja compilado e não tenhamos acesso ao seu código fonte. Ela é frequentemente usada para fins de depuração, monitoramento (profiling) ou para fornecer versões mais otimizadas de funções padrão do sistema, como malloc.
Exemplo Prático: Sobrescrevendo a printf
Vamos demonstrar essa técnica sobrescrevendo a função printf da biblioteca C.
Primeiro, temos um programa simples, app.asm, que usa a printf padrão para imprimir uma mensagem:
global _start
extern printf
section .data
message: db "Hello World", 10, 0
section .text
_start:
lea rdi, [rel message]
xor rax, rax
call printf
mov rax, 60
syscallVamos compilar e executar este programa normalmente:
$ nasm -felf64 app.asm -o app.o $ ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 -lc app.o -o app $ ./app Hello World
Como esperado, o programa imprime a mensagem "Hello World".
Agora, vamos criar nossa própria versão da printf em um arquivo chamado print.asm. Esta nova função irá ignorar quaisquer parâmetros e simplesmente imprimir uma mensagem fixa.
global printf:function
section .data
message: db "Função printf sobrescrita!", 10, 0
len equ $-message
section .text
printf:
mov rax, 1 ; syscall write
mov rdi, 1 ; stdout
lea rsi, [rel message] ; Nossa mensagem fixa
mov rdx, len ; Tamanho da nossa mensagem
syscall
retÉ crucial que nossa função tenha exatamente o mesmo nome do símbolo que queremos sobrescrever: printf.
Vamos compilar este arquivo como uma biblioteca compartilhada:
$ nasm -felf64 print.asm -o print.o $ ld -shared print.o -o libprint.so
Agora vem a parte principal. Usamos a variável LD_PRELOAD para instruir o loader a pré-carregar nossa libprint.so e, em seguida, executamos o programa app original sem recompilá-lo:
$ export LD_PRELOAD=./libprint.so $ ./app Função printf sobrescrita!
O resultado mudou! Embora o programa app não tenha sido modificado, a chamada call printf foi interceptada. O loader dinâmico, ao ver que o símbolo printf já estava disponível na nossa biblioteca pré-carregada, usou a nossa versão em vez da versão da libc.
Para desativar o pré-carregamento e voltar ao comportamento normal, basta limpar a variável de ambiente:
$ unset LD_PRELOAD $ ./app Hello World
Resumo do Processo
O processo completo para testar a sobrescrita de função é:
# 1. Compilar o programa original $ nasm -felf64 app.asm -o app.o $ ld --dynamic-linker=/lib64/ld-linux-x86-64.so.2 -lc app.o -o app $ ./app Hello World # 2. Criar a biblioteca com a função sobrescrita $ nasm -felf64 print.asm -o print.o $ ld -shared print.o -o libprint.so # 3. Usar LD_PRELOAD para executar o programa original com a função interceptada $ export LD_PRELOAD=./libprint.so $ ./app Função printf sobrescrita! # 4. Limpar a variável de ambiente para voltar ao normal $ unset LD_PRELOAD
Resumo
- A variável de ambiente
LD_PRELOADpermite forçar o carregamento de uma biblioteca compartilhada antes de todas as outras. - Se a biblioteca pré-carregada definir um símbolo (como uma função) com o mesmo nome de um símbolo em outra biblioteca, a versão pré-carregada terá precedência.
- Essa técnica permite interceptar e substituir funções de sistema ou de outras bibliotecas, modificando o comportamento de executáveis existentes sem a necessidade de recompilação.