domingo, 9 de setembro de 2007

Nosso Amigo Hash - Parte VI - Complemento

Hash Aplicado na Forense Computacional - Prática

Usando Banco de Dados e hash parcial

Este é um complemento do artigo anterior, onde estudamos o uso do utilitário ssdeep, que calcula hashes parciais dos arquivos, para fazer a classificação dos arquivos entre conhecidos e desconhecidos, ou ainda para localizar na imagem forense possíveis arquivos maliciosos ou criminosos, mas que receberam algum tipo de alteração para serem descaracterizados.

A técnica, inicialmente, seria substituir o uso da base NSRL (que só contém hashes MD5 e SHA-1) por uma criada por nós, onde o hash dos arquivos seriam computados pelo ssdeep. Essa base tem a tendência de crcescer cada vez mais (e quanto maior melhor, certo ?), porém o utilitário teria problemas de performance e de memória para realizar a operação. Esses problemas já são conhecidos nossos e falamos deles em um artigo passado (Parte V).

A idéia agora é aplicar o mesmo princípio, mas com algumas dificuldades a mais, porque enquanto a comparação com hashes MD5 ou SHA-1 é imediata (se são ou não iguais), a comparação dos hashes parciais precisa de um cálculo entre ambos, indicando o score de proximidade entre os arquivos. Por conta disso, estudei os fontes do ssdeep e converti as partes correlacionadas à nossa necessidade do C para o Transact SQL.

Vamos ao procedimento.

1) Montando uma base de hashes ssdeep:

A base pode ser obtida executando o utilitário ssdeep sobre os arquivos que quisermos catalogar. O comando é:
C:\catalogar>ssdeep -l * > base.txt

Esse comando deve ser usado na primeira vez que estiver carregando a base, e em todas as vezes que for necessário adicionar hashes a base.

2) Importando a base de hashes parciais:

O procedimento de importação é o seguinte (Antes de iniciar, remova a tabela Aux, se existir):

1) Clique com o botão direito sobre Tables e em seguida, clique na opção Import Tables.
2) Indique que o Data Source é um arquivo texto, e indique o arquivo base.txt
3) Deixe as opções default do File Format, mas marque First Row Has Column Names, pois a prmeira linha do arquivo base.txt é um cabeçalho.
4) Clique em Next, e marque Comma como delimitador.
5) Clique em Next e em Next novamente. Troque o nome da tabela destino para Aux.
6) Clique em Next e finalize o processo, importando os dados.

A tabela Aux será criada com os registros importados. Logo em seguida, faça o seguinte:

a) Se essa for a primeira importação da base, execute as seguintes queries no Query Analyser:

select
block=left(ssdeep,charindex(':',ssdeep)-1),
outro=substring(ssdeep, charindex(':',ssdeep)+1, 255),
nome=[1.0--blocksize:hash:hash]
into
#x
from
aux

select
block,
hash1=left(outro,charindex(':',outro)-1),
hash2=substring(outro, charindex(':',outro)+1, 255),
nome
into
sshash
from
#x

Após ser executado, o código acima cria a tabela sshash com a primeira importação dos hashes parciais.

b) Se essa NÃO for a primeira importação da base, execute as seguintes queries no Query Analyser:

select
block=left(ssdeep,charindex(':',ssdeep)-1),
outro=substring(ssdeep, charindex(':',ssdeep)+1, 255),
nome=[1.0--blocksize:hash:hash]
into
#x
from
aux


Insert
sshash
select
block,
hash1=left(outro,charindex(':',outro)-1),
hash2=substring(outro, charindex(':',outro)+1, 255),
nome
from
#x

Após ser executado, o código acima adiciona à tabela sshash os hashes parciais recém importados.

3) Calculando os hashes ssdeep de uma imagem:

Após a imagem montada, deve-se ir para o diretório raiz da mesma e executar o seguinte comando:
/media/mnt>ssdeep -r * > /reports/sshashes.txt

Esse comando calculará os hashes de todos os arquivos em todos os diretórios da imagem.

