Cisco IOS GDB RSP Debugger Script for PowerPC-based models

This is a GDB remote debugging protocol implementation for debugging PowerPC-based Cisco IOS models.

The original MIPS version is available here: https://github.com/klsecservices/ios_mips_gdb

#!/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'
Home