Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion csrc/inject/LibraryInjector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,18 @@ LibraryInjector::initializeInjectionEnvironment(long &code_injection_address,
// Copy original registers to working registers
*working_registers = *original_registers;

// Find a good address to copy code to
// Find a good address to copy code to.
// The findFreeMemoryAddress function returns the END of the first executable
// region minus a safe offset, placing shellcode in the alignment padding area
// that is typically unused but still has execute permissions.
code_injection_address =
ProcessUtils::findFreeMemoryAddress(target_process_id_) + 8;

if (process_tracer_.isDebugMode()) {
std::cout << "[DEBUG] PyFlightProfiler: Using injection address at 0x"
<< std::hex << code_injection_address << std::dec << std::endl;
}

// Set the target's rip to the injection address
// Advance by 2 bytes because rip gets incremented by the size of the current
// instruction
Expand Down
33 changes: 27 additions & 6 deletions csrc/inject/ProcessTracer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ bool ProcessTracer::continueExecution() {
int result = ptrace(PTRACE_CONT, process_id_, NULL, NULL);
CHECK_PTRACE_RESULT(result, PTRACE_CONT);

// Wait for the target process to stop (e.g., hit INT3 breakpoint)
int wait_status;
pid_t waited_pid = waitpid(process_id_, &wait_status, 0);
if (waited_pid != process_id_) {
if (debug_mode_) {
std::cerr << "[ERROR] PyFlightProfiler: waitpid(" << process_id_
<< ") failed or returned unexpected pid " << waited_pid << ": "
<< strerror(errno) << std::endl;
}
return false;
}

// Check if the process stopped (not exited or terminated)
if (!WIFSTOPPED(wait_status)) {
if (debug_mode_) {
std::cerr << "[ERROR] PyFlightProfiler: process " << process_id_
<< " did not stop as expected, wait_status=" << wait_status
<< std::endl;
}
return false;
}

// Make sure the target process received SIGTRAP after stopping.
return verifySignalStatus();
}
Expand Down Expand Up @@ -201,16 +223,15 @@ bool ProcessTracer::writeMemory(unsigned long address, const void *buffer,
* @return siginfo_t structure containing signal information
*/
siginfo_t ProcessTracer::getSignalInfo() {
sleepMs(5);

siginfo_t signal_info;
// When PTRACE_GETSIGINFO returns -1, tracee may not reach int3 point, so
// spin on it waiting at most 500ms
for (int i = 0; i < 100; i++) {
// With waitpid() in continueExecution(), the process should already be
// stopped. Retry a few times just in case, but much shorter timeout is
// needed.
for (int i = 0; i < 10; i++) {
if (ptrace(PTRACE_GETSIGINFO, process_id_, NULL, &signal_info) != -1) {
return signal_info;
}
sleepMs(5);
sleepMs(1);
}

// this is mostly due to gil lock not released, so injected code cannot
Expand Down
41 changes: 30 additions & 11 deletions csrc/inject/ProcessUtils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,16 @@
/**
* @brief Find a free memory address in the target process
*
* Parses /proc/[pid]/maps to find a memory region with execute permissions.
* Parses /proc/[pid]/maps to find the END of the first executable memory
* region. We use the end of the region (with a small offset back) because:
* 1. The end of code segments typically has alignment padding (unused space)
* 2. This avoids overwriting active code at the beginning of the segment
* 3. The padding area is still executable (same permissions as the code
* segment)
*
* @param process_id PID of the target process
* @return Address of free memory, or 0 on failure
* @return Address of free memory (end of executable region minus offset), or 0
* on failure
*/
long ProcessUtils::findFreeMemoryAddress(pid_t process_id) {
std::string filename = "/proc/" + std::to_string(process_id) + "/maps";
Expand All @@ -30,27 +36,40 @@ long ProcessUtils::findFreeMemoryAddress(pid_t process_id) {
}

std::string line;
long address = 0;
long end_address = 0;

while (std::getline(maps_file, line)) {
std::istringstream iss(line);
std::string range, permissions, offset, device, inode, path;

if (iss >> range >> permissions >> offset >> device >> inode) {
// Extract address from range (format: address1-address2)
size_t dash_pos = range.find('-');
if (dash_pos != std::string::npos) {
std::string address_str = range.substr(0, dash_pos);
address = std::stol(address_str, nullptr, 16);
}

// Check if this is an executable region
if (permissions.find('x') != std::string::npos) {
// Extract end address from range (format: start_address-end_address)
size_t dash_pos = range.find('-');
if (dash_pos != std::string::npos) {
std::string end_address_str = range.substr(dash_pos + 1);
end_address = std::stol(end_address_str, nullptr, 16);
}
break;
}
}
}

return address;
// Calculate the minimum safe offset for shellcode injection.
// The shellcode (inject_shared_library function) is approximately:
// - ~80 bytes of assembly instructions (stack ops, calls, int3 breakpoints)
// - +2 bytes NOP prefix (for syscall restart handling)
// - +16 bytes alignment padding (x86_64 ABI requires 16-byte stack alignment)
// - +16 bytes safety margin
// Total: ~114 bytes, rounded up to 128 bytes (0x80) for 16-byte alignment
const long SHELLCODE_MIN_SIZE = 128;

// Use the end of the executable region minus the minimum required offset.
// This minimizes the risk of overwriting active code while ensuring enough
// space for the shellcode in the alignment padding area.
return (end_address > SHELLCODE_MIN_SIZE) ? (end_address - SHELLCODE_MIN_SIZE)
: 0;
}

/**
Expand Down
Loading