Home | Chi sono | Contattami
 

Progr. lineare

Delphi
 
Componenti
  Database
 
Miei articoli

Windows

Miei articoli 

 

LSA Secrets


Devo essere sincero: tra i miei programmi preferiti c'è Cain&Abel, un programma freeware (senza sorgente però) per il recupero di password e sniffing di rete; per il network sniffing utilizza la libreria Open Source WinpCap. Behh, tra le tante opzioni c'è anche quella di ottenere gli LSA Secretes: basta selezionare il Tab "LSA Secrets" premere il pulsante "+" ed ecco comparire una bella sbrodolata di dati; osservando bene il risultato, si possono trovare informazioni interessanti: ad esempio il valore DefaultPassword oppure RasDialParams.... La prima cosa che mi sono chiesto è stata: da dove vengono queste informazioni? Forse dal registro? Saranno criptate, come ottenerle in chiaro? Dando un'occhiata all' articolo54 sulla composizione del registro di Windows si può vedere che nella HKEY_LOCAL_MACHINE c'è il ramo Security che raccoglie gli elementi inclusi nel file "C:\Windows\System32\Config\Security". Lanciando regedit.exe da una utenza amministrativa, il ramo HKEY_LOCAL_MACHINE\Security può essere aperto ma le sue sottochiavi hanno le ACL che consentono l'accesso solo all'utenza System e quindi non sono esplorabili. Per vedere tutto il contenuto occorre lanciare regedit con l'utenza Sytem. Per fare questo possiamo usare l'applicativo descritto nell'articolo Esegui come SYSTEM e scaricabile all'indirizzo ProcessAsSystem. Una volta lanciato regedit.exe con l'utenza System, possiamo finalmente esplorare completamento l'albero al nodo Security e guardacaso nel ramo HKEY_LOCAL_MACHINE\SECURITY\Policy\Secrets troviamo come sottochiavi gli elementi restituiti da Cain&Abel

A questo punto abbiamo quindi trovato gli LSA Secrets; purtroppo ancora non siamo in grado di ottenere i valori associati. Aprendo una delle chiavi in questione abbiamo il seguente contenuto

Ognuna contiene le seguenti sottochiavi:

  • CupdTime
  • CurrVal
  • OldVal
  • OupdTime
  • SecDesc

Ognuna di esse contiene il solo valore predefinito: il valore del Secret dovrebbe trovarsi qui dentro; visualizzando il valore predefinito relativo ad ognuna delle sottochiavi in questione, non si visualizzano i dati restituiti da Cain&Abel. Del resto è evidente che le informazioni sono criptate.

Un'altra cosa interessante è che alcune delle sottochiavi di Secrets, se selezionate, restituiscono il seguente messaggio di errore

In particolare si tratta delle sottochiavi seguenti:

  • SAC
  • SAI
  • SCM:{6c736d4F-CBD1-11D0-B3A2-00A0C91E29FE}

L'errore è dovuto al fatto che i nomi in questioni includono anche il cosidetto "trailing null" ossia un carattere nullo al termine della stringa. Le api win32 interpretano il carattere nullo (\0) come carattere di terminazione stringa, mentre le api Native, per la rappresentazione di stringhe, usano il tipo strutturato UNICODE_STRING che contiene appunto la dimensione della stringa; quindi nel caso Win32, la stringa termina quando si incontra un \0 mentre nel caso Native la stringa termina quando si raggiunge il numero di caratteri specificato dal campo che definisce appunto la lunghezza della stringa. Di conseguenza ad esempio la stringa "ciao\0" in Win32 viene per forza interpretata come "ciao" mentre in ambito Native viene tranquillamente interpretata come "ciao\0" includendo quindi anche il \0 come parte della stringa (mentre nell'ambito Win32 ha solo la funzione di terminatore di stringa). Le sottochiavi sopra contengono un \0 finale e la loro selezione all'interno di regedit genera l'errore sopra: infatti è risaputo che regedit.exe è stato costruito usando le api win32. Questo concetto viene esaminato anche da Mark Russinovich all'indirizzo http://www.microsoft.com/technet/sysinternals/information/tipsandtrivia.mspx nel paragrafo denominato "Hidden Registry Keys" e proprio su questo concetto lo stesso Mark Russinovich ha creato l'esempio RegHide . Di seguito riporto il testo estratto dall'articolo originale

Hidden Registry Keys
A subtle but significant difference between the Win32 API and the Native API (see Inside the Native API for more information on this largely undocumented interface) is the way that names are described. In the Win32 API strings are interpreted as NULL-terminated ANSI (8-bit) or wide character (16-bit) strings. In the Native API names are counted Unicode (16-bit) strings. While this distinction is usually not important, it leaves open an interesting situation: there is a class of names that can be referenced using the Native API, but that cannot be described using the Win32 API.

