Noções de Segurança no SQL Server - Parte I

Por Robson Brandão, MVP

Uma frase que gosto muito de falar é: "Segurança não é um produto, mas sim um processo". Não existe um produto seguro, se os processos adotados para segurança não são seguros.

O primeiro item da segurança em um servidor é o LOGIN. E por isso, é por ele que você deve começar os procedimentos de segurança, como senhas longas e complexas.

Se você for capaz de garantir que sua rede está totalmente protegida de ferramentas de captura de pacotes, que todo mundo na rede é honesto e confiável, e que na sua empresa não há segredos e que nenhum dado precisa ser protegido ou escondido, então, você pode ignorar como as contas de usuários e senhas trafegam entre o cliente e o servidor da sua empresa e continuar usando senhas simples como teste123.

Como esse cenário não existe você precisa impedir que as senhas estejam roubadas, que as identidades dos usuários sejam descobertas, e que os dados possam ser vistos por pessoas erradas.

Estratégias de Senhas no SQL Server

Não pretendo falar nesse artigo sobre os métodos de autenticação do SQL Server, mas sim, de mostrar como trabalha a comunicação entre o cliente e o servidor.

Em versões anteriores do SQL Server, as senhas dos usuários não eram criptografadas na tabela syslogins. O sa podia ler a senha de qualquer usuário simplesmente consultando a syslogins.

A partir da versão 6.5, o SQL Server passou a criptografar as senhas dos usuários num algoritmo de hashing antes de armazená-las na tabela syslogins (SQL Server 6.5) e sysxlogins (SQL Server 7 em diante).

Observando o código da stored procedure de sistema sp_addlogin, podemos entender como esse preocesso é feito:

create procedure sp_addlogin
    @loginame		sysname
   ,@passwd         sysname = Null
   ,@defdb          sysname = 'master'      -- UNDONE: DEFAULT CONFIGURABLE???
   ,@deflanguage    sysname = Null
   ,@sid			varbinary(16) = Null
   ,@encryptopt		varchar(20) = Null
