Skip to content

Instantly share code, notes, and snippets.

@jmahmood
Created April 10, 2017 00:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jmahmood/d7feb32d902e266609dbc7ec22575a8d to your computer and use it in GitHub Desktop.
Save jmahmood/d7feb32d902e266609dbc7ec22575a8d to your computer and use it in GitHub Desktop.
Python; learning debugging for the mac
# Creating a debugger on Mac OS using Python and Python CTypes
# ------------------------------------------------------------
# If you are like me, you are following along the Grey Hat Python book that was on the "Humble Bundle" sale and are
# a bit frustrated that everything is about Windows. Also that it requires Python 2.5.
#
# (I haven't used a Windows machine for years and doubt I will be doing dev for it in my career.)
#
# I am trying to implement the Chapter 3 debugger stuff in this file, using Python 3.5.
# This is not necessarily something pythonic or using best practices, I just want to get it working, after which
# I may clean it up.
#
# I am not too familiar with the Windows API, so I could be using the wrong system calls here. I am making no attempt
# to make this cross-platform, as I am only interested in having this work on the Mac for now.
#
# A lot of useful information about necessary C is here:
# http://system.joekain.com/2015/06/08/debugger.html
#
# Basically, it seems that you can create a process in Windows using CreateProcessA that is in "debug mode" based on
# the data you pass in as dwCreationFlags. There is no single flag you can pass to execv that does the same, but you
# can set ptrace on the forked process before running execv with a similar outcome.
#
# Goals:
# Create a process that is paused by the debugger: COMPLETE
# Create a process that is paused and resumed by the bugger: COMPLETE
# Create a class called "Debugger" that lets you load a program
# Examine the dwDesiredAccess settings in Windows and determine if something similar exists w/ Ptrace
# Get a list of the current active thread in the debugging program when it is blocked
# Grab value of all registers wrt the active thread
import ctypes
import os
import signal
from ctypes import *
libc = CDLL("libc.dylib") # we use dylib on mac instead of .so
PT_TRACE_ME = 0 # Taken from the mac ptrace header file: https://opensource.apple.com/source/xnu/xnu-344/bsd/sys/ptrace.h
PT_CONTINUE = 7 # Taken from the mac ptrace header file: https://opensource.apple.com/source/xnu/xnu-344/bsd/sys/ptrace.h
EAGAIN = 35
result = libc.fork()
first = True
while first or (result == -1 and libc.errno == EAGAIN):
first = False
if result == 0:
# setup_inferior
print("Inferior process being activated / Ptraced")
libc.ptrace(PT_TRACE_ME, None, None, None)
os.execv('/Users/jawaad/PycharmProjects/DebuggerTutorial/basic_program/hw', [''])
elif result == -1:
# error
print("Error")
else:
# debugger
print("Debugger")
while True:
status_ptr_type = ctypes.POINTER(ctypes.c_int)
status_ptr = status_ptr_type(ctypes.c_int(25))
# print("Waiting for pid")
# print("Result: " + str(result))
print("Debugger waiting for next step is run in inferior process.")
print("Process ID: " + str(libc.waitpid(result, status_ptr, 0)))
# print("Status pointer: " + str(status_ptr[0]))
# print(repr(status_ptr.contents))
# print(repr(os.WIFSTOPPED(status_ptr[0])))
# print(repr(os.WSTOPSIG(status_ptr[0])))
# print(repr(os.WIFEXITED(status_ptr[0])))
if os.WIFSTOPPED(status_ptr[0]) and os.WSTOPSIG(status_ptr[0]) == signal.SIGTRAP:
print("Inferior stopped on SIGTRAP - continuing...\n")
libc.ptrace(PT_CONTINUE, result, 1, 0) # caddr_t addr == 1 to indicate "continue".
if os.WIFEXITED(status_ptr[0]):
print("Inferior exited - debugger terminating...\n")
exit(0)
@zanapher
Copy link

Hi.
I'm really glad that I found this code because I'm exactly in the same case as you. I also got the Gray Hat book on Humble Bundle, and wanted to use it on a Mac with Python 3.
The link given is a nice start too. I also picked some ideas from your code. I was so focused on ctypes that I even forgot to use the o module... for instance for exec (which makes the code much cleaner).

There are a few things I don't understand well in your code, like for instance why you do a while loop with the EAGAIN condition and the 'first' boolean variable. The blog post uses waitpid which seems much simpler and way cleaner code...

Here's my current file (that does mostly what the blog post does).

Note: The first function load_defines is an automatic way to get constant definitions from the .h files. It's very rough as it is, but works on the constants I wanted to get and will hopefully be reusable later instead of reading and copying values by hand (will probably have to patch it as I go to account for new syntax from time to time).

#!/usr/bin/env python3

from ctypes import *
import os
import re

# ******************************************************************************

# path to .h libraries for included macros
INCLUDE_PATH = "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/"
# global dictionary containing all loaded macro definitions
DEFINE = {}
# standard C library
libc = CDLL("libc.dylib")

# ******************************************************************************

def load_defines(lib_name):
	"""Parse the library passed as argument for all #define instructions and store all the definitions in the global DEFINE dictionary.
	This function is very basic and will only work for the simplest defined macros (mainly integer constants)
	"""
	global DEFINE	# the dictionary that will store the macros
	whites = re.compile(r"\s+")	# whitespace characters for delimiters
	
	f = open(INCLUDE_PATH + lib_name) # library file
	for l in f.readlines():
		if l.startswith("#define"):	# a macro definition line
			key, value = whites.split(l, 2)[1:]	# split line on whitespaces
			value = re.sub("/\*.*", "", value)	# ignore comments
			value = re.sub("//.*", "", value)
			value = value.rstrip()	# remove unwanted whitespace
			try:
				# evaluate the macro (to get integers instead of strings)
				DEFINE[key] = eval(value)
			except:
				# if evaluation fails, no error is raised
				pass
# load macros from libraries (constants)
load_defines("sys/ptrace.h")
load_defines("sys/errno.h")
load_defines("sys/signal.h")

# ******************************************************************************

class Debugger():
	"""The main debugger class"""
	def __init__(self):
		pass
	
	def load(self, path_to_exec, args=[]):
		"""Start inferior process and trace it"""
		
		# add the executable name to the arguments
		args.append(os.path.basename(path_to_exec))
		
		pid = os.fork()
		if pid == 0:
			# child process
			libc.ptrace(DEFINE['PT_TRACE_ME'], 0, 0, 0)	# set for trace
			os.execvp(path_to_exec, args)	# exec traced process
		else:
			# parent process
			while(True):
				pid, status = os.waitpid(pid, 0)
				if (os.WIFSTOPPED(status) 
					and os.WSTOPSIG(status) == DEFINE['SIGTRAP']):
					# inferior stopped because of SIGTRAP
					print("Inferior stopped on SIGTRAP. Continuing...")
					libc.ptrace(DEFINE['PT_CONTINUE'], pid, 1, 0)
				elif (os.WIFEXITED(status)):
					# inferior exited
					print("Inferior exited")
					break

if __name__ == '__main__':
	d = Debugger()
	# d.load("/Applications/Calculator.app/Contents/MacOS/Calculator")
	d.load("./inferior")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment