class Metasm::BinDiffWidget

Constants

COLORS

Attributes

status[RW]

Public Instance Methods

colorize_blocks(a1, a2) click to toggle source
# File samples/bindiff.rb, line 399
def colorize_blocks(a1, a2)
        b1 = @dasm1.decoded[a1].block
        b2 = @dasm2.decoded[a2].block

        common_start = common_end = 0
        match_block(b1, b2) { |a, b| common_start = a ; common_end = b }

        b1.list[0..-1-common_end].zip(b2.list[0..-1-common_end]).each { |di1, di2|
                next if not di1 or not di2
                @dasmcol1[di1.address] = @dasmcol2[di2.address] = [:same, :similar, :badarg, :badop][match_instr(di1, di2)]
        }
        b1.list[-common_end..-1].zip(b2.list[-common_end..-1]).each { |di1, di2|
                next if not di1 or not di2
                @dasmcol1[di1.address] = @dasmcol2[di2.address] = [:same, :similar, :badarg, :badop][match_instr(di1, di2)]
        }
end
create_func(dasm, a) click to toggle source
# File samples/bindiff.rb, line 179
def create_func(dasm, a)
        h = {}
        todo = [a]
        while a = todo.pop
                next if h[a]
                h[a] = []
                dasm.decoded[a].block.each_to_samefunc(dasm) { |ta|
                        next if not dasm.di_at(ta)
                        todo << ta
                        h[a] << ta
                }
        end
        h
end
create_func_stats(dasm, a, g) click to toggle source
# File samples/bindiff.rb, line 203
def create_func_stats(dasm, a, g)
        s = {}
        s[:blocks] = g.length

        s[:edges] = 0 # nr of edges
        s[:leaves] = 0        # nr of nodes with no successor
        s[:ext_calls] = 0     # nr of jumps out_of_func
        s[:loops] = 0 # nr of jump back

        todo = [a]
        done = []
        while aa = todo.pop
                next if done.include? aa
                done << aa
                todo.concat g[aa]

                s[:edges] += g[aa].length
                s[:leaves] += 1 if g[aa].empty?
                dasm.decoded[aa].block.each_to_otherfunc(dasm) { s[:ext_calls] += 1 }
        end

        # loop detection
        # find the longest distance to the root w/o loops
        g = g.dup
        while eliminate_one_loop(a, g)
                s[:loops] += 1
        end

        s
end
create_funcs(dasm) click to toggle source

func addr => { funcblock => list of funcblock to }

# File samples/bindiff.rb, line 168
def create_funcs(dasm)
        f = {}
        dasm.entrypoints.to_a.each { |ep| dasm.function[ep] ||= DecodedFunction.new }
        dasm.function.each_key { |a|
                next if not dasm.di_at(a)
                f[a] = create_func(dasm, a)
                Gui.main_iter
        }
        f
end
create_funcs_stats(f, dasm) click to toggle source
# File samples/bindiff.rb, line 194
def create_funcs_stats(f, dasm)
        fs = {}
        f.each { |a, g|
                fs[a] = create_func_stats(dasm, a, g)
                Gui.main_iter
        }
        fs
end
curaddr1() click to toggle source
# File samples/bindiff.rb, line 52
def curaddr1; @dasm1.gui.curaddr end
curaddr2() click to toggle source
# File samples/bindiff.rb, line 53
def curaddr2; @dasm2.gui.curaddr end
curfunc1() click to toggle source
# File samples/bindiff.rb, line 54
def curfunc1; @dasm1.find_function_start(curaddr1) end
curfunc2() click to toggle source
# File samples/bindiff.rb, line 55
def curfunc2; @dasm2.find_function_start(curaddr2) end
dasm1() click to toggle source
# File samples/bindiff.rb, line 32
def dasm1; @dasm1 end
dasm1=(d) click to toggle source
# File samples/bindiff.rb, line 33
def dasm1=(d)
        @dasm1 = d
        @func1 = nil
        @funcstat1 = nil
        @dasmcol1 = {}
        @dasm1.gui.bg_color_callback = lambda { |a1| COLORS[@dasmcol1[a1] || :default] }
        @match_func = nil