How is this possible? The answer is that a name which is a counted Unicode string can explicitly include NULL characters (0) as part of the name. For example, "Key\0". To include the NULL at the end the length of the Unicode string is specified as 4. There is absolutely no way to specify this name using the Win32 API since if "Key\0" is passed as a name, the API will determine that the name is "Key" (3 characters in length) because the "\0" indicates the end of the name.

When a key (or any other object with a name such as a named Event, Semaphore or Mutex) is created with such a name any applications using the Win32 API will be unable to open the name, even though they might seem to see it. The program below, RegHide, illustrates this point. It creates a key called "HKEY_LOCAL_MACHINE\Software\Sysinternals\Can't touch me!\0" using the Native API, and inside this key it creates a value. Then the program pauses to give you an opportunity to see if you can view the value using any Registry editor you have handy (Regedit, Regedt32 or a third-party Registry editor). Because Regedit and Regedt32 (and likely an third party Registry editor) use the Win32 API, they will see the key listed as a child of Sysinternals, but when you try to open the key you'll get an error. This is because the Registry editor will try to open "Can't touch me!" without the trailing NULL (which is interpreted as the end of the string) and won't find this name. After you've verified this exit the program and this special key will be deleted.

 

Sul web esiste un ottimo editor di registro (shareware) che consente di visualizzare, senza bisogno di essere eseguito come System, tutte le sottochiavi di Secrets ed anche le ultime 3 sopra descritte: si chiama RegDatXP e può essere scaricato all'indirizzo

http://freenet-homepage.de/h.ulbrich/

Di seguito un esempio di lettura dei Secrets da RegDatXP

  

Bene, ora che ci siamo impratichiti un pò con la locazione di questi benedetti LSA Secrets, è arrivato il momento di trovare il modo per estrarre i valori. La strada che porta alla soluzione passa per il concetto di Private Data Object: cercando nel Platform SDK si arriva alla seguente pagina che riporto

Private Data Object
A limited number of private data objects are available on each system for the purpose of storing information in a protected, encrypted, fashion.

Private data objects are provided primarily to support storage of server account passwords. This is useful for servers that run in a specific account. The password of the server account is private data that should be secured but is needed to log the server on.

Private data objects may be general purpose, or they may be one of three specialized types: local, global, and machine.

Local private data objects can only be read locally from the computer storing the object. Attempting to read them remotely results in a STATUS_ACCESS_DENIED error. Local private data objects have key names that begin with the prefix "L$". In addition to the local private objects you create, the operating system defines the following local private objects: $machine.acc, SAC, SAI, and SANSC.

Global private data objects are global in the sense that if they are created on a domain controller, they will be automatically replicated to all domain controllers in that domain. In other words, each domain controller in that domain will have access to the values the global private data object contains. In contrast, global private data objects created on a system that is not a domain controller, as well as nonglobal private data objects, are not replicated. Global private data objects have key names beginning with "G$".

Machine private data objects can be accessed only by the operating system. These objects have key names that begin with "M$".

Note You can set, but you cannot retrieve, machine private data objects.

In addition to these prefixes, the following values also indicate local or machine objects. These values are supported for backward compatibility and should not be used when you create new local or machine objects. The key name of local private data objects may also start with "RasDialParms" or "RasCredentials". The key name for machine objects may also start with, "NL$" or "_sc_".

Private data objects that are not one of the preceding specialized types use key names that do not start with a prefix. These objects are not replicated and can be read either locally or remotely by applications.

Windows NT 4.0 SP3 and earlier: Private information stored using a private data object is encrypted using a generated system-specific key. The operating system does not include secure boot, and thus these operating systems have no way to securely store and later retrieve a cryptographic key. Because of this limitation, private information cannot truly be considered secure against sophisticated attacks.
 

La prima cosa che si può notare è che esistono le seguenti 3 principali categorie di Private Data Objects

  • Local Private Data Object: in generale iniziano con "L$"; altri esempi includono nomi che iniziano con "RasDialParams" (i cui valori sono username e password delle varie connessioni RAS presenti sul sistema), "RasCredentials"
  • Global Private Data Object: in generale iniziano con "G$"
  • Machine Private Data Object: in generale iniziano con "M$"; altri esempi includono nomi che iniziano con "NL$" o "_SC_"; possono essere esaminati solo dal sistema operativo. Tra questi rientrano anche quelli corrispondenti alle 3 chiavi viste in precedenza (SAC, SAI, SCM:{6c736d4F-CBD1-11D0-B3A2-00A0C91E29FE}) che non possono essere aperti da regedit.  

A questo punto la domanda che è naturale porsi è la seguente: esiste qualche api (Win32 o Native, documentata o non documentata) che ci consenta di prelevare il valore di ogni LSA Secret? La risposta è si: si tratta dell'api LsaRetrievePrivateData che andiamo ad esaminare

