Database
 sql >> Database >  >> RDS >> Database

Gli interni di WITH ENCRYPTION

È abbastanza facile per un amministratore di SQL Server recuperare il testo di stored procedure, viste, funzioni e trigger protetti utilizzando WITH ENCRYPTION . Sono stati scritti molti articoli su questo e sono disponibili diversi strumenti commerciali. Lo schema di base del metodo comune è:

  1. Ottieni il modulo crittografato (A) utilizzando la Connessione amministratore dedicata.
  2. Avvia una transazione.
  3. Sostituisci la definizione dell'oggetto con un testo noto (B) almeno della stessa lunghezza dell'originale.
  4. Ottieni il modulo crittografato per il testo noto (C).
  5. Ripristina la transazione per lasciare l'oggetto di destinazione nel suo stato iniziale.
  6. Ottieni l'originale non crittografato applicando un'esclusiva o a ciascun carattere:A XOR (B XOR C)

È tutto abbastanza semplice, ma sembra un po' magico:non spiega molto su come e perché funziona . Questo articolo copre questo aspetto per quelli di voi che trovano questo tipo di dettagli interessanti e fornisce un metodo alternativo per la decrittazione che è più illustrativo del processo.

Il codice di flusso

L'algoritmo di crittografia sottostante utilizzato da SQL Server per la crittografia del modulo è il cifrario a flusso RC4™. Uno schema del processo di crittografia è:

  1. Inizializza la cifratura RC4 con una chiave crittografica.
  2. Genera un flusso pseudocasuale di byte.
  3. Combina il testo normale del modulo con il flusso di byte usando Exclusive-or.

Possiamo vedere questo processo che si verifica utilizzando un debugger e simboli pubblici. Ad esempio, la traccia dello stack seguente mostra SQL Server che inizializza la chiave RC4 mentre si prepara a crittografare il testo del modulo:

Il prossimo mostra SQL Server che crittografa il testo utilizzando il flusso di byte pseudocasuale RC4:

Come la maggior parte dei cifrari a flusso, il processo di decrittazione è lo stesso della crittografia, sfruttando il fatto che esclusivo-o è reversibile (A XOR B XOR B = A ).

L'uso di un codice di flusso è il motivo esclusivo-o viene utilizzato nel metodo descritto all'inizio dell'articolo. Non c'è nulla di intrinsecamente pericoloso nell'uso esclusivo o, a condizione che venga utilizzato un metodo di crittografia sicuro, la chiave di inizializzazione viene mantenuta segreta e la chiave non viene riutilizzata.

RC4 non è particolarmente forte, ma non è questo il problema principale qui. Detto questo, vale la pena notare che la crittografia tramite RC4 viene gradualmente rimossa da SQL Server ed è deprecata (o disabilitata, a seconda della versione e del livello di compatibilità del database) per operazioni utente come la creazione di una chiave simmetrica.

La chiave di inizializzazione RC4

SQL Server utilizza tre informazioni per generare la chiave utilizzata per inizializzare il cifrario di flusso RC4:

  1. Il GUID della famiglia di database.

    Questo può essere ottenuto più facilmente interrogando sys.database_recovery_status . È anche visibile in comandi non documentati come DBCC DBINFO e DBCC DBTABLE .

  2. L'ID oggetto del modulo di destinazione.

    Questo è solo l'ID oggetto familiare. Si noti che non tutti i moduli che consentono la crittografia sono con ambito schema. Dovrai utilizzare le visualizzazioni dei metadati (sys.triggers o sys.server_triggers ) per ottenere l'ID oggetto per i trigger DDL e con ambito server, anziché sys.objects o OBJECT_ID , poiché funzionano solo con oggetti con ambito schema.

  3. ID oggetto secondario del modulo di destinazione.

    Questo è il numero di procedura per le stored procedure numerate. È 1 per una stored procedure non numerata e zero in tutti gli altri casi.

Usando di nuovo il debugger, possiamo vedere il GUID della famiglia che viene recuperato durante l'inizializzazione della chiave:

Il GUID della famiglia di database è identificatore univoco , l'ID oggetto è intero e l'ID oggetto secondario è smallint .

