Broadcom eCos | Reversing Interrupt and Exception Handling

head

In this article I’ll go through the different steps I followed when trying to understand the interrupt and exception handling on eCos. I initially wanted to cover this material in the Reversing eCos Memory Layout article but I started to divert too much from actual memory mappings.

While it may not be quite clear now, documenting this will be helpful in the future. It will be a time saver when reversing firmware files given that you’ll have a clear memory map, and will provide the necessary background when thinking about persistent backdoor mechanisms, custom code injection, or even building your own eCos debugger.

By reading the eCos source code for MIPS and doing some research into dedicated vectors, I identified the following locations:

Vector/TableAddress
Common Vector0x80000000
Stub Entry Vector0x80000100
Debug Vector0x80000200
Virtual Service Routine Table0x80000300
Virtual Vector Table0x80000400

Let’s go through each of these locations one by one.

Common Vector (0x80000000)

The CPU delivers all exceptions, whether synchronous faults or asynchronous interrupts, to a set of hardware defined vectors. Depending on the architecture, these may be implemented in a number of different ways.

With such a wide variety of hardware approaches, it is not possible to provide a generic mechanism for the substitution of exception vectors directly. Therefore, eCos translates all of these mechanisms in to a common approach that can be used by portable code on all platforms.

On MIPS, most exceptions and all interrupts are vectored to a single address at either 0x80000000 or 0xBFC00180. Software is responsible for reading the exception code from the CPU cause register to discover its true source. One of the exception codes in the cause register indicates an external interrupt. Additional bits in the cause register provide a first-level decode for the interrupt source, one of which represents an architecture defined timer.

Source: https://ecos.sourceware.org/ecos/docs-latest/ref/hal-vectors-and-vsrs.html

The mechanism implemented is to attach to each hardware vector a short piece of trampoline code that makes an indirect jump via a table to the actual handler for the exception. This handler is called the Vector Service Routine (VSR) and the table is called the VSR table.

The trampoline code performs the absolute minimum processing necessary to identify the exception source, and jump to the VSR. The VSR is then responsible for saving the CPU state and taking the necessary actions to handle the exception or interrupt. The entry conditions for the VSR are as close to the raw hardware exception entry state as possible - although on some platforms the trampoline will have had to move or reorganize some registers to do its job.

Let’s read the content at offset 0x80000000:

CM> read_memory -n 32 0x80000000
80000000: 40 1a 68 00  00 00 00 00  33 5a 00 7f  3c 1b 80 00 | @.h.....3Z..<...
80000010: 27 7b 03 00  03 7a d8 20  8f 7b 00 00  03 60 00 08 | '{...z. .{...`..

By disassembling the obtained bytes, we uncover the trampoline code:

rasm2 -a mips -b 32 -e -d '401a680000000000335a007f3c1b8000277b0300037ad8208f7b000003600008'
mfc0 k0, t5, 0          ; move from coprocessor 0 into $k0
nop                     ; no operation
andi k0, k0, 0x7f       ; $k0 = $k0 ^ 0x7f
lui k1, 0x8000          ; $k1 = 0x80000000
addiu k1, k1, 0x300     ; $k1 = 0x80000300
add k1, k1, k0          ; $k1 = $k1 + $k0
lw k1, (k1)             ; $k1 = *$k1
jr k1                   ; jump and register to $k1

It’s a perfect match for this piece of assembly from eCos 2.0 source:

FUNC_START(other_vector)
     mfc0    k0,cause        # K0 = exception cause
     nop
     andi    k0,k0,0x7F      # isolate exception code
     la  k1,hal_vsr_table    # address of VSR table
     add k1,k1,k0        # offset of VSR entry
     lw  k1,0(k1)        # k1 = pointer to VSR
     jr  k1          # go there
     nop             # (delay slot)
FUNC_END(other_vector)

The code isolate the exception code and use it as an index for the virtual service routine table. From the trampoline code we disassembled, we know the VSR table starts at offset 0x80000300.

The hal_vsr_table is defined in assembly in ./packages/hal/mips/arch/v2_0/src/vectors.S. We can see that it has 16 entries (64 / 4).

##  .section ".vsr_table","a"

    .data

    .globl  hal_vsr_table

hal_vsr_table:
    .long   __default_interrupt_vsr
    .rept   63
    .long   __default_exception_vsr
    .endr

#endif

Let’s read 64 bytes starting from offset 0x80000300:

Console/vendor> read_memory -n 64 0x80000300