function LsaRetrievePrivateData( PolicyHandle: Pointer; const KeyName: UNICODE_STRING; var PrivateData: PUNICODE_STRING ): Integer; stdcall; external 'advapi32.dll';

  • PolicyHandle[input]: handle ad un oggetto Policy; tale handle può ad esempio essere ottenuto tramite l'api LsaOpenPolicy; al termine dei lavori, tale handle deve essere chiuso tramite l'api LsaCloseHandle.
  • KeyName[input]: nome dell'LSA Secret di cui ci interessa prelevare il valore; corrisponde al nome della sottochiave della chiave Secrets (come già visto in precedenza).
  • PrivateData[output]: puntatore ad una UNICODE_STRING che raccoglie il valore del Secret. L'allocazione di memoria viene effettuata dall'api LsaRetrievePrivateData. Al termine dei lavori la deallocazione di memoria deve essere effettuata tramite l'api LsaFreeMemory.

Behh, decisamente semplice (o almeno relativamente semplice se confrontato con altri contesti). La cosa da fare è esporre quindi una procedura che prende in input il nome di un LSA Secret e restituisce il valore

function RetrieveSecret(KeyName: PAnsiChar; var Secret: string): Boolean; var ObjectAttributes: LSA_OBJECT_ATTRIBUTES; LsaStatus, Status: Integer; pol: Pointer; AnsString: ANSI_STRING; UncString: UNICODE_STRING; PrivateData: PUNICODE_STRING; begin try Result := False; AnsString.Buffer := PAnsiChar(KeyName); AnsString.Length := Length(KeyName); AnsString.MaximumLength := AnsString.Length; Status := RtlAnsiStringToUnicodeString( @UncString, @AnsString, True ); if Status < 0 then begin ErrStrNative('RtlAnsiStringToUnicodeString', status); Exit; end; FillChar(ObjectAttributes, SizeOf(ObjectAttributes), #0); LsaStatus := LsaOpenPolicy(nil, ObjectAttributes, 0, pol); if LsaStatus < 0 then begin ErrStrLsa('LsaOpenPolicy', LsaStatus); Exit; end; LsaStatus := LsaRetrievePrivateData(pol, UncString, PrivateData); if LsaStatus < 0 then begin ErrStrLsa('LsaRetrievePrivateData', LsaStatus); Exit; end; //DumpData: procedura per visualizzare il contenuto di un Buffer //in stile Hex editor Secret := DumpData(PrivateData.Buffer, PrivateData.Length); Result := True; finally RtlFreeUnicodeString(@UncString); if pol <> nil then LsaClose(pol); if PrivateData <> nil then LsaFreeMemory(PrivateData); end; end;

Behh, chiaramente ho volontariamente omesso le dichiarazioni delle altre api interessate e soprattutto l'implementazione delle varie procedure di supporto: tutta la pappina sarà inclusa nell'esempio che troverete allegato al termine dell'articolo, quindi no problem.

Ora un pò di osservazioni:

1) in corrispondenza dei Secret il cui nome inizia per "_SC_" la LsaRetrievePrivateData restituisce il codice "C0000022" corrispondente all'errore win32 "5" che ha come descrizione la stringa "Accesso negato"

2) in corrispondenza del Secret "NL$KM" la LsaRetrievePrivateData restituisce il codice "C0000022" corrispondente all'errore win32 "5" che ha come descrizione la stringa "Accesso negato"

3) in corrispondenza dei Secret SAC, SAI, SCM:{6c736d4F-CBD1-11D0-B3A2-00A0C91E29FE} la LsaRetrievePrivateData restituisce il codice "C0000034" corrispondente all'errore win32 "2" che ha come descrizione la stringa "Impossibile trovare il file specificato"

Nei primi 2 casi si tratta di Machine Data Objects che, come già detto in precedenza sono accessibili solo dal sistema operativo. Nel terzo caso il discorso si ricollega alle problematiche già analizzate in precedenza. Proprio relativamente al terzo caso, visto e considerato che questi elementi contengono un carattere nullo alla fine, nel caso che la LsaRetrievePrivateData restituisca il codice "C0000034", possiamo provare ad aggiungere un carattere nullo al buffer relativo alla UNICODE_STRING: la funzione RetrieveSecret diventa quindi