end
dasm2() click to toggle source
# File samples/bindiff.rb, line 42
def dasm2; @dasm2 end
dasm2=(d) click to toggle source
# File samples/bindiff.rb, line 43
def dasm2=(d)
        @dasm2 = d
        @func2 = nil
        @funcstat1 = nil
        @dasmcol2 = {}
        @dasm2.gui.bg_color_callback = lambda { |a2| COLORS[@dasmcol2[a2] || :default] }
        @match_func = nil
end
disassemble(method=:disassemble) click to toggle source
# File samples/bindiff.rb, line 135
def disassemble(method=:disassemble)
        @func1 = @func2 = @funcstat1 = @funcstat2 = nil
        set_status('dasm 1') {
                @dasm1.send(method, curaddr1)
                @dasm1.gui.focus_addr(curaddr1, :graph)
        }
        set_status('dasm 2') {
                @dasm2.send(method, curaddr2)
                @dasm2.gui.focus_addr(curaddr2, :graph)
        }
        gui_update
end
disassemble_all() click to toggle source
# File samples/bindiff.rb, line 126
def disassemble_all
        @func1 = @func2 = @funcstat1 = @funcstat2 = nil
        @dasm1.load_plugin 'dasm_all'
        @dasm2.load_plugin 'dasm_all'
        set_status('dasm_all 1') { @dasm1.dasm_all_section '.text' }
        set_status('dasm_all 2') { @dasm2.dasm_all_section '.text' }
        gui_update
end
eliminate_one_loop(a, g) click to toggle source
# File samples/bindiff.rb, line 234
def eliminate_one_loop(a, g)
        stack = []
        index = {}
        reach_index = {}
        done = false

        curindex = 0
        
        trajan = lambda { |e|
                index[e] = curindex
                reach_index[e] = curindex
                curindex += 1
                stack << e
                g[e].each { |ne|
                        if not index[ne]
                                trajan[ne]
                                break if done
                                reach_index[e] = [reach_index[e], reach_index[ne]].min
                        elsif stack.include? ne
                                reach_index[e] = [reach_index[e], reach_index[ne]].min
                        end
                }
                break if done
                if index[e] == reach_index[e]
                        if (e == stack.last and not g[e].include? e)
                                stack.pop
                                next
                        end
                        # e is the entry in the loop, cut the loop here
                        tail = reach_index.keys.find { |ee| reach_index[ee] == index[e] and g[ee].include? e }
                        g[tail] -= [e]      # patch g, but don't modify the original g value (ie -= instead of delete)
                        done = true # one loop found & removed, try again
                end
        }

        trajan[a]
        done
end
func1() click to toggle source
# File samples/bindiff.rb, line 56
def func1; @func1 ||= set_status('funcs 1') { create_funcs(@dasm1) } end
func2() click to toggle source
# File samples/bindiff.rb, line 57
def func2; @func2 ||= set_status('funcs 2') { create_funcs(@dasm2) } end
funcstat1() click to toggle source
# File samples/bindiff.rb, line 58
def funcstat1; @funcstat1 ||= set_status('func stats 1') { create_funcs_stats(func1, @dasm1) } end
funcstat2() click to toggle source
# File samples/bindiff.rb, line 59
def funcstat2; @funcstat2 ||= set_status('func stats 2') { create_funcs_stats(func2, @dasm2) } end
gui_update() click to toggle source
# File samples/bindiff.rb, line 65
def gui_update
        @dasm1.gui.gui_update rescue nil
        @dasm2.gui.gui_update rescue nil
        redraw
end
initialize_widget(d1=nil, d2=nil) click to toggle source
# File samples/bindiff.rb, line 26
def initialize_widget(d1=nil, d2=nil)
        self.dasm1 = d1 if d1
        self.dasm2 = d2 if d2
        @status = nil
