Desvendando a Resolução de Funções em Tempo de Execução no Linux: PLT, GOT e Relocações

Desvendando a Resolução de Funções em Tempo de Execução no Linux: PLT, GOT e Relocações

Este artigo explora os mecanismos subjacentes que permitem que programas Linux encontrem e executem funções de bibliotecas dinâmicas em tempo de execução, mesmo com ASLR. Detalhamos o funcionamento do PLT, GOT e relocações, utilizando ferramentas como objdump, readelf e GDB para uma análise prática.

MundiX News·09 de julho de 2026·3 min de leitura·👁 1 views

Quando um programa em Linux chama printf, sleep ou qualquer outra função de uma biblioteca dinâmica, seu endereço real é desconhecido antecipadamente devido ao ASLR (Address Space Layout Randomization). No entanto, a chamada é executada com sucesso. Neste artigo, vamos detalhar passo a passo como os mecanismos de PLT (Procedure Linkage Table), GOT (Global Offset Table) e relocações funcionam. Acompanharemos o processo de resolução de endereços usando objdump, readelf e GDB, e observaremos em tempo real o que acontece nas primeiras e subsequentes chamadas de uma função de biblioteca.

No momento do lançamento de um programa em Linux, o kernel o carrega na memória e o linker dinâmico carrega todas as bibliotecas necessárias. O mecanismo que vamos analisar funciona de forma idêntica para qualquer biblioteca dinâmica, mas como exemplo, utilizaremos a biblioteca padrão da linguagem C, libc.so.6. Ela é carregada praticamente sempre e contém funções bem conhecidas como printf, puts ou sleep.

No entanto, há um obstáculo. Devido ao ASLR, um mecanismo de segurança que posiciona bibliotecas aleatoriamente na memória, o endereço de libc.so.6 e todas as funções dentro dela muda a cada execução. O compilador não pode saber esses endereços antecipadamente. Como, então, o programa encontra a função desejada se seu endereço só se torna conhecido durante a execução?

A resposta reside no mecanismo de relocações: PLT, GOT e lazy binding. Vamos entender como isso funciona com o exemplo de um programa simples, empty_sleep, que chama a função sleep(30) e termina. Não apenas estudaremos a teoria, mas também veremos as relocações em ação com o depurador GDB.

Programa Vazio para Experimento

Para o nosso exemplo, escreveremos um programa simples chamado empty_sleep. Ele não faz nada além de chamar sleep(30):

c
#include <unistd.h>

int main() {
    sleep(30);
    return 0;
}

Vamos compilá-lo em um arquivo objeto:

bash
gcc -c empty_sleep.c -o empty_sleep.o

Em seguida, examinaremos o código desmontado. É importante notar que a saída dos comandos pode variar dependendo das flags usadas para compilar o GCC e o glibc. Apresentaremos duas variantes. As diferenças residem nos offsets do código, na presença da instrução endbr64 e na estrutura dos stubs do PLT. No entanto, a lógica de relocações não é afetada.

Agora, vejamos o código desmontado:

bash
objdump -d -M intel empty_sleep.o

Primeiro Exemplo (com endbr64):

assembly
0000000000000000 <main>:
   0:   f3 0f 1e fa          endbr64
   4:   55                   push   rbp
   5:   48 89 e5             mov    rbp,rsp
   8:   bf 1e 00 00 00       mov    edi,0x1e
   d:   e8 00 00 00 00       call   12 <main+0x12>
  12:   b8 00 00 00 00       mov    eax,0x0
  17:   5d                   pop    rbp
  18:   c3                   ret

Segundo Exemplo (sem endbr64):

assembly
0000000000000000 <main>:
   0:   55                   push   rbp
   1:   48 89 e5             mov    rbp,rsp
   4:   bf 1e 00 00 00       mov    edi,0x1e
   9:   e8 00 00 00 00       call   e <main+0xe>
   e:   b8 00 00 00 00       mov    eax,0x0
  13:   5d                   pop    rbp
  14:   c3                   ret

O primeiro exemplo inclui a instrução endbr64, enquanto o segundo não. No primeiro caso, o stub está no offset 0xd; no segundo, está no offset 0x9. Fora isso, não há diferença.

O ponto crucial aqui é a instrução call com o operando 0x00000000 – o stub. O compilador não sabe onde sleep será localizado, então ele deixa um espaço em branco. Os quatro bytes nulos são um placeholder temporário que será substituído pelo endereço real posteriormente.

A instrução endbr64 faz parte da tecnologia Intel CET (Control-Flow Enforcement Technology), uma proteção de hardware contra ataques de fluxo de controle indireto, como JOP (Jump-Oriented Programming) e similares. Ela não está relacionada ao mecanismo de relocações. Se o seu GCC foi compilado sem suporte a CET, essa instrução não estará presente, e o offset do stub será quatro bytes menor. Isso também não afeta a lógica de relocações.

Repetindo, os quatro bytes nulos são uma marca temporária. O endereço real será gravado mais tarde, e por enquanto, o compilador deixou uma entrada em uma tabela especial.

Como o Compilador Informa sobre Edições Futuras

O compilador grava informações em uma tabela especial: no offset especificado (imediatamente após o código da operação e8), o endereço da função sleep precisará ser inserido. Vamos verificar:

bash
readelf -r empty_sleep.o

Saída:

Relocation section '.rela.text' at offset 0x178 contains 1 entries:
  Offset          Info             Type               Sym. Value    Sym. Name + Addend
00000000000e  000400000004 R_X86_64_PLT32    0000000000000000 sleep-4

O valor específico do offset depende do tamanho do código da função main e pode variar no seu caso. O importante é que ele aponta exatamente para os quatro bytes que vimos no desassembler.

Vamos analisar a entrada por campos. O offset 0xe corresponde ao exemplo com endbr64. Se o seu output não tiver endbr64, o offset será 0xa.

CampoValor (com endbr64)Valor (sem endbr64)Descrição
Offset0xe0xaOnde na seção .text o stub está localizado.
Info (bits altos)0x00040x0004Índice do símbolo na tabela de símbolos = 4.
Info (bits baixos)0x000000040x00000004Tipo de relocalização = R_X86_64_PLT32.
Nome do SímbolosleepsleepÉ necessário inserir o endereço do símbolo sleep.

De onde veio o índice 4? Vamos consultar a tabela de símbolos:

bash
readelf -s empty_sleep.o

Saída:

Symbol table '.symtab' contains 5 entries:
   Num:    Value          Size Type    Bind Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS empty_sleep.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 .text
     3: 0000000000000000    25 FUNC    GLOBAL DEFAULT    1 main
     4: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND sleep

O símbolo número 4 é sleep. Seu valor ainda é 0x0 porque o endereço real é desconhecido. As letras UND (undefined) significam que o símbolo é definido em outro lugar, neste caso, na biblioteca libc.so.6.

Portanto, no arquivo objeto, temos:

  • Um stub 00000000 no código.
  • Uma entrada na tabela de relocações: "neste offset, insira o endereço de sleep".

Em seguida, entra em ação o linker estático.

Como o Linker Estático Cria o Mecanismo de Substituição

Continuaremos com o primeiro exemplo (com endbr64). Para a segunda variante, as diferenças são apenas nos offsets. Vamos linkar o programa:

bash
gcc empty_sleep.o -o empty_sleep

Agora, vamos examinar a função main no arquivo executável final:

bash
objdump -d -M intel empty_sleep | grep -A10 '<main>:'

Saída:

assembly
0000000000001149 <main>:
1149:   f3 0f 1e fa          endbr64
114d:   55                   push   rbp
114e:   48 89 e5             mov    rbp,rsp
1151:   bf 1e 00 00 00       mov    edi,0x1e
1156:   e8 f5 fe ff ff       call   1050 <sleep@plt>
115b:   b8 00 00 00 00       mov    eax,0x0
1160:   5d                   pop    rbp
1161:   c3                   ret

O endereço de main mudou de 0x0 para 0x1149. O linker estático posicionou o código no arquivo final, adicionando cabeçalhos e outras seções. Os endereços específicos serão diferentes para você, e isso é normal.

Em vez de um call com zeros, agora temos call 1050 <sleep@plt>. O linker estático não inseriu o endereço real de sleep – ele não pode, pois a biblioteca é carregada dinamicamente e seu endereço só será conhecido em tempo de execução. Em vez disso, ele criou dois mecanismos cruciais:

  • PLT (Procedure Linkage Table): Uma tabela de stubs para funções de bibliotecas dinâmicas.
  • GOT (Global Offset Table): Uma tabela onde o linker dinâmico posteriormente gravará os endereços reais.

Agora, a instrução call em main aponta não diretamente para sleep, mas para uma entrada na PLT. O que há dentro do stub da PLT? Vamos investigar.

O que há dentro do Stub da PLT

Apresentaremos ambos os tipos de stub da PLT; eles parecem diferentes, mas fazem a mesma coisa.

