EnvFuzz 实战 evince

EnvFuzz 实战 evince

henry Lv4

EnvFuzz 实战 evince

如果对 envfuzz 不了解,可以看我前面的分析文章

本来想着捡捡漏,看看能不能出点比较有价值的洞,结果令人大失所望,再原论文中他们所 fuzz 出来的漏洞大多是在配置文件上出现了类似的解析错误,从而导致程序会出现一些异常。我自己的复现的过程中爆出的洞也大多是这类错误,我这里使用 envfuzz 来将 evince 作为测试对象,来简单复现一下一个关于 evince 配置文件的 crash,修改这个配置文件中的一个字节可以导致空指针。

SIGSEGV_fdbe_m00854.patch

_nl_find_msg

该函数位于 glibc 中

patch脚本:

1
2
3
4
5
cp /usr/share/locale-langpack/en/LC_MESSAGES/gtk30.mo /usr/share/locale-langpack/en/LC_MESSAGES/gtk30.mo.bak

sudo cp gtk30.mo /usr/share/locale-langpack/en/LC_MESSAGES/

cp /usr/share/locale-langpack/en/LC_MESSAGES/gtk30.mo.bak /usr/share/locale-langpack/en/LC_MESSAGES/gtk30.mo

调试脚本:

1
2
3
4
import gdb

gdb.execute("b gtk_get_option_group")
gdb.execute("b *0x7d1ba283c5cc") #<_nl_load_domain+1212>: call 0x7d1ba283abb0 <_nl_find_msg>

out/crash/SIGSEGV_fdbe_m00854.patch: Segmentation fault

backtrace

1
2
3
4
5
6
7
8
► 0   0x7ffff6a3ad31 _nl_find_msg+385
1 0x7ffff6a3c5d1 _nl_load_domain+1217
2 0x7ffff6a3c0e5 _nl_find_domain+597
3 0x7ffff6a3b845 __dcigettext+773
4 0x7ffff7648a45 gtk_get_option_group+53
5 0x55555557e3cd main+157
6 0x7ffff6a29d90 __libc_start_call_main+128
7 0x7ffff6a29e40 __libc_start_main+128

数据结构

mo_file_header

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
/* Header for binary .mo file format.  */
struct mo_file_header
{
/* The magic number. */
nls_uint32 magic;
/* The revision number of the file format. */
nls_uint32 revision;

/* The following are only used in .mo files with major revision 0 or 1. */

/* The number of strings pairs. */
nls_uint32 nstrings;
/* Offset of table with start offsets of original strings. */
nls_uint32 orig_tab_offset;
/* Offset of table with start offsets of translated strings. */
nls_uint32 trans_tab_offset;
/* Size of hash table. */
nls_uint32 hash_tab_size;
/* Offset of first hash table entry. */
nls_uint32 hash_tab_offset;

/* The following are only used in .mo files with minor revision >= 1. */

/* The number of system dependent segments. */
nls_uint32 n_sysdep_segments;
/* Offset of table describing system dependent segments. */
nls_uint32 sysdep_segments_offset;
/* The number of system dependent strings pairs. */
nls_uint32 n_sysdep_strings;
/* Offset of table with start offsets of original sysdep strings. */
nls_uint32 orig_sysdep_tab_offset;
/* Offset of table with start offsets of translated sysdep strings. */
nls_uint32 trans_sysdep_tab_offset;
};

loaded_l10nfile

1
2
3
4
5
6
7
8
9
10
struct loaded_l10nfile
{
const char *filename;
int decided;

const void *data;

struct loaded_l10nfile *next;
struct loaded_l10nfile *successor[1];
};

loaded_domain

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
/* The representation of an opened message catalog.  */
struct loaded_domain
{
/* Pointer to memory containing the .mo file. */
0x0: const char *data;
/* 1 if the memory is mmap()ed, 0 if the memory is malloc()ed. */
0x8: int use_mmap;
/* Size of mmap()ed memory. */
0x10: size_t mmap_size;
/* 1 if the .mo file uses a different endianness than this machine. */
0x18: int must_swap;
/* Pointer to additional malloc()ed memory. */
0x20: void *malloced;

/* Number of static strings pairs. */
0x28: nls_uint32 nstrings;
/* Pointer to descriptors of original strings in the file. */
0x30: const struct string_desc *orig_tab;
/* Pointer to descriptors of translated strings in the file. */
0x38: const struct string_desc *trans_tab;

/* Number of system dependent strings pairs. */
0x40: nls_uint32 n_sysdep_strings;
/* Pointer to descriptors of original sysdep strings. */
0x48: const struct sysdep_string_desc *orig_sysdep_tab;
/* Pointer to descriptors of translated sysdep strings. */
0x50: const struct sysdep_string_desc *trans_sysdep_tab;

/* Size of hash table. */
0x58: nls_uint32 hash_size;
/* Pointer to hash table. */
0x60: const nls_uint32 *hash_tab;
/* 1 if the hash table uses a different endianness than this machine. */
0x68: int must_swap_hash_tab;

/* Cache of charset conversions of the translated strings. */
struct converted_domain *conversions;
size_t nconversions;
gl_rwlock_define (, conversions_lock)

const struct expression *plural;
unsigned long int nplurals;
};