Ogni parte della chiave deve essere convertito in un formato binario specifico. Per il GUID della famiglia di database, conversione dell'identificatore univoco digita su binary(16) produce la rappresentazione binaria corretta. I due ID devono essere convertiti in binari nella rappresentazione little-endian (prima il byte meno significativo).

Nota: Fai molta attenzione a non fornire accidentalmente il GUID come stringa! Deve essere digitato identificatore univoco .

Lo snippet di codice seguente mostra le operazioni di conversione corrette per alcuni valori di esempio:

DECLARE 
    @family_guid binary(16) = CONVERT(binary(16), {guid 'B1FC892E-5824-4FD3-AC48-FBCD91D57763'}),
    @objid binary(4) = CONVERT(binary(4), REVERSE(CONVERT(binary(4), 800266156))),
    @subobjid binary(2) = CONVERT(binary(2), REVERSE(CONVERT(binary(2), 0)));

Il passaggio finale per generare la chiave di inizializzazione RC4 consiste nel concatenare i tre valori binari sopra in un unico binary(22) e calcolare l'hash SHA-1 del risultato:

DECLARE 
    @RC4key binary(20) = HASHBYTES('SHA1', @family_guid + @objid + @subobjid);

Per i dati di esempio forniti sopra, la chiave di inizializzazione finale è:

0x6C914908E041A08DD8766A0CFEDC113585D69AF8

Il contributo dell'ID oggetto e dell'ID oggetto secondario del modulo di destinazione all'hash SHA-1 è difficile da vedere in un singolo screenshot del debugger, ma il lettore interessato può fare riferimento allo smontaggio di una porzione di initspkey sotto:

