├── .gitignore ├── LICENSE ├── README.md ├── examples ├── breakpoints.py ├── exception.py └── ie_log.py ├── pycdb ├── __init__.py └── pycdb.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | __MACOSX 4 | */.DS_Store 5 | .DS_Store 6 | .DS_Store? 7 | ._* 8 | .Spotlight-V100 9 | .Trashes 10 | Icon? 11 | ehthumbs.db 12 | Thumbs.db 13 | 14 | # Byte-compiled / optimized / DLL files 15 | __pycache__/ 16 | *.py[cod] 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | env/ 24 | bin/ 25 | build/ 26 | develop-eggs/ 27 | dist/ 28 | eggs/ 29 | lib/ 30 | lib64/ 31 | parts/ 32 | sdist/ 33 | var/ 34 | *.egg-info/ 35 | .installed.cfg 36 | *.egg 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | 50 | # Translations 51 | *.mo 52 | 53 | # Mr Developer 54 | .mr.developer.cfg 55 | .project 56 | .pydevproject 57 | 58 | # Rope 59 | .ropeproject 60 | 61 | # Django stuff: 62 | *.log 63 | *.pot 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Justin Fisher 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pycdb 2 | ===== 3 | 4 | Python wrapper for the Windows CDB Debugger 5 | -------------------------------------------------------------------------------- /examples/breakpoints.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import struct 4 | import getopt 5 | 6 | sys.path.append(os.path.join("..", "pycdb")) 7 | 8 | import pycdb 9 | from pycdb import PyCdb, PyCdbPipeClosedException, ExitProcessEvent 10 | 11 | 12 | class BreakpointExample(PyCdb): 13 | """ 14 | A simple demonstration class to show how to use PyCdb to set breakpoints 15 | """ 16 | def __init__(self): 17 | PyCdb.__init__(self) 18 | self.ignore_exceptions = [ 19 | 0x4000001f # wow64 exception 20 | ] 21 | 22 | def on_create_window_ex_w(self, event): 23 | print("BREAKPOINT #%u CreateWindowExW" % (event.bpnum)) 24 | print(self.execute('u @$scopeip L10')) 25 | 26 | def on_create_thread(self, event): 27 | print("BREAKPOINT #%u CreateThread" % (event.bpnum)) 28 | print(self.breakpoint_info(event.bpnum)) 29 | print(self.execute('~.')) 30 | print(self.execute('kb 3')) 31 | 32 | def on_load_module(self, event): 33 | print("MODLOAD: %08X: %s" % (event.base, event.module)) 34 | mod = os.path.split(event.module)[1] 35 | mod = os.path.splitext(mod)[0] 36 | print(self.execute('lm m %s' % (mod))) 37 | 38 | def run(self): 39 | try: 40 | # wait until the first prompt 41 | # this works because initial_breakpoint is True 42 | self.read_to_prompt() 43 | 44 | # set a breakpoint with a handler 45 | self.breakpoint("user32!CreateWindowExW", self.on_create_window_ex_w) 46 | self.breakpoint("kernel32!CreateThread", self.on_create_thread) 47 | 48 | # simple debugging loop 49 | while True: 50 | # continue and wait for a prompt 51 | self.continue_debugging() 52 | output = self.read_to_prompt() 53 | 54 | # what was the stop event? 55 | 56 | # processes the event which will automatically 57 | # call any handlers associated with the events 58 | event = self.process_event() 59 | print("got debugger event: %s" % (event.description)) 60 | 61 | except PyCdbPipeClosedException: 62 | print("pipe closed") 63 | 64 | except ExitProcessEvent: 65 | print("program closed") 66 | 67 | finally: 68 | if not self.closed(): 69 | self.write_pipe('q\r\n') 70 | 71 | 72 | def usage(): 73 | print("usage: %s [-p|--pid] " % (sys.argv[0])) 74 | 75 | 76 | if __name__ == "__main__": 77 | pid = False 78 | try: 79 | opts, args = getopt.getopt(sys.argv[1:], 'p', ['pid']) 80 | except getopt.GetoptError as err: 81 | print(str(err)) 82 | usage() 83 | sys.exit(1) 84 | for o, a in opts: 85 | if o in ('-p', '--pid'): 86 | pid = True 87 | else: 88 | assert False, 'unhandled option' 89 | 90 | dbg = BreakpointExample() 91 | dbg.break_on_load_modules = True 92 | 93 | if pid: 94 | # attach to the specified pid 95 | dbg.attach(int(args[0])) 96 | else: 97 | # run the specified command (or notepad) 98 | if not args or len(args) == 0: 99 | args = ['notepad.exe'] 100 | dbg.spawn(args) 101 | # run the debug session 102 | dbg.run() 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /examples/exception.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import sys 3 | import os 4 | import struct 5 | import getopt 6 | 7 | sys.path.append(os.path.join("..", "pycdb")) 8 | 9 | import pycdb 10 | from pycdb import PyCdb, PyCdbPipeClosedException, ExceptionEvent, ExitProcessEvent 11 | 12 | 13 | class ExceptionCatcher(PyCdb): 14 | """ 15 | A simple demonstration class to show how to use PyCdb to 16 | issue commands, install breakpoints, and catch exceptions 17 | """ 18 | def __init__(self): 19 | PyCdb.__init__(self) 20 | 21 | self.ignore_exceptions = [ 22 | 0x4000001f, # wow64 exception 23 | 0xe06d7363, # C++ exception 24 | 0x40080201,0x80010108, 0x8001010D, 0x6A6, 0x8001010D, 0x40080201 25 | ] 26 | 27 | def on_create_window_ex_w(self, event): 28 | """ 29 | when this breakpoint is hit, write some code at the current 30 | instruction pointer that will cause an access violation 31 | """ 32 | print("CreateWindowExW breakpoint hit, adding crash code") 33 | crash_code = "B8EFBEADDE" # mov eax, 0xdeadbeef 34 | crash_code += "C700EDACEF0D" # mov [eax], 0xdefaced 35 | self.write_mem('@$scopeip', codecs.decode(crash_code, 'hex')) 36 | 37 | def on_load_module(self, event): 38 | """ 39 | just print the module that was loaded. note that this does callback 40 | does not drop you to a prompt. it is simply a notification callback. 41 | """ 42 | print("on_load_module: %08X, %s" % (event.base, event.module)) 43 | 44 | def test_commands(self): 45 | """ 46 | run some commands... 47 | """ 48 | if self.cpu_type == pycdb.CPU_X64: 49 | print("X64") 50 | else: 51 | print("X86") 52 | 53 | print("lm output:") 54 | print(self.execute('lm')) 55 | 56 | print("bytes at ntdll headers:") 57 | print(self.read_mem('ntdll', 0x100).encode('hex')) 58 | print("%08X" % (self.read_u32("ntdll"))) 59 | 60 | print("read invalid address") 61 | badread = self.read_mem(0x00000012, 0x10) 62 | print("badread: %u" % (len(badread))) 63 | 64 | print("registers dict") 65 | print(self.registers) 66 | 67 | print("stack contents") 68 | stack = 0 69 | 70 | try: 71 | stack = self.registers.rsp 72 | except (AttributeError, KeyError) as ex: 73 | stack = self.registers.esp 74 | print("stack at %08X" % (stack)) 75 | contents = struct.unpack('" % (sys.argv[0])) 176 | 177 | 178 | if __name__ == "__main__": 179 | pid = False 180 | try: 181 | opts, args = getopt.getopt(sys.argv[1:], 'p', ['pid']) 182 | except getopt.GetoptError as err: 183 | print(str(err)) 184 | usage() 185 | sys.exit(1) 186 | for o, a in opts: 187 | if o in ('-p', '--pid'): 188 | pid = True 189 | else: 190 | assert False, 'unhandled option' 191 | 192 | dbg = ExceptionCatcher() 193 | 194 | if pid: 195 | # attach to the specified pid 196 | dbg.attach(int(args[0])) 197 | else: 198 | # run the specified command (or notepad) 199 | if not args or len(args) == 0: 200 | args = ['notepad.exe'] 201 | dbg.spawn(args) 202 | # run the debug session 203 | dbg.run() 204 | 205 | 206 | 207 | -------------------------------------------------------------------------------- /examples/ie_log.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import struct 4 | import getopt 5 | 6 | sys.path.append(os.path.join("..", "pycdb")) 7 | 8 | import pycdb 9 | from pycdb import PyCdb, PyCdbPipeClosedException, ExceptionEvent, ExitProcessEvent 10 | 11 | SYMPATH = "z:\work\win_syms\symbols" 12 | 13 | 14 | class IEDebugLog(PyCdb): 15 | """ 16 | A simple demonstration that uses a breakpoint on Math.min to log messages 17 | to the debugger from Internet Explorer 18 | """ 19 | def __init__(self): 20 | PyCdb.__init__(self) 21 | self.ignore_exceptions = [ 22 | 0x4000001f # wow64 exception 23 | ] 24 | 25 | def on_math_min(self, event): 26 | val = self.read_u32(self.registers.esp + 0x10) 27 | val = val >> 1 28 | if val == 0x111111: 29 | ptr_data = self.read_u32(self.read_u32(self.registers.esp + 0x14) + 0xC) 30 | len_data = self.read_u32(self.read_u32(self.registers.esp + 0x14) + 0x8) * 2 31 | data = self.read_mem(ptr_data, len_data) 32 | print(data.decode('utf-16le')) 33 | 34 | def run(self): 35 | try: 36 | # wait until the first prompt 37 | # this works because initial_breakpoint is True 38 | self.read_to_prompt() 39 | 40 | # set our sympath 41 | self.execute('.sympath %s' % (SYMPATH)) 42 | 43 | # set a breakpoint with a handler 44 | # break on jscript9!Js::Math::Min 45 | self.breakpoint("jscript9!Js::Math::Min", self.on_math_min) 46 | 47 | # simple debugging loop 48 | while True: 49 | # continue and wait for a prompt 50 | self.continue_debugging() 51 | output = self.read_to_prompt() 52 | 53 | # what was the stop event? 54 | 55 | # processes the event which will automatically 56 | # call any handlers associated with the events 57 | event = self.process_event() 58 | #print "got debugger event: %s: %s" % (type(event), event.description) 59 | 60 | if type(event) == ExceptionEvent: 61 | exception = event 62 | if exception.code in self.ignore_exceptions: 63 | print("ignoring exception: %08X" % (exception.code)) 64 | else: 65 | print("Exception %08X (%s) occured at %08X" % (exception.code, exception.description, exception.address)) 66 | 67 | print("") 68 | print("Disas:") 69 | pre = self.execute('ub @$scopeip L5').strip().splitlines() 70 | for l in pre: 71 | print(' '*3 + l.strip()) 72 | post = self.execute('u @$scopeip L5').strip().splitlines() 73 | for i, l in enumerate(post): 74 | c = ' '*3 75 | if i == 1: 76 | c = '>'*3 77 | print(c + l) 78 | 79 | print("") 80 | print("Registers:") 81 | print(self.execute('r')) 82 | 83 | self.shell() 84 | break 85 | 86 | except PyCdbPipeClosedException: 87 | print("pipe closed") 88 | 89 | except ExitProcessEvent: 90 | print("program closed") 91 | 92 | except Exception as ex: 93 | print(ex) 94 | raise ex 95 | 96 | finally: 97 | if not self.closed(): 98 | self.write_pipe('q\r\n') 99 | 100 | 101 | def usage(): 102 | print("usage: %s [-p|--pid] " % (sys.argv[0])) 103 | 104 | 105 | if __name__ == "__main__": 106 | pid = False 107 | try: 108 | opts, args = getopt.getopt(sys.argv[1:], 'p', ['pid']) 109 | except getopt.GetoptError as err: 110 | print(str(err)) 111 | usage() 112 | sys.exit(1) 113 | for o, a in opts: 114 | if o in ('-p', '--pid'): 115 | pid = True 116 | else: 117 | assert False, 'unhandled option' 118 | if not pid: 119 | usage() 120 | sys.exit(1) 121 | 122 | dbg = IEDebugLog() 123 | 124 | # attach to the specified pid 125 | dbg.attach(int(args[0])) 126 | # run the debug session 127 | dbg.run() 128 | 129 | -------------------------------------------------------------------------------- /pycdb/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from pycdb import PyCdb 4 | -------------------------------------------------------------------------------- /pycdb/pycdb.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from builtins import input, range 3 | 4 | import os 5 | import queue 6 | import re 7 | import shlex 8 | import struct 9 | import subprocess 10 | import sys 11 | import threading 12 | import time 13 | 14 | PYTHON3 = sys.version_info >= (3, 0) 15 | 16 | # breakpoint types 17 | BREAKPOINT_NORMAL = 1 18 | BREAKPOINT_UNRESOLVED = 2 19 | BREAKPOINT_HARDWARE = 3 20 | BREAKPOINT_SYMBOLIC = 4 21 | 22 | # cpu types 23 | CPU_X86 = 1 24 | CPU_X64 = 2 25 | 26 | # marker for prompt 27 | COMMAND_FINISHED_MARKER = "CMDH@ZF1N1SH3D" 28 | 29 | # max buffer size for output 30 | OUTPUT_BUF_MAX = 5 * 1024 * 1024 31 | 32 | DEBUG_INOUT = False 33 | 34 | 35 | def get_arch(): 36 | """ 37 | Return either 32 or 64 38 | """ 39 | return struct.calcsize("P") * 8 40 | 41 | 42 | def parse_addr(addrstr): 43 | """ 44 | parse 64 or 32-bit address from string into int 45 | """ 46 | if addrstr.startswith('??'): 47 | return 0 48 | return int(addrstr.replace('`', ''), 16) 49 | 50 | 51 | def addr_to_hex(address): 52 | """ 53 | convert an address as int or long into a string 54 | """ 55 | string = hex(address) 56 | if string[-1] == 'L': 57 | return string[:-1] 58 | return string 59 | 60 | 61 | class PyCdbException(Exception): 62 | pass 63 | 64 | 65 | class PyCdbInternalErrorException(PyCdbException): 66 | def __init__(self, output=None): 67 | self.output = output 68 | 69 | def __str__(self): 70 | return "PyCdbInternalErrorException: %s" % self.output 71 | 72 | 73 | class PyCdbPipeClosedException(PyCdbException): 74 | def __init__(self, output=None): 75 | self.output = output 76 | 77 | def __str__(self): 78 | if not self.output or len(self.output) == 0: 79 | return "cdb pipe is closed" 80 | else: 81 | return "cdb pipe is closed: last message was: %s" % self.output 82 | 83 | 84 | class PyCdbTimeoutException(PyCdbException): 85 | pass 86 | 87 | 88 | class AttrDict(dict): 89 | def __getattr__(self, key): 90 | return self[key] 91 | 92 | def __setattr__(self, key, value): 93 | if self.setterCallback: 94 | self.setterCallback(key, value) 95 | else: 96 | self[key] = value 97 | 98 | 99 | class CdbEvent(object): 100 | pass 101 | 102 | 103 | class InternalErrorEvent(CdbEvent): 104 | def __init__(self, output): 105 | self.output = output 106 | 107 | def __str__(self): 108 | return "InternalErrorEvent: %s" % self.output 109 | 110 | 111 | class OutputEvent(CdbEvent): 112 | def __init__(self, output, closed=False): 113 | self.output = output 114 | 115 | def __str__(self): 116 | return "OutputEvent: %s" % self.output 117 | 118 | 119 | class PipeClosedEvent(CdbEvent): 120 | def __str__(self): 121 | return "PipeClosedEvent" 122 | 123 | 124 | class DebuggerEvent(CdbEvent): 125 | def __init__(self): 126 | self.pid = 0 127 | self.tid = 0 128 | self.description = '' 129 | 130 | def set_base(self, pid=None, tid=None, desc=None): 131 | if type(pid) == str: 132 | pid = int(pid, 16) 133 | if type(tid) == str: 134 | tid = int(tid, 16) 135 | self.pid = pid 136 | self.tid = tid 137 | self.description = desc 138 | 139 | def __str__(self): 140 | return "DebuggerEvent(%x,%x)" % (self.pid, self.tid) 141 | 142 | 143 | class LoadModuleEvent(DebuggerEvent): 144 | def __init__(self, module, base): 145 | super(LoadModuleEvent, self).__init__() 146 | self.module = module 147 | self.base = base 148 | 149 | def __str__(self): 150 | return "LoadModuleEvent(%x,%x): %s, %08X" % (self.pid, self.tid, self.module, self.base) 151 | 152 | 153 | class BreakpointEvent(DebuggerEvent): 154 | def __init__(self, bpnum): 155 | super(BreakpointEvent, self).__init__() 156 | self.bpnum = bpnum 157 | 158 | def __str__(self): 159 | return "BreakpointEvent(%x,%x): %u" % (self.pid, self.tid, self.bpnum) 160 | 161 | 162 | class ExitProcessEvent(DebuggerEvent): 163 | def __init__(self, exit_code): 164 | super(ExitProcessEvent, self).__init__() 165 | self.exit_code = exit_code 166 | 167 | def __str__(self): 168 | return "ExitProcessEvent(): %u" % self.exit_code 169 | 170 | 171 | class ExceptionEvent(DebuggerEvent): 172 | """ 173 | class that represents an exception raised in the debuggee, such 174 | as an access violation. not to be confused with a python exception 175 | """ 176 | 177 | def __init__(self, address, code, description): 178 | super(ExceptionEvent, self).__init__() 179 | self.address = address 180 | self.code = code 181 | self.description = description 182 | self.params = [] 183 | self.details = "" 184 | self.fault_addr = None 185 | 186 | def __str__(self): 187 | s = "ExceptionEvent(%x,%x): %08X: code=%08X: %s" % ( 188 | self.pid, self.tid, self.address, self.code, self.description) 189 | if self.details: 190 | s += ": " + self.details 191 | return s 192 | 193 | 194 | class BreakpointInfo(object): 195 | def __init__(self, num, name, flags, enabled, resolved, address): 196 | self.num = num 197 | self.name = name 198 | self.flags = flags 199 | self.enabled = enabled 200 | self.resolved = resolved 201 | self.address = address 202 | 203 | def __str__(self): 204 | s = "Breakpoint #%u: %s (%s: %s,%s)" % (self.num, self.name, 205 | self.flags, 206 | "enabled" if self.enabled else "disabled", 207 | "resolved" if self.resolved else "unresolved") 208 | if self.address: 209 | s += " @ %08X" % self.address 210 | return s 211 | 212 | 213 | class CdbReaderThread(threading.Thread): 214 | def __init__(self, pipe): 215 | super(CdbReaderThread, self).__init__() 216 | self.setDaemon(True) 217 | self.queue = queue.Queue() 218 | self.pipe = pipe 219 | self.stop_reading = False 220 | 221 | def process_line(self, line): 222 | #print("line:", line) 223 | if line.startswith("ModLoad: "): 224 | # stuff a load module event into the queue 225 | elems = re.split(r'\s+', line, 3) 226 | base = parse_addr(elems[1]) 227 | end = parse_addr(elems[2]) 228 | self.queue.put(LoadModuleEvent(elems[3].strip(), base)) 229 | elif "offset expression evaluation failed" in line: 230 | self.queue.put(InternalErrorEvent(line)) 231 | elif line.startswith("WaitForEvent failed, Win32 error "): 232 | self.queue.put(InternalErrorEvent(line)) 233 | 234 | def run(self): 235 | # print 'ReaderThread.run()' 236 | curline = bytes() 237 | # read from the pipe 238 | while not self.stop_reading: 239 | ch = self.pipe.stdout.read(1) 240 | # print 'ReaderThread.run(): read %s' % ch 241 | if not ch: 242 | # add a closed event to the queue 243 | self.queue.put(PipeClosedEvent()) 244 | # print 'ReaderThread.run(): read nothing' 245 | break 246 | if PYTHON3: 247 | self.queue.put(OutputEvent(ch.decode("ISO-8859-1"))) 248 | else: 249 | self.queue.put(OutputEvent(ch)) 250 | curline += ch 251 | if b"debugee initialization failed" in curline.lower() or b"win32 error 0n216" in curline.lower(): 252 | print("cdb.exe error: \r\n{}".format(curline)) 253 | self.queue.put(PipeClosedEvent()) 254 | break 255 | if ch == b'\n': 256 | if PYTHON3: 257 | self.process_line(curline.decode("ISO-8859-1")) 258 | else: 259 | self.process_line(curline) 260 | curline = bytes() 261 | 262 | 263 | class Registers(object): 264 | def __init__(self, dbg): 265 | self.dbg = dbg 266 | self._initialized = True 267 | 268 | def get(self, name): 269 | buf = self.dbg.execute('r @%s' % name) 270 | if buf.find('Bad register') != -1: 271 | raise AttributeError('Bad register %s' % name) 272 | # TODO: implement xmm regs 273 | m = re.match(r'(.+)=([0-9A-Fa-f]+)', buf) 274 | if not m: 275 | raise AttributeError('Bad register %s (unable to parse value)' % name) 276 | val = int(m.group(2), 16) 277 | # print "Registers.get(%s) => %x" % (name, val) 278 | return val 279 | 280 | def set(self, name, value): 281 | # TODO: implement xmm regs 282 | buf = self.dbg.execute('r @%s=0x%x' % (name, value)) 283 | if buf.find('Bad register') != -1: 284 | raise AttributeError('Bad register %s' % name) 285 | if buf.find('Syntax error') != -1: 286 | raise AttributeError('Syntax error %s, %s' % (name, value)) 287 | 288 | def all(self): 289 | """ 290 | return a dict of registers and their current values. 291 | """ 292 | temp_map = AttrDict() 293 | regs = self.dbg.execute("r") 294 | temp_all = re.findall(r'([A-Za-z0-9]+)\=([0-9A-Fa-f]+)', regs) 295 | for entry in temp_all: 296 | temp_map[entry[0]] = int(entry[1], 16) 297 | return temp_map 298 | 299 | def __getattr__(self, item): 300 | """only called if there *isn't* an attribute with this name""" 301 | if not self.__dict__.has_key(item): 302 | return self.get(item) 303 | raise AttributeError(item) 304 | 305 | def __setattr__(self, item, value): 306 | if not self.__dict__.has_key('_initialized'): 307 | return dict.__setattr__(self, item, value) 308 | elif self.__dict__.has_key(item): 309 | return dict.__setattr__(self, item, value) 310 | else: 311 | return self.set(item, value) 312 | 313 | def __getitem__(self, item): 314 | try: 315 | return self.get(item) 316 | except AttributeError: 317 | raise KeyError(item) 318 | 319 | def __setitem__(self, item, value): 320 | try: 321 | self.set(item, value) 322 | except AttributeError: 323 | raise KeyError(item) 324 | 325 | 326 | class PyCdb(object): 327 | def __init__(self, cdb_path=None): 328 | self.pipe = None 329 | if cdb_path: 330 | self.cdb_path = cdb_path 331 | else: 332 | self.cdb_path = self._find_cdb_path() 333 | self.output_buf_max = OUTPUT_BUF_MAX 334 | self.initial_command = '' 335 | self.debug_children = False 336 | self.initial_breakpoint = True 337 | self.hide_debugger = False 338 | self.final_breakpoint = False 339 | self.break_on_load_modules = False 340 | self.pipe_closed = True 341 | self.qthread = None 342 | self.breakpoints = {} 343 | self.bit_width = 32 344 | self.first_prompt_read = False 345 | self.cdb_cmdline = [] 346 | self.is_debuggable = True 347 | self.read_to_prompt_timeout = 30 348 | 349 | def _find_cdb_path(self, include_x86=True, include_x64=True): 350 | # build program files paths 351 | pg_paths = [os.environ["PROGRAMFILES"]] 352 | if "ProgramW6432" in os.environ: 353 | self.bit_width = 64 354 | pg_paths.append(os.environ["ProgramW6432"]) 355 | if "ProgramFiles(x86)" in os.environ: 356 | pg_paths.append(os.environ["ProgramFiles(x86)"]) 357 | # potential paths to the debugger in program files 358 | dbg_paths = [] 359 | if include_x86: 360 | dbg_paths += [ 361 | "Windows Kits\\10\\Debuggers\\x86", 362 | "Windows Kits\\8.1\\Debuggers\\x86", 363 | "Windows Kits\\8.0\\Debuggers\\x86", 364 | "Debugging Tools for Windows (x86)", 365 | "Debugging Tools for Windows", 366 | ] 367 | if include_x64: 368 | dbg_paths += [ 369 | "Windows Kits\\10\\Debuggers\\x64", 370 | "Windows Kits\\8.1\\Debuggers\\x64", 371 | "Windows Kits\\8.0\\Debuggers\\x64", 372 | "Debugging Tools for Windows (x64)", 373 | "Debugging Tools for Windows", 374 | ] 375 | # search the paths 376 | for p in pg_paths: 377 | for d in dbg_paths: 378 | test_path = os.path.join(p, d, 'cdb.exe') 379 | if os.path.exists(test_path): 380 | return test_path 381 | # couldn't locate, raise an exception 382 | raise PyCdbException('Could not locate the cdb executable!') 383 | 384 | def _create_pipe(self, cmdline): 385 | self.pipe = subprocess.Popen(cmdline, 386 | stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) 387 | self.qthread = CdbReaderThread(self.pipe) 388 | self.qthread.start() 389 | self.pipe_closed = False 390 | 391 | def add_cmdline_option(self, option): 392 | if type(option) == list: 393 | self.cdb_cmdline.append(option) 394 | else: 395 | self.cdb_cmdline.append(shlex.split(option)) 396 | 397 | def _run_cdb(self, arguments): 398 | cmdline = [self.cdb_path] 399 | 400 | self._do_hide_debugger() 401 | 402 | if len(self.cdb_cmdline) > 0: 403 | cmdline += self.cdb_cmdline 404 | if self.debug_children: 405 | cmdline.append('-o') 406 | if not self.initial_breakpoint: 407 | cmdline.append('-g') 408 | if not self.final_breakpoint: 409 | cmdline.append('-G') 410 | if self.break_on_load_modules: 411 | cmdline += ['-xe', 'ld'] 412 | if self.initial_command and len(self.initial_command) > 0: 413 | cmdline += ['-c', self.initial_command] 414 | # print(" ".join(cmdline + arguments)) 415 | self._create_pipe(cmdline + arguments) 416 | 417 | def _do_hide_debugger(self): 418 | """ 419 | Modify startup options to hide the debugger (patch IsDebugPresent to 420 | always return 0). 421 | 422 | Note that there may be some unexpected breakpoints if self.debug_children 423 | is also set. 424 | """ 425 | if not self.hide_debugger: 426 | return 427 | 428 | initial_commands = [] 429 | if self.initial_command is not None and len(self.initial_command) > 0: 430 | initial_commands.append(self.initial_command) 431 | 432 | # patches kernelbase!IsDebuggerPresent to be 433 | # xor eax,eax 434 | # ret 435 | initial_commands.insert(0, "eb kernelbase!IsDebuggerPresent 31 c0 c3") 436 | 437 | if not self.initial_breakpoint: 438 | # we want to patch IsDebuggerPresent as soon as the process starts, so 439 | # we need the initial breakpoint 440 | self.initial_breakpoint = True 441 | initial_commands.append("g") 442 | 443 | self.initial_command = " ; ".join(initial_commands) 444 | 445 | def closed(self): 446 | """ 447 | return True if the pipe is closed 448 | """ 449 | return self.pipe_closed 450 | 451 | def read_to_prompt(self, timeout=None, keep_output=True, debug=False): 452 | """ 453 | This is the main 'wait' function, it dequeues event objects from 454 | the cdb output queue and acts on them. 455 | The actual output buffer is returned. 456 | """ 457 | buf = '' 458 | lastch = None 459 | 460 | end_time = None 461 | if timeout: 462 | end_time = time.time() + timeout 463 | else: 464 | timeout = self.read_to_prompt_timeout 465 | end_time = time.time() + timeout 466 | remaining_time = timeout 467 | 468 | while True: 469 | 470 | # lets check to see if we have timed out before we do anything 471 | # 472 | # the logic here is that queue.get() only times out on events placed 473 | # into the queue but NOT on whether or not a prompt has been read, which 474 | # is the job of THIS function. So, if we have been sitting in this while 475 | # loop reading OutputEvents continuously (debugging outputting lots of data) 476 | # we will spin forever w/o timing out. To fix, we just keep track of 477 | # when we started fix the queue_timeout appropriately as well as check it 478 | # to see if we are done each time through the loop 479 | 480 | if timeout: 481 | now = time.time() 482 | remaining_time = end_time - now 483 | if now > end_time: 484 | # we've been in this loop for too long, timeout! 485 | print("timing out in read_to_prompt before calling queue.get()!") 486 | raise PyCdbTimeoutException() 487 | 488 | try: 489 | event = self.qthread.queue.get(True, remaining_time) 490 | except queue.Empty: 491 | self.is_debuggable = False 492 | if timeout: 493 | # if the queue.get() times out, we definitely have a timeout 494 | print("timing out in read_to_prompt from call to queue.get()!") 495 | raise PyCdbTimeoutException() 496 | break 497 | 498 | # print "read_to_prompt: %s" % event 499 | if isinstance(event, OutputEvent): 500 | self.is_debuggable = True 501 | ch = event.output 502 | # read one character at a time until we see a '> ' 503 | if debug: 504 | print("read: %s" % ch) 505 | buf += ch 506 | 507 | if len(buf) >= self.output_buf_max: 508 | buf = buf[int(self.output_buf_max / 2):] 509 | 510 | # look for prompt 511 | if lastch == '>' and ch == ' ' and self.qthread.queue.empty(): 512 | # For initial breakpoint since we cant insert our marker 513 | # On this one 514 | if not self.first_prompt_read: 515 | self.first_prompt_read = True 516 | break 517 | 518 | if COMMAND_FINISHED_MARKER in buf: 519 | # remove the marker and the next prompt 520 | # this is ok since the marker is inserted via a newline 521 | # and not a semicolon. 522 | # buf = buf.replace("%s\n" % COMMAND_FINISHED_MARKER, "") 523 | pat = '%s\\n.+' % COMMAND_FINISHED_MARKER 524 | buf = re.sub(pat, '', buf, flags=re.MULTILINE) 525 | break 526 | lastch = ch 527 | elif isinstance(event, InternalErrorEvent): 528 | # might be possible to flag this and read the rest of the output 529 | # up to a prompt, but its safest to bail out now 530 | raise PyCdbInternalErrorException(buf) 531 | elif isinstance(event, PipeClosedEvent): 532 | self.pipe_closed = True 533 | raise PyCdbPipeClosedException(buf) 534 | elif isinstance(event, LoadModuleEvent): 535 | # this is a bit tricky, if we ARE breaking on modload 536 | # then we don't want to call the handler as it will be 537 | # called when process_event is called. However, if 538 | # we ARE NOT breaking on modload, then we probably should 539 | # call here 540 | if not self.break_on_load_modules: 541 | self.on_load_module(event) 542 | if DEBUG_INOUT: 543 | print("<< " + "\n<< ".join(buf.splitlines())) 544 | return buf if keep_output else "" 545 | 546 | def write_pipe(self, buf): 547 | if DEBUG_INOUT: 548 | print(">> " + buf) 549 | self.pipe.stdin.write('{}\r\n.echo {}\r\n'.format(buf, COMMAND_FINISHED_MARKER).encode("ISO-8859-1")) 550 | try: 551 | self.pipe.stdin.flush() 552 | except OSError: 553 | # handle occasional error `OSError: [Errno 22] Invalid argument` 554 | print("Ignoring OSError in flush() call") 555 | pass 556 | 557 | def continue_debugging(self): 558 | """ 559 | tell cdb to go but don't issue a read to prompt 560 | """ 561 | self.write_pipe('g') 562 | 563 | def on_load_module(self, event): 564 | pass 565 | 566 | def unhandled_breakpoint(self, bpnum): 567 | # no handler, just continue execution 568 | self.continue_debugging() 569 | 570 | def on_breakpoint(self, event): 571 | handled = False 572 | bpnum = event.bpnum 573 | # call the handler if there is one 574 | if bpnum in self.breakpoints: 575 | handler, address = self.breakpoints[bpnum] 576 | if handler: 577 | handler(event) 578 | # continue execution 579 | self.continue_debugging() 580 | handled = True 581 | if not handled: 582 | self.unhandled_breakpoint(bpnum) 583 | 584 | def on_exception(self, event): 585 | pass 586 | 587 | def spawn(self, arguments): 588 | self._run_cdb(arguments) 589 | 590 | def attach(self, pid): 591 | self._run_cdb(['-p', str(pid)]) 592 | 593 | def _quit(self): 594 | try: 595 | self.write_pipe('q\r\n') 596 | except IOError as ioe: 597 | if ioe.errno != 22: # ignore EINVAL 598 | raise ioe 599 | 600 | def quit(self): 601 | self._quit() 602 | self.qthread.stop_reading = True 603 | self.pipe.kill() 604 | self.is_debuggable = True 605 | 606 | def interrupt(self): 607 | self.pipe.send_signal(subprocess.signal.CTRL_BREAK_EVENT) 608 | 609 | def delayed_break(self, seconds): 610 | def do_break(): 611 | time.sleep(seconds) 612 | self.interrupt() 613 | 614 | t = threading.Thread(target=do_break) 615 | t.setDaemon(True) 616 | t.daemon = True 617 | t.start() 618 | 619 | def execute(self, command): 620 | if not self.is_debuggable: 621 | raise Exception("PyCdb is not at a breakpoint. This is the only time " 622 | "you may execute commands") 623 | 624 | self.write_pipe(command) 625 | # return the entire output except the prompt string 626 | output = self.read_to_prompt() 627 | return "\n".join(output.splitlines()[:-1]) + "\n" 628 | 629 | def evaluate(self, expression): 630 | output = self.execute("? " + expression) 631 | # take the last line of output as there can be annoying warnings interspersed 632 | # like: WARNING: Unable to verify checksum... 633 | # print "evaluate(" + expression + "): " + output 634 | lastline = output.splitlines()[-1] 635 | m = re.match(r'Evaluate expression: (\-?[0-9]+) = ([0-9A-Fa-f`]+)', lastline) 636 | if m: 637 | return parse_addr(m.group(2)) 638 | else: 639 | return None 640 | 641 | def backtrace(self, frames=None): 642 | if frames is None: 643 | output = self.execute("k") 644 | else: 645 | output = self.execute("k %d" % frames) 646 | 647 | # Strip header line 648 | return "\n".join(output.split("\n")[1:]) 649 | 650 | def cpp_object_type(self, ptr): 651 | if isinstance(ptr, str): 652 | ptr = int(ptr, 16) 653 | output = self.execute("ln poi(%x)" % ptr) 654 | matches = output.split("Exact matches:") 655 | if len(matches) != 2: 656 | return "Unknown Object" 657 | matches = matches[1].strip().split("\n") 658 | if len(matches) != 1: 659 | return "Unknown Object (multiple matches)" 660 | 661 | obj = matches[0].strip() 662 | if "vftable" not in obj: 663 | return "Object not recognizable vtable" 664 | 665 | return obj.split("::`vftable")[0] 666 | 667 | @property 668 | def cpu_type(self): 669 | buf = self.execute('.effmach') 670 | if buf.find('x86') != -1: 671 | return CPU_X86 672 | elif buf.find('x64') != -1: 673 | return CPU_X64 674 | else: 675 | raise PyCdbException('unknown architecture: %s' % buf.strip()) 676 | 677 | @property 678 | def registers(self): 679 | return Registers(self) 680 | 681 | def set_register(self, register, value): 682 | self.execute("r @%s=%x" % (register, value)) 683 | 684 | def read_mem(self, address, length): 685 | mem = '' 686 | rem = length 687 | if type(address) == int: 688 | address = addr_to_hex(address) 689 | output = self.execute('db %s L%X' % (address, length)) 690 | for line in output.splitlines(): 691 | chunk = 16 692 | if chunk > rem: 693 | chunk = rem 694 | elems = re.split(r'[\ -]+', line)[1:1 + chunk] 695 | for value in elems: 696 | if value == '??': 697 | break 698 | mem += value.decode('hex') 699 | rem -= chunk 700 | return mem 701 | 702 | def read_u64(self, address): 703 | try: 704 | return struct.unpack('= 4: 793 | base = parse_addr(elems[0]) 794 | end = parse_addr(elems[1]) 795 | temp_map[elems[2].lower()] = [base, end - base, elems[3].strip()] 796 | return temp_map 797 | 798 | def module_info_from_addr(self, address): 799 | mods = self.modules() 800 | for name in mods: 801 | modinfo = mods[name] 802 | base = modinfo[0] 803 | end = base + modinfo[1] 804 | if base <= address <= end: 805 | return self.module_info(name) 806 | return None 807 | 808 | def module_info(self, modname): 809 | buf = self.execute("lm vm %s" % modname) 810 | lines = buf.splitlines() 811 | if len(lines) < 2: 812 | return None 813 | modinfo = {} 814 | # parse the first line 815 | elems = re.split(r'\s+', lines[1], 3) 816 | modinfo['module_base'] = parse_addr(elems[0]) 817 | modinfo['module_end'] = parse_addr(elems[1]) 818 | modinfo['module_name'] = elems[2].strip() 819 | for line in lines[2:]: 820 | elems = line.split(':', 1) 821 | tok = elems[0].strip().replace(" ", "_").lower() 822 | val = elems[1].strip() 823 | modinfo[tok] = val 824 | return modinfo 825 | 826 | def _get_bp_nums(self): 827 | bpnums = [] 828 | output = self.execute('bl') 829 | for line in output.splitlines(): 830 | line = line.strip() 831 | elems = re.split(r'\s+', line) 832 | if len(elems) > 1 and len(elems[0]) > 0: 833 | bpnums.append(int(elems[0])) 834 | return bpnums 835 | 836 | def breakpoint(self, address, handler=None, bptype=BREAKPOINT_NORMAL, bpmode="e", condition=""): 837 | if type(address) == int: 838 | address = addr_to_hex(address) 839 | cmd = 'bp' 840 | if bptype == BREAKPOINT_UNRESOLVED: 841 | cmd = 'bu' 842 | elif bptype == BREAKPOINT_HARDWARE: 843 | cmd = 'ba %s 1' % bpmode 844 | elif bptype == BREAKPOINT_SYMBOLIC: 845 | # Try symbolic if bp fails 846 | cmd = 'bm' 847 | 848 | nums_before = self._get_bp_nums() 849 | if len(condition) > 0: 850 | if not (condition.startswith("j") or condition.startswith(".if")): 851 | # Add boiler plate 852 | condition = "j %s ''; 'gc' " % condition 853 | output = self.execute('%s %s "%s"' % (cmd, address, condition)) 854 | 855 | else: 856 | output = self.execute('%s %s' % (cmd, address)) 857 | 858 | nums_after = self._get_bp_nums() 859 | added_num = list(set(nums_after) - set(nums_before)) 860 | if len(added_num) == 0: 861 | if cmd == 'bp': 862 | return self.breakpoint(address, handler, BREAKPOINT_SYMBOLIC, bpmode, condition) 863 | raise PyCdbException(output.strip()) 864 | else: 865 | bpnum = added_num[0] 866 | self.breakpoints[bpnum] = [handler, address] 867 | return bpnum 868 | 869 | def breakpoint_disable(self, bpnum): 870 | self.execute("bd %u" % bpnum) 871 | 872 | def breakpoint_enable(self, bpnum): 873 | self.execute("be %u" % bpnum) 874 | 875 | def breakpoint_remove(self, bpnum): 876 | self.execute("bc %u" % bpnum) 877 | 878 | # convenience methods 879 | def bp(self, address, handler=None, condition=""): 880 | return self.breakpoint(address, handler, bptype=BREAKPOINT_NORMAL, 881 | condition=condition) 882 | 883 | def bu(self, address, handler=None, condition=""): 884 | return self.breakpoint(address, handler, bptype=BREAKPOINT_UNRESOLVED, 885 | condition=condition) 886 | 887 | # extract info about the breakpoint 888 | def breakpoint_info(self, bpnum): 889 | line = self.execute('bl %u' % bpnum) 890 | line = line.strip() 891 | elems = re.split(r'\s+', line) 892 | if len(elems) == 0 or len(elems[0]) == 0: 893 | return None 894 | num = int(elems[0]) 895 | flags = elems[1] 896 | unresolved = 'u' in flags 897 | enabled = 'e' in flags 898 | if unresolved: 899 | addr = None 900 | name = elems[4] 901 | if name[0] == '(' and name[-1] == ')': 902 | name = name[1:-1] 903 | else: 904 | addr = parse_addr(elems[2]) 905 | name = elems[6] 906 | return BreakpointInfo(num, name, flags, enabled, not unresolved, addr) 907 | 908 | def _exception_info(self): 909 | output = self.execute('.exr -1') 910 | if output.find('not an exception') != -1: 911 | return None 912 | 913 | address = 0 914 | code = 0 915 | desc = "Unknown" 916 | 917 | m = re.search(r'ExceptionAddress: ([0-9A-Fa-f`]+)', output) 918 | if m: 919 | address = parse_addr(m.group(1)) 920 | 921 | m = re.search(r'ExceptionCode: ([0-9A-Fa-f]+) \(([^\)]+)\)', output) 922 | if m: 923 | code = parse_addr(m.group(1)) 924 | desc = m.group(2) 925 | else: 926 | m = re.search(r'ExceptionCode: ([0-9A-Fa-f]+)', output) 927 | if m: 928 | code = parse_addr(m.group(1)) 929 | 930 | ex = ExceptionEvent(address, code, desc) 931 | m = re.search(r'NumberParameters: ([0-9]+)', output) 932 | num_params = int(m.group(1)) 933 | params = [] 934 | for n in range(num_params): 935 | m = re.search(r'Parameter\[%u\]: ([0-9A-Fa-f`]+)' % n, output) 936 | if not m: 937 | return None 938 | params.append(parse_addr(m.group(1))) 939 | ex.params = params 940 | 941 | # try to read the text details line 942 | last_line = output.splitlines()[-1].strip() 943 | if not last_line.startswith("Parameter["): 944 | ex.details = last_line 945 | # try to parse the details line to extract the address that caused the exception 946 | m = re.search(r'address ([0-9A-Fa-f]+)', ex.details) 947 | if m: 948 | ex.fault_addr = parse_addr(m.group(1)) 949 | 950 | return ex 951 | 952 | @staticmethod 953 | def _breakpoint_info(event_desc): 954 | m = re.search(r'Hit breakpoint ([0-9]+)', event_desc) 955 | if not m: 956 | return None 957 | bpnum = int(m.group(1)) 958 | bp = BreakpointEvent(bpnum) 959 | # print "_breakpoint_info: %s" % bp 960 | return bp 961 | 962 | @staticmethod 963 | def _load_module_info(event_desc): 964 | m = re.search(r'Load module (.*) at ([0-9A-Fa-f`]+)', event_desc) 965 | if not m: 966 | return None 967 | module_path = m.group(1) 968 | module_base = parse_addr(m.group(2)) 969 | lme = LoadModuleEvent(module_path, module_base) 970 | return lme 971 | 972 | @staticmethod 973 | def _exit_process_info(event_desc): 974 | m = re.search(r'Exit Process .*, code (\d+)', event_desc) 975 | if not m: 976 | return None 977 | exit_code = m.group(1) 978 | ep = ExitProcessEvent(exit_code) 979 | return ep 980 | 981 | def last_event(self): 982 | event = None 983 | output = self.execute('.lastevent') 984 | m = re.search(r'Last event: ([0-9A-Fa-f]+)\.([0-9A-Fa-f]+)\: (.*)$', 985 | output, re.MULTILINE) 986 | if m: 987 | pid, tid, desc = m.groups() 988 | # 989 | # what type of event was this? 990 | # 991 | while True: # always breaks at end 992 | # did the process exit? 993 | event = self._exit_process_info(desc) 994 | if event: 995 | break 996 | # was this a breakpoint? 997 | event = self._breakpoint_info(desc) 998 | if event: 999 | break 1000 | # was this a load module event? 1001 | event = self._load_module_info(desc) 1002 | if event: 1003 | break 1004 | # was it an exception? 1005 | event = self._exception_info() 1006 | # always break at the end 1007 | break 1008 | # if we have an event, set the base info 1009 | if event: 1010 | event.set_base(pid, tid, desc) 1011 | return event 1012 | 1013 | def process_event(self): 1014 | if not self.is_debuggable: 1015 | return None 1016 | event = self.last_event() 1017 | if type(event) == BreakpointEvent: 1018 | self.on_breakpoint(event) 1019 | elif type(event) == LoadModuleEvent: 1020 | self.on_load_module(event) 1021 | elif type(event) == ExceptionEvent: 1022 | self.on_exception(event) 1023 | return event 1024 | 1025 | def shell(self, debug=False): 1026 | print("Dropping to cdb shell. 'quit' to exit, 'pdb' for python debugger.") 1027 | p = 'cdb> ' 1028 | last_input = '' 1029 | while True: 1030 | try: 1031 | temp_input = input(p) 1032 | input_st = temp_input.strip().lower() 1033 | p = '' 1034 | if input_st == 'quit': 1035 | break 1036 | elif input_st == 'pdb': 1037 | import pdb 1038 | pdb.set_trace() 1039 | p = 'cdb> ' 1040 | else: 1041 | if len(input_st) == 0: 1042 | # nothing was entered, repeat last command like windbg 1043 | temp_input = last_input 1044 | self.write_pipe(temp_input) 1045 | output = self.read_to_prompt(debug=debug) 1046 | sys.stdout.write(output) 1047 | sys.stdout.flush() 1048 | last_input = temp_input 1049 | except EOFError: 1050 | break 1051 | 1052 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fishstiqz/pycdb/1aadb40ea99efe1891aa8a3f4f560e720b4aedeb/requirements.txt -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | import os, sys 5 | from setuptools import setup 6 | 7 | setup( 8 | # metadata 9 | name='PyCDB', 10 | description='A python wrapper for the CDB Debugger', 11 | long_description=""" 12 | A python wrapper for Microsoft's CDB command-line debugger. 13 | """, 14 | license='MIT', 15 | version='0.2', 16 | author='@fishstiqz', 17 | maintainer='@fishstiqz', 18 | author_email='', 19 | url='https://github.com/fishstiqz/pycdb', 20 | platforms='Microsoft Windows', 21 | install_requires = open(os.path.join(os.path.dirname(__file__), "requirements.txt")).read().split("\n"), 22 | classifiers = [ 23 | # has not been tested on Python 2 since Python 3 support was added 24 | # 'Programming Language :: Python :: 2', 25 | 'Programming Language :: Python :: 3', 26 | ], 27 | scripts = [ 28 | # no scripts current exist 29 | # os.path.join("bin", "pycdb"), 30 | ], 31 | 32 | # add non-python files to this array, relative to the project root and set 33 | # include_package_data to True 34 | package_data = { 35 | "pycdb": [], 36 | }, 37 | include_package_data=False, 38 | 39 | # include all modules/submodules here 40 | packages=['pycdb'] 41 | ) 42 | --------------------------------------------------------------------------------