The handling of communication between debugger and debuggee in .NET is managed by dbgtransportsession.cpp. This component sets up two named pipes per .NET process as seen in dbgtransportsession.cpp#L127, which are initiated via twowaypipe.cpp#L27. These pipes are suffixed with -in and -out.
By visiting the user's $TMPDIR, one can find debugging FIFOs available for debugging .Net applications.
DbgTransportSession::TransportWorker is responsible for managing communication from a debugger. To initiate a new debugging session, a debugger must send a message via the out pipe starting with a MessageHeader struct, detailed in the .NET source code:
struct MessageHeader { MessageType m_eType;// Message type DWORD m_cbDataBlock;// Size of following data block (can be zero) DWORD m_dwId;// Message ID from sender DWORD m_dwReplyId;// Reply-to Message ID DWORD m_dwLastSeenId;// Last seen Message ID by sender DWORD m_dwReserved;// Reserved for future (initialize to zero)union{struct{ DWORD m_dwMajorVersion;// Requested/accepted protocol version DWORD m_dwMinorVersion;} VersionInfo; ...} TypeSpecificData; BYTE m_sMustBeZero[8];}
To request a new session, this struct is populated as follows, setting the message type to MT_SessionRequest and the protocol version to the current version:
This header is then sent over to the target using the write syscall, followed by the sessionRequestData struct containing a GUID for the session:
A read operation on the out pipe confirms the success or failure of the debugging session establishment:
Reading Memory
Once a debugging session is established, memory can be read using the MT_ReadMemory message type. The function readMemory is detailed, performing the necessary steps to send a read request and retrieve the response:
The complete proof of concept (POC) is available here.
Writing Memory
Similarly, memory can be written using the writeMemory function. The process involves setting the message type to MT_WriteMemory, specifying the address and length of the data, and then sending the data:
To execute code, one needs to identify a memory region with rwx permissions, which can be done using vmmap -pages:
Locating a place to overwrite a function pointer is necessary, and in .NET Core, this can be done by targeting the Dynamic Function Table (DFT). This table, detailed in jithelpers.h, is used by the runtime for JIT compilation helper functions.
For x64 systems, signature hunting can be used to find a reference to the symbol _hlpDynamicFuncTable in libcorclr.dll.
The MT_GetDCB debugger function provides useful information, including the address of a helper function, m_helperRemoteStartAddr, indicating the location of libcorclr.dll in the process memory. This address is then used to start a search for the DFT and overwrite a function pointer with the shellcode's address.
The full POC code for injection into PowerShell is accessible here.