80000300: 80 00 43 ec  80 00 4b d8  80 00 4b d8  80 00 4b d8 | ..C...K...K...K.
80000310: 80 00 4b d8  80 00 4b d8  80 00 4b d8  80 00 4b d8 | ..K...K...K...K.
80000320: 80 00 4b d8  80 00 4b d8  80 00 4b d8  80 00 4b d8 | ..K...K...K...K.
80000330: 80 00 4b d8  80 00 4b d8  80 00 4b d8  80 00 4b d8 | ..K...K...K...K.

The virtual service routine table is a table with 16 entries, each of them pointing to a specific function. By comparing the disassembly and the actual assembly from ./packages/hal/mips/arch/v2_0/src/vectors.S, I was able to precisely identify the functions:

To fully understand how the trampoline index the VSR table, I wrote the following Python snippet:

#!/usr/bin/env python3

for i in range(0x7f, 0, -4):
    k = (i ^ 0x7f) >> 2
    offset = 0x80000300 + (i ^ 0x7f)
    if 0x80000340 > offset and offset >= 0x80000300:
        print("{} - 0x{:0x}".format(k, offset))
    elif offset >= 0x80000380 and 80000390 >= offset:
        print("{} - 0x{:0x}".format(k, offset))

Executing the code will give us the following output:

python3 vsr_id.py
0 - 0x80000300
1 - 0x80000304
2 - 0x80000308
3 - 0x8000030c
4 - 0x80000310
5 - 0x80000314
6 - 0x80000318
7 - 0x8000031c
8 - 0x80000320
9 - 0x80000324
10 - 0x80000328
11 - 0x8000032c
12 - 0x80000330
13 - 0x80000334
14 - 0x80000338
15 - 0x8000033c

As we can see, the only valid exception codes that index within the VSR table are values between 0 and 15 included. These are valid exception codes that we can find in the MIPS documentation:

Exception codeNameCause of exception
0IntInterrupt (hardware)
1UnkUnknown
2UnkUnknown
3UnkUnknown
4AdELAddress Error exception (Load or instruction fetch)
5AdESAddress Error exception (Store)
6IBEInstruction fetch Buss Error
7DBEData load or store Buss Error
8SysSyscall exception
9BpBreakpoint exception
10RIReversed Instruction exception
11CpUCoprocessor Unimplemented
12OvArithmetic Overflow exception
13TrTrap
14FPEFloating Point Exception
15UnkUnknown

Please note that item 1, 2, 3, and 15 are undocumented.

An interesting fact is that, due to the way eCos firmwares are compiled and assembled, the location of __default_interrupt_vsr and __default_exception_vsr is the same for all firmwares based on the Broadcom variant of eCos.

The following piece of code takes advantage of that fact and gather the VSR information from a live system over a serial connection. You can find the code in the dedicated repo.

#!/usr/bin/env python3
'''
Dump the virtual service routine table information from a live eCos BFC device.
You must have a serial connection on /dev/ttyUSB0 to either a CM> or RG> shell.

Author: Quentin Kaiser <quentin@ecos.wtf>
'''
import serial
import re

VSR_ADDR = 0x80000300
VSR_NAMES = {
    0x800043ec: "__default_interrupt_vsr",
    0x80004bd8: "__default_exception_vsr",
    0x800042e0: "__default_exception_vsr"
}

CAUSES = [
    "Int - Interrupt (hardware)",
    "Unk - Unknown",
    "Unk - Unknown",
    "Unk - Unknown",
    "AdEL - Address Error exception (Load or instruction fetch)",
    "AdES - Address Error exception (Store)",
    "IBE - Instruction fetch Buss Error",
    "DBE - Data load or store Buss Error",
    "Sys - Syscall exception",
    "Bp - Breakpoint exception",
    "RI - Reversed Instruction exception",
    "CpU - Coprocessor Unimplemented",
    "Ov - Arithmetic Overflow exception",
    "Tr - Trap",
    "FPE - Floating Point Exception",
    "Unk - Unknown"
]

