macOS .Net Applications Injection - HackTricks

# macOS .Net Applications Injection

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.

This is a summary of the post https://blog.xpnsec.com/macos-injection-via-third-party-frameworks/. Check it for further details!

## .NET Core Debugging

### Establishing a Debugging Session

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:

    static const DWORD kCurrentMajorVersion = 2;
    static const DWORD kCurrentMinorVersion = 0;

    // Configure the message type and version
    sSendHeader.m_eType = MT_SessionRequest;
    sSendHeader.TypeSpecificData.VersionInfo.m_dwMajorVersion = kCurrentMajorVersion;
    sSendHeader.TypeSpecificData.VersionInfo.m_dwMinorVersion = kCurrentMinorVersion;
    sSendHeader.m_cbDataBlock = sizeof(SessionRequestData);

This header is then sent over to the target using the `write` syscall, followed by the `sessionRequestData` struct containing a GUID for the session:

    write(wr, &sSendHeader, sizeof(MessageHeader));
    memset(&sDataBlock.m_sSessionID, 9, sizeof(SessionRequestData));
    write(wr, &sDataBlock, sizeof(SessionRequestData));

A read operation on the `out` pipe confirms the success or failure of the debugging session establishment:

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

## 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:

    bool readMemory(void *addr, int len, unsigned char **output) {
    // Allocation and initialization
    ...
    // Write header and read response
    ...
    // Read the memory from the debuggee
    ...
    return true;
    }

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:

    bool writeMemory(void *addr, int len, unsigned char *input) {
    // Increment IDs, set message type, and specify memory location
    ...
    // Write header and data, then read the response
    ...
    // Confirm memory write was successful
    ...
    return true;
    }

The associated POC is available here.

## .NET Core Code Execution

To execute code, one needs to identify a memory region with rwx permissions, which can be done using vmmap -pages:

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

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.

## References

- https://blog.xpnsec.com/macos-injection-via-third-party-frameworks/

> [!TIP] Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
> Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

> Support HackTricks
> - Check the subscription plans!
> - Join the 💬 Discord group or the telegram group or follow us on Twitter 🐦 @hacktricks_live.
> - Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.