Post

Root Cause Analysis of CVE-2024-22857

Root Cause Analysis of CVE-2024-22857

Root Cause Analysis | CVE-2024-22857

CVE-2024-22857 is a heap based buffer overflow in zlog library version 1.1.0 to 1.2.17. The vulnerability is triggered when creating a new rule that is already defined in the provided configuration file. This vulnerability could lead to a potential arbitrary code execution.

About Zlog

zlog is a logging library designed for C code bases. It is known for its high performance and thread safe logging capabilities. zlog is customizable through a user-defined configuration file.

zlog uses 3 concepts for its configuration files, these concepts guide the library on how to process and output log messages.

  • Categories: Used to specify different kinds of log entries.
    • A category in the source code is represented by zlog_category_t *
  • Formats: Used to describe log patterns.
  • Rules: Consists of category, level, output file and format.

Using zlog

This example is from the github repo of this library.

First define a configuration file. (path ./zlog.conf)

1
2
3
4
[formats]
simple = "%m%n"
[rules]
my_cat.DEBUG    >stdout; simple

Program that uses the library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h> 
#include "zlog.h"

int main(int argc, char** argv)
{
	int rc;
	zlog_category_t *c;

	rc = zlog_init("./zlog.conf"); // load configuration file path
	if (rc) {
		printf("init failed\n");
		return -1;
	}

	c = zlog_get_category("my_cat"); // 
	if (!c) {
		printf("get cat fail\n");
		zlog_fini();
		return -2;
	}
	zlog_info(c, "hello, zlog");
	zlog_fini();
	return 0;
} 

compile : gcc test.c -o test -lzlog

The vulnerability Explained

The commit message in the patch tells us all about the vulnerability.

commit message:

Size of record_name is MAXLEN_PATH(1024) + 1 but file_path may have data upto MAXLEN_CFG_LINE(MAXLEN_PATH*4) + 1. So a check was missing in zlog_rule_new() while copying the record_name from file_path + 1 which caused the buffer overflow. An attacker can exploit this vulnerability to overwrite the zlog_record_fn record_func function pointer to get arbitrary code execution.

This makes things for us straight forward.

The vulnerability lies in zlog_rule_new(). When is zlog_init() is called, each line of the configuration file is parsed.

This is evident by the backtrace from gdb.

#0  zlog_rule_new (line=line@entry=0x7fff5f9e64f0 "my_cat.DEBUG    >stdout; simple", levels=0x6366af94d2a0, default_format=0x6366af95b8f0, formats=0x6366af952b20, file_perms=0x180, 
    fsync_period=0x0, time_cache_count=0x7de19bd59490) at rule.c:571
#1  zlog_conf_parse_line (a_conf=a_conf@entry=0x7de19bcd7010, line=line@entry=0x7fff5f9e64f0 "my_cat.DEBUG    >stdout; simple", 
    section=section@entry=0x7fff5f9e641c) at conf.c:529
#2  zlog_conf_build_with_file (a_conf=a_conf@entry=0x7de19bcd7010) at conf.c:338
#3  zlog_conf_new (config=config@entry=0x636689dc5004 "./zlog.conf") at conf.c:176
#4  zlog_init_inner (config=config@entry=0x636689dc5004 "./zlog.conf") at zlog.c:91
#5  zlog_init (config=0x636689dc5004 "./zlog.conf") at zlog.c:134
#6  main ()

The rules are read into memory from the user-defined configuration file. The library uses zlog_rule_s which is a struct to hold each rule.

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
#define MAXLEN_PATH 1024
#define MAXLEN_CFG_LINE (MAXLEN_DATA*4)

struct zlog_rule_s {
	char category[MAXLEN_CFG_LINE + 1];
	char compare_char;
	int level;
	unsigned char level_bitmap[32]; /* for category determine whether output or not */
	unsigned int file_perms;
	int file_open_flags;
	char file_path[MAXLEN_PATH + 1];
	zc_arraylist_t *dynamic_specs;
	int static_fd;
	dev_t static_dev;
	ino_t static_ino;
	long archive_max_size;
	int archive_max_count;
	char archive_path[MAXLEN_PATH + 1];
	zc_arraylist_t *archive_specs;
	FILE *pipe_fp;
	int pipe_fd;
	size_t fsync_period;
	size_t fsync_count;
	zc_arraylist_t *levels;
	int syslog_facility;
	zlog_format_t *format;
	zlog_rule_output_fn output;
	char record_name[MAXLEN_PATH + 1];
	char record_path[MAXLEN_PATH + 1];
	zlog_record_fn record_func;
};