AS
    -- SETUP RUNTIME OPTIONS / DECLARE VARIABLES --
	set nocount on
	Declare @ret    int    -- return value of sp call

    -- CHECK PERMISSIONS --
	IF (not is_srvrolemember('securityadmin') = 1)
	begin
	   dbcc auditevent (104, 1, 0, @loginame, NULL, NULL, @sid)
	   raiserror(15247,-1,-1)
	   return (1)
	end
	ELSE
	begin
	   dbcc auditevent (104, 1, 1, @loginame, NULL, NULL, @sid)
	end

    -- DISALLOW USER TRANSACTION --
	set implicit_transactions off
	IF (@@trancount > 0)
	begin
		raiserror(15002,-1,-1,'sp_addlogin')
		return (1)
	end

    -- VALIDATE LOGIN NAME AS:
    --  (1) Valid SQL Name (SQL LOGIN)
    --  (2) No backslash (NT users only)
    --  (3) Not a reserved login name
	execute @ret = sp_validname @loginame
	if (@ret <> 0)
        return (1)
    if (charindex('\', @loginame) > 0)
    begin
        raiserror(15006,-1,-1,@loginame)
        return (1)
    end

	--Note: different case sa is allowed.
	if (@loginame = 'sa' or lower(@loginame) in ('public'))
	begin
		raiserror(15405, -1 ,-1, @loginame)
		return (1)
	end

    -- LOGIN NAME MUST NOT ALREADY EXIST --
	if exists(select * from master.dbo.syslogins where loginname = @loginame)
	begin
		raiserror(15025,-1,-1,@loginame)
		return (1)
	end

	-- VALIDATE DEFAULT DATABASE --
	IF db_id(@defdb) IS NULL
	begin
		raiserror(15010,-1,-1,@defdb)
	    return (1)
	end

	-- VALIDATE DEFAULT LANGUAGE --
	IF (@deflanguage IS NOT Null)
	begin
		Execute @ret = sp_validlang @deflanguage
		IF (@ret <> 0)
			return (1)
	end
	ELSE
	begin
		select @deflanguage = name from master.dbo.syslanguages
		where langid = @@default_langid	--server default language

		if @deflanguage is null
			select @deflanguage = N'us_english'
	end

	-- VALIDATE SID IF GIVEN --
	if ((@sid IS NOT Null) and (datalength(@sid) <> 16))
	begin
		raiserror(15419,-1,-1)
	 	return (1)
	end
	else if @sid is null
		select @sid = newid()
	if (suser_sname(@sid) IS NOT Null)
	begin
		raiserror(15433,-1,-1)
	 	return (1)
	end

	-- VALIDATE AND USE ENCRYPTION OPTION --
	declare @xstatus smallint
	select @xstatus = 2	-- access
	if @encryptopt is null
		select @passwd = pwdencrypt(@passwd)
	else if @encryptopt = 'skip_encryption_old'
	begin
		select @xstatus = @xstatus | 0x800,	-- old-style encryption
			@passwd = convert(sysname, convert(varbinary(30), convert(varchar(30), @passwd)))
	end
	else if @encryptopt <> 'skip_encryption'
	begin
		raiserror(15600,-1,-1,'sp_addlogin')
		return 1
	end

    -- ATTEMPT THE INSERT OF THE NEW LOGIN --
	INSERT INTO master.dbo.sysxlogins VALUES
        (NULL, @sid, @xstatus, getdate(),
            getdate(), @loginame, convert(varbinary(256), @passwd),
            db_id(@defdb), @deflanguage)
	if @@error <> 0		-- this indicates we saw duplicate row
        return (1)

	-- UPDATE PROTECTION TIMESTAMP FOR MASTER DB, TO INDICATE SYSLOGINS CHANGE --
	exec('use master grant all to null')

    -- FINALIZATION: RETURN SUCCESS/FAILURE --
	raiserror(15298,-1,-1)
	return  (0)	-- sp_addlogin

GO

Note que para criptografar as senhas de usuários, a stored procedure usa uma função não documentada chamada pwdencrypt(), que pega a senha sem criptografia como parâmetro e retorna o valor criptografado, sendo impossível reverter o processo para descriptografar a senha. O algoritmo dessa função é guardado a sete chaves pela Microsoft.

Outra função não documentada, é a pwdcompare().

Ela pega dois parâmetros para comparação, a senha não-criptografada e um texto criptografado. Se o retorno for TRUE, é porque o hash da senha não-criptografada bateu com o texto criptografado, caso contrário, o valor será FALSE. É assim que o SQL Server verifica se a senha digitada em texto puro do usuário é a mesma daquela que está na tabela sysxlogins criptografada.

É possível fazer um ataque de força bruta nas senhas dos usuários gerando todas as combinações possíveis de caracteres e testá-los com a função pwdcompare(), mas senhas longas fará com que um ataque demore muito tempo.

Criando senhas complexas

Nem o melhor esquema de criptografia do mundo pode proteger você com senhas fracas.

A sp_password é uma stored procedure de sistema usado pelo SQL Server desde a versão 6.5 que serve para "setar" e alterar senhas. No SQL Server 6.5, usuários podiam ter senhas de até 30 caracteres. O SQL Server 7 e o 2000 permitem ter até 128 caracteres e permitem todos os caracteres do teclado. O número de caracteres numa senha é muito importante, pois determina maior complexidade da senha.

Todas as versões do SQL Server interpretam senhas diferenciando case-sensitive e case-insensitive. Se o servidor usa case-insensitive, as senhas são convertidas para maiúsculas antes de serem guardadas na syslogins (SQL Server 6.5) e sysxlogins (SQL Server 7 em diante) e são comparadas com as entradas nessas tabelas quando o usuário logar.

Ex: a senha Password0 será armazenada como PASSWORD0 no servidor com case-insensitive sort order.

Vamos começar com um exemplo de senha que usa letras de A a Z em maiúsculas. Cada caractere da senha poderá ter até 26 opções possíveis e nenhum caractere é dependente de outro. Essas 26 opções tem n cominações possíveis, sendo que n = número de caracteres da senha.

Exemplo prático:

Normalmente nas empresas, usam-se senhas com 6 caracteres. Isso significa que há de 266 a 308.915.776 senhas possíveis usando letras maiúsculas.

Se o padrão de senhas conter letras maiúsculas e minúsculas, as opções mudam para 52 e o número de senhas possíveis vão de 336 a 19.770.609.664.

Considerando um ataque que cheque 1000 senhas por segundo, o ataque demoraria 85,81 horas.

Vamos mais além, se você adicionar, letras maiúsculas e minúsculas, e números, o número de combinações passa para 72n e se você mudar a senha de 6 para 8 caracteres, para quebrar a senha vai levar de 604,66 a 783.641,64 horas, considerando as 1000 senhas por segundo.

Agora se você acrescentar 1 caractere especial ! @ # $ % ^ & * ( ) à sua senha com 8 caracteres e caracteres em maiúsculos e minúsculos, você terá de 468 a 20.047.612.231.936 possibilidades de combinações de senha. O ataque demoraria 5.568.781,17 horas (635,7 anos).

Bom, acho que te convenci sobre a importância de uma senha complexa, principalmente para contas administrativas, que recomendo 20 caracteres pelo menos.