sysdep_string_desc

1
2
3
4
5
6
7
8
/* In-memory representation of system dependent string.  */
struct sysdep_string_desc
{
/* Length of addressed string, including the trailing NUL. */
size_t length;
/* Pointer to addressed string. */
const char *pointer;
};

复现

漏洞代码(注释来自GPT)

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
/* 查找 `msgid` 在 `DOMAIN_FILE` 和 `DOMAINBINDING` 中的翻译。
如果找到翻译,返回它。
如果没有找到翻译,或者在转换过程中出现错误(例如特定消息目录的问题),则返回 `NULL`。
如果在转换过程中内存分配失败(仅在 `ENCODING` 不为空或 `CONVERT` 为真时发生),返回 `(char *) -1`。 */
char *
#ifdef IN_LIBGLOCALE
_nl_find_msg (struct loaded_l10nfile *domain_file,
struct binding *domainbinding, const char *encoding,
const char *msgid,
size_t *lengthp)
#else
_nl_find_msg (struct loaded_l10nfile *domain_file,
struct binding *domainbinding,
const char *msgid, int convert,
size_t *lengthp)
#endif
{
struct loaded_domain *domain;
nls_uint32 nstrings;
size_t act;
char *result;
size_t resultlen;

/* 如果 `domain_file` 尚未决定是否加载,调用 `_nl_load_domain` 来加载它。 */
if (domain_file->decided <= 0)
_nl_load_domain (domain_file, domainbinding);

/* 如果 `domain_file` 的数据为空,则返回 `NULL` 表示找不到翻译。 */
if (domain_file->data == NULL)
return NULL;

/* 将 `domain_file->data` 转换为 `loaded_domain` 结构,准备查找翻译。 */
domain = (struct loaded_domain *) domain_file->data;

nstrings = domain->nstrings;

/* 开始定位 `msgid` 及其翻译。 */
if (domain->hash_tab != NULL)
{
/* 如果存在哈希表,则使用哈希表来加速查找。 */
nls_uint32 len = strlen (msgid);
nls_uint32 hash_val = __hash_string (msgid);
nls_uint32 idx = hash_val % domain->hash_size;
nls_uint32 incr = 1 + (hash_val % (domain->hash_size - 2));

while (1)
{
/* 使用哈希表中的索引查找对应的字符串条目。 */
nls_uint32 nstr =
W (domain->must_swap_hash_tab, domain->hash_tab[idx]);

if (nstr == 0)
/* 如果哈希表条目为空,表示找不到该 `msgid`,返回 `NULL`。 */
return NULL;

nstr--;

/* 比较 `msgid` 与哈希表中索引的原始字符串,以确定是否匹配。
使用 `>=` 而不是 `==` 来比较长度,因为复数条目由带有嵌入式 NUL 的字符串表示。 */
if (nstr < nstrings
? W (domain->must_swap, domain->orig_tab[nstr].length) >= len
&& (strcmp (msgid,
domain->data + W (domain->must_swap,
domain->orig_tab[nstr].offset))
== 0)
: domain->orig_sysdep_tab[nstr - nstrings].length > len
&& (strcmp (msgid,
domain->orig_sysdep_tab[nstr - nstrings].pointer)
== 0))
{
/* 如果找到匹配的字符串,记录其位置并跳转到 `found` 标签进行处理。 */
act = nstr;
goto found;
}

/* 如果没有找到匹配的字符串,按照哈希增量继续查找下一个可能的条目。 */
if (idx >= domain->hash_size - incr)
idx -= domain->hash_size - incr;
else
idx += incr;
}
/* NOTREACHED */
}

触发过程

nipaste_2024-08-15_18-20-3

首先 hash_tab 不为 NULL,进入到 if 语句内,

nipaste_2024-08-15_18-24-4

然后执行第一条语句

nipaste_2024-08-15_18-54-4

如下图所示由于 nstr > nstrings,就会进入到第二个语句,即domain->orig_sysdep_tab[nstr - nstrings].length > len&& (strcmp (msgid,domain->orig_sysdep_tab[nstr - nstrings].pointer) == 0)

nipaste_2024-08-15_18-57-5

后面就会紧接着进入到下面的漏洞点

漏洞点

nipaste_2024-08-15_17-47-2

由于 domain->orig_sysdep_tab 对应的结构体处的数据为 0,从而使得 add rax, qword ptr [r14 + 0x48] (0x48对应orig_sysdep_tab的偏移),计算出来的是一个无效地址,从而在执行 cmp qword ptr [rax], rdi 的时候出现非法地址引用错误

nipaste_2024-08-15_17-48-1

ns_find_msg

让我们再来梳理下 ns_find_msg 的逻辑,正常只要 domain->hash_tab != NULL 就可以进入到下面这个 while 循环。

nipaste_2024-08-15_21-29-5

部分变量内容数据如下

nipaste_2024-08-15_21-34-0

domain->data = 0x00007ffff7ffa000,而 domain->data 指向的正是前面 ns_load_domain 映射 gtk30.mo 文件内容,需要注意的是 domain->hash_tab[idx] 指针指向的是 gtk30.mo 起始偏移处 0x3cc 处的位置

nipaste_2024-08-15_21-46-5

然后为了触发漏洞函数,我们需要使 nstr > nstrings 即可,然后 nstr 的值为 nls_uint32 nstr = W (domain->must_swap_hash_tab, domain->hash_tab[idx]); ,其实在这里就是 nstr =domain->hash_tab[idx] ,而如果我们对源文件 gtk30.mo 这里进行恶意修改,就可以轻松实现 nstr > nstrings 这一条件,从而触发漏洞。

溯源

下面再来看看这个非法值是怎么一步一步来的,根据调用栈,我们逆向调试

nipaste_2024-08-15_19-08-0

通过调试,在从 ns_load_domain 进入 ns_find_msg 的时候,domain已被破坏。

call _nl_load_domain

nipaste_2024-08-15_19-29-3

下图为对应此时 _nl_load_domain 的domain_file参数信息

nipaste_2024-08-15_19-33-3

可以看到此时 domain_file 已经初始化完成,但是 domain_file->data 仍为空,这一步在 _ns_load_domain 函数中完成。

以下是 _nl_load_domain 函数的简化伪代码和注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void _nl_load_domain(struct loaded_l10nfile *domain_file, struct binding *domainbinding) {
// 1. 检查本地化域是否已经加载过
if (domain_file->decided > 0) {
return; // 如果已经加载,则直接返回
}

// 2. 尝试打开对应的 .mo 文件
FILE *file = open_mo_file(domain_file->filename);
if (file == NULL) {
domain_file->data = NULL; // 如果文件不存在或打开失败,将数据指针设置为 NULL
return;
}

// 3. 读取并解析 .mo 文件
struct loaded_domain *domain = parse_mo_file(file);

// 4. 如果解析成功,将数据存储在 domain_file 结构体中
domain_file->data = domain;
domain_file->decided = 1; // 更新加载状态为已加载

// 5. 关闭文件
close(file);
}

_nl_load_domain 函数是 GNU C 库本地化系统中的一个关键函数。它负责从磁盘加载和解析本地化消息文件,将翻译数据存储在内存中,供程序运行时使用。通过这种方式,程序可以根据用户的语言环境显示对应的本地化消息。

需要关注的是第四步,正是这一步使得domain_file->data = domain; 从而建立联系,对应源码中的这里。

nipaste_2024-08-15_19-37-1

nipaste_2024-08-15_20-50-3

nipaste_2024-08-15_20-56-4

revision = W (domain->must_swap, data->revision);

这一句命令的作用是从结构体 data 中获取成员 revision 的值,并根据 domain->must_swap 的情况决定是否需要进行字节序转换。

nipaste_2024-08-15_21-01-3

nipaste_2024-08-15_21-02-1

nipaste_2024-08-15_21-11-2

call _nl_find_domain

nipaste_2024-08-15_19-18-1

下面是 _nl_find_domain 函数的简化伪代码和注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct loaded_domain *
_nl_find_domain (const char *dirname, const char *locale,
const char *domainname,
struct binding *domainbinding)
{
// 1. 通过域绑定结构体和目录路径查找缓存中是否已经存在该本地化域。
struct loaded_domain *domain = lookup_cache(dirname, locale, domainname);

// 2. 如果找到缓存中的域,则返回它。
if (domain != NULL) {
return domain;
}

// 3. 如果没有找到,则尝试从文件系统中加载本地化文件。
domain = load_localization_file(dirname, locale, domainname);

// 4. 将加载的域缓存起来,以便将来可以重复使用。
cache_domain(domainbinding, domain);

// 5. 返回加载的域。
return domain;
}

_nl_find_domain 函数是GNU C库中的一个重要函数,用于查找和加载本地化域。它涉及缓存管理和本地化文件的加载,确保在程序运行时能够高效地进行翻译和国际化支持。

nipaste_2024-08-15_19-20-4

此时 domain 还未被初始化完成。

call __dcigettext

nipaste_2024-08-15_19-15-4

总结

在询问开发者之后(很耐心很细心也给了很多建议),遗憾的被告诉这个配置文件需要 root 权限才能改,所以严格意义上它并不算一个漏洞(但原论文中有一个类似的因配置文件而导致的空指针,却被给了 CVE,且这个配置文件也需要 root 才能改),但为什么要在这里分享,有一个原因就是 envfuzz 其所采用的思想还是非常不错的。但是我自己在使用的过程中,也发现了一些问题,其中包括没有很好的对crash进行识别,只是简单的将所有通过变异 read ,recv 类的系统调用造成的crash全部记录,导致了一些crash并不是能够很好的复现,或者换句话说不能通过用户直接对这些数据进行修改,而是需要提权或者其他的一些额外的操作来进行复现。

  • Title: EnvFuzz 实战 evince
  • Author: henry
  • Created at : 2024-08-30 14:02:03
  • Updated at : 2024-08-30 14:06:03
  • Link: https://henrymartin262.github.io/2024/08/30/ns_find_msg/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments