Análise de código malicioso em Linux

Você é acionado para fazer a análise de um arquivo desconhecido, querem saber o que esse arquivo faz, não é revelado mais nenhum detalhe, apenas que ele está envolvido em um incidente de segurança. O que fazer?

Essa situação é baseada em um caso real.

Identificação

Assim como em outras análises o primeiro passo é identificar o arquivo, descobrir para qual sistema operacional ele foi compilado, qual arquitetura, a presença de proteções, etc. No Linux podemos utilizar o comando “file para descobrirmos o tipo do nosso arquivo suspeito:

:~#: file suspeito

suspeito: ELF 64-bit LSB executable, x86-64,
version 1 (SYSV), dynamically linked (uses shared libs),
for GNU/Linux 2.6.18, not stripped

O arquivo suspeito trata-se de um executável do Linux (ELF, Executable and Linkable Format), para a arquitetura x86 de 64 bits. Usa bibliotecas compartilhadas e foi compilado para o Kernel 2.6.18.

Strings

Agora vamos buscar as strings .

:~#: strings -a suspeito

Exibindo apenas as mais interessantes:

/lib64/ld-linux-x86-64.so.2
GLIBC_2.2.5
GCC: (Debian 4.4.5-8) 4.4.5
GCC: (Debian 4.4.5-10) 4.4.5
file.c
__libc_start_main@@GLIBC_2.2.5
fork@@GLIBC_2.2.5

Vemos que o arquivo usa a biblioteca compartilhada da linguagem C conhecida como GLIBC. Também há referências ao GCC e a um arquivo chamado file.c. Com isso já presumimos que a linguagem de programação utilizada é o C.

Vemos que foi compilado com o GCC 4.4.5 do sistema Debian, com o comando “file” vimos o kernel 2.6.18, assim poderíamos descobrir qual a versão do Debian que foi utilizada.

Por fim ainda há referência à função fork() da GLIBC.

Não vemos referência a mais nenhuma outra função da GLIBC, vamos continuar com a análise.

Sessões do arquivo

Agora vamos utilizar o utilitário “readelf” para obter mais informações sobre o arquivo, exibir seus headers.

:~# readelf -S suspeito

There are 31 section headers, starting at offset 0x9c0:

Section Headers:
[Nr] Name Type Address Offset
[ 0] NULL 0000000000000000 00000000
[ 1] .interp PROGBITS 0000000000400200 00000200
[ 2] .note.ABI-tag NOTE 000000000040021c 0000021c
[ 3] .note.gnu.build-i NOTE 000000000040023c 0000023c
[ 4] .hash HASH 0000000000400260 00000260
[ 5] .gnu.hash GNU_HASH 0000000000400288 00000288
[ 6] .dynsym DYNSYM 00000000004002a8 000002a8
[ 7] .dynstr STRTAB 0000000000400308 00000308
[ 8] .gnu.version VERSYM 0000000000400346 00000346
[ 9] .gnu.version_r VERNEED 0000000000400350 00000350
[10] .rela.dyn RELA 0000000000400370 00000370
[11] .rela.plt RELA 0000000000400388 00000388
[12] .init PROGBITS 00000000004003b8 000003b8
[13] .plt PROGBITS 00000000004003d0 000003d0
[14] .text PROGBITS 0000000000400400 00000400
[15] .fini PROGBITS 00000000004005c8 000005c8
[16] .rodata PROGBITS 00000000004005d8 000005d8
[17] .eh_frame_hdr PROGBITS 00000000004005dc 000005dc
[18] .eh_frame PROGBITS 0000000000400600 00000600
[19] .ctors PROGBITS 0000000000600680 00000680
[20] .dtors PROGBITS 0000000000600690 00000690
[21] .jcr PROGBITS 00000000006006a0 000006a0
[22] .dynamic DYNAMIC 00000000006006a8 000006a8
[23] .got PROGBITS 0000000000600848 00000848
[24] .got.plt PROGBITS 0000000000600850 00000850
[25] .data PROGBITS 0000000000600878 00000878
[26] .bss NOBITS 0000000000600888 00000888
[27] .comment PROGBITS 0000000000000000 00000888
[28] .shstrtab STRTAB 0000000000000000 000008c1
[29] .symtab SYMTAB 0000000000000000 00001180
[30] .strtab STRTAB 0000000000000000 00001798

Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings)
I (info), L (link order), G (group), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

Breve descrição de alguns headers:

.rodata: dados read-only
.dynsym: tabela de símbolos de linking dinâmico
.symtab: tabela de símbolos
.debug: informações para debugging
.dynstr: strings utilizadas pelo linking dinâmico
.comment: informações de controle de versão
.strtab: strings que representam nomes associados com entradas da tabela de símbolos
.text: instruções executáveis do programa.

O que são símbolos?

