Zine.net online

Witaj na Zine.net online Zaloguj się | Rejestracja | Pomoc
w Szukaj

mgrzeg.net - Admin on Rails :)

DPAPI Internals, a SecureString w PowerShellu (cz.1)

Jedną z bolączek wielu administratorów jest konieczność przechowywania ‘wrażliwych’ danych w bezpieczny sposób. Chodzi o wszelkiej maści pliki konfiguracyjne zawierające parametry połączenia, tudzież pliki zawierające login i hasło lokalnego administratora, które stanowią dane wejściowe dla programów typu runas, tworzących nowe procesy w kontekście użytkownika uprzywilejowanego. Jakiś czas temu opisywałem program cpau, który umożliwia przechowywanie danych w postaci zaszyfrowanej i odszyfrowywanie ich dopiero w momencie uruchomienia, co jednak dosyć łatwo udało nam się przechwycić korzystając wyłącznie z debuggera i dzięki czemu bez większych problemów offline’owo mogliśmy odczytać wszystkie dane: login, hasło, etc.
Microsoft rozumie potrzeby administratorów i między innymi dlatego udostępnił w ramach powershella zestaw kilku cmdletów umożliwiający bezpieczne zapisywanie i odczytywanie danych ‘wrażliwych’, którymi nie dzielimy się nawet z żoną :). Dwa z nich to ConvertTo-SecureString oraz ConvertFrom-SecureString. Użycie jest bardzo proste:

C:\PS>$secure = Read-Host -AsSecureString
C:\PS> $secure
System.Security.SecureString
C:\PS> $encrypted = ConvertFrom-SecureString -SecureString $secure
C:\PS> $encrypted
01000000d08c9ddf0115d1118c7a00c04fc297eb010000008727d6117f2b0c429d84f2b9f7e9ea35
0000000002000000000003660000a8000000100000009403327f1497e1a4266ca7d45f84266a0000
000004800000a000000010000000ed0b8a5247be59846e80ca735a1a3f9720000000a5025c6e7ee4
cad037fcd99a5cbbe4aef20d54e685352fa67c1deec0999779c7140000002f96e55035c6760aba6f
4bb9aeffc52616cb300c

W pierwszym poleceniu wczytujemy z klawiatury ciąg znaków, który trafia do zmiennej $secure, która - co widać po drugim poleceniu - jest typu SecureString. W kolejnym zawartość naszej zmiennej $secure zostaje przekonwertowana i zaszyfrowana, po czym trafia do zmiennej $encrypted. Na końcu wypisujemy zawartość zmiennej w postaci łańcucha znaków. Tak przygotowany łańcuch możemy zapisać w pliku, po czym w dowolnym momencie skorzystać z niego, np. korzystając z set-content oraz get-content:

C:\PS> $encrypted | set-content encrypted.txt
C:\PS> $secure2 = get-content encrypted.txt | ConvertTo-SecureString

Możemy jeszcze pokusić się o konwersję jawnego tekstu do securestringa, a następnie tego ostatniego przekonwertować do postaci zaszyfrowanej:

C:\PS> $secure_string_pwd = ConvertTo-SecureString "P@ssW0rD!" -asplaintext –force
C:\PS> $encrypted_pwd = ConvertFrom-SecureString -SecureString $secure_string_pwd
C:\PS> $encrypted_pwd
01000000d08c9ddf0115d1118c7a00c04fc297eb010000008727d6117f2b0c429d84f2b9f7e9ea35
0000000002000000000003660000a80000001000000037f547b0feab4a12ab792d809a63fd0a0000
000004800000a000000010000000426ad5282726129ddce3675e82c68ca018000000b00bf7525cca
296dc3753d4aaaf867deec089bcbc94c73c21400000091011d6139ce2150b304f0cabaa7dc8b21fb
65d3

Po czym ponownie zapisać to w pliku

C:\PS> $encrypted_pwd | Set-Content encrypted_pwd.txt

Tyle słowem wprowadzenia, nadszedł czas na zajrzenie nieco głębiej.
Rozpoczynamy od ustalenia biblioteki, w której znajduje się definicja ConvertFrom-SecureString oraz ConvertTo-SecureString. Mamy dostępnych kilka metod: począwszy od podejrzenia listy załadowanych modułów powershella w process explorerze, a skończywszy na uruchomieniu kilku poleceń w powershellu:

C:\PS> get-command ConvertTo-SecureString | fl -property pssnapin
PSSnapIn : Microsoft.PowerShell.Security
C:\PS> Get-PSSnapin -Name Microsoft.PowerShell.Security | fl -Property ModuleName
ModuleName : C:\WINDOWS\system32\WindowsPowerShell\v1.0\Microsoft.PowerShell.Security.dll

I już wszystko jasne :). Tak, wiem, powinienem używać najnowszej wersji :)
Skoro mamy już bibliotekę, możemy spróbować dowiedzieć się jaka funkcja odpowiada za szyfrowanie danych. Zaczynamy od załadowania biblioteki snapina do reflectora, dalej przechodzimy do przestrzeni Microsoft.PowerShell.Commands i znajdujemy klasę ConvertFromSecureStringCommand. Słusznie domyślamy się, że za obsługę polecenia odpowiada jedyna metoda klasy, czyli ProcessRecord i dosyć szybko trafiamy na następujący kawałek kodu:

//zrzut z reflectora
        if (base.SecureKey != null)
        {
            result = SecureStringHelper.Encrypt(this.SecureString, base.SecureKey);
        }
        else if (base.Key != null)
        {
            result = SecureStringHelper.Encrypt(this.SecureString, base.Key);
        }
        else
        {
            sendToPipeline = SecureStringHelper.Protect(this.SecureString);
        }

Przyznaję, że nieco z lenistwa niż jakiejś przebiegłości postanawiam, że ustalenie metody, która ma być wywołana przerzucę do WinDbg. A poza tym to jest dobra okazja, żeby zademonstrować zakładanie pułapek na kodzie zarządzanym, więc chyba nikt się nie obrazi ;)
Ładujemy zatem WinDbg, podpinamy się do aktywnego procesu powershella, po czym ładujemy standardowy zestaw sos.dll oraz sosex.dll i ostatecznie ustawiamy pułapkę:

0:003> .loadby sos mscorwks
0:003> .load sosex.dll
0:003> !mbm *!Microsoft.PowerShell.Commands.
ConvertFromSecureStringCommand.ProcessRecord 0

Breakpoint set at Microsoft.PowerShell.Commands.ConvertFromSecureStringCommand.ProcessRecord().
*** WARNING: Unable to verify checksum for C:\WINDOWS\assembly\NativeImages_v2.0.50727_32\Microsoft.PowerShel#\2529703d3e0d2f9fd06cc0230f2bda3f\Microsoft.PowerShell.Security.ni.dll
0:003> !mbl
0 e : *!MICROSOFT.POWERSHELL.COMMANDS.
CONVERTFROMSECURESTRINGCOMMAND.PROCESSRECORD ILOffset=0: pass=1 oneshot=false thread=ANY
    Microsoft.PowerShell.Security!Microsoft.PowerShell.Commands.
ConvertFromSecureStringCommand.ProcessRecord()
        0 e 2242ffd4
0:003> g

Wykonujemy polecenia

C:\PS>$secure = Read-Host -AsSecureString

Oraz

C:\PS> $encrypted = ConvertFrom-SecureString -SecureString $secure

I po wykonaniu drugiego uaktywnia się nasza pułapka
W tym momencie dodajemy kolejne pułapki:

0:003> !mbm *!Microsoft.PowerShell.SecureStringHelper.Encrypt 0
Breakpoint set at Microsoft.PowerShell.SecureStringHelper.Encrypt(System.Security.SecureString, System.Security.SecureString).
Breakpoint set at Microsoft.PowerShell.SecureStringHelper.Encrypt(System.Security.SecureString, Byte[]).
0:003> !mbm *!Microsoft.PowerShell.SecureStringHelper.Protect 0
Breakpoint set at Microsoft.PowerShell.SecureStringHelper.Protect(System.Security.SecureString).
0:003> g

I puszczamy wykonanie dalej. Po chwili nasza pułapka uaktywnia się, więc znowu lądujemy w debuggerze:

0:003> !clrstack
OS Thread Id: 0x694 (3)
ESP       EIP    
03e1ee70 22430470 Microsoft.PowerShell.SecureStringHelper.Protect(System.Security.SecureString)
03e1eea4 2243007e Microsoft.PowerShell.Commands.ConvertFromSecureStringCommand.ProcessRecord()
03e1eedc 20612648 System.Management.Automation.Cmdlet.DoProcessRecord()
03e1eee4 2064e798 System.Management.Automation.CommandProcessor.ProcessRecord()
03e1ef24 205e8f1d System.Management.Automation.CommandProcessorBase.DoExecute()
03e1ef54 2061c287 System.Management.Automation.Internal.PipelineProcessor.Inject(System.Object, Boolean)

Na początek podejrzyjmy stos. Aha, to była jednak metoda Protect, a nie Encrypt. Przeglądamy pobieżnie reflektorem kod metody Protect i trafiamy szybko na wywołanie:

data = ProtectedData.Protect(userData, null, DataProtectionScope.CurrentUser);

A zaglądając głębiej trafiamy na

if (!CAPI.CryptProtectData(new IntPtr((void*) &cryptoapi_blob2), string.Empty, new IntPtr((void*) &cryptoapi_blob3), IntPtr.Zero, IntPtr.Zero, dwFlags, new IntPtr((void*) &cryptoapi_blob)))

co z kolei dosyć szybko sprowadza nas do wywołania:

[DllImport("crypt32.dll", CharSet=CharSet.Unicode, SetLastError=true)]
internal static extern bool CryptProtectData([In] IntPtr pDataIn, [In] string szDataDescr, [In] IntPtr pOptionalEntropy, [In] IntPtr pvReserved, [In] IntPtr pPromptStruct, [In] uint dwFlags, [In, Out] IntPtr pDataBlob);

Ustawiamy zatem kolejną pułapkę, puszczamy wykonanie i patrzymy co będzie dalej:

0:003> !mbm *!System.Security.Cryptography.ProtectedData.Protect
Breakpoint set at System.Security.Cryptography.ProtectedData.Protect(Byte[], Byte[], System.Security.Cryptography.DataProtectionScope).
0:003> g
Breakpoint 4 hit
eax=00000000 ebx=00000000 ecx=01403418 edx=00000000 esi=01403418 edi=03e1ee54
eip=67982e82 esp=03e1ee10 ebp=03e1ee64 iopl=0         nv up ei pl zr na pe nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00000246
System_Security_ni+0x62e82:
67982e82 85f6            test    esi,esi
0:003> !clrstack -a
OS Thread Id: 0x694 (3)
ESP       EIP    
03e1ee10 67982e82 System.Security.Cryptography.ProtectedData.Protect(Byte[], Byte[], System.Security.Cryptography.DataProtectionScope)
    PARAMETERS:
        userData = 0x0142e7a4
        optionalEntropy = 0x00000000
        scope = 0x00000000
    LOCALS:
        <no data>
        <no data>
        <no data>
        <no data>
        <no data>
        <no data>
        <no data>
        0x03e1ee10 = 0x00000000

Jak widać, drugi i trzeci parametr to po prostu 0, a pierwszy parametr to tablica bajtów, co szybko możemy udowodnić:

0:003> !do 0x0142e7a4
Name: System.Byte[]
MethodTable: 79333470
EEClass: 790eeb6c
Size: 36(0x24) bytes
Array: Rank 1, Number of elements 24, Type Byte
Element Type: System.Byte
Fields:
None

OK, 24 bajty - brzmi znajomo. Z klawiatury wprowadziłem bowiem 12 znaków…
Robimy zatem zrzut tablicy:

0:003> !dumparray -details 0x0142e7a4
Name: System.Byte[]
MethodTable: 79333470
EEClass: 790eeb6c
Size: 36(0x24) bytes
Array: Rank 1, Number of elements 24, Type Byte
Element Methodtable: 79333520
[0] 0142e7ac
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance       97 m_value
[1] 0142e7ad
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[2] 0142e7ae
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance      108 m_value
[3] 0142e7af
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[4] 0142e7b0
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance       97 m_value
[5] 0142e7b1
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[6] 0142e7b2
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance      109 m_value
[7] 0142e7b3
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[8] 0142e7b4
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance       97 m_value
[9] 0142e7b5
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[10] 0142e7b6
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance      107 m_value
[11] 0142e7b7
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[12] 0142e7b8
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance      111 m_value
[13] 0142e7b9
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[14] 0142e7ba
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance      116 m_value
[15] 0142e7bb
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[16] 0142e7bc
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance       97 m_value
[17] 0142e7bd
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[18] 0142e7be
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance       50 m_value
[19] 0142e7bf
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[20] 0142e7c0
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance       53 m_value
[21] 0142e7c1
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value
[22] 0142e7c2
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance       54 m_value
[23] 0142e7c3
    Name: System.Byte
    MethodTable 79333520
    EEClass: 790eebe0
    Size: 12(0xc) bytes
     (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
    Fields:
          MT    Field   Offset                 Type VT     Attr    Value Name
    79333520  4000216        0          System.Byte  1 instance        0 m_value

I wszystko jasne - tablica bajtów zawiera łańcuch unikodowy zawierający wpisane przeze mnie hasło. Tak przygotowana tablica opakowywana jest w strukturę DATA_BLOB, która następnie trafia do funkcji CryptProtectData z biblioteki crypt32.dll.
Czas zatem zajrzeć do dokumentacji. Z pozostałych parametrów ustalamy jeszcze, że skoro optionalEntropy = null, to do funkcji trafia blob z domyślnymi polami, ponadto scope = 0, więc dwFlags = 1. Zaglądając do pliku nagłówkowego szybko ustalamy, że jest to

//Wincrypt.h
#define CRYPTPROTECT_UI_FORBIDDEN        0x1

W uwagach do funkcji CryptProtectData znajdujemy kilka ciekawych informacji:

"Typically, only a user with logon credentials that match those of the user who encrypted the data can decrypt the data. In addition, decryption usually can only be done on the computer where the data was encrypted. However, a user with a roaming profile can decrypt the data from another computer on the network.
If the CRYPTPROTECT_LOCAL_MACHINE flag is set when the data is encrypted, any user on the computer where the encryption was done can decrypt the data.
The function creates a session key to perform the encryption. The session key is derived again when the data is to be decrypted."

Wygląda zatem na to, że nasze dane mogą zostać odszyfrowane wyłącznie przez użytkownika, który je wygenerował i to wyłącznie po zalogowaniu na tej, lub innej należącej do jednej domeny maszynie, o ile dla tego użytkownika jest ustawiony profil mobilny.

DPAPI Internals

Funkcja CryptProtectData oraz bliźniacza CryptUnprotectData stanowią podstawę DPAPI (Data Protection API), czyli usługi ochrony danych opartych o hasła. - Jakie hasło? - ktoś spyta. - Hasło używane podczas logowania do systemu - odpowie dokumentacja. Na jego podstawie wyprowadzane są kolejno: MasterKey, a z niego oraz garści entropii i przypadkowych danych dalej klucze sesyjne. I to za ich pomocą szyfrowane są dane przekazane jawnym tekstem do funkcji CryptProtectData, a zaszyfrowany ciąg bajtów zwracany jest przez funkcję do aplikacji. Tym samym DPAPI zajmuje się wyłącznie szyfrowaniem danych, przechowywanie ich pozostawia jednak samej aplikacji. W konsekwencji zaszyfrowane dane można znaleźć w wielu miejscach systemu: w rejestrze, w katalogach aplikacji, profilu użytkownika, etc, zależnie od wyobraźni i kreatywności programisty :)
Wywołane funkcje DPAPI (z biblioteki crypt32.dll) kontaktują się poprzez RPC z LSA, które to z kolei odwołuje się do właściwych funkcji szyfrujących biblioteki crypt32.dll, jednak już w kontekście samego LSA.
Hasło użytkownika podawane jest raz - podczas logowania i na jego podstawie generowany jest MasterKey, który zmieniany raz na 3 miesiące, (przy kolejnym skorzystaniu DPAPI), lub bezpośrednio po zmianie hasła użytkownika. Dodatkowo, każdy MasterKey zaszyfrowany jest z użyciem hasła użytkownika i dopiero w takiej postaci zostaje zapisany na dysku. Spytacie pewnie: czy tylko ostatni MasterKey jest zapisany na dysku? A ja odpowiem: nie, wszystkie, ponieważ danych zaszyfrowanych z użyciem poprzednich MasterKeys nie da się odszyfrować wyłącznie z użyciem ostatniego MasterKey. Powiem więcej: do odszyfrowania poprzednich MasterKeys potrzebne są hasła użytkownika, które również przechowywane są na dysku w postaci skrótów zaszyfrowanych ostatnim hasłem w pliku CREDHIST.
Fizycznie katalogi, w których przechowywane są MasterKeys oraz plik CREDHIST znajdują się w:

MasterKeys: %APPDATA%\Microsoft\Protect\{UserSID}
CREDHIST: %APPDATA%\Microsoft\Protect\

Każdy MasterKey ma swój unikalny GUID i dla usprawnienia całego procesu plik zawierający zaszyfrowany MasterKey ma nazwę taką, jaki ma GUID. Dodatkowo, w katalogu zawierającym MasterKeys jest plik Preferred zawierający GUID ostatniego MasterKey i to właśnie on wybierany jest do szyfrowania danych.
W przypadku danych, które mają być dostępne dla wszystkich użytkowników systemu, używane są klucze systemowe, które można znaleźć w katalogu %SystemRoot%\System32\Microsoft\Protect\S-1-5-18
Konto systemu nie ma hasła, wobec czego nie ma konieczności przechowywania pliku CREDHIST.
Z tego krótkiego opisu wynika, że każda aplikacja uruchamiana w kontekście zalogowanego użytkownika będzie miała dostęp do wszystkich danych. Żeby się przed tym ustrzec, aplikacja może przekazać do CryptProtectData własny kluczyk (entropia), bez którego nie będzie można danych odszyfrować. Jak widzieliśmy jednak wcześniej, nasz PowerShell jednak z tej możliwości nie korzysta (optionalEntropy = 0x00000000 w parametrach funkcji System.Security.Cryptography.ProtectedData.Protect), co umożliwia każdej aplikacji uruchomionej przez tego samego użytkownika na odszyfrowanie danych. Sprawdźmy to!
W tym celu skorzystajmy z kodu (zamieszczam pełen, ponieważ nie jest aż taki długi, a dzięki temu każdy będzie mógł z niego skorzystać od razu):

using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Security.Cryptography;

namespace pl.net.zine.Articles.DPAPI
{
  public class SecureStringPSDecrypter
  {
    public static void Main(string[] args)
    {
      if (args.Length > 0)
      {
        byte[] encryptedData = BlobFileUtils.ReadBlobFromTextFile(args[0]);
        byte[] originalData = ProtectedData.Unprotect(encryptedData, null, DataProtectionScope.CurrentUser);
        Console.WriteLine("The original data is:");
        Console.WriteLine(BlobFileUtils.StringFromByteArray(originalData));
      }
      else
        Console.WriteLine("Usage: {0} encrypted_file.txt", System.Diagnostics.Process.GetCurrentProcess().ProcessName);
    }
  }

  public class BlobFileUtils
  {
    public static string StringFromByteArray(byte[] arr)
    {
      int len = arr.Length;
      StringBuilder sb = new StringBuilder();
      for(int i = 0; i < len; i+=2)
      {
        sb.Append((char)arr[i]);
      }
      return sb.ToString();
    }

    public static byte[] ReadBlobFromTextFile(string fileName)
    {
      return ConvertToByteArr(ReadSecureStringEncrypted(fileName));
    }

    private static byte[] ConvertToByteArr(string text)
    {
      int num = text.Length / 2;
      byte[] byteArr = new byte[num];
      if (text.Length > 0)
      {
        for (int i = 0; i < num; i++)
        {
          byteArr[i] = byte.Parse(text.Substring(2 * i, 2), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture);
        }
      }
      return byteArr;
    }

    private static string ReadSecureStringEncrypted(string file)
    {
      string text = "";
      using (TextReader reader = new StreamReader(file))
      {
        text = reader.ReadToEnd();
      }
      return text;
    }
   }
}

Kompilujemy i uruchamiamy dla naszych przykładowych danych:

>decryptPSSS.exe encrypted.txt
The original data is:
alamakota256
>decryptPSSS.exe encrypted_pwd.txt
The original data is:
P@ssW0rD!

Cała magia sprowadza się do linijki:

byte[] originalData = ProtectedData.Unprotect(encryptedData, null, DataProtectionScope.CurrentUser);

w której następuje właściwe odszyfrowanie danych, a zgodnie z naszymi wcześniejszymi obserwacjami drugi parametr (optionalEntropy) wystarczyło ustawić na null.

Proste? :)

Tekst zrobił się przepotwornie długi, więc w tym miejscu dokonuję cięcia. Dalsza część poświęcona głębszemu zajrzeniu do DPAPI pojawi się w następnym wpisie, gdzie spróbuję dodatkowo odpowiedzieć na pytanie 'czy jest możliwe offline’owe odszyfrowanie danych zapisanych przez DPAPI?' i jak to się ma do EFS-a, haseł Internet Explorera, etc.

Zainteresowani ciągiem dalszym?

Opublikowane 11 marca 2011 18:20 przez mgrzeg

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

 

Bob said:

Swietne. Czekam na dalsza czesc. Jakos nigdy mnie nie bawilo zagladanie do tzw. bebechow ale mnie zacheciles poraz kolejny.

Pozdrawiam

marca 12, 2011 14:44
 

M.SZ. said:

Świetny wpis, czekam na dalszą część.

!mbm *!zine.net.blogs.mgrzeg.PublishNewPost

marca 14, 2011 12:33
 

dotnetomaniak.pl said:

Dziękujemy za publikację - Trackback z dotnetomaniak.pl

marca 21, 2011 19:22
 

PH said:

Brawo! Wiecej!

marca 22, 2011 12:46
 

PH said:

Kontynuując jeszcze tamat o zachęcaniu cię do pisania.

Powiem tylko tyle - przesłuchując ludzi na rozmowach o pracę, wyciągnąłem jeden wniosek o większości polskich developerów. Nigdy nie przeczytali żadnej książki z fachowej literatury (niektórzy nawet nie słyszeli o algorytmach i strukturach danych z pierwszego roku studiów). Szczytem dla nich jest opanowanie podstawowej składni C# albo C++ i myślą tym samym, że teraz to innym pokażą. Ale już jakiekolwiek trudniejsze pytanie powoduje, że zaczynają się dusić (np. jak działa GC, jak wygląda alokacja pamięci, ile zajmuje pamięci/czasu utworzenie nowego wątku w C++ a ile w C#, co to jest vtable i tym podobne). Tak samo później wygląda kod, który produkują. Jakoś działa, ale nigdy nie wiadomo jak długo i dlaczego tak wolno.

Telefon z WP7 muszę 2 razy dziennie resetować, bo zacina się nawet na alarmie!

Strasznie trudno jest znaleźć ludzi z pasją. Ludzi, którzy sami zgłębiają swoją wiedzę i zadają pytanie 'dlaczego'. A jeszcze trudniej wśród nich znaleźć tych, którzy mają czas i chęci dzielić się nią z innymi! Michał, wierzę, że nie poddasz się i reaktywujesz tego bloga w pełni :) Liczę, że przybliżysz innym swoją praktyczną wiedzę zaczerpniętą z książek tj.: Advanced Windows Debugging, książek Jeffreya Richtera, Krisa Kaspersky'ego, Johna Robbinsa i pewno miliona innych, o których nie słyszałem (coś polecasz?). Trzymam kciuki, że zainspiruje to także innych czytelników!

Na koniec - może teraz czas na moją wersję konkursu o magic-numbers - dlaczego nowy obiekt typu System.Object w pamięci zajmuje 8 bajtów ? Czyż nie powinien zajmować 0, skoro System.Object nie ma żadnych pól :)

marca 22, 2011 16:08
 

mgrzeg said:

Dzięki za dobre słowa i zachętę do dalszego pisania! :)

@PH: domyślam się, że Tobie czasu i chęci brak, więc pewnie nie prowadzisz nigdzie żadnego bloga... a szkoda!

Odnośnie książek, to trochę tego jest, faktycznie, choć wydaje mi się, że 'tak na szybko' do listy, którą podałeś, dorzuciłbym jedno nazwisko: Russinovich.

Dużo materiału jest w necie, czasem to i owo można usłyszeć na spotkaniach różnych grup (tu zawsze warto polować na duet Grzegorz Tworek+Paula Januszkiewicz).

Odnośnie Twojej zagadki: chyba muszę pospieszyć się z tekstem o synchronizacji ;) i dodam do Twojej kolejną 'magiczną liczbę': 0f78734a :)

marca 22, 2011 19:45
 

PH said:

Racja, Mark Russinovich i Tess Ferrandez (http://blogs.msdn.com/b/tess/) to bardzo dobre propozycje! Także przyspieszaj z publikacją ;)

U mnie to zawsze czasu brak, a głowa pełna pomysłów i pisanie bloga nie ma wysokiego priorytetu... Jednak możesz go znaleźć bardzo łatwo. Ostrzegam, że super rewelacją to on nie jest :) Przypomnij sobie tylko komu dodałeś skrzydeł subskrypcją MSDN za dodatek do Visual Studio, a wszystko stanie się jasne.

Do kolejnego posta. Salute!

marca 23, 2011 16:44
 

omeg said:

Świetny tekst, dał mi kolejny powód na zaprzyjaźnienie się z windbg'owym sos-em. ;)

Trafiłem na tego bloga szukając informacji o klasie SecureString (a raczej powodów, by jej w ogóle używać ;) i muszę powiedzieć, że jestem mile zaskoczony poziomem. Tak trzymać!

maja 5, 2011 15:43

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)
Wyślij

Subskrypcje

W oparciu o Community Server (Personal Edition), Telligent Systems