def dump_vector_table():
    with serial.Serial() as ser:
        ser.baudrate = 115200
        ser.port = '/dev/ttyUSB0'
        ser.open()
        ser.write(b"\n")
        ser.readline()
        for i in range(0, 0x10):
            offset = VSR_ADDR + (i * 4)
            ser.write(
                "read_memory -n 4 0x{:0x}\n"\
                .format(offset)\
                .encode('utf-8')
            )
            ser.readline() # echo
            ser.readline() # newline
            output = ser.readline() # content
            ser.readline() # newline
            match = re.findall(b"[0-9-a-f]{8}: ([0-9-a-f ]{11})",output)
            address = int(match[0].replace(b" ", b"").decode('utf-8'), 16)
            if address in VSR_NAMES:
                name = VSR_NAMES[address]
            else:
                name = "UNKNOWN"
            print(
                    "{}: 0x{:0x}\t{:<5}0x{:0x} {}".format(
                    i,
                    offset,
                    name,
                    address,
                    CAUSES[i]
                )
            )

if __name__ == "__main__":
    dump_vector_table()

Running the code against a Netgear CG3700B device:

python3 dump_vsr.py
0: 0x80000300	__default_interrupt_vsr                 0x800043ec
1: 0x80000304	__default_exception_vsr                 0x80004bd8
2: 0x80000308	__default_exception_vsr                 0x80004bd8
3: 0x8000030c	__default_exception_vsr                 0x80004bd8
4: 0x80000310	__default_exception_vsr                 0x80004bd8
5: 0x80000314	__default_exception_vsr                 0x80004bd8
6: 0x80000318	__default_exception_vsr                 0x80004bd8
7: 0x8000031c	__default_exception_vsr                 0x80004bd8
8: 0x80000320	__default_exception_vsr                 0x80004bd8
9: 0x80000324	__default_exception_vsr                 0x80004bd8
10: 0x80000328	__default_exception_vsr                 0x80004bd8
11: 0x8000032c	__default_exception_vsr                 0x80004bd8
12: 0x80000330	__default_exception_vsr                 0x80004bd8
13: 0x80000334	__default_exception_vsr                 0x80004bd8
14: 0x80000338	__default_exception_vsr                 0x80004bd8
15: 0x8000033c	__default_exception_vsr                 0x80004bd8

The visual representation below should help you understand how all these different components interact with each other:

ecos_isr_vsr_handling

__default_interrupt_vsr

An annotated version of __default_interrupt_vsr assembly is provided below to help you dig even more into the subject.

MIPS exceptions are handled by a peripheral device to the CPU called coprocessor 0 (cp0). Coprocessor 0 contains a number of registers used to configure exception handling and to report the status of current exceptions.

__default_interrupt_vsr:

; save ALL registers to the stack, including Hi and Low
800043ec 03 a0 d8 21     move       k1,sp
800043f0 23 bd fe b0     addi       sp,sp,-0x150
800043f4 af ba 00 88     sw         k0,0x88(sp)
800043f8 af a0 00 00     sw         zero,0x0(sp)
800043fc af a1 00 04     sw         at,0x4(sp)
80004400 af a2 00 08     sw         v0,0x8(sp)
80004404 af a3 00 0c     sw         v1,0xc(sp)
80004408 af a4 00 10     sw         a0,0x10(sp)
8000440c af a5 00 14     sw         a1,0x14(sp)
80004410 af a6 00 18     sw         a2,0x18(sp)
80004414 af a7 00 1c     sw         a3,0x1c(sp)
80004418 af a8 00 20     sw         t0,0x20(sp)
8000441c af a9 00 24     sw         t1,0x24(sp)
80004420 af aa 00 28     sw         t2,0x28(sp)
80004424 af ab 00 2c     sw         t3,0x2c(sp)
80004428 af ac 00 30     sw         t4,0x30(sp)
8000442c af ad 00 34     sw         t5,0x34(sp)
80004430 af ae 00 38     sw         t6,0x38(sp)
80004434 af af 00 3c     sw         t7,0x3c(sp)
80004438 af b0 00 40     sw         s0,0x40(sp)
8000443c af b1 00 44     sw         s1,0x44(sp)
80004440 af b2 00 48     sw         s2,0x48(sp)
80004444 af b3 00 4c     sw         s3,0x4c(sp)
80004448 af b4 00 50     sw         s4,0x50(sp)
8000444c af b5 00 54     sw         s5,0x54(sp)
80004450 af b6 00 58     sw         s6,0x58(sp)
80004454 af b7 00 5c     sw         s7,0x5c(sp)
80004458 af b8 00 60     sw         t8,0x60(sp)
8000445c af b9 00 64     sw         t9,0x64(sp)
80004460 af bc 00 70     sw         gp,0x70(sp)
80004464 af be 00 78     sw         s8,0x78(sp)
80004468 af bf 00 7c     sw         ra,0x7c(sp)
8000446c 00 00 20 10     mfhi       a0
80004470 00 00 28 12     mflo       a1
80004474 af a4 00 80     sw         a0,0x80(sp)
80004478 af a5 00 84     sw         a1,0x84(sp)
8000447c af bb 00 74     sw         k1,0x74(sp)