Primeiro, alguns termos:

  • Stub da PLT: Um pequeno fragmento de código que main chama em vez da função real da biblioteca.
  • Resolver: O código do linker dinâmico que, na primeira chamada, encontra o endereço real da função na biblioteca e o grava na GOT.

Vamos examinar o stub de sleep@plt:

bash
objdump -d -M intel --no-show-raw-insn empty_sleep | grep -A6 '<sleep@plt>:'

A flag --no-show-raw-insn oculta a coluna com os bytes brutos, tornando a saída mais limpa e fácil de ler.

A saída pode variar. No primeiro exemplo (com endbr64), o stub e o resolver estarão em seções diferentes. No segundo (sem endbr64), eles estarão combinados no mesmo local.

Exemplo com Stub e Resolver Combinados (Segundo exemplo da seção "Programa Vazio para Experimento", doravante chamado de exemplo com três instruções):

assembly
0000000000001030 <sleep@plt>:
1030:   jmp    QWORD PTR [rip+0x2fca]        # 4000 <sleep@GLIBC_2.2.5>
1036:   push   0x0
103b:   jmp    1020 <_init+0x20>

Exemplo com Stub e Resolver Separados (Primeiro exemplo da seção "Programa Vazio para Experimento", doravante chamado de exemplo com instruções separadas):

assembly
# Seção .plt.sec - stub chamado por main:
0000000000001050 <sleep@plt>:  # Nota: Este rótulo pode variar dependendo da versão do objdump
1050:   endbr64
1054:   jmp    QWORD PTR [rip+0x2f76]        # 3fd0 <sleep@GLIBC_2.2.5>
105a:   nop    WORD PTR [rax+rax*1+0x0]

No caso separado, o stub não possui push nem jmp para o resolver – apenas jmp para a GOT e nop. No entanto, o comentário # 3fd0 <sleep@GLIBC_2.2.5> sugere que o jmp acessa a GOT em um offset de 0x3fd0. Vamos verificar lá:

bash
objdump -D -M intel empty_sleep | grep '3fd0:'

Resultado:

assembly
3fd0:   30 10                   xor    BYTE PTR [rax], dl

Os bytes 30 10 não são um comando xor, mas os bytes inferiores do endereço 0x1030, gravados em ordem reversa (little-endian). Assim, a GOT aponta para o endereço 0x1030 dentro da seção .plt. Vamos ver o que está lá:

bash
objdump -d -M intel --no-show-raw-insn empty_sleep | grep -A4 '1030:'

Veremos o seguinte resultado:

assembly
1030:   endbr64
1034:   push   0x0
1039:   jmp    1020 <_init+0x20>
103e:   xchg   ax, ax

Aqui estão o push e o jmp para o resolver, no endereço 0x1030 na seção .plt. Nós os encontramos não pelo nome <sleep@plt>, mas pelo endereço que estava na GOT. A GOT, por sua vez, aponta não para o stub em .plt.sec, mas para o código do resolver em .plt.

Ambos os casos implementam o mesmo mecanismo. No primeiro, o stub e o resolver são combinados em um só lugar; no segundo, são divididos em seções diferentes: .plt.sec contém apenas o stub (para onde o call de main vai), e .plt contém o código do resolver com push e jmp (para onde se vai apenas na primeira chamada através da GOT).

Agora, vamos analisar cada instrução em detalhe.

Instrução 1: jmp para a GOT

Começaremos a análise com o exemplo de três instruções:

assembly
0000000000001030 <sleep@plt>:
1030:   jmp    QWORD PTR [rip+0x2fca]        # 4000 <sleep@GLIBC_2.2.5>
1036:   push   0x0
103b:   jmp    1020 <_init+0x20>

A instrução jmp QWORD PTR [rip+0x2fca] é um salto para o endereço armazenado na GOT. No exemplo acima, o comentário # 4000 <sleep@GLIBC_2.2.5> indica que o endereço está no offset 0x4000. É para lá que o linker dinâmico gravará o endereço real de sleep.

No entanto, na primeira chamada, não há o endereço de sleep lá, mas um endereço que aponta de volta para o código dentro da PLT. Vamos verificar a GOT:

bash
objdump -D -M intel empty_sleep | grep -E '\.got(\.plt)?:'

Dependendo da versão do objdump, o nome da seção pode ser .got ou .got.plt – é a mesma área de memória. A saída também pode variar.

Para o exemplo, vamos usar a variante com três instruções:

Desmontagem da seção .got.plt:

assembly
0000000000003fe8 <_GLOBAL_OFFSET_TABLE_>:
3fe8:   e0 3d                   loopne 4027 <_end+0x7>
...
3ffe:   00 00                   add    BYTE PTR [rax], al
4000:   36 10 00                adc    BYTE PTR [rax], al

