Questão:
Desmontar ELF - PC está definido como 0?
IvanaGyro
2018-09-18 10:49:25 UTC
view on stackexchange narkive permalink

Tentei desmontar um arquivo ELF que é um arquivo de objeto compartilhado executado em armv7a (Android). Eu vi um bloco estranho. Parece que o PC , registro do contador do programa, está definido como 0 . Perdi algo ou fiz algo errado?


O processo vai para 0x1708 no modo ARM. Abaixo está o estranho bloco de código ASM que desmontei do arquivo ELF.

 ; seção: .plt; função: function_1708 em 0x1708 - 0x17180x1708: 04 e0 2d e5 str lr, [sp, # -4]! 0x170c: 04 e0 9f e5 ldr lr, [pc, # 4] 0x1710: 0e e0 8f e0 add lr, pc, lr0x1714: 08 f0 seja e5 ldr pc, [lr, # 8] !; dados dentro da seção de código em 0x1718 - 0x171c0x1718: b4 77 00 00 | .w .. |  

Depois de executar a linha 0x170c , o LR register deve ser definido como o valor no endereço 0x1718 . O valor é 0x77b4 (este arquivo é armazenado em little-endian). E vá em frente.

  0x1710: lr + = 0x1710 + 8 // lr = 0x8ecc0x1714: pc = * (lr + 8) // pc = * (0x8ed4) lr + = 8  

E 0x8ed4 está na seção .got .

 ; seção: .got0x8eac: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 | ................ | 0x8ebc: 00 00 00 00 58 70 00 00 e0 6e 00 00 00 00 00 00 | .... Xp ... n ...... | 0x8ecc: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 08 17 00 00 | ....... ......... | 0x8edc: 08 17 00 00 08 17 00 00 08 17 00 00 08 17 00 00 | ................ | 0x8eec: 08 17 00 00 08 17 00 00 08 17 00 00 08 17 00 00 | ................ | 0x8efc: 08 17 00 00 08 17 00 00 08 17 00 00 08 17 00 00 | ................ | 0x8f0c: 08 17 00 00 08 17 00 00 08 17 00 00 08 17 00 00 | ............. ... | 0x8f1c: 08 17 00 00 08 17 00 00 08 17 00 00 08 17 00 00 | ................ | 0x8f2c: 08 17 00 00 08 17 00 00 08 17 00 00 08 17 00 00 | ................ | 0x8f3c: 08 17 00 00 08 17 00 00 08 17 00 00 08 17 00 00 | ..... ........... |

Parece que o valor em 0x8ed4 é zero. Eu rastreei este bloco estranho de JNI_OnLoad () , então nenhum dado deve ser modificado antes de executar este bloco.

Eu fiz algo errado ou este é um comportamento específico do ARM arquitetura?

Você está familiarizado com GOT / PLT em outras linguagens assembly? (normalmente, o x86 é mais conhecido). Porque sua pergunta está, de fato, fortemente relacionada ao comportamento de GOT / PLT em geral e à arquitetura ARM em particular.
Não. Sou novo na engenharia reversa. Estou pesquisando informações relativas. Existe algum artigo bom para leitura?
Ok, vou escrever uma resposta completa, então.
Observe que isso não é trivial! Se você realmente não entendeu na primeira leitura, isso é perfeitamente normal. Tente buscar um depurador e percorrer seu programa para entender melhor (e manter a fé!).
Dois respostas:
perror
2018-09-18 11:52:47 UTC
view on stackexchange narkive permalink

Na verdade, o comportamento que você está descrevendo vem do comportamento normal das seções GOT / PLT. Eles são usados ​​para vincular dinamicamente as chamadas do programa a funções de bibliotecas compartilhadas em tempo de execução.

Na verdade, uma biblioteca compartilhada pode ser carregada em qualquer lugar na memória do processo, não há como prever estaticamente onde ela será Aparecer. Assim, o GOT / PLT carrega o endereço da função de cada biblioteca dinamicamente em tempo de execução e armazena em cache para uso posterior.

A forma como funciona é bastante simples, o PLT ( Tabela de ligação de procedimentos ) é apenas um monte de pequenos dispositivos de código (um para cada função de biblioteca chamada no programa, mais um código genérico localizado no início da seção PLT).

E o GOT ( Global Offset Table ) é apenas uma tabela usada para armazenar (e armazenar em cache) os endereços das funções da biblioteca.

No início, o GOT é definido como zero porque nenhum endereço de função foi resolvido ainda. O GOT fica preenchido ao longo do programa é executado e chama novas funções (algumas funções podem ser chamadas apenas raramente e seus endereços podem ser raramente preenchidos no GOT).

Quando uma função de biblioteca é chamada de programa counter vai para o PLT (veja a figura abaixo) e executa o código escrito especificamente para esta função (cada um dos dispositivos de código tem seus próprios deslocamentos para obter o endereço adequado do GOT). Mas, a princípio, o GOT contém apenas zeros, então você precisa inicializá-lo.

Para inicializá-lo, você pula no início do PLT e encontra o endereço da função da biblioteca que o programa deseja chamar . Você executa este código e, em seguida, escreve o endereço da função no GOT.

PLT/GOT Schema (before)

Depois de armazenar em cache o endereço do , você não precisa mais recalcular o endereço se chamar sua função e pular diretamente para ela (como na imagem a seguir).

PLT/GOT Schema (after)

No caso do ARM, você tem dois tipos de esquemas GOT / PLT:

No seu caso, este é um PLT direto (mas parece ser um código bem antigo, eu suspeito de um compilador antigo ou algo assim , porque é melhor que os novos usem ip no lugar de lr para isso).

 ; seção: .plt; função: function_1708 em 0x1708 - 0x17180x1708: 04 e0 2d e5 str lr, [sp, # -4]! <-- Salve lr na pilha0x170c: 04 e0 9f e5 ldr lr, [pc, # 4] <-- Obtenha um ponto de referência na memória0x1710: 0e e0 8f e0 add lr, pc, lr <-- Compensação de carga para o próximo passo0x1714: 08 f0 seja e5 ldr pc, [lr, # 8]! <-- Pule para a próxima etapa  

Observe que a PLT é uma seção estática, portanto, o 0x170c sempre permanecerá o mesmo (e, portanto, o pc será sempre carregado com o mesmo valor em 0x170c).

Referências

Então, na minha situação, function_1708 é PLT [0], o código para chamar resolvedor, certo? Como o vinculador sabe onde preencher seu endereço de memória para o endereço correto da biblioteca compartilhada? No meu caso, `0x8ed4` não é o primeiro endereço em` .got`. O primeiro é `0x8eac`. A estratégia de preencher o endereço do resolvedor depende do formato do arquivo ELF, do sistema operacional em execução, ou da arquitetura ou CPU?
No início de cada execução de seu programa, o vinculador define o endereço onde a biblioteca compartilhada está no PLT e cada função usada pelo programa recebe um deslocamento na biblioteca compartilhada atual. Assim, se você souber o endereço da biblioteca compartilhada na memória e o deslocamento de uma função específica, poderá obter o endereço da função na memória virtual. Você calcula isso apenas uma vez, graças ao GOT (que armazena o resultado em cache).
Acho que fiz as perguntas erradas. No entanto, obtive minha resposta em algumas especificações ELF. Assim como @Igor Skochinsky ♦ respondeu, o local onde o linker escolhe colocar o endereço do resolvedor é definido nas especificações ELF. A definição depende do tipo de arquivo, ELF. Enfim, você me ajudou muito. Obrigado!
Igor Skochinsky
2018-09-18 18:43:21 UTC
view on stackexchange narkive permalink

Embora a outra resposta não esteja errada, ela não cobre o verdadeiro problema: como é que chamar um endereço zero não leva a um travamento?

AFAIK, não há um padrão oficial ou documentação completa para isso, mas de fato as primeiras entradas do GOT são especiais / reservadas e são usadas pelo carregador / vinculador dinâmico (às vezes também chamado de interpretador ) para seus próprios fins:

  • GOT [0] aponta para o símbolo _DYNAMIC do módulo atual que é o início da lista de tags ( Elf32_Dyn entradas) com as várias informações necessárias para a resolução adequada de símbolos dinâmicos. Veja a especificação ELF.

  • GOT [1] é preenchido em tempo de execução com o ponteiro para a estrutura link_map contendo a lista de todas as imagens dinâmicas presentes no processo. Esta lista reside no vinculador dinâmico ( ld.so em sistemas Linux e vinculador binário no Android).

  • GOT [2] também é preenchido pelo vinculador dinâmico e aponta para a função de resolvedor ( _dl_runtime_resolve em glibc, não tenho certeza no Android).

O snippet que você citou é o stub do resolvedor , que é chamado na primeira chamada de um símbolo externo (você pode ver que todas as entradas GOT são inicializadas para seu endereço, 1708 ). Ele busca GOT [2] e salta para ele, que deve terminar no vinculador dinâmico, que iria procurar o símbolo, corrigir a entrada GOT e pular para a função como a última etapa.

Como eu disse, não há documentos oficiais (AFAIK) sobre isso, apenas partes aleatórias da especificação ELF ABI, código-fonte glibc e várias postagens de blog. Eu sugeriria que você percorresse este código em um depurador para ver o que realmente acontece e compará-lo com o código-fonte. Além dos links de @ perror , encontrei este post que explica algumas das entradas GOT reservadas (embora para Linux x64 e não para Android ARM).

Tentei encontrar as especificações oficiais do ELF. De acordo com a Wikipedia, existem muitas especificações diferentes. E a organização desenvolveu o ELF, UNIX System Laboratories, estava extinto. A primeira especificação é lançada pela AT&T. Estou curioso para saber por que tantas empresas divulgaram suas especificações ELF ou incluíram a especificação ELF em suas especificações ABI e qual é o contexto histórico. É interessante.
@IgorSkochinsky: Eu não sabia de tudo isso! Na verdade, presumi que a análise feita por IvenCJ7 foi feita estaticamente, então o GOT foi definido como zero originalmente e definido como início do PLT depois. Vejo agora que provavelmente estava errado. Boa explicação Igor (mais uma vez) e bom link, obrigado!


Estas perguntas e respostas foram traduzidas automaticamente do idioma inglês.O conteúdo original está disponível em stackexchange, que agradecemos pela licença cc by-sa 4.0 sob a qual é distribuído.
Loading...