zlog_conf_parse_line() parses the line (char *line) and calls zlog_rule_new() with the char* line. zlog_rule_new() parses the line and initializes the correct properties.

Here is how it parses the line:

1
2
3
4
5
6
7
8
Line: my_cat.* ~/zlog.log; simple

selector : my_cat.*  |  sepreated by space
	category : my_cat | ends with .
	level : *
action : ~/zlog.log; simple
	output : ~/zlog.log | ends with ; 
		file_path : ~/zlog.log 

Now that we understand how zlog_rule_new() parses the line lets look at how the vulnerability is introduced.

The function first separates the selector and action:

1
2
3
4
5
6
7
8
9
10
char *action;
char selector[MAXLEN_CFG_LINE + 1];
int nscan = 0;

nscan = sscanf(line, "%s %n", selector, &nread);
if (nscan != 1) {
	zc_error("sscanf [%s] fail, selector", line);
	goto err;
}
action = line + nread;

The from the selector, the category and level are separated and placed in their own variables. These are not important for us right now, so we will just continue to when the output was separated from the action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char format_name[MAXLEN_CFG_LINE + 1];
char output[MAXLEN_CFG_LINE + 1];
char file_path[MAXLEN_CFG_LINE + 1];

memset(output, 0x00, sizeof(output));
memset(format_name, 0x00, sizeof(format_name));
nscan = sscanf(action, " %[^;];%s", output, format_name);
if (nscan < 1) {
	zc_error("sscanf [%s] fail", action);
	goto err;
}

/* Then file_path is seperated from output*/

memset(file_path, 0x00, sizeof(file_path));
nscan = sscanf(output, " %[^,],", file_path);
if (nscan < 1) {
	zc_error("sscanf [%s] fail", action);
	goto err;
}

The vulnerability happens on the condition that file_path[0] == '$'.

1
2
3
case '$' :
	sscanf(file_path + 1, "%s", a_rule->record_name); // vulnerability
	/*...*/

This code copies whats in file_path into a_rule->record_name. This is a vulnerable because the a_rule->record_name buffer holds only MAXLEN_PATH + 1 which is equal to 1024 and the file_path buffer can hold data upto MAXLEN_CFG_LINE + 1 which is equal to MAXLEN_PATH*4 + 1. a_rule->record_name resides in the heap (a_rule = calloc(1, sizeof(zlog_rule_t));)

The is no bounds check to limit the size of the string copied into a_rule->record_name.

Patch

The patch just removed the sscanf() line and used the safer strncpy().

1
2
3
4
case '$' :
	// sscanf(file_path + 1, "%s", a_rule->record_name); // vulnerability
	strncpy(a_rule->record_name, file_path + 1, MAXLEN_PATH);
	/*...*/

Exploitation Overview

Because a_rule->record_name is stored inside the zlog_rule_t heap structure, overflowing it allows corruption of adjacent fields.

1
2
3
4
5
6
struct zlog_rule_s {
	/* ... */
	char record_name[MAXLEN_PATH + 1];
	char record_path[MAXLEN_PATH + 1];
	zlog_record_fn record_func;
};

If an attacker supplies a configuration line where file_path begins with $, the following code is executed:

1
sscanf(file_path + 1, "%s", a_rule->record_name);

Because %s reads an string without bounds checking, an attacker controlled configuration line longer than MAXLEN_PATH bytes will overflow a_rule->record_name.

This overflow can overwrite:

1
2
record_path
record_func

Since record_func is a function pointer, an attacker can overwrite it with a attacker controlled address. This may redirect execution to attacker controlled code, leading to arbitrary code execution.

By overflowing a_rule->record_name, an attacker can overwrite the a_rule->record_func pointer. When logging rule executes, the program will call this function pointer, allowing an attacker to redirect execution.

References

  • https://nvd.nist.gov/vuln/detail/CVE-2024-22857
  • https://www.cybersecurity-help.cz/vdb/SB2024022842
  • https://github.com/HardySimpson/zlog/
This post is licensed under CC BY 4.0 by the author.