Back-End

27 abr, 2012

Desenvolva um aplicativo em Python para analisar Dumps de memória compartilhada

Publicidade

Aprenda a analisar um dump de memória compartilhada que pode ser interpretado por máquina em uma plataforma Linux e extrair o formato esperado de dados usando o Python e o utilitário struct.

Neste artigo, primeiro será apresentado como determinar o formato dos dados lendo o formato do arquivo binário do arquivo de dump. Isso é necessário para analisar, extrair e examinar os dados. Em seguida, você verá como analisar o arquivo com base no formato e depois estabelecer a correspondência entre os resultados e o formato esperado para realizar a saída de um resultado de validação.

Os dumps de memória revelam o estado registrado da memória de
trabalho em um determinado momento da operação. Eles são ferramentas
importantes na administração de ferramenta, porque fornecem provas
“forenses” da condição do sistema.

Antes de iniciar

Para as instruções e amostras de código deste artigo, eu usei o
Python versão 2.4, que pode ser obtida por download a partir do site do
Python. Talvez você obtenha resultados diferentes com outras versões.

Antes de começar, certifique-se de estar familiarizado com o seguinte:

  • A implementação /dev/shm da memória compartilhada tradicional
  • A visualização de dados de memória compartilhada manualmente em um sistema Linux
  • Certas dependências (conceitos de abertura, leitura,
    gravação e fechamento de arquivos em Linux; uso do descritor de arquivo
    e dos modos em que o arquivo pode ser aberto; conceitos básicos de
    estrutura do Python)
  • GNU/Linux, em geral

Entendendo os dumps de memória compartilhada no Linux

/dev/shm é uma implementação do conceito tradicional de memória
compartilhada. É um meio amplamente usado e aceito de passar dados entre
programas. Em /dev/shm, um programa ou daemon cria uma parte da memória
que outros processos (em níveis relevantes de permissão) podem acessar.
É um método fácil e rápido de compartilhar dados entre processos.

Cada programa cria o seu próprio arquivo. Nos meus exemplos, eu uso o nome de arquivo dumpmem localizado em /dev/shm/dumpmem.

Visualizando o dump de memória compartilhada manualmente no Linux

Não é possível visualizar os arquivos de memória compartilhada (normalmente conhecidos como arquivos shm) usando o utilitário cat – que geralmente é utilizado para exibir arquivos em Linux.

Já que esses arquivos shm estão em um formato binário. Se você tentar visualizá-los com os métodos genéricos de visualização de arquivos, eles parecerão um monte de caracteres distorcidos.

Eu uso o utilitário hexdump) para ler os arquivos de memória e visualizá-los em um formato legível. Há outros utilitários disponíveis para esse fim.

Para este artigo, o padrão de uso de hexdump é assim:

hexdump <optional switches> /dev/shm/dumpmem for <switches> supported

Ao final do artigo consulte a seção Recursos para obter um link com mais informações sobre o hexdump.

Definindo o cenário

O cenário com o qual trabalharemos é um farejador de rede que
analisa os pacotes recebidos pelo host e armazena os dados em um arquivo
de memória compartilhada, o /dev/shm/dumpmem. Esses dados contêm
informações sobre o pacote recebido.

Geralmente o arquivo é assim:

  • O armazenamento do arquivo de memória é /dev/shm/dumpmem
  • O formato de arquivo dumpmem contém:
  1. 4 bytes de endereço de origem para notificar quem o enviou
  2. 4 bytes de endereço de destino para notificar para quem ele vai
  3. 2 bytes de porta de origem (em outras palavras, a porta na origem que o pacote usou)
  4. 2 bytes de porta de destino (da mesma forma, a porta no destino que o pacote usará)
  5. 2 bytes de protocolo (o protocolo do qual o pacote faz parte)
  6. 4 bytes de hora para indicar a indicação da hora em que o pacote foi visto pelo fragmento da rede
  • 1 comprimento de registro = soma das especificações de dumpmem (ou seja, 18 bytes)
  • O tamanho máximo do arquivo de memória é 1 KByte, portanto, ele pode conter 1024 bytes (1024 / 18 = 56 registros)

Se você usar o hexdump e exibir o arquivo manualmente em um terminal Linux, ele ficará assim:

Listagem 1. Exibindo um arquivo de dump

# hexdump /dev/shm/fdshm
0000000 0004 0000 0400 0000 fc64 0a00 00fb e000
0000010 14e9 14e9 0011 0000 0000 0000 0000 0000
0000020 0000 0000 0000 0000 0000 0000 0000 0800
0000030 1668 0000 0000 0000 0032 0000 0000 0000
0000040 0000 0000 0001 e000 0000 0000 0002 0000
0000050 0000 0000 0000 0000 0000 0000 0000 0000
0000060 0000 0000 0000 0800 0100 0000 0000 0000
0000070 0008 0000 0000 0000 fc64 0a00 fd64 0a00
0000080 2328 03ea 0006 0000 0000 0000 0000 0000
0000090 0000 0000 0000 0000 0000 0000 0000 0800
00000a0 7700 0001 0000 0000 0040 0000 0000 0000
00000b0 fd64 0a00 fc64 0a00 03ea 2328 0006 0000
00000c0 0000 0000 0000 0000 0000 0000 0000 0000
00000d0 0000 0000 0000 0800 0a00 0000 0000 0000
00000e0 0040 0000 0000 0000 fc64 0a00 fd64 0a00
00000f0 2328 03ec 0006 0000 0000 0000 0000 0000
0000100 0000 0000 0000 0000 0000 0000 0000 0800
0000110 7700 0001 0000 0000 0040 0000 0000 0000

Vamos examinar as etapas envolvidas na análise do arquivo.

Analisando o arquivo de dump

As etapas para entender os dados em um arquivo de dump de memória
(identificar o formato, analisar e ler o arquivo) são relativamente
simples:

  1. Abra o arquivo
  2. Leia os bytes com o descritor de arquivos
  3. Converta os dados para um formato legível de cadeia de caractere quando for necessário
  4. Verifique se o buffer que foi lido está intacto e se tem truncamentos ou erros
  5. Desempacote os dados do buffer
  6. Extraia as informações
  7. Imprima os dados
  8. Desenvolva um loop para realizar as etapas de 1 a 7 em cada registro em um dump de dados compartilhados.
    (Você não quer fazer isso manualmente, não é?)

Vamos repassar o fluxo de processo com mais detalhes.

Abra o arquivo

Para abrir um arquivo de memória compartilhada, use a forma geral fd = open(fileName, mode).
fd
é o descritor de arquivo, um apontador para o arquivo. Para este exemplo, use o seguinte:

  • fileName: /dev/shm/dumpmem
  • mode: rb (somente leitura no modo binário)

Listagem 2. Abrindo um arquivo de memória compartilhada

fd = open('/dev/shm/dumpmem ','rb')

Leia os bytes

Para ler os bytes usando o descritor de arquivo obtido na chamada de
função anterior, eu uso o código a seguir. Ele lê o número indicado de
bytes a partir do parâmetro de arquivo que é passado:

Listagem 3. Abrindo um arquivo de memória compartilhada

def ReadBytes(self, fd, noBytes):
 '''
 Read file and return the number of bytes as specified
 '''
 data = fd.read(noBytes)
 return data

buffer = ReadBytes('/dev/shm/dumpmem ', 18)
# Pass the file name and pass the number of bytes
# Number of bytes is 18 since in the example scenario each record
#  is of length 18 

Aqui, a leitura dos bytes não é suficiente para extrair as informações
necessárias. Ela retorna um buffer se a cadeia de caractere é lida. É
necessário analisar e converter para um formato de cadeia de caractere
inteligível.

Converta os dados

O struct do Python pode ser usado para tratar os dados binários armazenados em arquivos ou a partir de conexões de rede, entre outras origens. O struct do Python tem duas funcionalidades amplas: pack e unpack.

A tarefa de pack é retornar uma cadeia de caractere contendo os valores v1, v2, empacotados de acordo com o formato dado. Os argumentos devem corresponder exatamente aos valores de que o formato precisa.