function RetrieveSecret(KeyName: PAnsiChar; var Secret: string): Boolean; var ObjectAttributes: LSA_OBJECT_ATTRIBUTES; LsaStatus, Status: Integer; pol: Pointer; AnsString: ANSI_STRING; UncString: UNICODE_STRING; PrivateData: PUNICODE_STRING; begin try Result := False; AnsString.Buffer := PAnsiChar(KeyName); AnsString.Length := Length(KeyName); AnsString.MaximumLength := AnsString.Length; Status := RtlAnsiStringToUnicodeString( @UncString, @AnsString, True ); if Status < 0 then begin ErrStrNative('RtlAnsiStringToUnicodeString', status); Exit; end; //incremento la MaximumLength per poter aggiungere un trailing null Inc(UncString.MaximumLength, 2); FillChar(ObjectAttributes, SizeOf(ObjectAttributes), #0); LsaStatus := LsaOpenPolicy(nil, ObjectAttributes, 0, pol); if LsaStatus < 0 then begin ErrStrLsa('LsaOpenPolicy', LsaStatus); Exit; end; LsaStatus := LsaRetrievePrivateData(pol, UncString, PrivateData); if LsaStatus < 0 then begin //se non esiste un Lsa Secret col nome in questione //proviamo ad aggiungere un trailing null al nome if LsaStatus = Integer($C0000034) then begin //proviamo ad aggiungere un trailing null FillChar((UncString.Buffer + UncString.Length)^, 2, #0); Inc(UncString.Length, 2); LsaStatus := LsaRetrievePrivateData(pol, UncString, PrivateData); if LsaStatus < 0 then begin ErrStrLsa('LsaRetrievePrivateData', LsaStatus); Exit; end; end else begin ErrStrLsa('LsaRetrievePrivateData', LsaStatus); Exit; end; end; Secret := DumpData(PrivateData^.Buffer, PrivateData^.Length); Result := True; finally RtlFreeUnicodeString(@UncString); if pol <> nil then LsaClose(pol); if PrivateData <> nil then LsaFreeMemory(PrivateData); end; end;

Ok, ora ci rimane una sola cosa da fare: fare un programmino che enumera tutti i Secrets disponibili (quindi enumera le chiavi di registro corrispondenti) e per ognuno visualizza il valore corrispondente. Dunque, la cosa non è affatto triviale: abbiamo già visto come l'enumerazione delle chiavi di registro incriminate debba per forza passare attraverso una esecuzione con l'utenza System. Allo stesso tempo, bisogna rendere la procedura più modulare possibile (della serie prendi la procedura, la chiami e via). Uhmm si potrebbe cambiare temporaneamente le ACL sul ramo di registro in questione per poi ripristinarle alla fine; tuttavia tale soluzione ha un piccolo problema: mi fa schifo; il motivo?? Behh, pensiamo a tutte le volte che sviluppiamo un software: si passa attraverso errori di vario tipo ed il risultato finale è il frutto di centinaia di correzioni. Ora: per ogni errore che vado a fare nella procedura di modifica delle ACL su quella parte delicata di registro, c'è un alta probabilità di danneggiare gravemente il sistema. Detto questo, la soluzione migliore è ricorrere al Code Injection: andrò ad eseguire nello spazio di memoria di un processo eseguito con l'utenza System (ad esempio winlogon.exe) la procedura di enumerazione delle chiavi; tale elenco verrà inviato al processo chiamante tramite un Named Pipe, e l'elenco dei valori verrà processato localmente chiamando la procedura RetrieveSecret definita sopra.

Prendendo spunto dagli articoli inerenti il Code Injection, mi sono costruito questa funzione che consente l'enumerazione delle sottochiavi di una chiave di registro, nello spazio di memoria di un processo remoto