; move values from coprocessor 0 into $t1-$t$3
80004480 40 09 60 00     mfc0       t1,Status
80004484 40 0a 38 00     mfc0       t2,HWREna
80004488 40 0b 70 00     mfc0       t3,EPC

; ; save values $t1-$t3 onto the stack
8000448c af a9 00 8c     sw         t1,0x8c(sp)
80004490 af aa 00 94     sw         t2,0x94(sp)
80004494 af ab 00 90     sw         t3,0x90(sp)

; set global pointer ($gp) to 0x81971b10
80004498 3c 1c 81 97     lui        gp,0x8197
8000449c 27 9c 1b 10     addiu      gp,gp,0x1b10

; $v0 = 0x81925168
800044a0 3c 02 81 92     lui        v0,0x8192
800044a4 24 42 51 68     addiu      v0,v0,0x5168

$a0 = ($v0); $a0 += 1; ($v0) = $a0
800044a8 8c 44 00 00     lw         a0,0x0(v0)=>DAT_81925168                         = 00000001h
800044ac 20 84 00 01     addi       a0,a0,0x1
800044b0 ac 44 00 00     sw         a0,0x0(v0)=>DAT_81925168                         = 00000001h


800044b4 03 a0 80 21     move       s0,sp

; $a0 = stack top (0x8196b470)
; $a1 = stack base (0x8196a470)
; $a3 = sp - base
; # not on interrupt stack if < 0
; delay slot 
800044b8 3c 04 81 97     lui        a0,0x8197
800044bc 24 84 b4 70     addiu      a0,a0,-0x4b90
800044c0 24 85 f0 00     addiu      a1,a0,-0x1000
800044c4 03 a5 38 22     sub        a3,sp,a1
800044c8 04 e0 00 04     bltz       a3,LAB_800044dc
800044cc 00 00 00 00     _nop                               

; t0 = top - sp
; already on interrupt stack if > 0
; delay slot
800044d0 00 9d 40 22     sub        t0,a0,sp
800044d4 1d 00 00 02     bgtz       t0,LAB_800044e0
800044d8 00 00 00 00     _nop

; switch to interrupt stack
; space for old SP (8 to keep dword alignment!)
; save old SP on stack
800044dc 00 80 e8 21     move       sp,a0
800044e0 23 bd ff f8     addi       sp,sp,-0x8
800044e4 af b0 00 00     sw         s0,0x0(sp)=>DAT_8196b468

; make a null frame
800044e8 27 bd ff e0     addiu      sp,sp,-0x20

; hal_intc_decode
; Decode external interrupt via interrupt controller
800044ec 40 03 60 00     mfc0       v1,Status
800044f0 00 00 00 00     nop
800044f4 40 02 68 00     mfc0       v0,Cause
800044f8 00 00 00 00     nop
800044fc 00 43 10 24     and        v0,v0,v1
80004500 00 02 12 02     srl        v0,v0,0x8
80004504 30 42 00 ff     andi       v0,v0,0xff
80004508 3c 03 80 00     lui        v1,0x8000
8000450c 24 63 46 a0     addiu      v1,v1,0x46a0
80004510 00 43 10 20     add        v0,v0,v1
80004514 80 52 00 00     lb         s2,0x0(v0)=>DAT_800046a0

; hal_intc_translate
; Here, s2 contains the number of the interrupt being serviced,
; we need to derive from that the vector number to call in the ISR
; table.
80004518 02 40 88 21     move       s1,s2

; s1 = byte offset of vector
8000451c 00 11 88 80     sll        s1,s1,0x2

; reenable exceptions
80004520 40 02 60 00     mfc0       v0,Status
80004524 3c 03 ff ff     lui        v1,0xffff
80004528 34 63 ff f0     ori        v1,v1,0xfff0
8000452c 00 43 10 24     and        v0,v0,v1
80004530 40 82 60 00     mtc0       v0,Status,0x0
80004534 00 00 00 00     nop
80004538 00 00 00 00     nop
8000453c 00 00 00 00     nop

; $t2 = 0x8137c730 (hal_interrupt_handler, handler table)
80004540 3c 0a 81 38     lui        t2,0x8138
80004544 25 4a c7 30     addiu      t2,t2,-0x38d0