A função de unpack é desempacotar a cadeia de caractere (supostamente empacotada por pack(fmt, …)) de acordo com o formato dado. O resultado é uma tupla, mesmo se ele contiver exatamente um item. A cadeia de caractere deve conter exatamente a quantidade de dados de que o formato precisa: len(string) deve ser igual a calcsize(fmt).

Os formatos aceitáveis:

  • Formatos de 1 byte
  1. b para caractere assinado
  2. B para caractere não assinado
  • Formatos de 2 bytes
  1. h para inteiro curto
  2. H para curto não assinado
  • Formatos de 4 bytes:
  1. l para longo
  2. L para longo não assinado
  • Formatos de 8 bytes:
  1. q para longo longo
  2. Q para longo longo não assinado

Para ver os outros formatos suportados para empacotar e desempacotar os bytes de buffer, consulte a literatura do Python listada em Recursos.

Verifique o buffer

Para se certificar de que o buffer de 18 bytes que foi lido esteja intacto e não tenha truncamentos ou erros, pode-se usar a função calcsize para verificar se o tamanho de byte ainda é 18 conforme o esperado quando é lido. Pode-se usar a função asserts do Python para esse fim.

Listagem 4. Certificando-se de que o buffer está correto

self.assertEqual(len(buffer), struct.calcsize('llllh'))

# 4 l's is 4*4 bytes = 16 bytes and h is 2 bytes so that is 18 bytes
# we could use QQh which is 2*8 + 2 = 18 bytes as well

Desempacote os dados

Agora que você verificou que o buffer é realmente de 18 bytes, é possível desempacotar os dados do buffer. struct fornece uma função unpack_from bastante útil que fornece o número de bytes, o nome do buffer e a compensação com a qual deve ser lido:

struct.unpack_from(fmt, buffer[, offset=0])

Extraia os detalhes

No nosso cenário, estes são os detalhes que queremos extrair:

Listagem 5. Detalhes a serem extraídos

sourceAddress = (struct.unpack_from('B', buffer,0),
                     struct.unpack_from('B', buffer,1),
                     struct.unpack_from('B', buffer,2),
                     struct.unpack_from('B', buffer,3))
destinationAddress = (struct.unpack_from('B', buffer,4),
                     struct.unpack_from('B', buffer,5),
                     struct.unpack_from('B', buffer,6),
                     struct.unpack_from('B', buffer,7))
sourcePort = (struct.unpack_from('B', buffer,8),
                     struct.unpack_from('B', buffer,9))
destinationPort = (struct.unpack_from('B', buffer,10),
                     struct.unpack_from('B', buffer,11))
protocolUsed = (struct.unpack_from('B', buffer,12),
                     struct.unpack_from('B', buffer,13))
timeStamp = (struct.unpack_from('B', buffer,14),
                     struct.unpack_from('B', buffer,15),
                     struct.unpack_from('B', buffer,16),
                     struct.unpack_from('B', buffer,17))

Observação: dependendo da plataforma e da estrutura da memória (big
endian ou little endian) talvez seja necessário trocar a ordem de
leitura dos dados.

Imprima a saída

Agora que você tem os valores desempacotados do buffer binário que você leu, pode usar os comandos padrão de print para obter a saída necessária.

Listagem 6. Imprimindo os detalhes

print "sourceAddress =" ,  
      (struct.unpack_from('B', buffer,0),struct.unpack_from('B', buffer,1),
      struct.unpack_from('B', buffer,2),struct.unpack_from('B', buffer,3))
print "destinationAddress = " ,
      (struct.unpack_from('B', buffer,4),struct.unpack_from('B', buffer,5),
      struct.unpack_from('B', buffer,6),struct.unpack_from('B', buffer,7))
print "sourcePort = " , (struct.unpack_from('H',buffer,8))
print "destinationPort = " , (struct.unpack_from('H',buffer,10))
print "protocolUsed = " , (struct.unpack_from('H',buffer,12))
print "timeStamp = " ,  
      (struct.unpack_from('B', buffer,14),struct.unpack_from('B', buffer,15),
      struct.unpack_from('B', buffer,16),struct.unpack_from('B', buffer,17))

A saída esperada da Listagem 6 deve ter este formato:

Listagem 7. Saída da impressão

sourceAddress =  ((192,), (168,), (10,), (102,))
destinationAddress =  ((207,), (168,), (1,), (103,))
sourcePort =  (11299,)
destinationPort =  (11555,)
protocolUsed =  (256,)
timeStamp =  ((1,), (12,), (0,), (1,))

Automatize o processo para todos os registros

Agora, para ler e imprimir todos os registros de todo o arquivo de memória compartilhada, crie um loop:

Listagem 8. Criando um loop para ler e imprimir todos os registros

for element in range (0,56):
#loop 18 since we know the file size and
#the record length: 1024/18 = 56 records
		
      buffer = ReadBytes('/dev/shm/dumpmem ', 18)
      self.assertEqual(len(buffer), struct.calcsize('llllh'))
        
      sourceAddress = struct.unpack_from('B', buffer,0),
                  struct.unpack_from('B', buffer,1),
                  struct.unpack_from('B', buffer,2),
                  struct.unpack_from('B', buffer,3))
      destinationAddress = struct.unpack_from('B', buffer,4),
                       struct.unpack_from('B', buffer,5),
                       struct.unpack_from('B', buffer,6),
                       struct.unpack_from('B', buffer,7))
      sourcePort = struct.unpack_from('B', buffer,8),
                 struct.unpack_from('B', buffer,9)
      destinationPort = struct.unpack_from('B', buffer,10),
                    struct.unpack_from('B', buffer,11))
      protocolUsed = ,struct.unpack_from('B', buffer,12),
                  struct.unpack_from('B', buffer,13))
      timeStamp = struct.unpack_from('B', buffer,14),
                struct.unpack_from('B', buffer,15),
                struct.unpack_from('B', buffer,16),
                struct.unpack_from('B', buffer,17))
        
      print "sourceAddress = " ,  
            struct.unpack_from('B', buffer,0),
            struct.unpack_from('B', buffer,1),
            struct.unpack_from('B', buffer,2),
            struct.unpack_from('B', buffer,3))
      print "destinationAddress =  " ,
            struct.unpack_from('B', buffer,4),
            struct.unpack_from('B', buffer,5),
            struct.unpack_from('B', buffer,6),
            struct.unpack_from('B', buffer,7))
      print "sourcePort =  " ,
            struct.unpack_from('H',buffer,8))
      print "destinationPort =  " ,
            struct.unpack_from('H',buffer,10))
      print "protocolUsed =  " ,
            struct.unpack_from('H',buffer,12))
      print "timeStamp = " ,  
            struct.unpack_from('B', buffer,14),
            struct.unpack_from('B', buffer,15),
            struct.unpack_from('B', buffer,16),
            struct.unpack_from('B', buffer,17))

E isso é tudo! Analisamos um formato conhecido de dump de memória binário no Linux e usamos struct do Python para ler o dump de dados binários e exibi-los em um formato legível.

Recursos

Aprender

Obter produtos e tecnologias

  • Faça o download do Python a partir do Web site do Python.
  • Avalie produtos IBM

    da maneira que for melhor para você: faça download da versão de teste de
    um produto, avalie um produto on-line, use-o em um ambiente de nuvem ou
    passe algumas horas na SOA Sandbox
    aprendendo a implementar Arquitetura Orientada a Serviços de modo eficiente.

Discutir

  • Participe da comunidade My developerWorks.
    Entre em contato com outros usuários do developerWorks, enquanto
    explora os blogs, fóruns, grupos e wikis orientados ao desenvolvedor.

***

artigo publicado originalmente no developerWorks Brasil, por Asha Shivalingaiah

Asha Shivalingaiah é engenheira de software e trabalha como testadora no
Australia Development Lab para a IBM Security Solution, Tivoli. Desde
que passou a fazer parte da equipe IBM Rational em 2008, trabalha com
Rational Rhapsody e outras ferramentas do portfólio Rational para
gerenciar o ciclo de vida de desenvolvimento de software. É especialista
em linguagens de modelagem como UML 2 e SysML e no uso do Rational
Rhapsody para desenvolvimento baseado em modelo.