São nomes de variáveis e funções utilizadas no binário. Ao invés de deixar apenas os endereços hexadecimais o compilador mantém uma tabela com os nomes relativos a cada função. Essa informação pode ser retirada (stripped) do binário, mas no comando “file” vimos que no nosso arquivo não havia sido retirada, “not stripped”.

Vamos analisar conteúdo de algumas seções do nosso arquivo, vou retirar o hex do resultado para facilitar a visualização:

:~# readelf -x 1 suspeito

Hex dump of section '.interp':
/lib64/ld-linux-
x86-64.so.2.

:~# readelf -x 7 suspeito

Hex dump of section '.dynstr':
.__gmon_start__.
libc.so.6.fork._
_libc_start_main
.GLIBC_2.2.5.

:~# readelf -x 27 suspeito

Hex dump of section '.comment':
GCC: (Debian 4.4
.5-8) 4.4.5.GCC:
(Debian 4.4.5-1
0) 4.4.5.

:~# readelf -x 30 suspeito

Hex dump of section '.strtab':

.call_gmon_start
.crtstuff.c.__CT
OR_LIST__.__DTOR
LIST__.__JCR_LI
ST__.__do_global
_dtors_aux.compl
eted.6341.dtor_i
dx.6343.frame_du
mmy.__CTOR_END__
.__FRAME_END__._
_JCR_END__.__do_
global_ctors_aux
.file.c._GLOBAL_
OFFSET_TABLE_.__
init_array_end._
_init_array_star
t._DYNAMIC.data_
start.__libc_csu
_fini._start.__g
mon_start__._Jv_
RegisterClasses.
_fini.__libc_sta
rt_main@@GLIBC_2
.2.5._IO_stdin_u
sed.__data_start
.__dso_handle.__
DTOR_END__.__lib
c_csu_init.__bss
_start._end.fork
@@GLIBC_2.2.5._e
data.main._init.

Com isso apenas confirmamos o que já tínhamos encontrado, não vemos nenhuma função nova sendo utilizada, apenas a fork() que já mencionamos. Agora utilizaremos o comando “nm” que lista informações sobre símbolos de um arquivo objeto.

:~# nm -D suspeito

w __gmon_start__
U __libc_start_main
U fork

A opção “-D” lista os símbolos utilizados na biblioteca compartilhada GLIBC que o binário utiliza, ele só usa o fork() dela.

Disassembly

Com a ajuda do “objdump” vamos verificar o disassembly da função “main” do arquivo suspeito com o comando abaixo para exibir na síntaxe Intel:

:~# objdump -M intel -d suspeito |grep -A10 main

00000000004004e4 <main>:
4004e4:  push rbp
4004e5:  mov rbp,rsp
4004e8:  call 4003f0 <fork@plt>
4004ed:  jmp 4004e8 <main+0x4>
4004ef:   nop

O main do arquivo é apenas isso, agora as coisas ficaram mais claras, veja a chamada para a função fork() e abaixo dela um jump incondicional para ela novamente. O que é isso? Um loop infinito.

Loop infinito para a função fork(), o que acontecerá?

A função fork() cria um processo filho que difere do processo pai apenas pelo seu PID e PPID. É duplicada toda a estrutura de memória do processo pai.

O processo vai sendo duplicado infinitamente e utilizando mais e mais memória esgotando todos os recursos do computador. Então nosso arquivo suspeito pode ser caracterizado como uma ferramenta de negação de serviço, Denial of Service - DoS.

Fiz o teste de execução em uma máquina virtual rodando Debian 6.0 64 bits, o computador host possui 3 GB RAM e para a VM reservei 1 GB. Ao executá-lo a VM ficou inoperante e visualizando o processo dela no Windows foi possível ver que estava consumindo praticamente a metade da memória RAM do host.

Conclusão

O atacante realizou um ataque DoS executando esse arquivo no servidor Linux alvo. Com as informações obtidas na análise foi possível dizer se o arquivo foi criado no próprio servidor ou em um computador externo.

O código-fonte para essa simples ferramenta de DoS poderia ser esse:

// suspeito.c
#include <sys/types.h>
#include <unistd.h>

main(){
    while(1){
       fork();
    }
}

Ronaldo P. Lima

3 comentários:

  1. Muito bom, Ronaldo. Parabéns! Gosto muito de seus textos.

    É legal notar também que a call para a fork ainda é um wrapper. Em algum momento dentro dela, o nome da função vai para o EAX/RAX, mesmo sem símbolos. ;)

    Um abraço!

    ResponderExcluir
  2. Esse código faz jus ao termo vírus. Tão pequeno e olha o estrago que causa...

    Ótimo texto!

    ResponderExcluir
  3. Artigo Fantástico. Parabéns ao Autor

    ResponderExcluir

Related Posts Plugin for WordPress, Blogger...