; address of ISR ptr
80004548 01 51 50 20     add        t2,t2,s1

; ISR pointer
8000454c 8d 4a 00 00     lw         t2,0x0(t2)=>->FUN_80e2c5d0                       = 80e2c5d0

; $a1 = 0x8137c750 (hal_interrupt_data)
80004550 3c 05 81 38     lui        a1,0x8138
80004554 24 a5 c7 50     addiu      a1,a1,-0x38b0

; address of data ptr
80004558 00 b1 28 20     add        a1,a1,s1

; data pointer
8000455c 8c a5 00 00     lw         a1,0x0(a1)=>DAT_8137c750

; pass interrupt number
80004560 02 40 20 21     move       a0,s2

; call ISR via t2
80004564 01 40 f8 09     jalr       t2

; delay slot
80004568 00 00 00 00     _nop

; $sp = *$sp
8000456c 8f bd 00 20     lw         sp,0x20(sp)=>DAT_8196b468
; make a null frame
80004570 27 bd ff e0     addiu      sp,sp,-0x20


80004574 00 40 90 21     move       s2,v0

; $a1 = 0x8137c770 (hal_interrupt_objects) 
80004578 3c 05 81 38     lui        a1,0x8138
8000457c 24 a5 c7 70     addiu      a1,a1,-0x3890

; address of object ptr
80004580 00 b1 28 20     add        a1,a1,s1
;  a1 = object ptr
80004584 8c a5 00 00     lw         a1,0x0(a1)=>DAT_8137c770

; arg3 = saved register dump
80004588 02 00 30 21     move       a2,s0

; call into C to finish off
8000458c 0c 38 c2 f2     jal        interrupt_end

; put ISR result in arg0
80004590 00 40 20 21     _move      a0,v0

; return value from isr
80004594 02 40 10 21     move       v0,s2

__default_exception_vsr

An annotated version of __default_exception_vsr assembly is provided below to help you dig even more into the subject.

default_exception_vsr:

; save ALL registers to the stack, including Hi and Low
800042e0 03 a0 d8 21     move       k1,sp
800042e4 23 bd fe b0     addi       sp,sp,-0x150
800042e8 af ba 00 88     sw         k0,0x88(sp)
800042ec af a0 00 00     sw         zero,0x0(sp)
800042f0 af a1 00 04     sw         at,0x4(sp)
800042f4 af a2 00 08     sw         v0,0x8(sp)
800042f8 af a3 00 0c     sw         v1,0xc(sp)
800042fc af a4 00 10     sw         a0,0x10(sp)
80004300 af a5 00 14     sw         a1,0x14(sp)
80004304 af a6 00 18     sw         a2,0x18(sp)
80004308 af a7 00 1c     sw         a3,0x1c(sp)
8000430c af a8 00 20     sw         t0,0x20(sp)
80004310 af a9 00 24     sw         t1,0x24(sp)
80004314 af aa 00 28     sw         t2,0x28(sp)
80004318 af ab 00 2c     sw         t3,0x2c(sp)
8000431c af ac 00 30     sw         t4,0x30(sp)
80004320 af ad 00 34     sw         t5,0x34(sp)
80004324 af ae 00 38     sw         t6,0x38(sp)
80004328 af af 00 3c     sw         t7,0x3c(sp)
8000432c af b0 00 40     sw         s0,0x40(sp)
80004330 af b1 00 44     sw         s1,0x44(sp)
80004334 af b2 00 48     sw         s2,0x48(sp)
80004338 af b3 00 4c     sw         s3,0x4c(sp)
8000433c af b4 00 50     sw         s4,0x50(sp)
80004340 af b5 00 54     sw         s5,0x54(sp)
80004344 af b6 00 58     sw         s6,0x58(sp)
80004348 af b7 00 5c     sw         s7,0x5c(sp)
8000434c af b8 00 60     sw         t8,0x60(sp)
80004350 af b9 00 64     sw         t9,0x64(sp)
80004354 af bc 00 70     sw         gp,0x70(sp)
80004358 af be 00 78     sw         s8,0x78(sp)
8000435c af bf 00 7c     sw         ra,0x7c(sp)
80004360 00 00 20 10     mfhi       a0
80004364 00 00 28 12     mflo       a1
80004368 af a4 00 80     sw         a0,0x80(sp)
8000436c af a5 00 84     sw         a1,0x84(sp)
80004370 af bb 00 74     sw         k1,0x74(sp)