call    sqllang!A_SHAInit
lea     rdx,[rsp+40h]
lea     rcx,[rsp+50h]
mov     r8d,10h
call    sqllang!A_SHAUpdate
lea     rdx,[rsp+24h]
lea     rcx,[rsp+50h]
mov     r8d,4
call    sqllang!A_SHAUpdate
lea     rdx,[rsp+20h]
lea     rcx,[rsp+50h]
mov     r8d,2
call    sqllang!A_SHAUpdate
lea     rdx,[rsp+0D0h]
lea     rcx,[rsp+50h]
call    sqllang!A_SHAFinal
lea     r8,[rsp+0D0h]
mov     edx,14h
mov     rcx,rbx
call    sqllang!rc4_key (00007fff`89672090)

Lo SHAINit e SHAUpdate le chiamate aggiungono componenti all'hash SHA, che viene infine calcolato da una chiamata a SHAFinal .

Lo SHAINit call contribuisce con 10 h byte (16 decimali) archiviati in [rsp+40h], che è il GUID della famiglia . Il primo SHAUpdate call aggiunge 4 byte (come indicato nel registro r8d), memorizzati in [rsp+24h], che è l'oggetto ID. Il secondo SHAUpdate call aggiunge 2 byte, memorizzati in [rsp+20h], che è il subobjid .

Le istruzioni finali passano l'hash SHA-1 calcolato alla routine di inizializzazione della chiave RC4 rc4_key . La lunghezza dell'hash è memorizzata nel registro edx:14h (20 decimali) byte, che è la lunghezza dell'hash definita per SHA e SHA-1 (160 bit).

L'implementazione di RC4

L'algoritmo di base RC4 è ben noto e relativamente semplice. Sarebbe meglio implementato in un linguaggio .Net per motivi di efficienza e prestazioni, ma di seguito è presente un'implementazione T-SQL.

Queste due funzioni T-SQL implementano l'algoritmo di pianificazione delle chiavi RC4 e il generatore di numeri pseudocasuali e sono state originariamente scritte dall'MVP di SQL Server Peter Larsson. Ho apportato alcune modifiche minori per migliorare un po' le prestazioni e consentire la codifica e la decodifica di binari di lunghezza LOB. Questa parte del processo potrebbe essere sostituita da qualsiasi implementazione RC4 standard.

/*
** RC4 functions
** Based on http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=76258
** by Peter Larsson (SwePeso)
*/
IF OBJECT_ID(N'dbo.fnEncDecRc4', N'FN') IS NOT NULL
    DROP FUNCTION dbo.fnEncDecRc4;
GO
IF OBJECT_ID(N'dbo.fnInitRc4', N'TF') IS NOT NULL
    DROP FUNCTION dbo.fnInitRc4;
GO
CREATE FUNCTION dbo.fnInitRc4
    (@Pwd varbinary(256))
RETURNS @Box table
    (
        i tinyint PRIMARY KEY, 
        v tinyint NOT NULL
    )
WITH SCHEMABINDING
AS
BEGIN
    DECLARE @Key table
    (
        i tinyint PRIMARY KEY,
        v tinyint NOT NULL
    );
 
    DECLARE
        @Index smallint = 0,
        @PwdLen tinyint = DATALENGTH(@Pwd);
 
    WHILE @Index <= 255
    BEGIN
        INSERT @Key
            (i, v)
        VALUES
            (@Index, CONVERT(tinyint, SUBSTRING(@Pwd, @Index % @PwdLen + 1, 1)));
 
        INSERT @Box (i, v)
        VALUES (@Index, @Index);
 
        SET @Index += 1;
    END;
 
    DECLARE
        @t tinyint = NULL,
        @b smallint = 0;
 
    SET @Index = 0;
 
    WHILE @Index <= 255
    BEGIN
        SELECT @b = (@b + b.v + k.v) % 256
        FROM @Box AS b
        JOIN @Key AS k
            ON k.i = b.i
        WHERE b.i = @Index;
 
        SELECT @t = b.v
        FROM @Box AS b
        WHERE b.i = @Index;
 
        UPDATE b1
        SET b1.v = (SELECT b2.v FROM @Box AS b2 WHERE b2.i = @b)
        FROM @Box AS b1
        WHERE b1.i = @Index;
 
        UPDATE @Box
        SET v = @t
        WHERE i = @b;
 
        SET @Index += 1;
    END;
 
    RETURN;
END;
GO
CREATE FUNCTION dbo.fnEncDecRc4
(
    @Pwd varbinary(256),
    @Text varbinary(MAX)
)
RETURNS varbinary(MAX)
WITH 
    SCHEMABINDING, 
    RETURNS NULL ON NULL INPUT
AS
BEGIN
    DECLARE @Box AS table 
    (
        i tinyint PRIMARY KEY, 
        v tinyint NOT NULL
    );
 
    INSERT @Box
        (i, v)
    SELECT
        FIR.i, FIR.v
    FROM dbo.fnInitRc4(@Pwd) AS FIR;
 
    DECLARE
        @Index integer = 1,
        @i smallint = 0,
        @j smallint = 0,
        @t tinyint = NULL,
        @k smallint = NULL,
        @CipherBy tinyint = NULL,
        @Cipher varbinary(MAX) = 0x;
 
    WHILE @Index <= DATALENGTH(@Text)
    BEGIN
        SET @i = (@i + 1) % 256;
 
        SELECT
            @j = (@j + b.v) % 256,
            @t = b.v
        FROM @Box AS b
        WHERE b.i = @i;
 
        UPDATE b
        SET b.v = (SELECT w.v FROM @Box AS w WHERE w.i = @j)
        FROM @Box AS b
        WHERE b.i = @i;
 
        UPDATE @Box
        SET v = @t
        WHERE i = @j;
 
        SELECT @k = b.v
        FROM @Box AS b
        WHERE b.i = @i;
 
        SELECT @k = (@k + b.v) % 256
        FROM @Box AS b
        WHERE b.i = @j;
 
        SELECT @k = b.v
        FROM @Box AS b
        WHERE b.i = @k;
 
        SELECT
            @CipherBy = CONVERT(tinyint, SUBSTRING(@Text, @Index, 1)) ^ @k,
            @Cipher = @Cipher + CONVERT(binary(1), @CipherBy);
 
        SET @Index += 1;
    END;
 
    RETURN @Cipher;
END;
GO

Il testo del modulo crittografato

Il modo più semplice per un amministratore di SQL Server di ottenerlo è leggere varbinary(max) valore memorizzato nel valore immagine colonna di sys.sysobjvalues , accessibile solo tramite il DAC (Dedicated Administrator Connection).

Questa è la stessa idea del metodo di routine descritto nell'introduzione, anche se aggiungiamo un filtro su valclass =1. Questa tabella interna è anche un luogo conveniente per ottenere il subobjid . Altrimenti, dovremmo controllare sys.numbered_procedures quando l'oggetto di destinazione è una procedura, usa 1 per una procedura non numerata o zero per qualsiasi altra cosa, come descritto in precedenza.

È possibile evitare di utilizzare il DAC leggendo il imageval da sys.sysobjvalues direttamente, utilizzando più DBCC PAGE chiamate. Ciò comporta un po' più di lavoro per individuare le pagine dai metadati, segui il imageval catena LOB e leggere i dati binari di destinazione da ciascuna pagina. L'ultimo passaggio è molto più semplice da eseguire in un linguaggio di programmazione diverso da T-SQL. Nota che DBCC PAGE funzionerà, anche se l'oggetto di base non è normalmente leggibile da una connessione non DAC. Se la pagina non è in memoria, verrà letta normalmente dalla memoria permanente.

Lo sforzo aggiuntivo per evitare il requisito DAC ripaga consentendo a più utenti di utilizzare il processo di decrittazione contemporaneamente. Userò l'approccio DAC in questo articolo per motivi di semplicità e spazio.

Esempio funzionante

Il codice seguente crea una funzione scalare crittografata di prova:

CREATE FUNCTION dbo.FS()
RETURNS varchar(255)
WITH ENCRYPTION, SCHEMABINDING AS
BEGIN
    RETURN 
    (
        SELECT 'My code is so awesome is needs to be encrypted!'
    );
END;

L'implementazione completa della decrittazione è riportata di seguito. L'unico parametro che deve essere modificato per funzionare con altri oggetti è il valore iniziale di @objectid impostato nel primo DECLARE dichiarazione.

-- *** DAC connection required! ***
-- Make sure the target database is the context
USE Sandpit;
 
DECLARE
    -- Note: OBJECT_ID only works for schema-scoped objects
    @objectid integer = OBJECT_ID(N'dbo.FS', N'FN'),
    @family_guid binary(16),
    @objid binary(4),
    @subobjid binary(2),
    @imageval varbinary(MAX),
    @RC4key binary(20);
 
-- Find the database family GUID
SELECT @family_guid = CONVERT(binary(16), DRS.family_guid)
FROM sys.database_recovery_status AS DRS
WHERE DRS.database_id = DB_ID();
 
-- Convert object ID to little-endian binary(4)
SET @objid = CONVERT(binary(4), REVERSE(CONVERT(binary(4), @objectid)));
 
SELECT
    -- Read the encrypted value
    @imageval = SOV.imageval,
    -- Get the subobjid and convert to little-endian binary
    @subobjid = CONVERT(binary(2), REVERSE(CONVERT(binary(2), SOV.subobjid)))
FROM sys.sysobjvalues AS SOV
WHERE 
    SOV.[objid] = @objectid
    AND SOV.valclass = 1;
 
-- Compute the RC4 initialization key
SET @RC4key = HASHBYTES('SHA1', @family_guid + @objid + @subobjid);
 
-- Apply the standard RC4 algorithm and
-- convert the result back to nvarchar
PRINT CONVERT
    (
        nvarchar(MAX),
        dbo.fnEncDecRc4
        (
            @RC4key,
            @imageval
        )
    );

Nota la conversione finale in nvarchar perché il testo del modulo è digitato come nvarchar(max) .

L'output è:

Conclusione

I motivi per cui il metodo descritto nell'introduzione funziona sono:

  • SQL Server usa il codice di flusso RC4 per l'esclusiva reversibile o il testo di origine.
  • La chiave RC4 dipende solo dal guid della famiglia di database, dall'id dell'oggetto e dal subobjid.
  • La sostituzione temporanea del testo del modulo significa che viene generata la stessa chiave RC4 (hash SHA-1).
  • Con la stessa chiave, viene generato lo stesso flusso RC4, consentendo l'esclusiva o la decrittazione.

Gli utenti che non hanno accesso a tabelle di sistema, file di database o altri accessi a livello di amministratore non possono recuperare il testo del modulo crittografato. Poiché lo stesso SQL Server deve essere in grado di decrittografare il modulo, non c'è modo di impedire agli utenti con privilegi adeguati di fare lo stesso.