end
keypress(key) click to toggle source
# File samples/bindiff.rb, line 82
def keypress(key)
        case key
        when A
                keypress(D)
                keypress(f)
                keypress(i)
        when D
                disassemble_all
        when c
                disassemble
        when C
                disassemble(:disassemble_fast)
        when f
                funcstat1
                funcstat2
        when g
                inputbox('address to go', :text => Expression[@dasm1.gui.curaddr]) { |v|
                        @dasm1.gui.focus_addr_autocomplete(v)
                        @dasm2.gui.focus_addr_autocomplete(v)
                }
        when M
                show_match_funcs
        when m
                match_one_func(curfunc1, curfunc2)

        when r
                puts 'reload'
                load __FILE__
                gui_update

        when Q
                Gui.main_quit
        end
end
keypress_ctrl(key) click to toggle source
# File samples/bindiff.rb, line 117
def keypress_ctrl(key)
        case key
        when C
                disassemble(:disassemble_fast_deep)
        when r
                inputbox('code to eval') { |c| messagebox eval(c).inspect[0, 512], 'eval' }
        end
end
match_block(b1, b2) { |common_start, common_end| ... } click to toggle source
# File samples/bindiff.rb, line 363
def match_block(b1, b2)
        # 0 = perfect match (same opcodes, same args)
        # 1 = same opcodes, same arg type
        # 2 = same opcodes, diff argtypes
        # 3 = some opcode difference
        # 4 = full block difference
        score = 0
        score_div = [b1.list.length, b2.list.length].max.to_f
        common_start = 0
        common_end = 0

        # basic diff-style: compare start while it's good, then end, then whats left
        # should handle most simples cases well
        len = [b1.list.length, b2.list.length].min
        while common_start < len and (s = match_instr(b1.list[common_start], b2.list[common_start])) <= 1
                score += s / score_div
                common_start += 1
        end

        while common_start+common_end < len and (s = match_instr(b1.list[-1-common_end], b2.list[-1-common_end])) <= 1
                score += s / score_div
                common_end += 1
        end

        # TODO improve the middle part matching (allow insertions/suppressions/swapping)
        b1.list[common_start..-1-common_end].zip(b2.list[common_start..-1-common_end]).each { |di1, di2|
                score += match_instr(di1, di2) / score_div
        }

        yield(common_start, common_end) if block_given?       # used by colorize_blocks

        score += (b1.list.length - b2.list.length).abs * 3 / score_div        # instr count difference -> +3 per instr

        score
end
match_func(a1, a2, do_colorize=true, verb=true) click to toggle source

return how much match a func in d1 and a func in d2

# File samples/bindiff.rb, line 327
def match_func(a1, a2, do_colorize=true, verb=true)
        f1 = func1[a1]
        f2 = func2[a2]
        raise "dasm1 has no function at #{Expression[a1]}" if not f1
        raise "dasm2 has no function at #{Expression[a2]}" if not f2
        todo1 = [a1]
        todo2 = [a2]
        done1 = []
        done2 = []
        score = 0.0   # average of the (local best) match_block scores
        score += 0.01 if @dasm1.get_label_at(a1) != @dasm2.get_label_at(a2)   # for thunks
        score_div = [f1.length, f2.length].max.to_f
        # XXX this is stupid and only good for perfect matches (and even then it may fail)
        # TODO handle block split etc (eg instr-level diff VS block-level)
        while a1 = todo1.shift
                next if done1.include? a1
                t = todo2.map { |a| [a, match_block(@dasm1.decoded[a1].block, @dasm2.decoded[a].block)] }
                a2 = t.sort_by { |a, s| s }.first
                if not a2
                        break
                end
                score += a2[1] / score_div
                a2 = a2[0]
                done1 << a1
                done2 << a2
                todo1.concat f1[a1]
                todo2.concat f2[a2]
                todo2 -= done2
                colorize_blocks(a1, a2) if do_colorize
        end

        score += (f1.length - f2.length).abs * 3 / score_div  # block count difference -> +3 per block

        score