; move values from coprocessor 0 into $t0-$t$6 
80004374 40 08 68 00     mfc0       t0,Cause
80004378 40 09 60 00     mfc0       t1,Status
8000437c 40 0a 38 00     mfc0       t2,HWREna
80004380 40 0b 40 00     mfc0       t3,BadVAddr
80004384 40 0c 18 00     mfc0       t4,EntryLo1
80004388 40 0d 78 00     mfc0       t5,PRId
8000438c 40 0e 70 00     mfc0       t6,EPC

; save values $t0-$t6 onto the stack
80004390 af a8 00 98     sw         t0,0x98(sp)
80004394 af a9 00 8c     sw         t1,0x8c(sp)
80004398 af aa 00 94     sw         t2,0x94(sp)
8000439c af ab 00 9c     sw         t3,0x9c(sp)
800043a0 af ac 00 a4     sw         t4,0xa4(sp)
800043a4 af ad 00 a0     sw         t5,0xa0(sp)
800043a8 af ae 00 90     sw         t6,0x90(sp)

; set global pointer ($gp) to 0x8161e5d0
800043ac 3c 1c 81 62     lui        gp,0x8162
800043b0 27 9c e5 d0     addiu      gp,gp,-0x1a30
; s0 = $sp
800043b4 03 a0 80 21     move       s0,sp
; sp = $sp - 0x20
800043b8 23 bd ff e0     addi       sp,sp,-0x20
; move status register from cp0 into $v0
; apply mask to $v0
; save $v0 back to cp0 status register
800043bc 40 02 60 00     mfc0       v0,Status
800043c0 3c 03 ff ff     lui        v1,0xffff
800043c4 34 63 ff f0     ori        v1,v1,0xfff0
800043c8 00 43 10 24     and        v0,v0,v1
800043cc 40 82 60 00     mtc0       v0,Status,0x0
800043d0 00 00 00 00     nop
800043d4 00 00 00 00     nop
800043d8 00 00 00 00     nop
; call cyg_hal_exception_handler
800043dc 0c 35 07 3d     jal        cyg_hal_exception_handler

Stub Entry Vector (0x80000100)

The stub entry vector is supposedly located at 0x80000100, so let’s read 32 bytes from starting from there.

CM> read_memory -n 32 0x80000100
80000100: 40 1a 68 00  00 00 00 00  33 5a 00 7f  3c 1b 80 00 | @.h.....3Z..<...
80000110: 27 7b 03 00  03 7a d8 20  8f 7b 00 00  03 60 00 08 | '{...z. .{...`..

The disassembly is exactly the same than the common vector:

rasm2 -a mips -b 32 -e -d '401a680000000000335a007f3c1b8000277b0300037ad8208f7b000003600008'
mfc0 k0, t5, 0
nop
andi k0, k0, 0x7f
lui k1, 0x8000
addiu k1, k1, 0x300
add k1, k1, k0
lw k1, (k1)
jr k1

It’s a perfect match for this piece of assembly from eCos 2.0 source:

FUNC_START(utlb_vector)
     mfc0    k0,cause        # K0 = exception cause
     nop
     andi    k0,k0,0x7F      # isolate exception code
     la  k1,hal_vsr_table    # address of VSR table
     add k1,k1,k0        # offset of VSR entry
     lw  k1,0(k1)        # k1 = pointer to VSR
     jr  k1          # go there
     nop             # (delay slot)
FUNC_END(utlb_vector)

Similarly, this trampoline will fetch an address from the VSR table and jump to it.

Debug Vector (0x80000200)

Debug vectors are not used in production system, but let’s document it for completeness sake.

Let’s read the first 32 bytes starting at offset 0x80000200 and disassemble them with rasm2.