4) Importando o arquivo de hashes

Esse processo é idêntico ao procedimento 2 em relação a Importação do arquivo para a tabela Aux.

As queries a serem aplicadas logo após são:

a) Caso seja a primeira vez que esse processo estiver sendo usado:

select
block=left(ssdeep,charindex(':',ssdeep)-1),
outro=substring(ssdeep, charindex(':',ssdeep)+1, 255),
nome=[1.0--blocksize:hash:hash]
into
#x
from
aux


select
[case] = 'Codigo-do-caso',
block,
hash1=left(outro,charindex(':',outro)-1),
hash2=substring(outro, charindex(':',outro)+1, 255),
nome
into
sshashComp
from
#x


b) Se essa NÃO for o primeiro caso a ser verificado, execute as seguintes queries no Query Analyser:

select
block=left(ssdeep,charindex(':',ssdeep)-1),
outro=substring(ssdeep, charindex(':',ssdeep)+1, 255),
nome=[1.0--blocksize:hash:hash]
into
#x
from
aux

Insert
sshashComp

select
[Case] = 'Codigo-do-caso'
block,
hash1=left(outro,charindex(':',outro)-1),
hash2=substring(outro, charindex(':',outro)+1, 255),
nome
from
#x

5) Comparando os arquivos da imagem:

Execute no Query Analyser a rotina sscompara. O primeiro parâmetro é o código do caso e o segundo é o score mínimo que desejamos acusar como arquivos próximos.
Ex:
exec sscompara '2007-001', 80

O comando acima vai separar os arquivos do caso 2007-001 que tenham no mínimo 80 de score de proximidade com os arquivos cadastrados, indicando qual é o arquivo na imagem e qual é o arquivo cadastrado.

Ilustrando ...

Imagine que você trabalhe para algum órgão de investigação contra a pedofilia e tenha um diretório cheio de imagens e vídeos com esse conteúdo, capturados e catalogados. Seguindo os passos acima, você poderá criar e manter uma base de hashes parciais, e ao analisar uma imagem forense de um hard disk suspeito, verificar se há algum arquivo na imagem que se assemelhe aos existentes em sua base. Note que se o suspeito usou algum tipo de descaracterização dos vídeos e imagens (troca de bits, crop, etc), ainda assim o método descrito irá indicar a semelhança.

Sugestões de melhoria

- Criar índices nas tabelas
- Criar stored procedures para os comandos após a carga, listados acima.
- Adaptar a rotina sscompara para dar como saída os arquivos da imagem que não tem proximidade nenhuma com a base. Essa alteração é bem simples e vai permitir anular a técnica de Anti-Forense comentada no artigo Parte VI.

Um estudo interessante seria apurar a média do score obtido entre uma imagem original e imagens alteradas a partir dessa, para situações onde apenas um bit é modificado, uso de crops diversos, modificações na resolução, tamanho, ou até mesmo inserção de informações por esteganografia. Alguém se habilita ?

Até o próximo post !

Fontes dos SQLs e Stored Procedures:

CREATE Procedure dbo.eliminate_sequences @seq varchar(255) OUTPUT
as

Declare @ret varchar(255),
@i int,
@j int,
@len int

Set @ret = substring(@seq,1,3)

IF (@ret ='')
return

Set @len = Len(@seq)

Set @i = 4
Set @j = 4

While (@i <= @len) Begin
IF (substring(@seq,@i,1) <> substring(@seq,@i-1,1)) Or
(substring(@seq,@i,1) <> substring(@seq,@i-2,1)) Or
(substring(@seq,@i,1) <> substring(@seq,@i-3,1)) Begin
Set @ret = @ret + substring(@seq, @i,1)

Set @j = @j + 1
End


Set @i = @i + 1
End

Set @seq = @ret

Return

GO

----------------------------------------------------------------------------
Create Procedure dbo.has_common_substring @s1 varchar(255), @s2 varchar(255)

as