function RemoteRegEnumKeyEx(PID: Cardinal; //PID del processo remoto KeyName: PAnsiChar; NamedPipeName: PAnsiChar; //valore importante ottenuto dalla funzione remota var outValue: Cardinal; //errore riscontrato dalla funzione remota var codError: Cardinal; //esecuzione sincrona del thread remoto synch: Boolean): Boolean; type TRemoteRegEnumKeyExData = record errorcod: Cardinal; //codice di errore rilevato nella funzione remota output: Cardinal; //risultato significativo nella funzione remota //Indirizzi delle API usate: elenco puntatori a funzioni; //N.B. rigorosamente stdcall //Es. pLoadLibraryA: function(pLibFileName: PAnsiChar): Cardinal; stdcall; pExitThread: procedure (dwExitCode: Cardinal); stdcall; //API obbligatoria pGetLastError: function(): Cardinal; stdcall; //api di interazione col registro di windows pRegOpenKeyExA: function( hKey: Cardinal; lpSubKey: PAnsiChar; ulOptions: Cardinal; samDesired: Cardinal; var phkResult: Cardinal ): Integer; stdcall; pRegQueryInfoKeyA: function( hKey: Cardinal; lpClass: PAnsiChar; lpcbClass: PCardinal; lpReserved: Pointer; lpcSubKeys, lpcbMaxSubKeyLen, lpcbMaxClassLen, lpcValues, lpcbMaxValueNameLen, lpcbMaxValueLen, lpcbSecurityDescriptor: PCardinal; lpftLastWriteTime: Pointer ): Integer; stdcall; pRegEnumKeyExA: function( hKey: Cardinal; dwIndex: Cardinal; lpName: PAnsiChar; lpcbName: PCardinal; lpReserved: Pointer; lpClass: PAnsiChar; lpcbClass: PCardinal; lpftLastWriteTime: Pointer ): Integer; stdcall; pRegCloseKey: function( hKey: Cardinal ): Integer; stdcall; pCreateFileA: function( lpFileName: PAnsiChar; dwDesiredAccess, dwShareMode: Cardinal; lpSecurityAttributes: Pointer; dwCreationDisposition, dwFlagsAndAttributes: Cardinal; hTemplateFile: Cardinal ): Cardinal; stdcall; pWriteFile: function( hFile: Cardinal; const Buffer; //Buffer: Pointer; nNumberOfBytesToWrite: Cardinal; var lpNumberOfBytesWritten: Cardinal; lpOverlapped: Pointer ): Boolean; stdcall; pCloseHandle: function( hnd: Cardinal ): Boolean; stdcall; //Puntatori a dati; Es. tutte le stringhe vanno messe qui dentro //Es. pNomeDll: Pointer; pKeyName: Pointer; pNamedPipeName: Pointer; end; var hProcess: Cardinal; //handle al processo remoto RemoteRegEnumKeyExData: TRemoteRegEnumKeyExData; //indirizzo del record nello spazio di memoria del processo remoto pRemoteRegEnumKeyExData: Pointer; //indirizzo della funzione del thread nello spazio di memoria del processo remoto pRemoteRegEnumKeyExThread: Pointer; output_value: Cardinal; error_code: Cardinal; //indirizzi di base delle dll che ci interessano, //nello spazio di memoria del processo remoto hKernel32, hAdvapi32: Cardinal; FuncAddr: Cardinal; functionSize, parameterSize: Cardinal; procedure RemoteRegEnumKeyExThread(lpParameter: pointer); stdcall; //var //qui posso definire delle variabili locali var hKey: Cardinal; NumSubKeys: Cardinal; i: Cardinal; SubKeyName: array[1..255] of Char; SubKeyNameSize: Cardinal; //interazione col named pipe PipeKeys: Cardinal; BytesWritten: Cardinal; sMessage: String; // begin with TRemoteRegEnumKeyExData(lpParameter^) do begin //apriamo il named pipe creato dal processo chiamante PipeKeys := pCreateFileA(pNamedPipeName, GENERIC_WRITE, 0, nil, //CREATE_NEW OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0); if (PipeKeys = INVALID_HANDLE_VALUE) then begin errorcod := pGetLastError; pExitThread(0); end; if (pRegOpenKeyExA( HKEY_LOCAL_MACHINE, pKeyName, 0, KEY_READ, hKey //handle alla chiave aperta ) <> 0) then begin errorcod := pGetLastError; pExitThread(0); end; if (pRegQueryInfoKeyA( hKey, nil, nil, nil, @NumSubKeys, nil, nil, nil, nil, nil, nil, nil ) <> 0) then begin errorcod := pGetLastError; pExitThread(0); end; for i := 0 to NumSubKeys - 1 do begin SubKeyNameSize := 255; pRegEnumKeyExA( hKey, i, @SubKeyName[1], @SubKeyNameSize, nil, nil, nil, nil ); //scrivo il nome sul Named Pipe pWriteFile(PipeKeys, SubKeyName, SubKeyNameSize, BytesWritten, nil); //pWriteFile(PipeKeys, #13#10, 2, BytesWritten, nil); end; //scrivo il nome del pipe come stringa di terminazione sul Named Pipe pWriteFile(PipeKeys, pNamedPipeName^, 18, BytesWritten, nil); pRegCloseKey(hKey); pCloseHandle(PipeKeys); pExitThread(1); //N.B. chiamare sempre pExitCodeThread per definire il codice //di uscita del thread //Es. pExitCodeThread(1) significa successo // pExitCodeThread(0) significa fallimento end; end; //dealloco la memoria in remoto function RemoteRegEnumKeyExUnloadData(): Boolean; begin Result := False; with RemoteRegEnumKeyExData do begin //chiamo UnloadData su tutti i puntatori a dati inclusi nel record UnloadData(hProcess, pKeyName); UnloadData(hProcess, pNamedPipeName); end; //deallocazione spazio per il parametro UnloadData(hProcess, pRemoteRegEnumKeyExData); //deallocazione spazio per la funzione UnloadData(hProcess, pRemoteRegEnumKeyExThread); Result := True; end; //definisco i valori dei campi del record e copio i dati in remoto function RemoteRegEnumKeyExInjectData(): Boolean; begin Result := False; try //inizializzazione valori campi del record: with RemoteRegEnumKeyExData do begin errorcod := 0; output := 0; //determino l'indirizzo di base di caricamento di kernel32.dll //e advapi32.dll nello spazio di memoria del processo remoto if not RemoteGetModuleHandle( hProcess, 'kernel32.dll', hKernel32 ) then begin Exit; end; if not RemoteGetModuleHandle( hProcess, 'advapi32.dll', hAdvapi32 ) then begin Exit; end; //assegno i valori ai puntatori a funzione //(tramite RemoteGetProcAddress) if not RemoteGetProcAddress( hProcess, hKernel32, 'ExitThread', FuncAddr ) then begin Exit; end; pExitThread := Pointer(FuncAddr); if not RemoteGetProcAddress( hProcess, hKernel32, 'GetLastError', FuncAddr ) then begin Exit; end; pGetLastError := Pointer(FuncAddr); if not RemoteGetProcAddress( hProcess, hKernel32, 'CreateFileA', FuncAddr ) then begin Exit; end; pCreateFileA := Pointer(FuncAddr); if not RemoteGetProcAddress( hProcess, hKernel32, 'WriteFile', FuncAddr ) then begin Exit; end; pWriteFile := Pointer(FuncAddr); if not RemoteGetProcAddress( hProcess, hKernel32, 'CloseHandle', FuncAddr ) then begin Exit; end; pCloseHandle := Pointer(FuncAddr); if not RemoteGetProcAddress( hProcess, hAdvapi32, 'RegOpenKeyExA', FuncAddr ) then begin Exit; end; pRegOpenKeyExA := Pointer(FuncAddr); if not RemoteGetProcAddress( hProcess, hAdvapi32, 'RegQueryInfoKeyA', FuncAddr ) then begin Exit; end; pRegQueryInfoKeyA := Pointer(FuncAddr); if not RemoteGetProcAddress( hProcess, hAdvapi32, 'RegEnumKeyExA', FuncAddr ) then begin Exit; end; pRegEnumKeyExA := Pointer(FuncAddr); if not RemoteGetProcAddress( hProcess, hAdvapi32, 'RegCloseKey', FuncAddr ) then begin Exit; end; pRegCloseKey := Pointer(FuncAddr); //assegno i valori ai puntatori ai dati (tramite InjectData); //N.B. se una InjectData ritorna False allora bisogna chiamare Exit if not InjectData(hProcess, KeyName, Length(KeyName), False, pKeyName) then begin Exit; end; if not InjectData(hProcess, NamedPipeName, Length(NamedPipeName), False, pNamedPipeName) then begin Exit; end; end; //copio il parametro parameterSize := SizeOf(RemoteRegEnumKeyExData); if not InjectData(hProcess, @RemoteRegEnumKeyExData, parameterSize, False, pRemoteRegEnumKeyExData) then begin Exit; end; //copio la funzione functionSize := SizeOfProc(@RemoteRegEnumKeyExThread); if not InjectData(hProcess, @RemoteRegEnumKeyExThread, functionSize, True, pRemoteRegEnumKeyExThread) then begin Exit; end; Result := True; finally if not Result then begin RemoteRegEnumKeyExUnloadData; end; end; end; begin //inizializzo a zero le variabili locali hProcess := 0; pRemoteRegEnumKeyExData := nil; pRemoteRegEnumKeyExThread := nil; output_value := 0; error_code := 0; try hProcess := OpenProcessEx(PROCESS_CREATE_THREAD + PROCESS_QUERY_INFORMATION + PROCESS_VM_OPERATION + PROCESS_VM_WRITE + PROCESS_VM_READ, False, PID ); if hProcess = 0 then begin ErrStr('OpenProcess'); Exit; end; if not RemoteRegEnumKeyExInjectData() then Exit; if not InjectThread(hProcess, pRemoteRegEnumKeyExThread, pRemoteRegEnumKeyExData, output_value, error_code, synch) then Exit; //output_value e error_code sono stati inizializzati a 0. //sono stati modificati dalla InjectThread solo se synch = true; //se synch=false sono rimasti uguali a zero outvalue := output_value; codError := error_code; Result := True; finally if synch or (not Result) then begin RemoteRegEnumKeyExUnloadData; end; if hProcess <> 0 then begin if not CloseHandle(hProcess) then begin ErrStr('CloseHandle'); end; end; end; end;

Anche in questo caso ho omesso un pò di roba ma nell'esempio allegato finale c'è tutto

Come si può vedere, i parametri principali sono i seguenti:

  • PID[input]: PID del processo nel cui spazio di memoria vogliamo eseguire l'enumerazione delle sottochiavi
  • KeyName[input]: nome della chiave (percorso completo relativo a HKLM) di cui si vogliono enumerare le sottochiavi.
  • NamedPipeName[input]: nome del Named Pipe su cui verranno scritte i nomi delle sottochiavi della chiave specificata da KeyName: il Named Pipe in questione deve essere creato dal processo chiamante, che si occuperà poi di eliminarlo a lavori terminati.

La cosa importante da sottolineare è che come ultima stringa invio il nome del Named Pipe: in pratica quando andrò a leggere dal Named Pipe, nel momento in cui preleverò una stringa equivalente al nome del Named Pipe allora vorrà dire che l'elenco è terminato.

A questo punto ci si può creare una funzione che raccoglie i vari nomi in una TStringList

function GetKeyNames( ProcName: PAnsiChar; KeyName: PAnsiChar; NamedPipeName: PAnsiChar; ElencoNomi: TStringList ): Boolean; const BytesToRead = 1000; var FPipeHandle: Cardinal; success: Boolean; output: Cardinal; err: Cardinal; PID: Cardinal; FReadMessage: String; BytesRead: Cardinal; DataRead: Array[0..BytesToRead] Of Char; begin Result := False; try FPipeHandle := CreateNamedPipe( NamedPipeName, PIPE_ACCESS_INBOUND, PIPE_TYPE_MESSAGE or PIPE_READMODE_MESSAGE or PIPE_WAIT, 1, 8096, 8096, 5000, nil); if (FPipeHandle = INVALID_HANDLE_VALUE) then Exit; PID := PidProcesso(ProcName); if not RemoteRegEnumKeyEx(PID, KeyName, NamedPipeName, output, err, True) then begin Exit; end; //leggo dal pipe repeat ReadFile(FPipeHandle, DataRead, BytesToRead, BytesRead, nil); if (BytesRead <> 0) then begin SetString(FReadMessage, DataRead, BytesRead); ElencoNomi.Add(FReadMessage); end; until (FReadMessage = NamedPipeName); Result := True; finally if (FPipeHandle <> INVALID_HANDLE_VALUE) then begin DisconnectNamedPipe(FPipeHandle); CloseHandle(FPipeHandle); end; end; end;

ProcName rappresenta il nome del processo remoto che si vuole usare mentre ElencoNomi raccoglie i nomi delle sottochiavi.

Behh mi pare proprio cha abbiamo tutti i mattoncini necessari.

Ho deciso di rendere l'utilizzo il più semplice possibile: in pratica il tutto si riduce alla chiamata ad una funzione alla quale viene passato come parametro un puntatore ad una funzione di callback adeguatamente definita (stessa tecnica usata dalle api win32 per l'enumerazione ad esempio di finestre o altri oggetti). Di seguito un esempio di utilizzo

