Olá pessoal,
Tudo bem com vocês ?
Neste post eu gostaria de demonstrar a vocês Como recuperar o código-fonte de um objeto criptografado (WITH ENCRYPTION) no SQL Server. Quantas vezes eu já vi programadores criptografando objetos no SQL Server na falsa esperança que esse código realmente vai ficar protegido contra alterações e visualizações por parte de outros usuários.
Introdução
Para os que já trabalham na TI há algum tempo, acho que é unanimidade entre eles de que não existe proteção de código-fonte realmente efetiva.. Se você programa em .NET, tem o JetBrains dotPeck. Se você criptografa seus objetos no Oracle Database, existe o site Unwrap It! que quebra essa criptografia ONLINE. No PHP, já utilizei o Zend Guard para criptografar os arquivos fonte e existem ferramentas online que também quebram a criptografia.
No SQL Server isso também não é diferente. Existem inclusive, ferramentas (Redgate SQL Prompt, ApexSQL Complete e várias outras) que fazem isso pra você de forma automática, mostrando o código-fonte de objetos criptografados de forma tão natural e transparente que você nem percebe que o objeto era criptografado.
Embora seja possível quebrar a criptografia de objetos no SQL Server, isso não significa diretamente uma falha de segurança. Para que seja possível quebrar essa criptografia, você precisará utilizar uma conexão DAC, o qual já expliquei como habilitar e utilizar no post Habilitando e utilizando a conexão remota dedicada para administrador (DAC) no SQL Server.
A conexão DAC é uma conexão dedicada para sysadmins (o que limita bastante os usuários que podem realizar esse procedimento) que precisam se conectar em instâncias que estão enfrentando problemas que impedem novas conexões, como por exemplo, limite de usuários simultâneos da instância atingido.
Como a forma de conexão é diferente (inclusive usa uma porta dedicada), essa conexão “especial” fica sempre disponível, mesmo nessa situação. Vale ressaltar que essa conexão só permite 1 usuário conectado por vez, então se alguém já está conectado, outra pessoa não vai conseguir utilizá-la.
Todo esse requisito de utilizar a conexão DAC é simplesmente para que você consiga ler os dados da tabela sys.sysobjvalues. Mesmo que você seja sysadmin, você só conseguirá acessar os dados dessa tabela utilizando a conexão DAC.
Exemplo SEM utilizar a conexão DAC
Msg 208, Level 16, State 1, Line 16
Invalid object name ‘sys.sysobjvalues’.
Exemplo utilizando a conexão DAC
O SQL Server utiliza o algoritmo de criptografia RC4™ stream cipher para criptografar objetos e armazena esses dados na coluna imageval da tabela sys.sysobjvalues. Neste post, vou demonstrar uma algoritmo que permite recuperar essa informação e retornar o código-fonte original do objeto criptografado.
Como listar os objetos criptografados no banco de dados
Para identificar quais objetos do seu banco de dados estão criptografados, separei 4 consultas para atingir esse objetivo:
Utilizando a view sys.sql_modules:
1 2 3 4 5 6 7 8 |
SELECT B.[name], B.[type_desc] FROM sys.sql_modules A JOIN sys.objects B ON A.[object_id] = B.[object_id] WHERE A.[definition] IS NULL |
Utilizando a função OBJECTPROPERTY:
1 2 3 4 5 6 7 |
SELECT [name], [type_desc] FROM sys.objects WHERE OBJECTPROPERTY([object_id], 'IsEncrypted') = 1 |
Utilizando a view sys.syscomments:
1 2 3 4 5 6 7 8 |
SELECT A.[name], A.[type_desc] FROM sys.objects A JOIN sys.syscomments B ON A.[object_id] = B.id WHERE B.[encrypted] = 1 |
Utilizando o database INFORMATION_SCHEMA:
1 2 3 4 5 6 7 |
SELECT ROUTINE_NAME, ROUTINE_TYPE FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_DEFINITION IS NULL |
Como criar uma SP criptografada
Para este post, vou criar uma SP bem simples e criptografada, e vou demonstrar como obter o código fonte dela.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
USE [dirceuresende] GO CREATE PROCEDURE dbo.stpTeste_Decrypt WITH ENCRYPTION AS BEGIN ----------------------------- Comentário da SP ----------------------------- PRINT 'dirceuresende' SELECT 'dirceuresende' END |
Como vocês podem observar abaixo, não é possível visualizar o código-fonte de um objeto criptografado utilizando a view sys.sql_modules, mesmo que você seja membro da role sysadmin.
Nem mesmo utilizando a procedure de sistema sp_helptext é possível obter o código-fonte desse objeto, retornando a mensagem de alerta “The text for object ‘dbo.stpTeste_Decrypt’ is encrypted.”, conforme podemos visualizar abaixo:
Quebrando a criptografia de objetos pelo SSMS
Parar quebrar a criptografia de objetos pelo SQL Server Management Studio (SSMS), inicie uma conexão DAC na instância desejada. Para isso, basta adicionar o prefixo “ADMIN:” antes do nome da sua instância, ficando assim: “ADMIN:servidor\instancia”, conforme a imagem abaixo:
Agora utilize o código abaixo e você já poderá visualizar o código-fonte do seu objeto criptografado.
Código-fonte
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
USE [dirceuresende] GO SET NOCOUNT ON -- Aqui você tem que alterar para o objeto que você quer descriptografar DECLARE @ObjectOwnerOrSchema NVARCHAR(128) = 'dbo', @ObjectName NVARCHAR(128) = 'stpTeste_Decrypt' DECLARE @i INT, @ObjectDataLength INT, @ContentOfEncryptedObject NVARCHAR(MAX), @ContentOfDecryptedObject NVARCHAR(MAX), @ContentOfFakeObject NVARCHAR(MAX), @ContentOfFakeEncryptedObject NVARCHAR(MAX), @ObjectType NVARCHAR(128), @ObjectID INT SET @ObjectID = OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']') IF @ObjectID IS NULL BEGIN RAISERROR('The object name or schema provided does not exist in the database', 16, 1) RETURN END IF NOT EXISTS(SELECT TOP 1 * FROM sys.syscomments WHERE id = @ObjectID AND encrypted = 1) BEGIN RAISERROR('The object provided exists however it is not encrypted. Aborting.', 16, 1) RETURN END IF OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']', 'PROCEDURE') IS NOT NULL SET @ObjectType = 'PROCEDURE' ELSE IF OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']', 'TRIGGER') IS NOT NULL SET @ObjectType = 'TRIGGER' ELSE IF OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']', 'VIEW') IS NOT NULL SET @ObjectType = 'VIEW' ELSE SET @ObjectType = 'FUNCTION' SELECT TOP 1 @ContentOfEncryptedObject = imageval FROM sys.sysobjvalues WHERE [objid] = OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']') AND valclass = 1 and subobjid = 1 SET @ObjectDataLength = DATALENGTH(@ContentOfEncryptedObject)/2 SET @ContentOfFakeObject = N'ALTER ' + @ObjectType + N' [' + @ObjectOwnerOrSchema + N'].[' + @ObjectName + N'] WITH ENCRYPTION AS' WHILE DATALENGTH(@ContentOfFakeObject)/2 < @ObjectDataLength BEGIN IF DATALENGTH(@ContentOfFakeObject)/2 + 4000 < @ObjectDataLength SET @ContentOfFakeObject = @ContentOfFakeObject + REPLICATE(N'-', 4000) ELSE SET @ContentOfFakeObject = @ContentOfFakeObject + REPLICATE(N'-', @ObjectDataLength - (DATALENGTH(@ContentOfFakeObject)/2)) END SET XACT_ABORT OFF BEGIN TRAN EXEC(@ContentOfFakeObject) IF @@ERROR <> 0 ROLLBACK TRAN SELECT TOP 1 @ContentOfFakeEncryptedObject = imageval FROM sys.sysobjvalues WHERE [objid] = OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']') AND valclass = 1 and subobjid = 1 IF @@TRANCOUNT > 0 ROLLBACK TRAN SET @ContentOfFakeObject = N'CREATE ' + @ObjectType + N' [' + @ObjectOwnerOrSchema + N'].[' + @ObjectName + N'] WITH ENCRYPTION AS' WHILE DATALENGTH(@ContentOfFakeObject)/2 < @ObjectDataLength BEGIN IF DATALENGTH(@ContentOfFakeObject)/2 + 4000 < @ObjectDataLength SET @ContentOfFakeObject = @ContentOfFakeObject + REPLICATE(N'-', 4000) ELSE SET @ContentOfFakeObject = @ContentOfFakeObject + REPLICATE(N'-', @ObjectDataLength - (DATALENGTH(@ContentOfFakeObject)/2)) END SET @i = 1 SET @ContentOfDecryptedObject = N'' WHILE DATALENGTH(@ContentOfDecryptedObject)/2 < @ObjectDataLength BEGIN IF DATALENGTH(@ContentOfDecryptedObject)/2 + 4000 < @ObjectDataLength SET @ContentOfDecryptedObject = @ContentOfDecryptedObject + REPLICATE(N'A', 4000) ELSE SET @ContentOfDecryptedObject = @ContentOfDecryptedObject + REPLICATE(N'A', @ObjectDataLength - (DATALENGTH(@ContentOfDecryptedObject)/2)) END WHILE (@i <= @ObjectDataLength) BEGIN SET @ContentOfDecryptedObject = STUFF(@ContentOfDecryptedObject, @i, 1, NCHAR( UNICODE(SUBSTRING(@ContentOfEncryptedObject, @i, 1)) ^ ( UNICODE(SUBSTRING(@ContentOfFakeObject, @i, 1)) ^ UNICODE(SUBSTRING(@ContentOfFakeEncryptedObject, @i, 1)) ))) SET @i = @i + 1 END SET @i = 0 WHILE DATALENGTH(@ContentOfDecryptedObject)/2 > (@i + 1)*2000 BEGIN PRINT(SUBSTRING(@ContentOfDecryptedObject, 1 + 2000*@i, 2000*(@i + 1))) SET @i = @i + 1 END PRINT(SUBSTRING(@ContentOfDecryptedObject, 1 + 2000*@i, 2000*(@i + 1))) |
Quebrando a criptografia de objetos pelo CLR (C#)
Agora que te expliquei como quebrar os objetos, vou te mostrar uma solução mais prática e funcional no dia a dia, e que você pode permitir que até mesmo usuários que não sejam sysadmin consigam visualizar o fonte de objetos criptografados, e sem precisar que você abra uma nova conexão DAC, podendo ser até mesmo de outro servidor ou instância.
Para isso, vou utilizar o CLR para realizar a conexão via DAC, utilizando um usuário sysadmin. Com isso, mesmo que a pessoa não seja sysadmin, basta que ela tenha permissão nessa função, ela poderá visualizar o fonte de objetos não criptografados. Para quebrar o código, vou utilizar o mesmo código mostrado acima, apenas executando ele pelo CLR (C#) em uma conexão DAC, transparente para o usuário que está utilizando a função.
Código-fonte
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
using System.Data; using System.Data.SqlTypes; using System.Data.SqlClient; using Microsoft.SqlServer.Server; public partial class UserDefinedFunctions { [Microsoft.SqlServer.Server.SqlFunction( DataAccess = DataAccessKind.Read, SystemDataAccess = SystemDataAccessKind.Read )] public static SqlString fncDescriptografa_Objeto(SqlString Ds_Servidor, SqlString Ds_Database, SqlString Ds_Schema, SqlString Ds_Objeto) { var conexaoLocal => "data source=LOCALHOST;initial catalog=CLR;Application Name=SQLCLR;persist security info=False;Enlist=False;packet size=4096;user id='usuario';password='senha'"; var servidor = conexaoLocal.Replace("LOCALHOST", $"ADMIN:{Ds_Servidor.Value}"); using (var con = new SqlConnection(servidor)) { con.Open(); using (var cmd = new SqlCommand($@" USE [{Ds_Database.Value}]; DECLARE @ObjectOwnerOrSchema NVARCHAR(128) = '{Ds_Schema}', @ObjectName NVARCHAR(128) = '{Ds_Objeto}' DECLARE @i INT, @ObjectDataLength INT, @ContentOfEncryptedObject NVARCHAR(MAX), @ContentOfDecryptedObject NVARCHAR(MAX), @ContentOfFakeObject NVARCHAR(MAX), @ContentOfFakeEncryptedObject NVARCHAR(MAX), @ObjectType NVARCHAR(128), @ObjectID INT SET NOCOUNT ON SET @ObjectID = OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']') IF (@ObjectID IS NULL) BEGIN RAISERROR('The object name or schema provided does not exist in the database', 16, 1) RETURN END IF NOT EXISTS(SELECT TOP 1 * FROM syscomments WHERE id = @ObjectID AND encrypted = 1) BEGIN RAISERROR('The object provided exists however it is not encrypted. Aborting.', 16, 1) RETURN END IF OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']', 'PROCEDURE') IS NOT NULL SET @ObjectType = 'PROCEDURE' ELSE IF OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']', 'TRIGGER') IS NOT NULL SET @ObjectType = 'TRIGGER' ELSE IF OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']', 'VIEW') IS NOT NULL SET @ObjectType = 'VIEW' ELSE SET @ObjectType = 'FUNCTION' SELECT TOP 1 @ContentOfEncryptedObject = imageval FROM sys.sysobjvalues WHERE objid = OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']') AND valclass = 1 and subobjid = 1 SET @ObjectDataLength = DATALENGTH(@ContentOfEncryptedObject)/2 SET @ContentOfFakeObject = N'ALTER ' + @ObjectType + N' [' + @ObjectOwnerOrSchema + N'].[' + @ObjectName + N'] WITH ENCRYPTION AS' WHILE DATALENGTH(@ContentOfFakeObject)/2 < @ObjectDataLength BEGIN IF DATALENGTH(@ContentOfFakeObject)/2 + 4000 < @ObjectDataLength SET @ContentOfFakeObject = @ContentOfFakeObject + REPLICATE(N'-', 4000) ELSE SET @ContentOfFakeObject = @ContentOfFakeObject + REPLICATE(N'-', @ObjectDataLength - (DATALENGTH(@ContentOfFakeObject)/2)) END SET XACT_ABORT OFF BEGIN TRAN EXEC(@ContentOfFakeObject) IF @@ERROR <> 0 ROLLBACK TRAN SELECT TOP 1 @ContentOfFakeEncryptedObject = imageval FROM sys.sysobjvalues WHERE objid = OBJECT_ID('[' + @ObjectOwnerOrSchema + '].[' + @ObjectName + ']') AND valclass = 1 and subobjid = 1 IF @@TRANCOUNT > 0 ROLLBACK TRAN SET @ContentOfFakeObject = N'CREATE ' + @ObjectType + N' [' + @ObjectOwnerOrSchema + N'].[' + @ObjectName + N'] WITH ENCRYPTION AS' WHILE DATALENGTH(@ContentOfFakeObject) / 2 < @ObjectDataLength BEGIN IF DATALENGTH(@ContentOfFakeObject) / 2 + 4000 < @ObjectDataLength SET @ContentOfFakeObject = @ContentOfFakeObject + REPLICATE(N'-', 4000) ELSE SET @ContentOfFakeObject = @ContentOfFakeObject + REPLICATE(N'-', @ObjectDataLength - (DATALENGTH(@ContentOfFakeObject) / 2)) END SET @i = 1 SET @ContentOfDecryptedObject = N'' WHILE DATALENGTH(@ContentOfDecryptedObject) / 2 < @ObjectDataLength BEGIN IF DATALENGTH(@ContentOfDecryptedObject) / 2 + 4000 < @ObjectDataLength SET @ContentOfDecryptedObject = @ContentOfDecryptedObject + REPLICATE(N'A', 4000) ELSE SET @ContentOfDecryptedObject = @ContentOfDecryptedObject + REPLICATE(N'A', @ObjectDataLength - (DATALENGTH(@ContentOfDecryptedObject) / 2)) END WHILE (@i <= @ObjectDataLength) BEGIN SET @ContentOfDecryptedObject = STUFF(@ContentOfDecryptedObject, @i, 1, NCHAR( UNICODE(SUBSTRING(@ContentOfEncryptedObject, @i, 1)) ^ ( UNICODE(SUBSTRING(@ContentOfFakeObject, @i, 1)) ^ UNICODE(SUBSTRING(@ContentOfFakeEncryptedObject, @i, 1)) )) ) SET @i = @i + 1 END SELECT @ContentOfDecryptedObject", con) { CommandType = CommandType.Text }) { var resultado = (cmd.ExecuteScalar() != null) ? (string) cmd.ExecuteScalar() : ""; return resultado; } } } } |
Após criar a função no seu projeto CLR e publicar a alteração, vamos utilizá-la:
Vale lembrar que nessa solução, o usuário que for utilizar a função não precisa (e nem deve) estar utilizando uma conexão DAC.
Referências:
https://sqlperformance.com/2016/05/sql-performance/the-internals-of-with-encryption
https://www.codeproject.com/Articles/5068/RC-Encryption-Algorithm-C-Version
https://sqljunkieshare.com/2012/03/07/decrypting-encrypted-stored-procedures-views-functions-in-sql-server-20052008-r2/
https://pt.linkedin.com/pulse/uma-procedure-criptografada-e-agora-f%C3%A1bio-oliveira
É isso aí, pessoal!
Espero que tenham gostado desse post e até a próxima.
SQL Server how to retrieve view get decrypt stored procedure source code encrypted objects WITH ENCRYPTION
SQL Server how to retrieve view get decrypt stored procedure source code encrypted objects WITH ENCRYPTION
Dirceu, ótimo post… e tem alguma forma de proteger o código realmente, para não ser modificado?
Oi Rodrigo, tudo bem?
O código só pode ser visualizado se o usuário tiver permissão de view definition. E só pode alterar, se ele tiver permissão de alter.
Então é só gerenciar as permissões que não tem problema.
Uma forma de aumentar mais a segurança e dificultar o acesso ao código-fonte, é criar as procedures utilizando SQLCLR. Aí só dá pra editar e visualizar o código exportando o assembly da dll compilada para o disco e fazer engenharia reversa do código C#.
Dirceu, parabéns pela postagem, excelente dica, aproveitando quero ver se realmente tem como proteger StoredProcedure com WITH ENCRYPTION
Bom dia Dirceu, ótimo material. Me ajudou muito. Obrigado pelo compartilhamento.
De nada, Marcos.
Fico feliz em saber que você gostou
Bom dia! Muito bom post, aliás como a maioria. Infelizmente habilitei o dac mais não estou conseguindo logar, esta dando o seguinte erro:
“nao há suporte para as conexoes de administrador dedicadas via ssms, pois isso estabelece varias conexoes por design”
Bom dia,
Ótimo post, mas não funcionou para mim
Olá Renato!
Tudo bem?
Qual a versão do seu SQL Server? Você conectou utilizando a conexão DAC ?