Skip to content

Instantly share code, notes, and snippets.

@nstarke
Last active August 10, 2021 07:08
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save nstarke/50a1519067f62c223e39a98ba32ed7d5 to your computer and use it in GitHub Desktop.
Save nstarke/50a1519067f62c223e39a98ba32ed7d5 to your computer and use it in GitHub Desktop.
Cisco IOS PowerPC GDB RSP Debugger
#!/usr/bin/python
#
# Cisco IOS GDB RSP Wrapper
# PowerPC Version
#
# Authors:
#
# Artem Kondratenko (@artkond) - original mips version
# Nicholas Starke (@nstarke) - powerpc version
# Adapted from https://github.com/klsecservices/ios_mips_gdb
#
# This does not take into account floating point registers
#
import serial
import time
import logging
from struct import pack, unpack
import sys
import capstone as cs
from termcolor import colored
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
tn = None
reg_map = {
1: 'cr', 2: 'lr', 3: 'ctr', 4:'gpr0',
5:'gpr1', 6: 'gpr2', 7: 'gpr3', 8: 'gpr4',
9: 'gpr5', 10: 'gpr6', 11: 'gpr7', 12: 'gpr8',
13: 'gpr9', 14: 'gpr10', 15: 'gpr11', 16: 'gpr12',
17: 'gpr13', 18: 'gpr14', 19: 'gpr15', 20: 'gpr16',
21: 'gpr17', 22: 'gpr19', 23: 'gpr19', 24: 'gpr20',
25: 'gpr21', 26: 'gpr22', 27: 'gpr23', 28:
'gpr24', 29:'gpr25', 30: 'gpr26', 31:'gpr27',
32: 'gpr28', 33: 'gpr29', 34: 'gpr30', 35: 'gpr31', 36: 'pc', 37: 'sp'
}
reg_map_rev = {}
breakpoints = {}
breakpoints_count = 0
aslr_offset = None
isSerial = True
for k, v in reg_map.iteritems():
reg_map_rev[v] = k
if len(sys.argv) < 2:
print 'Specify serial device as a parameter'
sys.exit(1)
ser = serial.Serial(
port=sys.argv[1],
timeout=5
)
def hexdump_gen(byte_string, _len=16, base_addr=0, n=0, sep='-'):
FMT = '{} {} |{}|'
not_shown = [' ']
leader = (base_addr + n) % _len
next_n = n + _len - leader
while byte_string[n:]:
col0 = format(n + base_addr - leader, '08x')
col1 = not_shown * leader
col2 = ' ' * leader
leader = 0
for i in bytearray(byte_string[n:next_n]):
col1 += [format(i, '02x')]
col2 += chr(i) if 31 < i < 127 else '.'
trailer = _len - len(col1)
if trailer:
col1 += not_shown * trailer
col2 += ' ' * trailer
col1.insert(_len // 2, sep)
yield FMT.format(col0, ' '.join(col1), col2)
n = next_n
next_n += _len
def isValidDword(hexdword):
if len(hexdword) != 8:
return False
try:
hexdword.decode('hex')
except TypeError:
return False
return True
def checksum(command):
csum = 0
reply = ""
for x in command:
csum = csum + ord(x)
csum = csum % 256
reply = "$" + command + "#%02x" % csum
return reply
def decodeRLE(data):
i=2
multiplier=0
reply=""
while i < len(data):
if data[i] == "*":
multiplier = int(data[i+1] + data[i+2],16)
for j in range (0, multiplier):
reply = reply + data[i-1]
i = i + 3
if data[i] == "#":
break
reply = reply + data[i]
i = i + 1
return reply
def print_help():
print '''Command reference:
c - continue program execution
stepi - step into
nexti - step over
reg - print registers
setreg <reg_name> <value> - set register value
break <addr> - set break point
info break - view breakpoints set
del <break_num> - delete breakpoint
read <addr> <len> - read memory
write <addr> <value - write memory
dump <startaddr> <endaddr> - dump memory within specified range
gdb kernel - send "gdb kernel" command to IOS to launch GDB. Does not work on recent IOS versions.
disas <addr> [aslr] - disassemble at address. Optional "aslr" parameter to account for code randomization
set_aslr_offset - set aslr offset for code section
you can also manually send any GDB RSP command
'''
def CreateGetMemoryReq(address, len):
address = "m" + address + "," + len
formatted = checksum(address)
formatted = formatted + "\n"
return formatted
def DisplayRegistersPPC(regbuffer):
regvals = [''] * 90
buf = regbuffer
for k, dword in enumerate([buf[i:i+8] for i in range(0, len(buf), 8)]):
regvals[k] = dword
return regvals
def GdbCommand(command):
global isSerial
logger.debug('GdbCommand sending: {}'.format(checksum(command)))
ser.write('{}'.format(checksum(command)))
if command == 'c':
return ''
out = ''
char =''
while char != "#":
char = ser.read(1)
out = out + char
ser.read(2)
logger.debug('Raw output from cisco: {}'.format(out))
newrle = decodeRLE(out)
logger.debug("Decode RLE: {}".format(newrle))
decoded = newrle.decode()
logger.debug("decoded: {}".format(decoded))
while decoded[0] == "|" or decoded[0] == "+" or decoded[0] == "$":
decoded = decoded[1:]
return decoded
def OnReadReg():
regs = DisplayRegistersPPC(GdbCommand('g'))
print 'All registers:'
for k, reg_name in reg_map.iteritems():
if regs[reg_map_rev[reg_name]]:
print "{}: {}".format(reg_name, regs[reg_map_rev[reg_name]])
#print 'Control registers:'
# print "PC: {} SP: {} RA: {}".format(regs[reg_map_rev['pc']],regs[reg_map_rev['sp']], regs[reg_map_rev['ra']])
return regs
def OnWriteReg(command):
lex = command.split(' ')
(_ , reg_name, reg_val) = lex[0:3]
if reg_name not in reg_map_rev:
logger.error('Unknown register specified')
return
if not isValidDword(reg_val):
logger.error('Invalid register value supplied')
return
logger.debug("Setting register {} with value {}".format(reg_name, reg_val))
regs = DisplayRegistersPPC(GdbCommand('g'))
regs[reg_map_rev[reg_name]] = reg_val.lower()
buf = ''.join(regs)
logger.debug("Writing register buffer: {}".format(buf))
res = GdbCommand('G{}'.format(buf))
if 'OK' in res:
return True
else:
return None
def OnReadMem(addr, length):
if not isValidDword(addr):
logger.error('Invalid address supplied')
return None
if length > 199:
logger.error('Maximum length of 199 exceeded')
return None
res = GdbCommand('m{},{}'.format(addr.lower(),hex(length)[2:]))
if res.startswith('E0'):
return None
else:
return res
def OnWriteMem(addr, data):
res = GdbCommand('M{},{}:{}'.format(addr.lower(), len(data)/2, data))
if 'OK' in res:
return True
else:
return None
def hex2int(s):
return unpack(">I", s.decode('hex'))[0]
def int2hex(num):
return pack(">I", num & 0xffffffff).encode('hex')
def OnBreak(command):
global breakpoints
global breakpoints_count
lex = command.split(' ')
(_ ,addr) = lex[0:2]
if not isValidDword(addr):
logger.error('Invalid address supplied')
return
if len(lex) == 3:
if lex[2] == 'aslr' and aslr_offset != None:
addr = int2hex(hex2int(addr) + aslr_offset)
addr = addr.lower().rstrip()
if addr in breakpoints:
logger.info('breakpoint already set')
return
opcode_to_save = OnReadMem(addr, 4)
if opcode_to_save is None:
logger.error('Can\'t set breakpoint at {}. Read error'.format(addr))
return
res = OnWriteMem(addr, '7fe00008')
if res:
breakpoints_count += 1
breakpoints[addr] = (breakpoints_count, opcode_to_save)
logger.info('Breakpoint set at {}'.format(addr))
else:
logger.error('Can\'t set breakpoint at {}. Error writing'.format(addr))
def OnDelBreak(command):
global breakpoints
global breakpoints_count
(_, b_num) = command.rstrip().split(' ')
logger.debug('OnDelBreak')
item_to_delete = None
for k, v in breakpoints.iteritems():
try:
if v[0] == int(b_num):
res = OnWriteMem(k, v[1])
if res:
item_to_delete = k
break
else:
logger.error('Error deleting breakpoint {} at {}'.format(b_num, k))
return
except ValueError:
logger.error('Invalid breakpoint num supplied')
return
if item_to_delete is not None:
del breakpoints[k]
logger.info('Deleted breakpoint {}'.format(b_num))
def OnSearchMem(addr, pattern):
cur_addr = addr.lower()
buf = ''
i = 0
while True:
i += 1
mem = GdbCommand('m{},00c7'.format(cur_addr))
buf += mem
if i %1000 == 0:
print cur_addr
print hexdump(mem.decode('hex'))
if pattern in buf[-100:-1]:
print 'FOUND at {}'.format(cur_addr)
return
cur_addr = pack(">I", unpack(">I",cur_addr.decode('hex'))[0] + 0xc7).encode('hex')
def OnListBreak():
global breakpoints
global breakpoints_count
for k, v in breakpoints.iteritems():
print '{}: {}'.format(v[0], k)
def OnStepInto():
ser.write("$s#73\r\n")
ser.read(5)
OnReadReg()
OnDisas('disas')
def OnNext():
regs = OnReadReg()
pc = unpack('>I', regs[reg_map_rev['pc']].decode('hex'))[0]
pc_after_branch = pc + 8
pc_in_hex = pack('>I', pc_after_branch).encode('hex')
OnBreak('break {}'.format(pc_in_hex))
GdbCommand('c')
OnReadReg()
OnDelBreak('del {}'.format(breakpoints[pc_in_hex][0]))
def OnDumpMemory(start, stop):
buf = ''
print start, stop
if not isValidDword(start) or not isValidDword(stop):
logger.error('Invalid memory range specified')
return
cur_addr = start
while unpack(">I",cur_addr.decode('hex'))[0] < unpack(">I", stop.decode('hex'))[0]:
res = GdbCommand('m{},00c7'.format(cur_addr))
logger.info('Dumping at {} len {}'.format(cur_addr, len(res)))
cur_addr = pack(">I", unpack(">I",cur_addr.decode('hex'))[0] + 0xc7).encode('hex')
buf += res
return buf
def OnSetAslrOffset():
global aslr_offset
(_, offset) = command.rstrip().split(' ')
aslr_offset = hex2int(offset)
logger.info('ASLR offset set to: 0x{}'.format(offset))
def OnDisas(command):
lex = command.rstrip().split(' ')
regs = DisplayRegistersPPC(GdbCommand('g'))
pc = hex2int(regs[reg_map_rev['pc']])
for lexem in lex[1:]:
if lexem != 'aslr':
if not isValidDword(lexem):
logger.error('Invalid address supplied')
return
pc = hex2int(lexem)
logger.debug('OnDisas PC = {}'.format(pc))
buf = OnReadMem(int2hex(pc - 20 * 4), 40 * 4)
md = cs.Cs(cs.CS_ARCH_PPC, cs.CS_MODE_BIG_ENDIAN)
if len(lex) > 1:
if lex[1] == 'aslr' and aslr_offset != None:
pc -= aslr_offset
for i in md.disasm(buf.decode('hex'), pc - 20 * 4):
color = 'green' if i.address == pc else 'blue'
print("0x%x:\t%s\t%s" %(i.address, colored(i.mnemonic, color), colored(i.op_str, color)))
while True:
try:
command = raw_input('> command: ').rstrip()
if command == 'exit':
sys.exit(0)
elif command == 'help':
print_help()
elif command == 'c':
GdbCommand('c')
elif command == 'stepi':
OnStepInto()
elif command == 'nexti':
OnNext()
elif command == 'reg':
OnReadReg()
elif command.startswith('setreg'):
OnWriteReg(command)
elif command.startswith('break'):
OnBreak(command)
elif command.startswith('del'):
OnDelBreak(command)
elif command.startswith('info b'):
OnListBreak()
elif command.startswith('read'):
_, start, length = command.split(' ')
buf = OnReadMem(start, int(length))
for line in hexdump_gen(buf.decode('hex'), base_addr=hex2int(start), sep=' '):
print line
elif command.startswith('write'):
_, dest, value = command.split(' ')
value.decode('hex')
OnWriteMem(dest, value)
elif command.startswith('search'):
_, addr, pattern = command.split(' ')
OnSearchMem(addr, pattern)
elif command.startswith('gdb kernel'):
ser.write('{}\n'.format('gdb kernel'))
elif command.startswith('dump'):
_, start, stop = command.split(' ')
buf = OnDumpMemory(start.lower(), stop.lower())
if buf is None:
continue
else:
with open('dump_file','wb') as f:
f.write(buf)
logger.info('Wrote memory dump to "dump_file"')
elif command.startswith('set_aslr_offset'):
OnSetAslrOffset()
elif command.startswith('disas'):
OnDisas(command)
else:
ans = raw_input('Command not recognized.\nDo you want to send raw command: {} ? [yes]'.format(checksum(command.rstrip())))
if ans == '' or ans == 'yes':
reply = GdbCommand(command.rstrip())
print 'Cisco response:', reply.rstrip()
except (KeyboardInterrupt, serial.serialutil.SerialException, ValueError, TypeError) as e:
print '\n{}'.format(e)
print 'Type "exit" to end debugging session'
@sezangel
Copy link

sezangel commented Apr 7, 2021

There is a small error in your code,when you set break,the register value should be 7fe00008 instead 0000000d. Mips arch should be 0000000d,but if you use this register value in powerpc, the router will run erroneously.

@nstarke
Copy link
Author

nstarke commented Apr 7, 2021

@rabbitboat thank you so much for pointing this out. I have updated the gist and the blog post that I ended up moving it to: https://nstarke.github.io/0022-cisco-ios-gdb-rsp-debugger-script-powerpc.html - note that I don't have a Cisco switch currently set up in my lab so I cannot test it immediately.

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