Consider posix_spawn as one-gadgets (#183)

* emulator: implement Processor#arg_to_lambda

* refine descriptions

* emulator: x86: enhance movq instruction

* emulator: x86: add punpcklqdq support

* enhance nested Lambda.parse

* emulators: support more amd64 arguments

* emulators: display XMM's lambda in a programmic way

* support posix_spawn

* Add spec for libc-2.31
This commit is contained in:
david942j 2022-03-20 21:59:23 +08:00 committed by GitHub
parent 8cc0b042f1
commit f3d63ab670
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 173 additions and 75 deletions

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module OneGadget
# Defines the abi of different architectures.
# Defines the ABI of different architectures.
module ABI
# Registers of i386.
X86_32 = %w[eax ebx ecx edx edi esi ebp esp] + 0.upto(7).map { |i| "xmm#{i}" }
@ -19,7 +19,7 @@ module OneGadget
# Registers' name of amd64.
# @return [Array<String>] List of registers.
def amd64
X86_64.uniq
X86_64
end
# Registers' name of i386.

View File

@ -49,8 +49,8 @@ module OneGadget
def inst_add(dst, src, op2, mode = 'sxtw')
check_register!(dst)
src = OneGadget::Emulators::Lambda.parse(src, predefined: registers)
op2 = OneGadget::Emulators::Lambda.parse(op2, predefined: registers)
src = arg_to_lambda(src)
op2 = arg_to_lambda(op2)
raise_unsupported('add', dst, src, op2) unless op2.is_a?(Integer) && mode == 'sxtw'
registers[dst] = src + op2
@ -84,7 +84,7 @@ module OneGadget
def inst_ldr(dst, src, index = 0)
check_register!(dst)
src_l = OneGadget::Emulators::Lambda.parse(src, predefined: registers)
src_l = arg_to_lambda(src)
registers[dst] = src_l
raise_unsupported('ldr', dst, src, index) unless OneGadget::Helper.integer?(index)
@ -101,28 +101,27 @@ module OneGadget
def inst_mov(dst, src)
check_register!(dst)
src = OneGadget::Emulators::Lambda.parse(src, predefined: registers)
registers[dst] = src
registers[dst] = arg_to_lambda(src)
end
def inst_stp(reg1, reg2, dst)
raise_unsupported('stp', reg1, reg2, dst) unless reg64?(reg1) && reg64?(reg2)
dst_l = OneGadget::Emulators::Lambda.parse(dst, predefined: registers).ref!
dst_l = arg_to_lambda(dst).ref!
raise_unsupported('stp', reg1, reg2, dst) unless dst_l.obj == sp && dst_l.deref_count.zero?
cur_top = dst_l.evaluate(eval_dict)
stack[cur_top] = registers[reg1]
stack[cur_top + size_t] = registers[reg2]
registers[sp] += OneGadget::Emulators::Lambda.parse(dst).immi if dst.end_with?('!')
registers[sp] += arg_to_lambda(dst).immi if dst.end_with?('!')
end
def inst_str(src, dst, index = 0)
check_register!(src)
raise_unsupported('str', src, dst, index) unless OneGadget::Helper.integer?(index)
dst_l = OneGadget::Emulators::Lambda.parse(dst, predefined: registers).ref!
dst_l = arg_to_lambda(dst).ref!
# Only stores on stack.
if dst_l.obj == sp && dst_l.deref_count.zero?
cur_top = dst_l.evaluate(eval_dict)

View File

@ -27,6 +27,9 @@ module OneGadget
when 0 then registers['rdi']
when 1 then registers['rsi']
when 2 then registers['rdx']
when 3 then registers['rcx']
when 4 then registers['r8']
when 5 then registers['r9']
end
end
end

View File

@ -46,13 +46,13 @@ module OneGadget
self.+(-other)
end
# Increase dereference count with 1.
# Increase dereference count by 1.
# @return [void]
def deref!
@deref_count += 1
end
# Decrease dereference count with 1.
# Decrease dereference count by 1.
# @return [self]
# @raise [Error::InstrutionArgumentError] When this object cannot be referenced anymore.
def ref!
@ -105,7 +105,7 @@ module OneGadget
# @param [Hash{String => Lambda}] predefined
# Predefined values.
# @return [OneGadget::Emulators::Lambda, Integer]
# If +argument+ contains number only, returns the value.
# If +argument+ contains numbers only, returns the value.
# Otherwise, returns a {Lambda} object.
# @example
# obj = Lambda.parse('[rsp+0x50]')
@ -117,9 +117,17 @@ module OneGadget
# #=> #<Lambda @obj='x0', @immi=-104, @deref_count=1>
def parse(argument, predefined: {})
arg = argument.dup
return 0 if arg.empty? || arg == '!'
return Integer(arg) if OneGadget::Helper.integer?(arg)
# nested []
return parse(arg[1...arg.rindex(']')], predefined: predefined).deref if arg[0] == '['
if arg[0] == '['
ridx = arg.rindex(']')
immi = parse(arg[(ridx + 1)..-1])
lm = parse(arg[1...ridx], predefined: predefined).deref
lm += immi unless immi.zero?
return lm
end
base, disp = mem_obj(arg)
obj = predefined[base] || Lambda.new(base)

View File

@ -47,7 +47,7 @@ module OneGadget
# @return [Boolean]
def process(cmd)
process!(cmd)
# rescue OneGadget::Error::UnsupportedError # for debugging
# rescue OneGadget::Error::UnsupportedError => e; p e # for debugging
rescue OneGadget::Error::Error
false
end
@ -115,6 +115,14 @@ module OneGadget
OneGadget::Emulators::Lambda.new(reg)
end
# Fetch the corresponding lambda value of instruction arguments from the current register sets.
#
# @param [String] arg The instruction argument passed to inst_* functions.
# @return [Lambda]
def arg_to_lambda(arg)
OneGadget::Emulators::Lambda.parse(arg, predefined: registers)
end
def raise_unsupported(inst, *args)
raise OneGadget::Error::UnsupportedInstructionArgumentError, "#{inst} #{args.join(', ')}"
end

View File

@ -45,14 +45,15 @@ module OneGadget
Instruction.new('xor', 2),
Instruction.new('movq', 2),
Instruction.new('movaps', 2),
Instruction.new('movhps', 2)
Instruction.new('movhps', 2),
Instruction.new('punpcklqdq', 2)
]
end
private
def inst_mov(dst, src)
src = OneGadget::Emulators::Lambda.parse(src, predefined: registers)
src = arg_to_lambda(src)
if register?(dst)
registers[dst] = src
else
@ -60,7 +61,7 @@ module OneGadget
# TODO(david942j): #120
return add_writable(dst) unless dst.include?(sp)
dst = OneGadget::Emulators::Lambda.parse(dst, predefined: registers)
dst = arg_to_lambda(dst)
return if dst.deref_count != 1 # should not happen
dst.ref!
@ -79,9 +80,17 @@ module OneGadget
end
end
# Move *src to dst[:64]
# Move src to dst[:64]
# Supported forms:
# movq xmm*, [sp+*]
# movq xmm*, reg64
def inst_movq(dst, src)
# XXX: here we only support `movq xmm*, [sp+*]`
if self.class.bits == 64 && xmm_reg?(dst) && src.start_with?('r') && register?(src)
dst = arg_to_lambda(dst)
src = arg_to_lambda(src)
dst[0] = src
return
end
dst, src = check_xmm_sp(dst, src) { raise_unsupported('movq', dst, src) }
off = src.evaluate(eval_dict)
(64 / self.class.bits).times do |i|
@ -89,7 +98,7 @@ module OneGadget
end
end
# Move *src to dst[64:128]
# Move src to dst[64:128]
def inst_movhps(dst, src)
# XXX: here we only support `movhps xmm*, [sp+*]`
dst, src = check_xmm_sp(dst, src) { raise_unsupported('movhps', dst, src) }
@ -99,28 +108,41 @@ module OneGadget
end
end
# check if (dst, src) in form (xmm*, [sp+*])
# check whether (dst, src) is in form (xmm*, [sp+*])
def check_xmm_sp(dst, src)
return yield unless dst.start_with?('xmm') && register?(dst) && src.include?(sp)
return yield unless xmm_reg?(dst) && src.include?(sp)
dst_lm = OneGadget::Emulators::Lambda.parse(dst, predefined: registers)
src_lm = OneGadget::Emulators::Lambda.parse(src, predefined: registers)
dst_lm = arg_to_lambda(dst)
src_lm = arg_to_lambda(src)
return yield if src_lm.deref_count != 1
src_lm.ref!
[dst_lm, src_lm]
end
def xmm_reg?(reg)
reg.start_with?('xmm') && register?(reg)
end
# dst[64:128] = src[0:64]
def inst_punpcklqdq(dst, src)
raise_unsupported('punpcklqdq', dst, src) unless xmm_reg?(dst) && xmm_reg?(src)
dst = arg_to_lambda(dst)
src = arg_to_lambda(src)
(64 / self.class.bits).times do |i|
dst[i + 64 / self.class.bits] = src[i]
end
end
def inst_lea(dst, src)
check_register!(dst)
src = OneGadget::Emulators::Lambda.parse(src, predefined: registers)
src.ref!
registers[dst] = src
registers[dst] = arg_to_lambda(src).ref!
end
def inst_push(val)
val = OneGadget::Emulators::Lambda.parse(val, predefined: registers)
val = arg_to_lambda(val)
registers[sp] -= size_t
cur_top = registers[sp].evaluate(eval_dict)
raise Error::InstructionArgumentError, "Corrupted stack pointer: #{cur_top}" unless cur_top.is_a?(Integer)
@ -141,12 +163,12 @@ module OneGadget
def inst_add(dst, src)
check_register!(dst)
src = OneGadget::Emulators::Lambda.parse(src, predefined: registers)
src = arg_to_lambda(src)
registers[dst] += src
end
def inst_sub(dst, src)
src = OneGadget::Emulators::Lambda.parse(src, predefined: registers)
src = arg_to_lambda(src)
raise Error::UnsupportedInstructionArgumentError, "Unhandled -= of type #{src.class}" unless src.is_a?(Integer)
registers[dst] -= src
@ -160,7 +182,7 @@ module OneGadget
# because it just invokes syscall.
def inst_call(addr)
# This is the last call
return registers[pc] = addr if %w[execve execl].any? { |n| addr.include?(n) }
return registers[pc] = addr if %w[execve execl posix_spawn].any? { |n| addr.include?(n) }
# TODO: handle some registers would be fucked after call
checker = {
@ -177,7 +199,7 @@ module OneGadget
end
def add_writable(dst)
lmda = OneGadget::Emulators::Lambda.parse(dst, predefined: registers).ref!
lmda = arg_to_lambda(dst).ref!
# pc-relative addresses should be writable
return if lmda.obj == pc
@ -188,7 +210,8 @@ module OneGadget
return super unless reg =~ /^xmm\d+$/
Array.new(128 / self.class.bits) do |i|
OneGadget::Emulators::Lambda.new("#{reg}__#{i}")
cast = "(u#{self.class.bits})"
OneGadget::Emulators::Lambda.new(i.zero? ? "#{cast}#{reg}" : "#{cast}(#{reg} >> #{self.class.bits * i})")
end
end
end

View File

@ -16,6 +16,7 @@ module OneGadget
def candidates
# one basic block case
cands = super do |candidate|
next true if candidate.include?('posix_spawn@')
next false unless candidate.include?(bin_sh_hex) # works in x86-64
next false unless candidate.lines.last.include?('execve') # only care execve

View File

@ -7,12 +7,12 @@ module OneGadget
module Fetcher
# Define common methods for gadget fetchers.
class Base
# The absolute path of glibc.
# The absolute path to glibc.
# @return [String] The filename.
attr_reader :file
# Instantiate a fetcher object.
# @param [String] file Absolute path of target libc.
# @param [String] file Absolute path to the target libc.
def initialize(file)
@file = file
arch = self.class.name.split('::').last.downcase.to_sym
@ -30,7 +30,7 @@ module OneGadget
(lines.size - 2).downto(0) do |i|
processor = emulate(lines[i..-1])
options = resolve(processor)
next if options.nil? # impossible be a gadget
next if options.nil? # impossible to be a gadget
offset = offset_of(lines[i])
gadgets << OneGadget::Gadget::Gadget.new(offset, **options)
@ -41,7 +41,7 @@ module OneGadget
# Fetch candidates that end with call exec*.
#
# Give a block to filter gadget candidates.
# Provide a block to filter gadget candidates.
# @yieldparam [String] cand
# Is this candidate valid?
# @yieldreturn [Boolean]
@ -49,7 +49,7 @@ module OneGadget
# @return [Array<String>]
# Each +String+ returned is multi-lines of assembly code.
def candidates(&block)
call_regexp = "#{call_str}.*<exec[^+]*>$"
call_regexp = "#{call_str}.*<(exec[^+]*|posix_spawn[^+]*)>$"
cands = []
`#{@objdump.command}|egrep '#{call_regexp}' -B 30`.split('--').each do |cand|
lines = cand.lines.map(&:strip).reject(&:empty?)
@ -69,7 +69,7 @@ module OneGadget
private
# Generating constraints to be a valid gadget.
# Generating constraints for being a valid gadget.
# @param [OneGadget::Emulators::Processor] processor The processor after executing the gadgets.
# @return [Hash{Symbol => Array<String>, String}?]
# The options to create a {OneGadget::Gadget::Gadget} object.
@ -79,28 +79,28 @@ module OneGadget
# If the constraints can never be satisfied, +nil+ is returned.
def resolve(processor)
call = processor.registers[processor.pc].to_s
# This costs cheaper, so check first.
# check call execve / execl
return unless %w[execve execl].any? { |n| call.include?(n) }
# check first argument contains /bin/sh
# since the logic is different between amd64 and i386,
# invoke str_bin_sh? for checking
return unless str_bin_sh?(processor.argument(0).to_s)
if call.include?('execve')
resolve_execve(processor)
elsif call.include?('execl')
resolve_execl(processor)
end
return resolve_posix_spawn(processor) if call.include?('posix_spawn')
return resolve_execve(processor) if call.include?('execve')
return resolve_execl(processor) if call.include?('execl')
end
def resolve_execve(processor)
# arg[1] == NULL || [arg[1]] == NULL
# arg[2] == NULL || [arg[2]] == NULL || arg[2] == envp
arg0 = processor.argument(0).to_s
arg1 = processor.argument(1).to_s
arg2 = processor.argument(2).to_s
res = resolve_execve_args(processor, arg0, arg1, arg2)
return nil if res.nil?
{ constraints: res[:constraints], effect: %(execve("/bin/sh", #{arg1}, #{res[:envp]})) }
end
def resolve_execve_args(processor, arg0, arg1, arg2, allow_null_argv: true)
return unless str_bin_sh?(arg0)
# arg1 == NULL || [arg1] == NULL
# arg2 == NULL || [arg2] == NULL || arg[2] == envp
cons = processor.constraints
cons << check_execve_arg(processor, arg1)
cons << check_execve_arg(processor, arg1, allow_null_argv)
return nil unless cons.all?
envp = 'environ'
@ -109,38 +109,42 @@ module OneGadget
envp = arg2
end
{ constraints: cons, effect: %(execve("/bin/sh", #{arg1}, #{envp})) }
{ constraints: cons, envp: envp }
end
# arg[1] == NULL || [arg[1]] == NULL
def check_execve_arg(processor, arg)
# arg == NULL || [arg] == NULL
def check_execve_arg(processor, arg, allow_null)
if arg.start_with?(processor.sp) # arg = sp+<num>
# in this case, the only constraint is [sp+<num>] == NULL
# in this case, the only chance is [sp+<num>] == NULL
num = Integer(arg[processor.sp.size..-1])
slot = processor.stack[num].to_s
return if global_var?(slot)
"#{slot} == NULL"
else
elsif allow_null
"[#{arg}] == NULL || #{arg} == NULL"
else
"[#{arg}] == NULL"
end
end
def check_envp(processor, arg)
# if str starts with [[ and is global var,
# believe it is environ
# if starts with [[ but not global, drop it.
# If str starts with [[ and is a global variable,
# believe it is environ.
# If it starts with [[ but not a global var, drop it.
return global_var?(arg) if arg.start_with?('[[')
# normal
cons = check_execve_arg(processor, arg)
cons = check_execve_arg(processor, arg, true)
return nil if cons.nil?
yield cons
end
# Resolve +call execl+ case.
# Resolve +call execl+ cases.
def resolve_execl(processor)
return unless str_bin_sh?(processor.argument(0).to_s)
args = []
arg = processor.argument(1).to_s
if str_sh?(arg)
@ -151,10 +155,37 @@ module OneGadget
args << arg
cons = processor.constraints + ["#{arg} == NULL"]
# now arg is the constraint.
{ constraints: cons, effect: %(execl("/bin/sh", #{args.join(', ')})) }
end
# posix_spawn (*pid, *path, *file_actions, *attrp, argv[], envp[])
# Constraints are
# * pid == NULL || *pid is writable
# * file_actions == NULL || (int) (file_actions->__used) <= 0
# * attrp == NULL || attrp->flags == 0
# Meet all constraints then posix_spawn eventually calls execve(path, argv, envp)
def resolve_posix_spawn(processor)
args = Array.new(6) { |i| processor.argument(i) }
res = resolve_execve_args(processor, args[1].to_s, args[4].to_s, args[5].to_s, allow_null_argv: false)
return nil if res.nil?
cons = res[:constraints]
arg0 = args[0]
if arg0.to_s != '0'
if arg0.deref_count.zero? && arg0.to_s.include?(processor.sp)
# Assume stack is always writable, no additional constraints.
else
cons << "#{arg0} == NULL || writable: #{arg0}"
end
end
arg2 = args[2]
cons << "#{arg2} == NULL || (s32)#{(arg2 + 4).deref} <= 0" if arg2.to_s != '0'
arg3 = args[3]
cons << "#{arg3} == NULL || (u16)#{arg3.deref} == NULL" if arg3.to_s != '0'
{ constraints: cons, effect: %(posix_spawn(#{arg0}, "/bin/sh", #{arg2}, #{arg3}, #{args[4]}, #{res[:envp]})) }
end
def global_var?(_str); raise NotImplementedError
end

View File

@ -62,12 +62,15 @@ module OneGadget
# REG: OneGadget::ABI.all
# IMM: [+-]0x[\da-f]+
# BITS: 8, 16, 32, 64
# CAST: (<s|u><BITS>)
# Identity: <REG><IMM>?
# Identity: [<Identity>]
# Expr: <REG> is the GOT address of libc
# Expr: writable: <Identity>
# Expr: <Identity> == NULL
# Expr: <CAST>?<Identity> == NULL
# Expr: <REG> & 0xf == <IMM>
# Expr: (s32)[<Identity>] <= 0
# Expr: <Expr> || <Expr>
def calculate_score(expr)
return expr.split(' || ').map(&method(:calculate_score)).max if expr.include?(' || ')
@ -76,15 +79,16 @@ module OneGadget
when / & 0xf/ then 0.95
when /GOT address/ then 0.9
when /^writable/ then 0.81
when / == NULL$/ then calculate_null_score(expr)
when / == NULL$/ then calculate_null_score(expr.slice(0...expr.rindex(' == NULL')))
when / <= 0$/ then calculate_null_score(expr.slice(0...expr.rindex(' <= ')))
end
end
def calculate_null_score(expr)
identity = expr.slice(0...expr.rindex(' == NULL'))
def calculate_null_score(identity)
# remove <CAST>
identity.sub!(/^\([s|u]\d+\)/, '')
# Thank God we are already able to parse this
lmda = OneGadget::Emulators::Lambda.parse(identity)
# raise Error::ArgumentError, expr unless OneGadget::ABI.all.include?(lmda.obj)
# rax == 0 is easy; rax + 0x10 == 0 is damn hard.
return lmda.immi.zero? ? 0.9 : 0.1 if lmda.deref_count.zero?

View File

@ -10,7 +10,6 @@ require 'one_gadget/version'
Gem::Specification.new do |s|
s.name = 'one_gadget'
s.version = ::OneGadget::VERSION
s.date = Date.today.to_s
s.summary = 'one_gadget'
s.description = <<-EOS
When playing ctf pwn challenges we usually needs the one-gadget of execve('/bin/sh', NULL, NULL).

View File

@ -47,6 +47,18 @@ describe OneGadget::Emulators::Amd64 do
expect(@processor.stack[0x48].to_s).to eq 'rax'
end
it 'punpcklqdq' do
gadget = <<-EOS
movq xmm1,rax
movq xmm0,rcx
punpcklqdq xmm0,xmm1
movaps XMMWORD PTR [rsp+0x50],xmm0
EOS
gadget.each_line { |s| @processor.process(s) }
expect(@processor.stack[0x50].to_s).to eq 'rcx'
expect(@processor.stack[0x58].to_s).to eq 'rax'
end
it 'unsupported form' do
expect { @processor.process!('movaps xmm0, [rsp+0x40]') }
.to raise_error(OneGadget::Error::UnsupportedInstructionArgumentError)

View File

@ -38,10 +38,10 @@ describe OneGadget::Emulators::Lambda do
it 'parse' do
expect(described_class.parse('[rsp+0x50]').to_s).to eq '[rsp+0x50]'
# ARM form
# ARM forms
expect(described_class.parse('[x0, 1160]').to_s).to eq '[x0+0x488]'
expect(described_class.parse('[x22, -104]').to_s).to eq '[x22-0x68]'
# test if OK with bang
# test argument with bang
expect(described_class.parse('[x2, -8]!').to_s).to eq '[x2-0x8]'
expect(described_class.parse('[rsp+80]').to_s).to eq '[rsp+0x50]'
expect(described_class.parse('esp').to_s).to eq 'esp'
@ -54,6 +54,7 @@ describe OneGadget::Emulators::Lambda do
# Nested []
expect(described_class.parse('[[rsp+0x33]]').to_s).to eq '[[rsp+0x33]]'
expect(described_class.parse('[[rdx+0x33]+0x4154]').to_s).to eq '[[rdx+0x33]+0x4154]'
end
it 'evaluate' do

View File

@ -33,6 +33,15 @@ describe 'one_gadget_amd64' do
expect(one_gadget(path)).to eq OneGadget.gadgets(file: path)
end
it 'libc-2.31' do
path = data_path('libc-2.31-9fdb74e7b217d06c93172a8243f8547f947ee6d1.so')
expect(OneGadget.gadgets(file: path, force_file: true,
level: 1)).to eq [0x51e39, 0x51e45, 0x51e5a, 0x51e62, 0x84173, 0x84180, 0x8418c,
0x84199, 0xe3b2e, 0xe3b31, 0xe3b34, 0xe3d23, 0xe3d26, 0xe3d99,
0xe3da0, 0xe3de5, 0xe3ded, 0x1075da, 0x1075e2, 0x1075e7, 0x1075f1]
expect(OneGadget.gadgets(file: path)).to eq [0xe3b2e, 0xe3b31, 0xe3b34]
end
it 'not ELF' do
expect { hook_logger { OneGadget.gadgets(file: __FILE__) } }.to output(<<-EOS).to_stdout
[OneGadget] ArgumentError: Not an ELF file, expected glibc as input