function GetDataCallBack( DataType: Cardinal; Data: Pointer; DataSize: Cardinal ): Boolean; begin case DataType of 1: //nome del Secret begin FrmMain.mmoLog.Lines.Add(PAnsiChar(Data)); end; 2: //valore del Secret begin FrmMain.mmoLog.Lines.Add(DumpData(Data, DataSize)); FrmMain.mmoLog.Lines.Add(' '); end; end; Result := True; end; procedure TFrmMain.btnDumpSecretsClick(Sender: TObject); begin mmoLog.Lines.Clear; GetSecrets(@GetDataCallBack); end;

Infine i sorgenti e l'eseguibile dell'esempio completo che esegue un Dump degli LSA Secrets

LsaSecrets.7z (VCL e KOL)

LsaSecrets.exe.7z (relativo alla versione KOL)

Implementazione differente della procedura di enumerazione delle sottochiavi di registro (09/09/07)

Mi sembra doveroso aggiungere questa nuova implementazione della funzione GetKeyNames esposta in precedenza e che aveva come obiettivo quello di restituire l'elenco dei nomi delle sottochiavi di una chiave di registro accessibile in lettura completa solo dall'utenza System. Questa nuova implementazione si basa sul Privilegio di Backup. Nell'articolo Privilegi Utente viene fatta una panoramica sull'argomento. Ma in cosa consiste il Privilegio di Backup? La sua efficacia è incredibile in quanto un utente che ha questo Privilegio (gli Administrators ed i Backup Operators sono 2 gruppi utente predefiniti i cui utenti godono di questo Privilegio) e ce lo ha abilitato, può effettuare operazioni di lettura su elementi del sistema operativo (file o chiavi di registro) i cui diritti di accesso negano l'accesso in lettura all'utente in questione. In parole povere, se sono un utente e non ho il diritto di accesso in lettura ad un file, se ho il Privilegio di Backup e ce l'ho abilitato, posso tranquillamente copiarmi (eseguire quindi un backup) il file in questione. Nella stessa maniera, se non ho l'accesso in lettura ad una chiave di registro, ma ho il Privilegio di Backup e ce l'ho abilitato, posso tranquillamente copiarmi (eseguire quindi un backup) la chiave in questione (questo significa chiaramente che posso enumerarne le sottochiavi, leggerne i valori e fare in pratica tutto quello che potrei fare se avessi accesso completo in lettura alla chiave). Non importa quali siano le ACL settate, con il Privilegio di Backup abilitato, posso leggere qualsiasi cosa; con il Privilegio di Backup abilitato, i diritti di accesso non vengono presi in considerazione e mi viene garantito l'accesso completo. Il tutto vale naturalmente anche per il restore dei file o delle chiavi di registro (del resto chi esegue un backup lo fa perchè un giorno magari dovrà eseguire un restore del backup in questione) e quindi per i diritti di accesso in scrittura. In parole povere: Privilegio di Backup = accesso totale senza che ti vengano applicate le ACL. Quindi possiamo enumerare le sottochiavi della chiave di registro HKEY_LOCAL_MACHINE\SECURITY\Policy\Secrets semplicemente disponendo del Privilegio di Backup. Gli Administrators ed i Backup Operators dispongono di questo privilegio. Tuttavia c'è un qualcosa in più da fare: in pratica l'handle alla chiave che vogliamo leggere, deve essere ottenuto tramite chiamata all'api win32 RegCreateKeyEx passandogli come 5° parametro il valore REG_OPTION_BACKUP_RESTORE (valore $00000004). In questo caso non verrà preso in considerazione il 6° parametro che viene usato per stabilire la modalità di accesso desiderata alla chiave in questione. La combinazione quindi del Privilegio di Backup abilitato e la restituzione di un handle alla chiave tramite la RegCreateKeyEx con la modalità appena descritta, consente di eseguire qualsiasi operazione sulla chiave in questione. Di seguito la funzione che restituisce l'elenco dei nomi delle sottochiavi di una chiave di registro

function RegEnumSubKeys( RootHandle: Cardinal; KeyName: PAnsiChar; BypassACL: Boolean; Elenco: TStringList ): Boolean; var hKey: Cardinal; NumSubKeys: Cardinal; i: Cardinal; SubKeyName: array[1..255] of Char; SubKeyNameSize: Cardinal; Disposition: Cardinal; begin // try if not BypassACL then begin if (RegOpenKeyExA( HKEY_LOCAL_MACHINE, //$80000002 KeyName, 0, KEY_READ, //$20019 hKey //handle alla chiave aperta ) <> 0) then begin ErrStr('RegOpenKeyEx'); Exit; end; end else begin //abilito il priviligio di backup if not ModificaPrivilegio('SeBackupPrivilege', True) then Exit; if RegCreateKeyExA( HKEY_LOCAL_MACHINE, KeyName, 0, nil, REG_OPTION_BACKUP_RESTORE, 0, nil, hKey, Disposition) <> 0 then begin ErrStr('RegCreateKeyEx'); Exit; end; end; if (RegQueryInfoKeyA( hKey, nil, nil, nil, @NumSubKeys, nil, nil, nil, nil, nil, nil, nil ) <> 0) then begin ErrStr('RegQueryInfoKeyA'); Exit; end; for i := 0 to NumSubKeys - 1 do begin SubKeyNameSize := 255; RegEnumKeyExA( hKey, i, @SubKeyName[1], @SubKeyNameSize, nil, nil, nil, nil ); Elenco.Add(SubKeyName); end; finally if hKey <> 0 then RegCloseKey(hKey); end; end;