If (Len(@s1) <>
Return 0

If (Len(@s2) <>
Return 0

Declare
@i int

Set @i = 1

While (@i <= (Len(@s1)-6)) Begin
If (charindex(substring(@s1,@i,7), @s2) > 0)
Return 1

Set @i = @i + 1
End

Return 0
go


-------------------------------------------------------------------------------
CREATE PROCEDURE dbo.edit_distn @from varchar(255), @from_len int,

@to varchar(255), @to_len int
as

Declare
@row int,
@col int,
@indice int,
@radix int,
@low int,
@swp_int int,
@swp_char varchar(255),
@menor int,
@valor int,
@prim int,
@seg int,
@ter int

Declare @buf table (indice int, valor int)

/* Handle trivial cases when one string is empty */
If ((@from = '') Or (@from_len = 0)) Begin

If ((@to = '') Or (@to_len = 0))
Return 0
Else
Return @to_len
End
Else
If ((@to = '') Or (@to_len = 0))
Return @from_len

Set @radix = 2 * @from_len + 3

If ((@from_len > @to_len) And (@from_len > 2000)) Begin
Set @swp_int = @to_len
Set @to_len = @from_len
Set @from_len = @swp_int

Set @swp_char = @to
Set @to = @from
Set @from = @swp_char
End


Set @indice = 0
If (substring(@from,1,1) = substring(@to,1,1))

Set @menor = 0
Else
Set @menor = 2

Insert @buf values (@indice, @menor)

Set @indice = @indice + 1
Set @low = @menor


Set @col = 1
While (@col < @from_len) Begin
If (substring(@from,@col+1,1) = substring(@to,1,1))

Set @menor = @col
Else
Set @menor = @col + 3

If (@menor > (@col+2))
Set @menor = @col+2

Select @valor = valor from @buf where indice = (@indice-1)

If (@menor > (@valor+1))
Set @menor = @valor + 1

Insert @buf values (@indice, @menor)

If (@menor < @low)
Set @low = @menor

Set @indice = @indice + 1
Set @col = @col + 1
End

/* Now handle the rest of the matrix */

Set @row = 1
While (@row < @to_len) Begin

Set @col = 0
While (@col < @from_len) Begin

If (@row = 0)
Set @prim = @col
Else
If (@col = 0)
Set @prim = @row
Else
Select @prim = valor from @buf where indice = ((@indice + @from_len + 2) % @radix)

If (substring(@from,@col+1,1) != substring(@to, @row+1,1))
Set @prim = @prim+3

If (@row = 0)
Set @seg = @col+1
Else
Select @seg = valor from @buf where indice = ((@indice + @from_len + 3) % @radix)

Set @seg = @seg+1

If (@col = 0)
Set @ter = @row+1
Else
Select @ter = valor from @buf where indice = ((@indice + @radix - 1) % @radix)

Set @ter = @ter+1
Set @menor = @prim


If (@seg < @menor)
Set @menor = @seg

If (@ter < @menor)
Set @menor = @ter

Insert @buf values (@indice, @menor)

If ((substring(@from,@col+1,1) = substring(@to,@row,1)) And
(@col > 0) And
(substring(@from, @col, 1) = substring(@to, @row+1,1))) Begin
If ((@row-1) = 0)
Set @prim = @col-1
Else
If ((@col-1) = 0)
Set @prim = @row-1
Else
Select @prim = valor from @buf where indice = ((@indice + 1) % @radix)

Set @prim = @prim+5

If (@menor > @prim)
Set @menor = @prim

Insert @buf values (@indice, @menor)
End


Select @valor = valor from @buf where indice = @indice

If ((@valor < @low) Or (@col = 0))
Set @low = @valor

Set @indice = (@indice+1) % @radix
Set @col = @col + 1

End

If (@low > 100)
Break
Set @row = @row+1
End

Select @row = valor from @buf where indice = ((@indice+@radix-1) % @radix)

Return @row

Go
---------------------------------------------------------------------------------
CREATE PROCEDURE dbo.score_strings @s1 varchar(255), @s2 varchar(255), @bloco int

as

Declare
@score int,
@len1 int,
@len2 int,
@menor int,
@ret int

Set @len1 = len(@s1)
Set @len2 = len(@s2)

if (@len1 > 64) Or (@len2 > 64)
Return 0

exec @ret = has_common_substring @s1, @s2

If (@ret = 0)
Return 0

exec @score = edit_distn @s1, @len1, @s2, @len2

Set @score = (@score * 64)/(@len1+@len2)

Set @score = (100 * @score) / 64

If (@score >= 100)
return 0

Set @score = 100 - @score
Set @menor = @len1

If (@len2 < @len1)
Set @menor = @len2

if (@score > (@bloco/3 * @menor))
Set @score = (@bloco/3 * @menor)

Return @score
go


----------------------------------------------------------------
CREATE PROCEDURE dbo.spamsum @bloco1 int, @hash11 varchar(255), @hash12 varchar(255), @bloco2 int, @hash21 varchar(255), @hash22 varchar(255)
as

Declare @score int,
@score1 int,
@score2 int

Set @score = 0

IF ((@bloco1 <> @bloco2) AND
(@bloco1 <> (@bloco2*2)) AND
(@bloco2 <> (@bloco1*2)))
return 0

exec eliminate_sequences @hash11 OUTPUT
exec eliminate_sequences @hash12 OUTPUT
exec eliminate_sequences @hash21 OUTPUT
exec eliminate_sequences @hash22 OUTPUT

IF (@hash12 = '') OR (@hash22 = '')
Return 0

If (@bloco1 = @bloco2) BEGIN
exec @score1 = score_strings @hash11, @hash21, @bloco1
exec @score2 = score_strings @hash12, @hash22, @bloco2

Set @score = @score1

IF (@score2 > @score)
Set @score = @score2

END
ELSE
IF (@bloco1 = @bloco2*2)
exec @score = score_strings @hash11, @hash22, @bloco1

ELSE
exec @score = score_strings @hash12, @hash21, @bloco2

return @score
go


------------------------------------------------------------------------------------
-------------------------------------------------------------------
Create Procedure dbo.sscompara @caso varchar(255), @minscore int
as
-- Separa quem tem algum match, obedecendo o score mínimo

Declare @tabCaso Cursor,
@tabBase Cursor,
@score int,
@lbloco1 int,
@lhash11 varchar(255),
@lhash12 varchar(255),
@filename varchar(255),
@lbloco2 int,
@lhash21 varchar(255),
@lhash22 varchar(255),
@nome varchar(255)

Declare @saida table (score int, arq1 varchar(255), base varchar(255))

Set @tabBase = Cursor Fast_Forward
For
Select block, hash1, hash2, [nome] from sshash

Set @tabCaso = Cursor Scroll Read_only
For
Select block, hash1, hash2, nome from sshashComp where [case] = @caso

Open @tabBase
Open @tabCaso


Fetch Next from @tabBaseinto @lbloco1, @lhash11, @lhash12, @filename
While (@@Fetch_Status = 0) Begin


Fetch First from @tabCaso
into @lbloco2, @lhash21, @lhash22, @nome
While (@@Fetch_Status = 0) Begin
exec @score = spamsum @lbloco1, @lhash11, @lhash12, @lbloco2, @lhash21,

@lhash22

if (@score >= @minscore)
Insert @saida values (@score, @nome, @filename)

Fetch Next from @tabCaso
into @lbloco2, @lhash21, @lhash22, @nome
End

Fetch Next from @tabBase
into @lbloco1, @lhash11, @lhash12, @filename

End

Close @tabBase
Close @tabCaso

Deallocate @tabBase
Deallocate @tabCaso

Select * from @saida

go

2 comentários:

Anônimo disse...

Bom dia.

Estou fazendo um trabalho sobre o ssdeep e gostaria de saber se você teria esta mesma procedure para o Postgres.

Obrigada
Daniele

Tony Rodrigues disse...

Infelizmente não, Dani. Imagino que não seja tão complicado fazer o port, pois o script usa funcionalidades presentes na maioria das sintaxes de SQL dos databases.

[]s

Tony