Desmontagem para o exemplo com instruções separadas (como vimos na seção "O que há dentro do Stub da PLT"):

Desmontagem da seção .got:

assembly
0000000000003fb8 <_GLOBAL_OFFSET_TABLE_>:
3fb8:   c8 3d 00 00             enter  0x3d,0x0
...
3fd0:   30 10                   xor    BYTE PTR [rax], dl

O objdump interpreta o conteúdo da GOT como código de máquina. Os bytes 36 10 00 no offset 0x4000 (exemplo com três instruções) ou 30 10 no offset 0x3fd0 (exemplo com instruções separadas) não são comandos ss adc ou xor, mas os bytes inferiores do endereço em ordem reversa (little-endian). No primeiro caso, é 0x1036; no segundo, 0x1030. Ambos os endereços apontam para o código dentro da PLT: ou para a próxima instrução após o jmp (exemplo com três instruções), ou para a entrada da seção .plt, onde estão o push e o jmp para o resolver (exemplo com instruções separadas).

Continuação disponível apenas para membros

Materiais das últimas edições tornam-se disponíveis individualmente apenas dois meses após a publicação. Para continuar a leitura, é necessário se tornar um membro da comunidade "Xakep.ru".

Junte-se à comunidade "Xakep.ru"!

A associação à comunidade durante o período especificado lhe dará acesso a TODO o material "Xakep", permitirá baixar edições em PDF, desativará a publicidade no site e aumentará seu desconto cumulativo pessoal!

Saiba mais

Já sou membro "Xakep.ru"

← Anterior O custo de acesso à infraestrutura de empresas começa em US$ 100

🛡️⚡

Pare de pesquisar. Comece a hackear.

O MundiX é seu copiloto de pentest com IA: comandos exatos, análise de outputs e próximo passo na kill chain — em segundos.

Testar grátis por 7 dias →

Sem cartão para começar · Planos a partir de R$49/mês

📤 Compartilhar & Baixar

🧰 Ferramentas recomendadas

Divulgação: alguns links são patrocinados. Podemos receber comissão se você comprar — sem custo extra para você. Só indicamos o que faz sentido para a comunidade.

Aprendendo Kali Linux: Teste de segurança, pentest e hacking ético

Aprendendo Kali Linux: Teste de segurança, pentest e hacking ético

Com centenas de ferramentas pré-instaladas, a distribuição Kali Linux facilita o trabalho de os profissionais de segurança começarem a fazer testes de segurança rapidamente. No entanto, com mais de 600 ferramentas em seu arsenal, o Kali Linux também pode ser desafiador. A nova edição deste prático livro abrange as atualizações nas ferramentas e inclui uma melhor abordagem da análise forense e da engenharia reversa. Ric Messier, autor, não fica apenas no teste de segurança, mas também faz uma abordagem sobre a execução de análise forense, incluindo a análise em disco e na memória, assim como alguma análise básica de malware. • Explore as diversas ferramentas disponíveis no Kali Linux • Entenda o valor do teste de segurança e examine os tipos de teste disponíveis • Aprenda os aspectos básicos do pentest em todo o ciclo de vida do ataque • Instale o Kali Linux em vários sistemas, tanto físicos quanto virtuais • Descubra como usar diferentes ferramentas destinadas à segurança • Estruture um teste de segurança baseado nas ferramentas do Kali Linux • Estenda as ferramentas do Kali para criar técnicas de ataque avançadas • Use o Kali Linux para ajudar a criar relatórios quando o teste terminar “A abordagem concisa, clara e baseada na experiência adotada por Ric Messier para a introdução do Kali Linux e dos testes de cibersegurança é incomparável. Este livro é uma leitura excelente e acessível para iniciantes e um recurso valioso para qualquer pessoa.” —Alexander Arlt, Consultor sênior de segurança, Google

Ver na Amazon
Gshield 2 em 1 Hub Extensor Conector USB-C + USB-A e Adaptador de Rede Ethernet LAN RJ45 com 3 Entradas USB 3.0 até 5 Gbps em Liga de Alumínio para Computador e Notebook, Cinza

Gshield 2 em 1 Hub Extensor Conector USB-C + USB-A e Adaptador de Rede Ethernet LAN RJ45 com 3 Entradas USB 3.0 até 5 Gbps em Liga de Alumínio para Computador e Notebook, Cinza