CM> read_memory -n 32 0x80000200
80000200: 40 1a 68 00  3b 5a 01 00  40 9a 68 00  3c 1a 81 32 | @.h.;Z..@.h.<..2
80000210: 8f 5a 88 58  8f 5a 00 00  8f 5b 00 00  13 60 00 03 | .Z.X.Z...[...`..
80000220: 00 00 00 00  42 00 00 18  00 00 00 00  8f 5d 00 08 | ....B........]..
80000230: 8f 5b 00 0c  40 9b 70 00  03 40 20 21  00 00 00 0f | .[..@.p..@ !....

Disassembly:

rasm2 -a mips -b 32 -e -d '401a68003b5a0100409a68003c1a81328f5a88588f5a00008f5b0000136000030000000042000018000000008f5d00088f5b000c409b7000034020210000000f'
mfc0 k0, t5, 0      ; move from coprocessor 0
xori k0, k0, 0x100  ; 
mtc0 k0, t5, 0      ; move to coprocessor 0
lui k0, 0x8132      ; load unsigned integer to $k0
lw k0, -0x77a8(k0)  ; load value from 0x81318858 into $k0
lw k0, (k0)         ;
lw k1, (k0)         ;
beqz k1, 0x2c       ; branch to $k1
nop
eret                ; return from interrupt               
nop

This is similar to the assembly, although a little more convoluted:

FUNC_START(debug_vector)
     la  k0,32*4
     la  k1,hal_vsr_table    # Get VSR table
     lw  k1,32*4(k1)     # load debug vector
     jr  k1          # go there
     nop             # (delay slot)
FUNC_END(debug_vector)

Virtual Vector Table (0x80000400)

“Virtual vectors” is the name of a table located at a static location in the target address space. This table contains 64 vectors that point to service functions or data. The fact that the vectors are always placed at the same location in the address space means that both ROM and RAM startup configurations can access these and thus the services pointed to. The primary goal is to allow services to be provided by ROM configurations (ROM monitors such as RedBoot in particular) with clients in RAM configurations being able to use these services. Without the table of pointers this would be impossible since the ROM and RAM applications would be linked separately - in effect having separate name spaces - preventing direct references from one to the other. This decoupling of service from client is needed by RedBoot, allowing among other things debugging of applications which do not contain debugging client code (stubs).

Source: https://ecos.sourceware.org/ecos/docs-latest/ref/hal-calling-if.html

The virtual vectors table is initialized by the hal_if_init function. A simplified decompiled version from an actual firmware is provided below:

void hal_if_init(void)

{
  int address_index;
  int index;

  index = 0;
  address_index = 0;
  do {
    *(undefined4 *)(&DAT_80000400 + address_index) = 0x80d97fb8;
    index = index + 1;
    address_index = index * 4;
  } while (index < 0x40);
  _DAT_80000400 = 0x80015;      //set virtual version table version
  _DAT_80000440 = FUN_80d97f68;     // reset
  _DAT_80000410 = &LAB_80d97f80;    // kill
  _DAT_80000448 = &LAB_80d97ecc;    // microsecond delay
  _DAT_80000420 = &LAB_80d982f0;    // flush instruction cache
  _DAT_8000041c = &LAB_80d98308;    // flush data cache
  _DAT_8000044c = 0;                // debug data
  _DAT_80000438 = 0;                // set serial baud rate
  _DAT_80000430 = set_debug_comm;   // set debug communication channel
  _DAT_80000434 = set_console_comm; // set console communication channel
  _DAT_80000444 = 0;                // console interrupt flag
  return;

What the function does is initializing the virtual vector table by setting all entries in the vector table so that they point to the function at offset 0x80d97fb8.

The function at 0x80d97fb8 is what I call nop_service:

undefined nop_service()
        80d97fb8 03 e0 00 08     jr         ra
        80d97fbc 00 00 10 21     _clear     v0

When all entries are initialized, the code set specific entries.

To understand these specific entries, we can look at the diagram below (inspired by “Embedded Software Development with eCos” by Anthony J. Massa).

virtual_vector_table_init_sequence

Supposedly, this is how the VVT version is built:

This value contains the total number of virtual vectors in the upper 16 bits, and the definition number of the last supported virtual vectors in the lower 16 bits. For this VVT, the total number of virtual vectors is 64d (0x40), and the definition number of the last virtual vector, Flash ROM Configuration, is 20d (0x14). The version is therefore 0x4014 (edit: actually 0x00400014).

Here, the virtual vector table version is set to 0x00080015.

The definition number of the last vector, Flash ROM Configuration is 21d (0x15). The total number of virtual vectors in the upper 16 bits should be 64d (0x40), but is actually 8d (0x8). It’s highly probable that Broadcom changed the initial eCos behavior. We can still rely on the lower 16 bits though.

To interact with the VVT, I wrote a piece of Python code that lists the entries from a live system by fetching the information over serial.

python3 dump_vector_table.py
0x80000400	Virtual Vector Table Version            0x80015 
0x80000404	Interrupt Table                         0x80d97fb8 (nop)
0x80000408	Exception Table                         0x80d97fb8 (nop)
0x8000040c	Debug Vector                            0x80d97fb8 (nop)
0x80000410	Kill Vector                             0x80d97f80 
0x80000414	Console I/O Procedure Table             0x81967908 
0x80000418	Debug I/O Procedure Table               0x81967908 
0x8000041c	Flush Data Cache                        0x80d98308 
0x80000420	Flush Instruction Cache                 0x80d982f0 
0x80000424	CPU Data                                0x80d97fb8 (nop)
0x80000428	Board Data                              0x80d97fb8 (nop)
0x8000042c	System Information                      0x80d97fb8 (nop)
0x80000430	Set Debug Communication Channel         0x80d97fc0 
0x80000434	Set Console Communication Channel       0x80d98230 
0x80000438	Set Serial Baud Rate                    0x0 
0x8000043c	Debug System Call                       0x80d97fb8 (nop)
0x80000440	Reset                                   0x80d97f68 
0x80000444	Console Interrupt Flag                  0x0 
0x80000448	Microsecond delay                       0x80d97ecc 
0x8000044c	Debug Data                              0x812b2894 
0x80000450	Flash ROM Configuration                 0x80d97fb8 (nop)
0x80000454	RESERVED                                0x80d97fb8 (nop)
0x80000458	RESERVED                                0x80d97fb8 (nop)
0x8000045c	RESERVED                                0x80d97fb8 (nop)
0x80000460	RESERVED                                0x80d97fb8 (nop)
0x80000464	RESERVED                                0x80d97fb8 (nop)
0x80000468	RESERVED                                0x80d97fb8 (nop)
0x8000046c	RESERVED                                0x80d97fb8 (nop)
0x80000470	RESERVED                                0x80d97fb8 (nop)
0x80000474	RESERVED                                0x80d97fb8 (nop)
0x80000478	RESERVED                                0x80d97fb8 (nop)
0x8000047c	RESERVED                                0x80d97fb8 (nop)
0x80000480	RESERVED                                0x80d97fb8 (nop)
0x80000484	RESERVED                                0x80d97fb8 (nop)
0x80000488	RESERVED                                0x80d97fb8 (nop)
0x8000048c	Install breakpoint                      0x80d97fb8 (nop)
0x80000490	RESERVED                                0x80d97fb8 (nop)
0x80000494	RESERVED                                0x80d97fb8 (nop)
0x80000498	RESERVED                                0x80d97fb8 (nop)
0x8000049c	RESERVED                                0x80d97fb8 (nop)
0x800004a0	RESERVED                                0x80d97fb8 (nop)
0x800004a4	RESERVED                                0x80d97fb8 (nop)
0x800004a8	RESERVED                                0x80d97fb8 (nop)
0x800004ac	RESERVED                                0x80d97fb8 (nop)
0x800004b0	RESERVED                                0x80d97fb8 (nop)
0x800004b4	RESERVED                                0x80d97fb8 (nop)
0x800004b8	RESERVED                                0x80d97fb8 (nop)
0x800004bc	RESERVED                                0x80d97fb8 (nop)
0x800004c0	RESERVED                                0x80d97fb8 (nop)
0x800004c4	RESERVED                                0x80d97fb8 (nop)
0x800004c8	RESERVED                                0x80d97fb8 (nop)
0x800004cc	RESERVED                                0x80d97fb8 (nop)
0x800004d0	RESERVED                                0x80d97fb8 (nop)
0x800004d4	RESERVED                                0x80d97fb8 (nop)
0x800004d8	RESERVED                                0x80d97fb8 (nop)
0x800004dc	RESERVED                                0x80d97fb8 (nop)
0x800004e0	RESERVED                                0x80d97fb8 (nop)
0x800004e4	RESERVED                                0x80d97fb8 (nop)
0x800004e8	RESERVED                                0x80d97fb8 (nop)
0x800004ec	RESERVED                                0x80d97fb8 (nop)
0x800004f0	RESERVED                                0x80d97fb8 (nop)
0x800004f4	RESERVED                                0x80d97fb8 (nop)
0x800004f8	RESERVED                                0x80d97fb8 (nop)
0x800004fc	Virtual Vector Table                    0x80d97fb8 (nop)

Conclusion

If you made it through here, congratulations ! Our acquired understanding of the inner workings of eCos interrupt/exception handling and dedicated vector tables will be helpful in the future when we try to inject GDB stubs into running production firmware. This will also prove useful when we will be designing backdoor persistence by hijacking vector table entries.

As always, if you have any question feel free to contact me via Twitter or email.


Tagged #ecos, #memory, #reversing.