Last active
August 10, 2021 07:08
-
-
Save nstarke/50a1519067f62c223e39a98ba32ed7d5 to your computer and use it in GitHub Desktop.
Cisco IOS PowerPC GDB RSP Debugger
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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' |
@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
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.