end
match_funcs() click to toggle source
# File samples/bindiff.rb, line 278
        def match_funcs
                @match_funcs ||= {}

                layout_match = {}

                set_status('match func layout') {
                funcstat1.each { |a, s|
                        next if @match_funcs[a]
                        layout_match[a] = []
                        funcstat2.each { |aa, ss|
                                layout_match[a] << aa if s == ss
                        }
                        Gui.main_iter
                }
                }

                set_status('match funcs') {
                # refine the layout matching with actual function matching
                already_matched = []
                layout_match.each { |f1, list|
puts "matching #{Expression[f1]}" if $VERBOSE
begin
                        f2 = (list - already_matched).sort_by { |f| match_func(f1, f, false, false) }.first
                        if f2
                                already_matched << f2
                                score = match_func(f1, f2)
                                @match_funcs[f1] = [f2, score]
                        end
rescue Interrupt
        puts 'abort this one'
        sleep 0.2      # allow a 2nd ^c do escalate
end
                        Gui.main_iter
                }
                }

                puts "matched #{@match_funcs.length} - unmatched #{func1.length - @match_funcs.length}"
                @match_funcs
        end
match_instr(di1, di2) click to toggle source
# File samples/bindiff.rb, line 416
def match_instr(di1, di2)
        if not di1 or not di2 or di1.opcode.name != di2.opcode.name
                3
        elsif di1.instruction.args.map { |a| a.class } != di2.instruction.args.map { |a| a.class }
                2
        elsif di1.instruction.to_s.gsub(/loc_\w+/, 'loc_') != di2.instruction.to_s.gsub(/loc_\w+/, 'loc_')    # local labels      TODO compare blocks targeted
                1
        else
                0
        end
end
match_one_func(a1, a2) click to toggle source
# File samples/bindiff.rb, line 318
def match_one_func(a1, a2)
        s = match_func(a1, a2)
        puts "match score: #{s}"
        @match_funcs ||= {}
        @match_funcs[a1] = [a2, s]
        gui_update
end
paint() click to toggle source
# File samples/bindiff.rb, line 61
def paint
        draw_string_color(:black, @font_width, 3*@font_height, @status || 'idle')
end
rematch_funcs() click to toggle source
# File samples/bindiff.rb, line 273
def rematch_funcs
        @match_funcs = nil
        match_funcs
end
set_status(st=nil) { || ... } click to toggle source
# File samples/bindiff.rb, line 71
def set_status(st=nil)
        ost = @status
        @status = st
        redraw
        if block_given?
                ret = protect { yield }
                set_status ost
                ret
        end
end
show_match_funcs() click to toggle source
# File samples/bindiff.rb, line 149
def show_match_funcs
        match_funcs

        gui_update
        Gui.main_iter
        list = [['addr 1', 'addr 2', 'score']]
        f1 = func1.keys
        f2 = func2.keys
        match_funcs.each { |a1, (a2, s)|
                list << [(@dasm1.get_label_at(a1) || Expression[a1]), (@dasm2.get_label_at(a2) || Expression[a2]), '%.4f' % s]
                f1.delete a1
                f2.delete a2
        }
        f1.each { |a1| list << [(@dasm1.get_label_at(a1) || Expression[a1]), '?', 'nomatch'] }
        f2.each { |a2| list << ['?', (@dasm2.get_label_at(a2) || Expression[a2]), 'nomatch'] }
        listwindow("matches", list) { |i| @dasm1.gui.focus_addr i[0], nil, true ; @dasm2.gui.focus_addr i[1], nil, true }
end
sync1() click to toggle source

show in window 1 the match of the function found in win 2

# File samples/bindiff.rb, line 429
def sync1
        c2 = curfunc2
        if a1 = match_funcs.find_key { |k| match_funcs[k][0] == c2 }
                @dasm1.gui.focus_addr(a1)
        end
end
sync2() click to toggle source
# File samples/bindiff.rb, line 436
def sync2
        if a2 = match_funcs[curfunc1]
                @dasm2.gui.focus_addr(a2[0])
        end
end