Come si può notare ho cambiato nome alla funzione: non più GetKeyNames ma RegEnumSubKeys, senza un motivo in particolare più che altro per evitare confusione, in soldoni comunque fa lo stesso sporco lavoro che fa la GetKeyNames.

Analizziamo ora i parametri

  • RootHandle[input]: Handle alla chiave di base; ad esempio nel nostro caso metteremo HKEY_LOCAL_MACHINE che corrisponde al valore $80000002. Nella GetKeyNames definita in precedenza non c'era questo parametro perchè partivamo dal presupposto che la root fosse appunto la HKEY_LOCAL_MACHINE; per generalizzare ulteriormente la questione ho deciso di aggiungerlo in questa nuova implementazione
  • KeyName[input]: Behh... il nome della chiave di registro con percorso completo relativo alla root
  • BypassACL[input]: valore booleano che mi consente di specificare se voglio bypassare le ACL e quindi provare ad abilitare il Privilegio di Backup e poi chiamare RegCreateKeyEx con il 5° parametro settato a REG_OPTION_BACKUP_RESTORE, oppure affidarmi all'approccio classico chiamando semplicemente RegOpenKeyEx con il 6° parametro settato a KEY_READ (valore $20019)
  • Elenco[output]: elenco dei nomi.

Di seguito sorgenti ed eseguibile di questa nuova versione del programma

LsaSecrets2.7z (VCL e KOL)

LsaSecrets2.exe.7z (relativo alla versione KOL)

Data ultimo aggiornamento: 09/09/2007 (aggiunta nuova implementazione)

 

 

 

 

 

 

 

 
 
Your Ad Here