Merge pull request #1 from david942j/feature/emulator

Feature/emulator
This commit is contained in:
david942j 2017-02-16 10:03:42 -06:00 committed by GitHub
commit 8eba48ebc6
12 changed files with 355 additions and 5 deletions

View File

@ -7,6 +7,12 @@ Metrics/ClassLength:
Metrics/MethodLength:
Enabled: false
Metrics/BlockLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Rails:
Enabled: false

View File

@ -6,7 +6,6 @@ task default: %i(rubocop spec gen_builds_list)
RuboCop::RakeTask.new(:rubocop) do |task|
task.patterns = ['lib/**/*.rb', 'spec/**/*.rb', 'bin/*']
task.formatters = ['files']
end
RSpec::Core::RakeTask.new(:spec) do |task|

View File

@ -0,0 +1,36 @@
require 'one_gadget/emulators/processor'
require 'one_gadget/emulators/instruction'
module OneGadget
module Emulators
# Emulator of amd64 instruction set.
class Amd64 < Processor
REGISTERS = %w(rax rbx rcx rdx rdi rsi rbp rsp rip) + 7.upto(15).map { |i| "r#{i}" }
def initialize
super(REGISTERS)
end
def process(cmd)
inst, args = parse(cmd)
# where should this be defined..?
return if inst.inst == 'call' # later
case inst.inst
when 'mov', 'lea'
tar = args[0]
src = OneGadget::Emulators::Lambda.parse(args[1], predefined: @registers)
end
src.ref! if inst.inst == 'lea'
@registers[tar] = src
end
def instructions
[
Instruction.new('mov', 2),
Instruction.new('lea', 2),
Instruction.new('call', 1)
]
end
end
end
end

View File

@ -0,0 +1,27 @@
module OneGadget
module Emulators
# Define instruction name and it's argument count.
class Instruction
attr_reader :inst # @return [String]
attr_reader :argc # @return [Integer]
def initialize(inst, argc)
@inst = inst
@argc = argc
end
def fetch_args(cmd)
idx = cmd.index(inst)
cmd = cmd[0...cmd.rindex('#')] if cmd.rindex('#')
args = cmd[idx + inst.size..-1].split(',')
raise ArgumentError, "Incorrect argument number in #{cmd}, expect: #{argc}" if args.size != argc
args.map do |arg|
arg.gsub(/QWORD|DWORD|WORD|BYTE|PTR/, '').strip
end
end
def match?(str)
str.include?(inst)
end
end
end
end

View File

@ -0,0 +1,89 @@
require 'one_gadget/helper'
module OneGadget
module Emulators
# A {Lambda} object can be:
# 1. {String} # variable name
# 2. {Numeric}
# 3. {Lambda} + {Numeric}
# 4. dereference {Lambda}
class Lambda
attr_accessor :obj # @return [String, Lambda]
attr_accessor :immi # @return [Integer]
attr_accessor :deref_count # @return [Integer] The times of dereference.
def initialize(val)
@immi = 0
@obj = val
@deref_count = 0
end
def +(other)
raise ArgumentError, 'Expect other to be Numeric.' unless other.is_a?(Numeric)
if deref_count > 0
ret = Lambda.new(self)
else
ret = Lambda.new(obj)
ret.immi = immi
end
ret.immi += other
ret
end
def -(other)
self.+(-other)
end
def deref!
@deref_count += 1
end
def ref!
raise ArgumentError, 'Cannot reference anymore!' if @deref_count <= 0
@deref_count -= 1
end
def deref
ret = Lambda.new(obj)
ret.immi = immi
ret.deref_count = deref_count + 1
ret
end
def to_s
str = ''
str += '[' * deref_count
str += obj.to_s unless obj.nil?
str += OneGadget::Helper.hex(immi, psign: true) unless immi.zero?
str += ']' * deref_count
str
end
class << self
# Target: parse something like +[rsp+0x50]+ into a {Lambda} object.
# @param [String] arg
# @param [Hash{String => Lambda}] predefined
# @return [OneGadget::Emulators::Lambda, Integer]
# If +arg+ contains number only, return it.
# Otherwise, return a {Lambda} object.
# @example
# parse('[rsp+0x50]') #=> #<Lambda @obj='rsp', @immi=80, @deref_count=1>
def parse(arg, predefined: {})
ret = Lambda.new('tmp')
if arg[0] == '[' # a little hack because there should nerver something like +[[rsp+1]+2]+ to parse.
arg = arg[1..-2]
ret.deref_count += 1
end
return Integer(arg) if OneGadget::Helper.integer?(arg)
sign = arg =~ /[+-]/
raise ArgumentError, "Not support #{arg}" if sign && !OneGadget::Helper.integer?(arg[sign..-1])
if sign
ret.immi = Integer(arg[sign..-1])
arg = arg[0, sign]
end
ret.obj = predefined[arg] || arg
ret
end
end
end
end
end

View File

@ -0,0 +1,28 @@
require 'one_gadget/emulators/lambda'
module OneGadget
# Instruction emulator to solve the constraint of gadgets.
module Emulators
# Base class of a processor.
class Processor
attr_reader :registers
attr_accessor :stack
def initialize(registers)
@registers = registers.map { |reg| [reg, OneGadget::Emulators::Lambda.new(reg)] }.to_h
@stack = []
end
def parse(cmd)
inst = instructions.find { |i| i.match?(cmd) }
raise ArgumentError, "Not implemented instruction in #{cmd}" if inst.nil?
[inst, inst.fetch_args(cmd)]
end
def process(_cmd); raise NotImplementedError
end
def instructions; raise NotImplementedError
end
end
end
end

View File

@ -1,4 +1,5 @@
require 'one_gadget/fetchers/base'
require 'one_gadget/emulators/amd64'
module OneGadget
module Fetcher
# Fetcher for amd64.
@ -13,10 +14,32 @@ module OneGadget
true
end
cands.map do |candidate|
convert_to_gadget(candidate) do |line|
['rsi'].any? { |r| line.include?(r) }
end
end
processor = OneGadget::Emulators::Amd64.new
candidate.lines.each { |l| processor.process(l) }
offset = offset_of(candidate)
constraints = gen_constraints(processor)
next nil if constraints.nil? # impossible be a gadget
OneGadget::Gadget::Gadget.new(offset, constraints: constraints)
end.compact
end
private
def gen_constraints(processor)
# check rdi should always related to rip
return unless processor.registers['rdi'].to_s.include?('rip')
# rsi or [rsi] should be zero
[
should_null(processor.registers['rsi'].to_s),
should_null(processor.registers['rdx'].to_s, allow_global: true)
].compact
end
def should_null(str, allow_global: false)
return nil if allow_global && str.include?('rip')
ret = "[#{str}] == NULL"
ret += " || #{str} == NULL" unless str.include?('rsp')
ret
end
end
end

View File

@ -54,6 +54,11 @@ module OneGadget
match.split.first.to_i(16)
end
def offset_of(assembly)
lines = assembly.lines
lines.first.scan(/^([\da-f]+):/)[0][0].to_i(16)
end
def convert_to_gadget(assembly, &block)
lines = assembly.lines
offset = lines.first.scan(/^([\da-f]+):/)[0][0].to_i(16)

View File

@ -137,6 +137,37 @@ module OneGadget
return :i386 if str.include?('Intel 80386')
:unknown
end
# Present number in hex format.
# @param [Integer] val The number.
# @param [Boolean] psign Need to show plus sign when +val >= 0+.
# @return [String] string in hex format.
# @example
# hex(32) #=> 0x20
# hex(32, psign: true) #=> +0x20
# hex(-40) #=> -0x28
# hex(0) #=> 0x0
# hex(0, psign: true) #=> +0x0
def hex(val, psign: false)
return format("#{psign ? '+' : ''}0x%x", val) if val >= 0
format('-0x%x', -val)
end
# For checking a string is actually an integer.
# @param [String] str String to be checked.
# @return [Boolean] If +str+ can be converted into integer.
# @example
# Helper.integer? '1234'
# # => true
# Helper.integer? '0x1234'
# # => true
# Helper.integer? '0xheapoverflow'
# # => false
def integer?(str)
true if Integer(str)
rescue ArgumentError, TypeError
false
end
end
extend ClassMethods
end

View File

@ -0,0 +1,35 @@
require 'one_gadget/emulators/amd64'
describe OneGadget::Emulators::Amd64 do
before(:each) do
@processor = OneGadget::Emulators::Amd64.new
end
describe 'process' do
it 'libc-2.24 gadget' do
gadget = <<-EOS
d67e5: 48 8b 05 b4 16 2c 00 mov rax,QWORD PTR [rip+0x2c16b4] # 397ea0 <_DYNAMIC+0x340>
d67ec: 48 8d 74 24 70 lea rsi,[rsp+0x70]
d67f1: 48 8d 3d 61 ab 08 00 lea rdi,[rip+0x8ab61] # 161359 <_nl_POSIX_name+0x154>
d67f8: 48 8b 10 mov rdx,QWORD PTR [rax]
d67fb: e8 70 1c fe ff call b8470 <execve>
EOS
gadget.lines.each { |s| @processor.process(s) }
expect(@processor.registers['rsi'].to_s).to eq 'rsp+0x70'
expect(@processor.registers['rdx'].to_s).to eq '[[rip+0x2c16b4]]'
end
it 'mov' do
gadget = <<-EOS
mov rax, rdx
mov rdx, [rax+0x10]
mov rdi, [rdx]
mov rdx, rax-0x30
EOS
gadget.lines.each { |s| @processor.process(s) }
expect(@processor.registers['rdi'].to_s).to eq '[[rdx+0x10]]'
expect(@processor.registers['rdx'].to_s).to eq 'rdx-0x30'
expect(@processor.registers['rax'].to_s).to eq 'rdx'
end
end
end

View File

@ -0,0 +1,26 @@
require 'one_gadget/emulators/instruction'
describe OneGadget::Emulators::Instruction do
before(:all) do
@mov = OneGadget::Emulators::Instruction.new('mov', 2)
@lea = OneGadget::Emulators::Instruction.new('lea', 2)
@call = OneGadget::Emulators::Instruction.new('call', 1)
end
it 'fetch_args' do
expect(@mov.fetch_args(<<-'EOS')).to eq ['rax', '[rip+0x2c16b4]']
d67e5: 48 8b 05 b4 16 2c 00 mov rax,QWORD PTR [rip+0x2c16b4] # 397ea0 <_DYNAMIC+0x340>
EOS
expect(@mov.fetch_args(<<-'EOS')).to eq ['rdx', '[rax]']
d67f8: 48 8b 10 mov rdx,QWORD PTR [rax]
EOS
expect(@lea.fetch_args(<<-'EOS')).to eq ['rsi', '[rsp+0x70]']
d67ec: 48 8d 74 24 70 lea rsi,[rsp+0x70]
EOS
expect(@lea.fetch_args(<<-'EOS')).to eq ['rdi', '[rip+0x8ab61]']
d67f1: 48 8d 3d 61 ab 08 00 lea rdi,[rip+0x8ab61] # 161359 <_nl_POSIX_name+0x154>
EOS
expect(@call.fetch_args(<<-'EOS')).to eq ['b8470 <execve>']
d67fb: e8 70 1c fe ff call b8470 <execve>
EOS
end
end

View File

@ -0,0 +1,45 @@
require 'one_gadget/emulators/lambda'
describe OneGadget::Emulators::Lambda do
before(:each) do
@rsp = OneGadget::Emulators::Lambda.new('rsp')
end
describe '+' do
it 'normal' do
expect((@rsp + 0x50).to_s).to eq 'rsp+0x50'
expect((@rsp - 0x50).to_s).to eq 'rsp-0x50'
end
it 'mixed' do
rax = OneGadget::Emulators::Lambda.new('rax')
rax += 0x50
expect(rax.to_s).to eq 'rax+0x50'
rax += 0x50
expect(rax.to_s).to eq 'rax+0xa0'
rax -= 0xa0
expect(rax.to_s).to eq 'rax'
end
end
it 'dereference' do
drsp = @rsp.deref
expect(@rsp.to_s).to eq 'rsp'
expect(drsp.to_s).to eq '[rsp]'
expect(drsp.deref.to_s).to eq '[[rsp]]'
drsp.deref!
expect(drsp.to_s).to eq '[[rsp]]'
end
it 'mixed' do
expect(((@rsp + 0x50).deref - 0x30).deref.to_s).to eq '[[rsp+0x50]-0x30]'
end
it 'parse' do
expect(OneGadget::Emulators::Lambda.parse('[rsp+0x50]').to_s).to eq '[rsp+0x50]'
expect(OneGadget::Emulators::Lambda.parse('[rsp+80]').to_s).to eq '[rsp+0x50]'
expect(OneGadget::Emulators::Lambda.parse('esp').to_s).to eq 'esp'
expect(OneGadget::Emulators::Lambda.parse('esp-10').to_s).to eq 'esp-0xa'
expect(OneGadget::Emulators::Lambda.parse('123')).to be 123
expect(OneGadget::Emulators::Lambda.parse('0xabc123')).to be 0xabc123
end
end