Compatível com portas USB-C e USB-A, ideal para ampliar a conectividade de dispositivos como MacBook Pro e outros com portas USB-C. Inclui um adaptador USB-A extra, proporcionando uma conexão Ethernet estável e veloz de até 1 Gbps, perfeita para filmes, jogos online e videoconferências. Oferece três portas USB 3.0 com velocidades de transferência de até 5 Gbps, permitindo conectar mouse, teclado, discos rígidos e outros periféricos. Fabricado em alumínio durável, garantindo longa vida útil e resistência ao uso diário. Design compacto e leve, ideal para viagens de negócios e uso diário, facilitando o transporte e armazenamento. Funciona com Windows 10/8.1/8, Mac OS e Chrome OS, oferecendo versatilidade incomparável para diversas necessidades de conectividade. Assegura uma conectividade estável e rápida, perfeita para tarefas exigentes como transferência de dados, streaming e mais.

Ver na Amazon
Hacking APIs: Breaking Web Application Programming Interfaces

Hacking APIs: Breaking Web Application Programming Interfaces

Hacking APIs is a crash course on web API security testing that will prepare you to penetration-test APIs, reap high rewards on bug bounty programs, and make your own APIs more secure. You'll learn how REST and GraphQL APIs work in the wild and set up a streamlined API testing lab with Burp Suite and Postman. Then you'll master tools useful for reconnaissance, endpoint analysis, and fuzzing, such as Kiterunner and OWASP Amass. Next, you'll learn to perform common attacks, like those targeting an API's authentication mechanisms and the injection vulnerabilities commonly found in web applications. You'll also learn techniques for bypassing protections against these attacks. In the book's nine guided labs, which target intentionally vulnerable APIs, you'll practice: Enumerating APIs users and endpoints using fuzzing techniques Using Postman to discover an excessive data exposure vulnerability Performing a JSON Web Token attack against an API authentication process Combining multiple API attack techniques to perform a NoSQL injection Attacking a GraphQL API to uncover a broken object level authorization vulnerability

Ver oferta
Gray Hat Hacking: The Ethical Hacker's Handbook, Sixth Edition

Gray Hat Hacking: The Ethical Hacker's Handbook, Sixth Edition

Up-to-date strategies for thwarting the latest, most insidious network attacks This fully updated, industry-standard security resource shows, step by step, how to fortify computer networks by learning and applying effective ethical hacking techniques. Based on curricula developed by the authors at major security conferences and colleges, the book features actionable planning and analysis methods as well as practical steps for identifying and combating both targeted and opportunistic attacks. Gray Hat Hacking: The Ethical Hacker's Handbook, Sixth Edition clearly explains the enemy's devious weapons, skills, and tactics and offers field-tested remedies, case studies, and testing labs. You will get complete coverage of Internet of Things, mobile, and Cloud security along with penetration testing, malware analysis, and reverse engineering techniques. State-of-the-art malware, ransomware, and system exploits are thoroughly explained. Fully revised content includes 7 new chapters covering the latest threats Includes proof-of-concept code stored on the GitHub repository Authors train attendees at major security conferences, including RSA, Black Hat, Defcon, and B-Sides

Ver na Amazon
Bloqueador USB de privacidade de porta USB para PC, notebook, bloco de laptop,

Bloqueador USB de privacidade de porta USB para PC, notebook, bloco de laptop,

Proteção de privacidade aprimorada: protege o link de transmissão de dados para evitar roubo de informações, fornecendo proteção de segurança robusta que protege a privacidade do usuário durante transferências de arquivos e garante uma conexão segura para interações de dispositivos sem preocupações em vários ambientes Uso a longo prazo: a camada protetora resistente ao desgaste, combinada com um corpo de metal resistente, oferece gerenciamento de calor confiável e qualidade duradoura durante o uso diário Entrega eficiente de energia: a tecnologia de chip inteligente garante a identificação automática dos requisitos de energia, fornecendo carregamento eficiente alinhando-se com vários protocolos de carregamento rápido para maior conveniência Proteção contra sobrecarga: evitando riscos de sobrecarga, este bloqueador de dados USB protege a vida útil da bateria e garante um desempenho estável, mantendo um fluxo estável de energia para melhorar a longevidade do dispositivo de forma eficaz Prático de transportar: com atenção à portabilidade, este bloqueador de dados USB oferece um design compacto que é leve e fácil de transportar, melhorando a conveniência do usuário e operação eficiente

Ver na Amazon

📩 Newsletter MundiX

Receba novidades de cibersegurança + um checklist de pentest grátis. Sem spam.

Ao assinar você concorda em receber e-mails. Cancele quando quiser.