Jump to content

Como encontrar a função main?


gzn

Recommended Posts

Se vocês puderem apresentar as soluções mais triviais de se fazer isso acho que muitos iniciantes agradecerão! rs

Começando da situação mais simples à mais complicada:

Situações:

  1. Fácil B|: Ligado dinamicamente e sem símbolos.
  2. Um pouco difícil :o: Ligado estaticamente e sem símbolos!
  3. Muito difícil :ph34r:: Ligado estaticamente, sem símbolos e com um protector popular no mercado!!

Analisando um ELF na situação 1. parece que é fácil, é só procurar pelo símbolo __libc_start_main, colocar um bp nele e observar o primeiro argumento. Mas aí eu pensei: e no caso 2. em que nós não temos símbolos e a libc está dentro do nosso binário? :ph34r:

E se fosse um PE, quais dicas você daria para as situações acima?

Obs.: a situação número 3 se vocês não quiserem comentar deixem de lado, é um tanto difícil de dar dicas visto que não especifiquei nenhum protetor específico (aliás, nem sei quais existem e muito menos os mais populares! rs). Era só pra vocês comentarem quais seriam as técnicas gerais para se chegar na main (ex., como você começaria a analisar o binário e que tipo de técnicas você usaria como por exemplo assinaturas, algoritmos de comparação de código que procuram similaridades com funções previamente conhecidas, o uso de estatística e probabilidade, inteligência artificial, etc.).

Participe e comente o que você faria em cada situação!

 

Link to comment
Share on other sites

Você quer dizer com um debugger ou software automatizado?
No debugger eu encontro a main "na mão" mesmo.
O entry point aponta para _start.
O primeiro call dentro de _start é para __libc_start_main.

O primeiro argumento para __libc_start_main é o endereço da main.
Você pode pegar esse endereço sendo passado para ele ou acompanhar a execução até a hora que ele dá call na main.
Para x64 ele passa o primeiro argumento em rdi, e eu vi aqui que __libc_start_main preserva esse valor na pilha em [rsp+24]
Mais para frente ele pega o endereço da main e joga em rax, e depois dá call rax.
Como podemos ver no print abaixo:

5ad7ccdceb763_Capturadetelade2018-04-1819-53-39.png.b1044b7e98b2224d116aa2d62603d4ad.png

 

Mas como eu já disse, o primeiro argumento é o endereço da main. Se você quer pegar o endereço com uma ferramenta automática, eu pegaria daí.
Veja rdi recebendo o endereço da main:

5ad7cd9560a55_Capturadetelade2018-04-1819-58-05.png.3e3ee9f78dd2caf373825301ff40faaa.png

 

A seta verde está bem no entry point, isto é, no início de _start.
Se eu fosse fazer uma ferramenta eu pegaria o entry point e a partir dele buscaria pelo call.
Ao achar o call, os 4 bytes anteriores é o endereço da main.
Se assumirmos que o código de _start nunca muda, então nem precisaríamos fazer uma busca.
É só calcular entry_point + 0x20
Só que claro, o endereço "raw" e não virtual.

5ad7d129c7ea2_Capturadetelade2018-04-1820-12-56.png.0b01fa08d0b31fa24cb944377b439b38.png

Link to comment
Share on other sites

3 horas atrás, Felipe.Silva disse:

Você quer dizer com um debugger ou software automatizado?
No debugger eu encontro a main "na mão" mesmo.
O entry point aponta para _start.
O primeiro call dentro de _start é para __libc_start_main.

O primeiro argumento para __libc_start_main é o endereço da main.
Você pode pegar esse endereço sendo passado para ele ou acompanhar a execução até a hora que ele dá call na main.
Para x64 ele passa o primeiro argumento em rdi, e eu vi aqui que __libc_start_main preserva esse valor na pilha em [rsp+24]
Mais para frente ele pega o endereço da main e joga em rax, e depois dá call rax.
Como podemos ver no print abaixo:

5ad7ccdceb763_Capturadetelade2018-04-1819-53-39.png.b1044b7e98b2224d116aa2d62603d4ad.png

 

Mas como eu já disse, o primeiro argumento é o endereço da main. Se você quer pegar o endereço com uma ferramenta automática, eu pegaria daí.
Veja rdi recebendo o endereço da main:

5ad7cd9560a55_Capturadetelade2018-04-1819-58-05.png.3e3ee9f78dd2caf373825301ff40faaa.png

 

A seta verde está bem no entry point, isto é, no início de _start.
Se eu fosse fazer uma ferramenta eu pegaria o entry point e a partir dele buscaria pelo call.
Ao achar o call, os 4 bytes anteriores é o endereço da main.
Se assumirmos que o código de _start nunca muda, então nem precisaríamos fazer uma busca.
É só calcular entry_point + 0x20
Só que claro, o endereço "raw" e não virtual.

5ad7d129c7ea2_Capturadetelade2018-04-1820-12-56.png.0b01fa08d0b31fa24cb944377b439b38.png

Bacana meu amigo! Você comentou sobre a situação 1. (aquela situação que eu até comentei em que simplesmente poderíamos pegar o primeiro argumento de __libc_start_main).

Agora, minha dúvida mesmo é situação nº 2 [em diante] onde você pega um binário estático!

Um pouco difícil :o: Ligado estaticamente e sem símbolos!

Por exemplo: na sua máquina tenta fazer esse procedimento acima que você sugeriu no binário ola_mundo.static que você consegue com o seguinte código: https://github.com/gu10ng1t/re/tree/master/bin

É só clonar o repo, dar cd em bin/ e digitar make.

Muito obrigado pelo post amigo, achei muito bacana o seu passo a passo! :D Tenta tenta fazer esse "desafio" e compartilha com a gente o que você acha que daria para fazer pra achar a main de maneira que você pudesse aplicar essa técnica em outros binários compilados assim estaticamente e sem símbolos. 

Link to comment
Share on other sites

@Felipe.Silva realmente você está certo! Até para resolver no caso de um binário estático (pelo menos se for fazer o binário da forma tradicional com libc). Parece mesmo que esse deslocamento da entrada+0x20 não varia muito não (pelo menos, até agora nos meus testes não vi nada diferente).

Obs.: essa técnica parece funcionar melhor só em casos em que é usada a instrução mov (ex.: mov rdi, 0xFFF...). Algumas vezes nesse ponto (próximo a entrada+0x20) é usado LEA ao invés de MOV, nesse caso essa técnica precisa ser adaptada.

Link to comment
Share on other sites

Meus amigos, gostaria que alguém desse um exemplo para situação nº 3. Alguém pode escolher um protetor (protector) de binário popular para linux ou windows e dizer mais ou menos as etapas para se chegar a main? Não vale usar o UPX porque ele só comprime os dados! :) thank you

Link to comment
Share on other sites

19 horas atrás, gzn disse:

@Felipe.Silva realmente você está certo! Até para resolver no caso de um binário estático (pelo menos se for fazer o binário da forma tradicional com libc). Parece mesmo que esse deslocamento da entrada+0x20 não varia muito não (pelo menos, até agora nos meus testes não vi nada diferente).

Obs.: essa técnica parece funcionar melhor só em casos em que é usada a instrução mov (ex.: mov rdi, 0xFFF...). Algumas vezes nesse ponto (próximo a eip+0x20) é usado LEA ao invés de MOV, nesse caso essa técnica precisa ser adaptada.

Como eu disse:
« Se eu fosse fazer uma ferramenta eu pegaria o entry point e a partir dele buscaria pelo call.
Ao achar o call, os 4 bytes anteriores é o endereço da main »

Falei do +0x20 somente se assumirmos que _start nunca muda. Se é o caso de mudar, então podemos procurar pelo call. (só procurar por FF 15)
De acordo com os manuais da Intel o valor imediato sempre ficará em ultimo(como podemos ver aqui).
Então não importa que instrução seja usada, desde que ela esteja logo antes do call vai funcionar. (e se o call não mudar também)

Eu fiz um script em Python aqui. Testei no programa que você passou e em um Olá mundo.
Nos dois funcionou tranquilo.

5ad907ae78f4a_Capturadetelade2018-04-1918-18-01.png.a0644cd73035e261b8ca5ffe95f13ddb.png

 

Link: https://github.com/Silva97/tools/blob/master/mainsearch.py
Faz uns testes ai para ver o que dá.

EDIT:
Eu fiz um teste aqui com o ls e agora que entendi o que você quis dizer com usar o LEA.
Ele usa um endereço relativo de acordo com o valor de RIP... Muda muita coisa. '-'
Vou tentar adaptar o algoritmo para identificar isso.

Link to comment
Share on other sites

@gzn. Feito. Agora o script identifica quando usar a instrução LEA. Testei com o ls e funcionou.
Só que com isso nós conseguimos o endereço raw... Endereço na memória não vai rolar.
Mas com o endereço raw já nos dar a possibilidade de meter um breakpoint... E quando abrir no debugger nós restauramos a instrução que foi alterada.

5ad918ae8e2c7_Capturadetelade2018-04-1919-29-03.png.3102832674464a5efdf1f3159b04ca6f.png

Em relação a packers eu estou na mesma que você. Não conheço nenhum :P
Se alguém puder ao menos indicar um packer para que a gente possa fazer uns testes...
Eu nem sei como um packer funciona. kkkkk

Link de novo para o script: https://github.com/Silva97/tools/blob/master/mainsearch.py

Link to comment
Share on other sites

Muito bacana seu script @Felipe.Silva! Creio que não só eu mas os outros que estão iniciando acharam motivador você escrever esse script e ainda na linguagem python (o que é até bom pra quem ta fazendo o curso Pythonicos!).

Citar

Em relação a packers eu estou na mesma que você. Não conheço nenhum :P
Se alguém puder ao menos indicar um packer para que a gente possa fazer uns testes...
Eu nem sei como um packer funciona. kkkkk

kkk é isso aí amigo, quando você souber de um e fizer uns testes não deixa de comenta ai no fórum. Seus posts são muito bem organizados e você tem uma didática legal (a maneira como você explica passo a passo).

valeu!

Link to comment
Share on other sites

  • 6 months later...
  • 1 month later...

Legal o post e a discussão, well done!

 

Só uma dúvida de carater preciosista, @Felipe.Silva. Se bem entendi, quando você diz "endereço raw", você está se referindo ao RVA (relative virtual address), seria isso?

 

Aproveitando a deixa: @gzn, não vejo o porquê de linkar estaticamente e sem símbolos faria diferença na hora de achar a função main. Qual seria o impacto disso? Além disso, quando inclui a condição de ser modificado por um "protetor de mercado", você está interessado de achar a main do binário resultante ou do binário "original"?

 

[]s!

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...