Similar to Sark's solution, minsc at https://github.com/arizvisa/ida-minsc provides a wrapper around the same switch_info_t
that is fetched in IDAPython using the get_switch_info
function from nalt.hpp
. It is worth noting that these solutions use the disassembler exclusively (as opposed to the decompiler).
Disclaimer: I'm the maintainer of the ida-minsc plugin.
To get to the minsc wrapper, the database.get.switch
function is used to fetch a switch_t
. The following python disassembles a branch instruction and then uses its address to get the "switch" identified by the disassembler. The switch_t
that is returned contains 0x10 total cases and exposes numerous properties that can be used to extract information from the "switch". As per the question, the database.type.switch
function can be used to detect if an address branches to a table used by the "switch" (or belongs to a handler) that was detected by the disassembler.
Python>db.disasm()
'277ef564: jmp ds:jpt_277EF564[eax*4]'
Python>sw=db.get.switch()
Python>sw
<class 'switch_t{16}' at 0x277ef54f> default:0x277ef699 branch[8]:0x277ef6b3 indirect[16]:0x277ef6d3 register:%eax
Python>db.t.switch()
True
The switch_t.range
property returns the entire range of cases for the switch that was determined by the disassembler. Similarly, the switch_t.cases
property is used to return only the cases that do not dispatch to the "default" handler for the fetched "switch". Also, if one wants to create a breakpoint logging the case being dispatched to by the "switch", the switch_t.ea
property can be combined with the switch_t.register
property in order to identify the register needing to be captured.
# Return the full range of cases supported by the "switch".
Python>sw.range
(0x10000000, 0x10000001, 0x10000002, 0x10000003, 0x10000004, 0x10000005, 0x10000006, 0x10000007, 0x10000008, 0x10000009, 0x1000000a, 0x1000000b, 0x1000000c, 0x1000000d, 0x1000000e, 0x1000000f)
# Return the cases of the "switch" that dispatch to a non-default case.
Python>sw.cases
(0x10000000, 0x10000001, 0x10000002, 0x10000003, 0x10000004, 0x10000005, 0x10000007, 0x10000008, 0x10000009, 0x1000000a, 0x1000000b, 0x1000000c, 0x1000000d, 0x1000000e, 0x1000000f)
# Other properties that could be useful...
Python>db.disasm(sw.ea)
'277ef54f: add eax, 0F0000000h'
Python>sw.register
<class 'register_t' index=0 dtype=dt_dword name='eax' position=0+32>
Python>database.up(sw.branch_ea)
[ref_t(address=0x277ef564, access=access_t(r))]
Python>[db.disasm(ea) for ea in database.up(sw.branch_ea)]
['277ef564: jmp ds:jpt_277EF564[eax*4]']
# Display operand information that might be used for filtering.
Python>ea, = database.up(sw.branch_ea)
Python>ins.type.branch(ea)
True
Python>instruction.ops_access(ea)
(opref_t(address=0x277ef564, opnum=0, access=access_t(rx)))
Python>ins.ops(ea)
(SegmentOffsetBaseIndexScale(segment=%ds, offset=0x277ef6b3, base=None, index=%eax, scale=4))
For addresses being dispatched to by the "switch", the switch_t.handlers
property can be used to return the address of each handler, with the switch_t.default
property being responsible for returning the address of the "default" handler. To return the handler for a specific case, the switch_t.case()
function can be used as in the following python.
# Return all of the available handlers used by the "switch".
Python>sw.handlers
(0x277ef67e, 0x277ef56b, 0x277ef644, 0x277ef685, 0x277ef699)
# Return the default handler for the "switch".
Python>sw.default
0x277ef699
# Return the handler for a specific case.
Python>sw.case(0x10000000)
0x277ef67e
By combining the handler-related properties, one can create a python dictionary for looking up the dispatcher for a specific case as in the following code. However, it is probably more useful to determine the number of cases that are implemented by each handler in order to distinguish what is actually worth reversing. This can be done by inverting the dictionary so that its key references the handler, with its value containing the list of cases being supported.
Python>{case : sw.case(case) for case in sw.range}
{0x10000000: 0x277ef67e, 0x10000001: 0x277ef67e, 0x10000002: 0x277ef67e, 0x10000003: 0x277ef67e, 0x10000004: 0x277ef67e, 0x10000005: 0x277ef67e, 0x10000006: 0x277ef699, 0x10000007: 0x277ef67e, 0x10000008: 0x277ef56b, 0x10000009: 0x277ef67e, 0x1000000a: 0x277ef67e, 0x1000000b: 0x277ef644, 0x1000000c: 0x277ef67e, 0x1000000d: 0x277ef67e, 0x1000000e: 0x277ef67e, 0x1000000f: 0x277ef685}
# Invert the table of case<->handlers.
Python>handlers={}
Python>[ handlers.setdefault(sw.case(case), []).append(case) for case in sw.cases ]
[None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None]
Python>handlers
{0x277ef67e: [0x10000000, 0x10000001, 0x10000002, 0x10000003, 0x10000004, 0x10000005, 0x10000007, 0x10000009, 0x1000000a, 0x1000000c, 0x1000000d, 0x1000000e], 0x277ef699: [0x10000006], 0x277ef56b: [0x10000008], 0x277ef644: [0x1000000b], 0x277ef685: [0x1000000f]}
This dictionary can be used for operations such as notating the handler address with a descriptive name, color, etc. The following python produces a name following the format "case(number1,number2,number3,...)" and applies it as a label for each handler. Afterwards, the database.names
namespace is used with the "bounds" matcher and function.chunk
to list all the labels within the current function chunk...showing the labels we just applied.
# Assign a label to the address of each individual case.
Python>for ea, cases in handlers.items():
Python> db.name(ea, "case({:s})".format(','.join(hex(case) for case in sorted(cases))), listed=True)
Python>
# List all the labels within the current function chunk.
Python>db.names.list(bounds=func.chunk())
[322] 0x277ef53e : ^C* : jsvda_920
[323] 0x277ef56b : ^C* : case(10000008)
[324] 0x277ef644 : ^C* : case(1000000b)
[325] 0x277ef67e : ^C* : case(10000000,10000001,10000002,10000003,10000004,10000005,10000007,10000009,1000000a,1000000c,1000000d,1000000e)
[326] 0x277ef685 : ^C* : case(1000000f)
[327] 0x277ef699 : ^C* : def_277EF564
For enumerating switches, the function.switches
function can be used with an address belonging to a function. The next python combines this with the database.functions
namespace to identify every function containing 1 or more switches within the database. After all of the candidate functions have been enumerated, the function.tag
function is used to store the total number of cases that are handled by the switches within each function.
# Collect all functions that have at least one "switch" within its body.
Python>functions_with_switches = {ea for ea in db.functions() if next(func.switches(ea), None) is not None}
Python>functions_with_switches
set([0x277bd6cd, 0x277d5b8f, 0x2780c071, 0x2780e8b1, 0x277a7833, 0x277beb74, 0x277fef38, 0x277ac0fb, 0x277a2ebd, 0x277ef53e])
# Iterate through all functions and collect the number of "switch" statements detected by the disassembler.
Python>for ea in functions_with_switches:
Python> count = sum(len(sw.cases) for sw in func.switches(ea))
Python> fn.tag(ea, 'switch.cases', count)
Once a function has been tagged and indexed with some value, database.functions
can be used with the "tagged" matcher to narrow down the list of functions containing our previously identified switches. To query the index for the number of cases tagged to each function, the database.select
function can also be used.
Python>db.functions(tagged='switch.cases')
[0x277a2ebd, 0x277a7833, 0x277ac0fb, 0x277bd6cd, 0x277beb74, 0x277d5b8f, 0x277ef53e, 0x277fef38, 0x2780c071, 0x2780e8b1]
Python>db.functions.list(tagged='switch.cases')
[0] +0x02ebd : 0x277a2ebd..0x277a3331 : (1) Fvt* : __cdecl jsvda_919 : lvars:330 args:5 refs:4 exits:1
[1] +0x07833 : 0x277a7833..0x277a78ff : (1) Fv-* : __cdecl sub_277A7833 : lvars:010 args:4 refs:1 exits:1
[2] +0x0c0fb : 0x277ac0fb..0x277ac61b : (1) Fvt* : __cdecl sub_277AC0FB : lvars:024 args:7 refs:1 exits:1
[3] +0x1d6cd : 0x277bd6cd..0x277bd82c : (1) Fvt* : __cdecl jsvda_950 : lvars:010 args:3 refs:5 exits:1
[4] +0x1eb74 : 0x277beb74..0x277beca6 : (1) F-t* : __cdecl sub_277BEB74 : lvars:008 args:4 refs:1 exits:1
[5] +0x35b8f : 0x277d5b8f..0x277d6059 : (1) FvT* : __cdecl JSVDA::204 : lvars:02c args:8 refs:2 exits:1
[6] +0x4f53e : 0x277ef53e..0x277ef6b3 : (1) Fvt* : __cdecl jsvda_920 : lvars:014 args:4 refs:3 exits:1
[7] +0x5ef38 : 0x277fef38..0x277ff219 : (1) Fvt* : __cdecl sub_277FEF38 : lvars:040 args:3 refs:1 exits:1
[8] +0x6c071 : 0x2780c071..0x2780c5cc : (1) FvT* : __cdecl JSVDA::object_6b3a6::getPathAttributeFromBucket_6c071 : lvars:024 args:5 refs:6 exits:1
[9] +0x6e8b1 : 0x2780e8b1..0x2780eabb : (1) F--* : __cdecl sub_2780E8B1 : lvars:00c args:4 refs:1 exits:1
Python>for ea, res in db.select('switch.cases'):
Python> print(res['switch.cases'], hex(ea), func.name(ea), sum(1 for blk in function.blocks(ea)))
Python>
11 277a2ebd jsvda_919 111
8 277a7833 sub_277A7833 25
10 277ac0fb sub_277AC0FB 136
16 277bd6cd jsvda_950 47
15 277beb74 sub_277BEB74 30
9 277d5b8f JSVDA::204 116
15 277ef53e jsvda_920 35
13 277fef38 sub_277FEF38 76
15 2780c071 JSVDA::object_6b3a6::getPathAttributeFromBucket_6c071 120
11 2780e8b1 sub_2780E8B1 81