lib/pwnlib/reg_sort.rb
# encoding: ASCII-8BIT
# frozen_string_literal: true
require 'pwnlib/context'
require 'pwnlib/util/ruby'
module Pwnlib
# Do topological sort on register assignments.
module RegSort
module_function
# Sorts register dependencies.
#
# Given a dictionary of registers to desired register contents, return the optimal order in which to set the
# registers to those contents.
#
# The implementation assumes that it is possible to move from any register to any other register.
#
# @param [Hash<Symbol, String => Object>] in_out
# Dictionary of desired register states.
# Keys are registers, values are either registers or any other value.
# @param [Array<String>] all_regs
# List of all possible registers.
# Used to determine which values in +in_out+ are registers, versus regular values.
# @param [Boolean] randomize
# Randomize as much as possible about the order or registers.
#
# @return [Array]
# Array of instructions, see examples for more details.
#
# @diff We don't support +tmp+/+xchg+ options because there's no such usage at all.
#
# @example
# regs = %w(a b c d x y z)
# regsort({a: 1, b: 2}, regs)
# #=> [['mov', 'a', 1], ['mov', 'b', 2]]
# regsort({a: 'b', b: 'a'}, regs)
# #=> [['xchg', 'a', 'b']]
# regsort({a: 1, b: 'a'}, regs)
# #=> [['mov', 'b', 'a'], ['mov', 'a', 1]]
# regsort({a: 'b', b: 'a', c: 3}, regs)
# #=> [['mov', 'c', 3], ['xchg', 'a', 'b']]
# regsort({a: 'b', b: 'a', c: 'b'}, regs)
# #=> [['mov', 'c', 'b'], ['xchg', 'a', 'b']]
# regsort({a: 'b', b: 'c', c: 'a', x: '1', y: 'z', z: 'c'}, regs)
# #=> [['mov', 'x', '1'],
# ['mov', 'y', 'z'],
# ['mov', 'z', 'c'],
# ['xchg', 'a', 'b'],
# ['xchg', 'b', 'c']]
def regsort(in_out, all_regs, randomize: nil)
# randomize = context.randomize if randomize.nil?
# TODO(david942j): stringify_keys
in_out = in_out.transform_keys(&:to_s)
# Drop all registers which will be set to themselves.
# Ex. {eax: 'eax'}
in_out.reject! { |k, v| k == v }
# Check input
if (in_out.keys - all_regs).any?
raise ArgumentError, format('Unknown register! Know: %p. Got: %p', all_regs, in_out)
end
# Collapse constant values.
#
# Ex. {eax: 1, ebx: 1} can be collapsed to {eax: 1, ebx: 'eax'}.
# +post_mov+ are collapsed registers, set their values in the end.
post_mov = in_out.group_by { |_, v| v }.each_value.with_object({}) do |list, hash|
list.sort!
first_reg, val = list.shift
# Special case for val.zero? because zeroify registers is cheaper than mov.
next if list.empty? || all_regs.include?(val) || val.zero?
list.each do |(reg, _)|
hash[reg] = first_reg
in_out.delete(reg)
end
end
graph = in_out.dup
result = []
# Let's do the topological sort.
# so sad ruby 2.1 doesn't have +itself+...
deg = graph.values.group_by { |i| i }.transform_values(&:size)
graph.each_key { |k| deg[k] ||= 0 }
until deg.empty?
min_deg = deg.min_by { |_, v| v }[1]
break unless min_deg.zero? # remain are all cycles
min_pivs = deg.select { |_, v| v == min_deg }
piv = randomize ? min_pivs.sample : min_pivs.first
dst = piv.first
deg.delete(dst)
next unless graph.key?(dst) # Reach an end node.
deg[graph[dst]] -= 1
result << ['mov', dst, graph[dst]]
graph.delete(dst)
end
# Remain must be cycles.
graph.each_key do |reg|
cycle = check_cycle(reg, graph)
cycle.each_cons(2) do |d, s|
result << ['xchg', d, s]
end
cycle.each { |r| graph.delete(r) }
end
# Now assign those collapsed registers.
post_mov.sort.each do |dreg, sreg|
result << ['mov', dreg, sreg]
end
result
end
Pwnlib::Util::Ruby.private_class_method_block do
# Walk down the assignment list of a register, return the path walked if it is encountered again.
#
# @example
# check_cycle('a', {'a' => 1}) #=> []
# check_cycle('a', {'a' => 'a'}) #=> ['a']
# check_cycle('a', {'a' => 'b', 'b' => 'c', 'c' => 'b', 'd' => 'a'}) #=> []
# check_cycle('a', {'a' => 'b', 'b' => 'c', 'c' => 'd', 'd' => 'a'})
# #=> ['a', 'b', 'c', 'd']
def check_cycle(reg, assignments)
check_cycle_(reg, assignments, [])
end
def check_cycle_(reg, assignments, path)
target = assignments[reg]
path << reg
# No cycle, some other value (e.g. 1).
return [] unless assignments.key?(target)
# Found a cycle.
return target == path.first ? path : [] if path.include?(target)
check_cycle_(target, assignments, path)
end
end
end
end