7. Targeting NSS
From our previous deep dive, we know this bug is no toy: the heap overflow in set_cmnd() gives us a controllable, unbounded overwrite. By feeding sudoedit -[i|s] '\' aaaaa..., the de-escape copy loop will duplicate attacker input and corrupt adjacent heap chunks. The question now becomes: what's worth smashing?
With SUID-root binaries, the heap is littered with juicy targets: function pointers, virtual tables, linked-list nodes, parser state. A single overwrite here can flip execution straight into our payload.
From the heap trace call tree, one subsystem immediately stands out: glibc NSS.
7.1. Why NSS?
From the previous analysis, we are aware that this is an overflow of critical level—if we provide a long enough string as the 2nd argument for sudoedit -[i|s] \ aaaaaa... (aka NewArgv[1] for setcmnd()), the de-escape copy loop inside set_cmnd() will copy aaaaaa... 2 times corrupting the adjacent heap—we have an unlimited size heap overflow entry!
The key to privilege escalation is manipulating data on the heap—for example, using a heap overflow to overwrite critical elements such as virtual tables, function pointers, or structure pointers that reside near there—any of this operation in sudo is critical for it's SUID set and owned by root!
By correlating this insight with the information collected during earlier dynamic debugging and static analysis, we should start to target a victim outside the binary itself for the privesc purpose connected to the OS.
From the function call tree collected via the heap trace, we see under the MALLOC node there're some NSS operations manipulated by get_user_info():
During execution, sudo needs to resolve user information (via glibc APIs llike getpwuid(), getgrnam(), etc.) before deciding whether the user is allowed to run a command. Those libc lookups are not self-contained—they funnel into the Name Service Switch (NSS) layer, which dispatches queries to different back-ends as dictated by /etc/nsswitch.conf.
From our trace, we see sudo calls get_user_info in the early stage:
MALLOC (731 calls)
└─ main [731]
...
├─ get_user_info [120]
│ ├─ __GI___libc_malloc [1]
│ ├─ getpwuid [65]
│ │ ├─ __GI___libc_malloc [1]
│ │ └─ __getpwuid_r [64]
│ │ ├─ __GI___nss_passwd_lookup2 [41]
│ │ │ ├─ __GI___nss_database_lookup [35]
│ │ │ │ └─ nss_parse_file [35]
│ │ │ │ ├─ _IO_new_fopen [1]
│ │ │ │ │ └─ __fopen_internal [1]
│ │ │ │ │ └─ __GI___libc_malloc [1]
│ │ │ │ ├─ __GI___libc_malloc [1]
│ │ │ │ ├─ __getline [2]
│ │ │ │ │ └─ _IO_getdelim [2]
│ │ │ │ │ ├─ __GI___libc_malloc [1]
│ │ │ │ │ └─ _IO_new_file_underflow [1]
│ │ │ │ │ └─ __GI__IO_doallocbuf [1]
│ │ │ │ │ └─ __GI__IO_file_doallocate [1]
│ │ │ │ │ └─ __GI___libc_malloc [1]
│ │ │ │ └─ nss_getline [31]
│ │ │ │ ├─ __GI___libc_malloc [11]
│ │ │ │ └─ nss_parse_service_list [20]
│ │ │ │ └─ __GI___libc_malloc [20]
│ │ │ └─ __GI___nss_lookup [6]
│ │ │ └─ __GI___nss_lookup_function [6]
│ │ │ ├─ __GI___tsearch [1]
│ │ │ │ └─ __GI___libc_malloc [1]
│ │ │ ├─ __GI___libc_malloc [1]
│ │ │ ├─ __nss_disable_nscd [1]
│ │ │ │ └─ nss_load_all_libraries [1]
│ │ │ │ └─ nss_load_library [1]
│ │ │ │ └─ nss_new_service [1]
│ │ │ │ └─ __GI___libc_malloc [1]
...Translation: every sudo run triggers NSS lookups, which allocate heap structures and even load shared libraries dynamically (nss_load_library()). That's a goldmine for exploitation: heap metadata + dynamically linked .so + root privileges.
Think shared library hijacking or fake service descriptors.
7.2. NSS 101
Name Service Switch (NSS) is a pluggable framework inside glibc that lets user-space programs resolve “name service” data—users, groups, hosts, etc.—from one or more back-ends selected by /etc/nsswitch.conf (e.g., files, dns, ldap), describing the file format and databases.
7.1.1. Modern Layout
In newer glibc (e.g. 2.41), NSS state revolves around:
nss_action_list— the in-memory sequence of actions/modules to try (terminates with an entry whosemoduleisNULL).struct nss_module— one element per NSS module (name, state, function table, handle, next).
/* A NSS service module (potentially unloaded). Client code should
use the functions below. */
struct nss_module
{
/* Actual type is enum nss_module_state. Use int due to atomic
access. Used in a double-checked locking idiom. */
int state;
/* The function pointers in the module. */
union
{
struct nss_module_functions typed;
nss_module_functions_untyped untyped;
} functions;
/* Only used for __libc_freeres unloading. */
void *handle;
/* The next module in the list. */
struct nss_module *next;
/* The name of the module (as it appears in /etc/nsswitch.conf). */
char name[];
};7.1.2. Legacy Layout
Instead of referencing nss_module from nss_module.h, older releases like glibc 2.27 exposed service_user in nsswitch.h directly.
NSS keeps per-database state in heap objects:
typedef struct service_user
{
/* And the link to the next entry. */
struct service_user *next;
/* Action according to result. */
lookup_actions actions[5];
/* Link to the underlying library object. */
service_library *library;
/* Collection of known functions. */
void *known;
/* Name of the service (`files', `dns', `nis', ...). */
char name[0];
} service_user;service_library: the module record (name,lib_handle,next)service_user:- One list node per configured service for a database
- Embedded with a
nextpointer to the next same structure, meaning this is made for a single linked list - Holds policy actions and a pointer to its
service_library
Both of them are exploitable by overflow attack. But here, we will focus on glibc 2.27 source as the victim for analysis.
7.1.3. nsswitch.conf
The actual backend chain is chosen via nsswitch.conf. It tells glibc's NSS layer which back-ends to consult—and in what order—for each “system database” (passwd, hosts, etc.) when user-space functions like getpwuid(), getgrnam(), or getaddrinfo() are called.
Glibc provides a sample configuration file at nss/nsswitch.conf:
# /etc/nsswitch.conf
#
# Example configuration of GNU Name Service Switch functionality.
#
passwd: db files
group: db files
initgroups: db [SUCCESS=continue] files
shadow: db files
gshadow: files
hosts: files dns
networks: files dns
protocols: db files
services: db files
ethers: db files
rpc: db files
netgroup: db filesWhile the actual one in runtime will be /etc/nsswitch.conf on the target OS.
The NSS framework APIs interacts relying on this config file. For example when the sudo binary calls getpwuid() lookups, it:
- Tries the
dbbackend (Berkeley DB.dbfiles like/var/lib/misc/passwd.db). - If that fails, fall back to the
filesbackend (plain text/etc/passwd).
Overall, NSS is a actually a commonly seen glibc framework that routes lookups for system information like users or hosts. It parses and resolves config files like /etc/passwd or /etc/hosts by standard libc APIs like getpwuid() or getaddrinfo().
7.3. Vuln Entry
We mentioned nss_load_library could be a highly susceptible target, by hijacking the shared library loading path. Here we will explain how, and why.
7.3.1. nss_load_library
The suspicious call we flagged earlier — nss_load_library — is defined in nsswitch.c. It's a helper whose entire purpose is to make sure the requested NSS service (files, db, dns, etc.) has a service_library object, and if necessary, dynamically load the corresponding shared library. On glibc builds with dynamic NSS (default for Linux), this path is compiled in.
Its argument, struct service_user *ni, is the node from the service-user linked list (see §7.1). Annotated workflow:
#if !defined DO_STATIC_NSS || defined SHARED
/* Load library. */
static int
nss_load_library (service_user *ni)
{
// If no `service_library` yet, create one
if (ni->library == NULL)
{
/* This service has not yet been used. Fetch the service
library for it, creating a new one if need be. If there
is no service table from the file, this static variable
holds the head of the service_library list made from the
default configuration. */
static name_database default_table;
// `nss_new_service()` allocates/links a `service_library`
ni->library = nss_new_service (
// `service_table` points to the parsed `nsswitch.conf`
service_table ?:
// If no, falls back to a process-local default_table
&default_table,
// Binds it to the service
ni->name);
if (ni->library == NULL)
return -1;
}
// If the library hasn't been registered/loaded yet
if (ni->library->lib_handle == NULL)
{
/* Load the shared library. */
size_t shlen = (7 + strlen (ni->name) + 3
+ strlen (__nss_shlib_revision) + 1);
int saved_errno = errno;
char shlib_name[shlen];
/* Construct shared object name. */
// Name format: "libnss_<name>.so<revision>"
__stpcpy (__stpcpy (__stpcpy (__stpcpy (shlib_name,
"libnss_"),
ni->name),
".so"),
__nss_shlib_revision);
// [!] Load the library via `dlopen()`
ni->library->lib_handle = __libc_dlopen (shlib_name);
if (ni->library->lib_handle == NULL)
{
/* Failed to load the library. */
ni->library->lib_handle = (void *) -1l;
__set_errno (saved_errno);
}
# ifdef USE_NSCD
else if (is_nscd)
{
/* Call the init function when nscd is used. */
size_t initlen = (5 + strlen (ni->name)
+ strlen ("_init") + 1);
char init_name[initlen];
/* Construct the init function name. */
// Name format: _nss_<name>_init
__stpcpy (__stpcpy (__stpcpy (init_name,
"_nss_"),
ni->name),
"_init");
/* Find the optional init function. */
// look up with `dlsym()`
void (*ifct) (void (*) (size_t, struct traced_file *))
= __libc_dlsym (ni->library->lib_handle, init_name);
if (ifct != NULL)
{
void (*cb) (size_t, struct traced_file *) = nscd_init_cb;
# ifdef PTR_DEMANGLE
PTR_DEMANGLE (cb);
# endif
// Call the function
ifct (cb);
}
}
# endif
}
return 0;
}
#endifIn conclusion, given a heap node service_user *ni of a service (e.g., files, db, dns) pointing to a single linked list, this function:
- Checks if a
service_libraryexists, and callsnss_new_serviceto create a new one if none. - Internally, the
service_librarystructure contains a pointer to the loaded shared librarylibc_handle. Checks if it exists, or it constructs the SONAME"libnss_<name>.so<revision>"and forces to call__libc_dlopen()to load it. - If running inside
nscd, it looks up_nss_<name>_initvia__libc_dlsymand calls it.
Here lies the jackpot:
sudois setuid-root, meaning every NSS lookup it performs (getpwuid,getpwnam, etc.) executes with effective UID 0. So the glibc's dynamic loader enters secure-execution mode (AT_SECURE=1).In that mode
LD_LIBRARY_PATHand friends are ignored, and unnamed libraries are searched only in trusted dirs. See the loader's rules: if a library name contains a “/”, it is treated as a pathname and loaded from that path; otherwise, it is searched in cache/default paths, andLD_LIBRARY_PATHis ignored in secure mode.If we can corrupt a live
service_usernode beforenss_load_library()is invoked, we can steer it intodlopen()of an attacker-controlled path.
Therefore, if we are to leverage this attack entry, for example by overflowing the service_user heap object, here's:
#Requirement 1:
C(service_user *)ni->library->lib_handle == 0
But how can we control (service_user *)ni->library first? Do read on.
7.3.2. nss_new_servcie
From the previous snippet, we know nss_new_service() is called when ni->library == NULL to allocate a new service_library. The function is defined in the same file at line 805:
#if !defined DO_STATIC_NSS || defined SHARED
static service_library *
nss_new_service (name_database *database, const char *name)
{
service_library **currentp = &database->library;
// 1) Walk the per-process list to see if this service already exists
while (*currentp != NULL)
{
if (strcmp ((*currentp)->name, name) == 0)
return *currentp; // [!] Return if name found in database
currentp = &(*currentp)->next;
}
// 2) Not found: allocate a new node
/* We have to add the new service. */
*currentp = (service_library *) malloc (sizeof (service_library));
if (*currentp == NULL)
return NULL;
// 3) Initialize it (note: NO strdup)
(*currentp)->name = name; // copies the service name we pass in
(*currentp)->lib_handle = NULL; // [!] “needs loading”: lib_handle is set to 0
(*currentp)->next = NULL;
return *currentp;
}
#endifWe see a delightful configuration for our exploit purpose to satisfy #Requirement 1:
(*currentp)->lib_handle = NULL;
return *currentp;This initializes the lib_handle field of the returned service_library * to 0, which then drives execution into the external-library loading path described earlier.
So, to call nss_new_service() and reach this code path, we have:
#Requirement 2:
C(service_user *)ni->library == NULL
This ensures nss_load_library() enters its if (ni->library == NULL) branch and invokes nss_new_service().
But this is not enough. Before zeroing lib_handle, the code checks whether the provided service_user *ni has a name matching an existing service. If it does, the function immediately returns the existing entry and the zero-initialization will not occur.
For example, in the caller nss_load_library the service library is instantiated as:
static name_database *service_table; // The root of the whole data base
static name_database default_table;
static name_database default_table;
ni->library = nss_new_service (
service_table ?: &default_table,
ni->name
);If ni->name is an existing one like "passwd", nss_new_service immediately returns the matching library and our desired (*currentp)->lib_handle = NULL will never be triggered!
Therefore, here's:
#Requirement 3:
(service_user *)ni->nameshould be hijacked to a nonexistent one!
This means if we want to privesc via nss_load_library() by loading an implanted shared library, the decisive controls are on the service_user node:
ni->libary- Overwrite its value as
0. - Trigger
nss_new_service()to step in.
- Overwrite its value as
ni->name:- We need to overwrite it as a nonexistent database entry name, like
"X"⟶nss_new_service()setsni->library->lib_handle == 0 - Its value will be directly passed to newly created
service_library.name((*currentp)->name). - It also controls the middle of the SONAME:
- Further, if it contains
/(e.g.,"X/Y"), the whole thing becomes a direct path (no trusted-dir search, no env vars needed). - So the constructed SONAME becomes a pathname:
"libnss_X/Y.so<rev>".
- Further, if it contains
- We need to overwrite it as a nonexistent database entry name, like
If we then provide that file at ./libnss_X/Y.so.2, it will be loaded via:
(service_user *)ni->library->lib_handle = __libc_dlopen ("./libnss_X/Y.so.2");Wonderful attack chain! But how is it invoked via sudo? Can we corrupt the values required by #Requirement 1, 2, 3? Do read on.
7.4. Backtrace
To see how our target nss_load_library is reached during the heap-overflow primitive in sudo, we can instrument execution with breakpoints and trace the call stack. The goal: confirm whether we can hijack the relevant NSS heap objects in the right context.
Set up breakpoints:
gdb -q \
-ex 'set pagination off' \
-ex 'set breakpoint pending on' \
-ex 'b nss_load_library' \
-ex 'b set_cmnd' \
--args "$HOME/fuzz/proj/sudo-1.9.5p1/install/bin/sudoedit" -s '\' abcdefghijkl7.4.1. Target Initialization
Execution halts first at nss_load_library, before set_cmnd is ever hit:

This is the initialization path for a service_user node: NSS resolving the passwd database (_nss_<svc>_getpwuid_r), as instructed by the passwd: entry in nsswitch.conf.
We're at:
nss_load_library(service_user *ni)
if (ni->library == NULL) { ... }Disassembly confirms the check:
mov r15, qword ptr [rdi + 0x20] ; rdi = ni, offset 0x20 = ni->library
test r15, r15
je nss_load_library+96 ; branch to allocate/init service_libraryA dump of ni shows this instance corresponds to the passwd DB's first service, compat (Ubuntu 18.04 defaults to passwd: compat). Because ni->library == NULL, the function proceeds to:
ni->library = nss_new_service(service_table ?: &default_table, ni->name)At this point *currentp is still NULL:

So a new service_library is allocated for the “first use”:

The freshly allocated service_library for "compat" has its lib_handle initialized to NULL, which makes the caller (nss_load_library) immediately attempt to dlopen() it:

At this point the dynamic linker pulls in libnss_compat.so.2:

Once this completes, the "passwd" database chain (__nss_passwd_database) is fully initialized:

Execution then continues into further initialization of the name-database list, keeps calling nss_load_library for each backend specified in /etc/nsswitch.conf:

It parses our local /etc/nsswitch.conf:
$ cat /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 mdns4_minimal [NOTFOUND=return] dns myhostname
networks: files
protocols: db files
services: db files
ethers: db files
rpc: db files
netgroup: nisThe backtrace shows how this is called for the "first-use" initialization by getpwuid:
► 0 0x7ffff72b3752 nss_load_library+322
1 0x7ffff72b3f38 __nss_lookup_function+296
2 0x7ffff72b404d __nss_lookup+61
3 0x7ffff72b6390 __nss_passwd_lookup2+64
4 0x7ffff724eb93 getpwuid_r+755
5 0x7ffff724e148 getpwuid+152
6 0x5555555717c2 get_user_info.constprop+258
7 0x55555555e062 main+5627.3.2. After Overlow
By the time execution reaches set_cmnd, our nss_load_library breakpoint has already been hit seven times, corresponding to the initialization of the seven default name databases (passwd, group, hosts, etc.):

Once the overflow occurs, nss_load_library is invoked again—this time while NSS resolves the initgroups entry point (_nss_<svc>_initgroups_dyn) to build the target user's supplementary group list, required for sudo's policy checks and privilege switching. This step often calls into a different NSS module depending on the system's group: or initgroups: configuration:

This is where things get juicy: it means we can potentially corrupt and re-use already-initialized service_user heap objects to control how nss_load_library behaves.
From the backtrace we observe: once
set_cmndcompletes, the chain eventually reachessudoers_lookup, which in turn calls the glibc APIgetgrouplist. That call specifically uses the “group” (and related) databases—skipping over the initial “passwd” DB.Exploitation strategy, therefore, must focus on precisely targeting the
service_userstructures for group/initgroups lookups, not the earlier passwd node. We'll dive into that in later sections.
The process does not free these heap objects once they are created. For example, our earlier "compat" service node persists exactly as it was initialized:

Its members (library, lib_handle, etc.) remain allocated and reused across lookups—never released.
This maps directly to a classic heap exploitation principle:
Heap objects allocated during global initialization tend to stay alive, effectively acting like a cached data structure. If we can corrupt them once, you control them for the remainder of the process. Think of it like a userspace analogy to the Linux kernel's SLUB allocator: initialize once during boot (or sudo startup), keep around forever, and exploit them if they're tainted.
In our case, the idea boils down to:
heap object malloc'ed ⟶ [HEAP OVERFLOW] ⟶ tainted heap object loadedFull backtrace on this run after overflow occurs:
#0 __GI___nss_lookup_function (ni=ni@entry=0x555555802eb0, fct_name=<optimized out>, fct_name@entry=0x7ffff73201be "initgroups_dyn") at nsswitch.c:498
#1 0x00007ffff724b6c7 in internal_getgrouplist (user=user@entry=0x55555580a278 "root", group=group@entry=0, size=size@entry=0x7fffffffd748, groupsp=groupsp@entry=0x7fffffffd750, limit=limit@entry=-1) at initgroups.c:105
#2 0x00007ffff724b991 in getgrouplist (user=user@entry=0x55555580a278 "root", group=group@entry=0, groups=groups@entry=0x7ffff7f9f010, ngroups=ngroups@entry=0x7fffffffd7a4) at initgroups.c:169
#3 0x0000555555578efd in sudo_getgrouplist2_v1 (name=0x55555580a278 "root", basegid=0, groupsp=groupsp@entry=0x7fffffffd800, ngroupsp=ngroupsp@entry=0x7fffffffd7fc) at ./getgrouplist.c:98
#4 0x00005555555a3edf in sudo_make_gidlist_item (pw=0x55555580a248, unused1=<optimized out>, type=1) at ./pwutil_impl.c:269
#5 0x00005555555a2be6 in sudo_get_gidlist (pw=0x55555580a248, type=type@entry=1) at ./pwutil.c:926
#6 0x000055555559c41c in runas_getgroups () at ./match.c:141
#7 0x000055555558e565 in runas_setgroups () at ./set_perms.c:1584
#8 set_perms (perm=perm@entry=5) at ./set_perms.c:275
#9 0x00005555555bfa98 in sudoers_lookup (snl=0x5555557fd9e0 <snl>, pw=0x55555580a248, cmnd_status=0x5555557fb61c <cmnd_status>, pwflag=0) at ./parse.c:355
#10 0x00005555555915bd in sudoers_policy_main (argc=argc@entry=3, argv=argv@entry=0x555555805b90, pwflag=pwflag@entry=0, env_add=env_add@entry=0x0, verbose=verbose@entry=false, closure=closure@entry=0x7fffffffdf60) at ./sudoers.c:420
#11 0x000055555558a379 in sudoers_policy_check (argc=3, argv=0x555555805b90, env_add=0x0, command_infop=0x7fffffffe020, argv_out=0x7fffffffe028, user_env_out=0x7fffffffe030, errstr=0x7fffffffe048) at ./policy.c:1028
#12 0x000055555555e4a0 in policy_check (user_env_out=0x7fffffffe030, argv_out=0x7fffffffe028, command_info=0x7fffffffe020, env_add=0x0, argv=0x555555805b90, argc=3) at ./sudo.c:1171
#13 main (argc=argc@entry=4, argv=argv@entry=0x7fffffffe2b8, envp=0x7fffffffe2e0) at ./sudo.c:269
#14 0x00007ffff718cc87 in __libc_start_main (main=0x55555555de30 <main>, argc=4, argv=0x7fffffffe2b8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe2a8) at ../csu/libc-start.c:310
#15 0x00005555555604fa in _start ()Next, we'll walk through the the call chain from main into NSS, to illustrate where the corruption lands.
7.5. Attack Chain
From the dumped call tree, we see exactly how nss_load_library is first invoked during NSS resolution via getpwuid:
main
└─ get_user_info (sudo.c: ~541)
└─ getpwuid(uid_t uid) // from glibc
└─ __getpwuid_r(...) // reentrant core
└─ __nss_passwd_lookup2(...) // pick service chain for “passwd”
└─ __nss_lookup(...) // iterate services per policy
└─ __nss_lookup_function(...) // resolve function pointer
└─ nss_load_library(...) // create/cache + dlopen libnss_<svc>.so.2After the vuln entry (set_cmnd), the nss_load_library call loop is invoked again—this time when resolving the initgroups entry point during supplementary group setup via sudoers_lookup:
main
└─ policy_check(...) // policy orchestration/glue
└─ sudoers_policy_check(...) // invokes sudoers plugin
└─ sudoers_policy_main(...) // top-level plugin logic
├─ set_cmnd(...) // resolve/validate command path
│ ...
└─ sudoers_lookup(...) // evaluate rules; prepares runas ctx
└─ runas_setgroups() // plugins/sudoers/set_perms.c: set target user's suppl. groups
└─ runas_getgroups() // plugins/sudoers/match.c: assemble group list for runas user
└─ sudo_get_gidlist(...)
└─ sudo_make_gidlist_item(...)
└─ sudo_getgrouplist2_v1()
└─ getgrouplist(...) // glibc: public API
└─ internal_getgrouplist(...) // glibc: initgroups.c core
└─ __nss_lookup_function("initgroups_dyn") // glibc: nss/nsswitch.c
└─ nss_load_library(...) // may dlopen libnss_<service>.so.<rev>
└─ dlsym("_nss_<service>_initgroups_dyn")Understanding this chain is critical: it shows both entry points (getpwuid and getgrouplist) where our heap-overflow-primed structures get hit.
7.5.1. getpwuid
This function is not that important for our exploit. I were just being paranoid to find out how glibc hides it (and its friends) from the call stack.
This function is deceptively “missing” from symbols during static analysis, but it's absolutely present—just macro-generated by glibc templates.

In sudo, it is called inside get_user_info, see sudo.c:541:
/*
* Return user information as an array of name=value pairs.
* and fill in struct user_details (which shares the same strings).
*/
static char **
get_user_info(struct user_details *ud)
{
struct passwd *pw;
...
pw = getpwuid(ud->cred.uid);
...
if (pw == NULL)
sudo_fatalx(U_("you do not exist in the %s database"), "passwd");
...
}This is because it's generated by macros, in depth.
In glibc, pwd/getpwuid.c defines the per-function knobs and includes the generic template:
#include <pwd.h>
#define LOOKUP_TYPE struct passwd
#define FUNCTION_NAME getpwuid
#define DATABASE_NAME passwd
#define ADD_PARAMS uid_t uid
#define ADD_VARIABLES uid
#define BUFLEN NSS_BUFLEN_PASSWD
#include "../nss/getXXbyYY.c" // Generic templateThe included /nss/getXXbyYY.c is the generic non-reentrant wrapper. With the macros above, it materializes a real function:
#define REENTRANT_NAME APPEND_R (FUNCTION_NAME)
#define APPEND_R(name) APPEND_R1 (name)
#define APPEND_R1(name) name##_r
/* Prototype for reentrant version we use here. */
extern int INTERNAL (REENTRANT_NAME) (ADD_PARAMS, LOOKUP_TYPE *resbuf,
char *buffer, size_t buflen,
LOOKUP_TYPE **result H_ERRNO_PARM)
attribute_hidden;
LOOKUP_TYPE *
FUNCTION_NAME (ADD_PARAMS)
{
... INTERNAL(REENTRANT_NAME)(...) ...
}
nss_interface_function (FUNCTION_NAME)With those macros, this becomes:
LOOKUP_TYPE * FUNCTION_NAME (ADD_PARAMS)⇒struct passwd * getpwuid(uid_t uid)REENTRANT_NAMEis defined asAPPEND_R(FUNCTION_NAME)⇒getpwuid_rINTERNAL(name)prefixes with__(from glibc's internal headers) ⇒INTERNAL(REENTRANT_NAME)⇒__getpwuid_r
So the core call inside the wrapper is __getpwuid_r declared in pwd.h:
extern int __getpwuid_r (__uid_t __uid, struct passwd *__resultbuf,
char *__buffer, size_t __buflen,
struct passwd **__result) attribute_hidden;Then it goes back and look for get_pwuid_r.c, which includes nss/getXXbyYY_r.c. It is just another template—the real function designer:
/* To make the real sources a bit prettier. */
#define REENTRANT_NAME APPEND_R (FUNCTION_NAME) // e.g., getpwuid_r
...
#define INTERNAL(name) INTERNAL1 (name)
#define INTERNAL1(name) __##name // e.g., __getpwuid_r
...
# define DB_LOOKUP_FCT CONCAT3_1 (__nss_, DATABASE_NAME, _lookup2) // e.g., __nss_passwd_lookup2
...
/* Type of the lookup function we need here. */
typedef enum nss_status (*lookup_function) (ADD_PARAMS, LOOKUP_TYPE *, char *,
size_t, int * H_ERRNO_PARM
EXTRA_PARAMS);
// [!] Actual function designer
int
INTERNAL (REENTRANT_NAME) (ADD_PARAMS, LOOKUP_TYPE *resbuf, char *buffer,
size_t buflen, LOOKUP_TYPE **result H_ERRNO_PARM
EXTRA_PARAMS)
{
static bool startp_initialized;
static service_user *startp;
static lookup_function start_fct;
service_user *nip;
...
// At the bottom, symbol-versioning & aliases export the public getpwuid_r
// while keeping __getpwuid_r as the hidden/internal entry.So this template materializes a real function like __getpwuid_r:
int __getpwuid_r(uid_t uid,
struct passwd *resbuf, char *buffer, size_t buflen,
struct passwd **result /*, … */);And its actual definition content is then filled with the template, for example (the interesting parts):
int
__getpwuid_r(ADD_PARAMS, LOOKUP_TYPE *resbuf, char *buffer, size_t buflen,
LOOKUP_TYPE **result /*, … */)
{
static bool startp_initialized;
static service_user *startp; // cached head of the service chain
static lookup_function start_fct; // cached first backend function pointer
service_user *nip; // iterator (current node)
union { lookup_function l; void *ptr; } fct;
int no_more;
enum nss_status status = NSS_STATUS_UNAVAIL;
if (!startp_initialized) {
// 1) Build/find the passwd service list and resolve the first function:
// __nss_passwd_lookup2(&nip, "getpwuid_r", NULL, &fct.ptr)
no_more = __nss_passwd_lookup2(&nip, "getpwuid_r", NULL, &fct.ptr);
// 2) Cache results in statics (with PTR_MANGLE for hardening)
// (no_more != 0 means: there are no services at all)
startp = no_more ? (service_user *)-1l : nip;
start_fct = no_more ? NULL : fct.l;
atomic_write_barrier();
startp_initialized = true;
} else {
// Reuse cached start node + function (PTR_DEMANGLE)
fct.l = start_fct;
nip = startp;
no_more = (nip == (service_user *)-1l);
}
while (no_more == 0) {
// 3) Call the backend: fct.l points to _nss_<service>_getpwuid_r
status = DL_CALL_FCT(fct.l, (uid, resbuf, buffer, buflen, &errno /* ... */));
// 4) Policy: decide whether to continue to next service or stop
// This consults nip->actions[status] and advances nip/fct if needed:
no_more = __nss_next2(&nip, "getpwuid_r", NULL, &fct.ptr, status, 0);
}
*result = (status == NSS_STATUS_SUCCESS) ? resbuf : NULL;
return errno_or_mapped_value(status, /*h_errno*/);
}The TLDR:
getXXbyYY_r.cis a macro template. Withpwd/getpwuid_r.cit transforms the callee symbolgetpwuidinto__getpwuid_r.__getpwuid_r:- asks
__nss_passwd_lookup2to prepare thepasswdservice chain and the first function pointer, - calls
_nss_<service>_getpwuid_rfor each service according to policy, - the first time a service is used,
nss_load_librarydlopenslibnss_<service>.so.2anddlsyms the symbol (explain later).
- asks
- The iterator
service_user **nipadvances via__nss_next2according toactions[]and the result status; the first node/function are cached across calls instartp/start_fct(with pointer mangling).
It acts as the initiator for our target heap objects.
7.5.2. __nss_passwd_lookup2
The hide-and-seek continues.
Being paranoid again. Feel free to skip this part.
The function __nss_passwd_lookup2 (and its global head pointer __nss_passwd_database) are not hand-written; they're macro-generated from nss/pwd-lookup.c and the generic nss/XXX-lookup.c template, just like their caller __getpwuid_r.
nss/pwd-lookup.c sets up macros for this specific database:
#include <config.h>
#define DATABASE_NAME passwd
#ifdef LINK_OBSOLETE_NSL
# define DEFAULT_CONFIG "compat [NOTFOUND=return] files"
#else
# define DEFAULT_CONFIG "files"
#endif
#include "XXX-lookup.c"It doesn't define any function body itself; it just wires in the template.
Inside XXX-lookup.c, token-pasting macros expand into the concrete function names:
#include "nsswitch.h"
#define DB_LOOKUP_FCT CONCAT3_1 (__nss_, DATABASE_NAME, _lookup2)
#define CONCAT3_1(Pre, Name, Post) CONCAT3_2 (Pre, Name, Post)
#define CONCAT3_2(Pre, Name, Post) Pre##Name##Post
#define DATABASE_NAME_SYMBOL CONCAT3_1 (__nss_, DATABASE_NAME, _database)
#define DATABASE_NAME_STRING STRINGIFY1 (DATABASE_NAME)
#define STRINGIFY1(Name) STRINGIFY2 (Name)
#define STRINGIFY2(Name) #Name
#ifdef ALTERNATE_NAME
#define ALTERNATE_NAME_STRING STRINGIFY1 (ALTERNATE_NAME)
#else
#define ALTERNATE_NAME_STRING NULL
#endif
#ifndef DEFAULT_CONFIG
#define DEFAULT_CONFIG NULL
#endifWith DATABASE_NAME = passwd (parsed from the included nsswitch.h), these expand to:
DB_LOOKUP_FCT=__nss+passwd+_lookup2→__nss_passwd_lookup2DATABASE_NAME_SYMBOL=__nss+passwd+_database→__nss_passwd_database(aservice_user *head)DATABASE_NAME_STRING→"passwd"
The template then produces the real function body:
int
DB_LOOKUP_FCT(service_user **ni, const char *fct_name, const char *fct2_name,
void **fctp)
{
if (DATABASE_NAME_SYMBOL == NULL
&& __nss_database_lookup(DATABASE_NAME_STRING, ALTERNATE_NAME_STRING,
DEFAULT_CONFIG, &DATABASE_NAME_SYMBOL) < 0)
return -1;
*ni = DATABASE_NAME_SYMBOL; // head of the “passwd” chain: __nss_passwd_database
return __nss_lookup(ni, fct_name, fct2_name, fctp);
}
libc_hidden_def(DB_LOOKUP_FCT)So after preprocessing via macro:
#define DB_LOOKUP_FCT CONCAT3_1 (__nss_, DATABASE_NAME, _lookup2)We get a concrete function for DATABASE_NAME = passwd after DB_LOOKUP_FCT is expanded:
int __nss_passwd_lookup2(service_user **ni,
const char *fct_name, const char *fct2_name,
void **fctp);Inside, it initializes the database (first call only):
__nss_database_lookup("passwd", NULL, DEFAULT_CONFIG, &__nss_passwd_database)
__nss_database_lookupis defined innss/nsswitch.c. This parses/etc/nsswitch.conf(vianss_parse_file) and builds the linked list ofservice_usernodes for thepasswdDB (namely a name_database). If the file has nopasswd:line, it usesDEFAULT_CONFIG(here"files"or"compat … files"ifLINK_OBSOLETE_NSL), which was initialized as NULL.
Then, it sets *ni = __nss_passwd_database via another macro and tail-calls:
__nss_lookup(ni, fct_name, fct2_name, fctp)to resolve the first backend function pointer (e.g., _nss_files_getpwuid_r).
This is exactly the call in
__getpwuid_r( template reference):Cno_more = __nss_passwd_lookup2(&nip, "getpwuid_r", NULL, &fct.ptr);If successful,
fct.ptris a pointer to the module entry_nss_<service>_getpwuid_r, and*nipoints at the currentservice_user.__nss_lookup(and later__nss_next2) handle policy and advance through(*ni)->next.
7.5.3. __nss_lookup
The tail called __nss_lookup in last round is defined in nss/nsswitch.c:
/* -1 == not found
0 == function found
1 == finished */
int
__nss_lookup (service_user **ni, const char *fct_name, const char *fct2_name,
void **fctp)
{
// 1) Try to resolve in the current service
*fctp = __nss_lookup_function (*ni, fct_name);
if (*fctp == NULL && fct2_name != NULL)
*fctp = __nss_lookup_function (*ni, fct2_name);
// If still not found, consult policy for this service
while (*fctp == NULL
// `nss_next_action` reads (*ni)->actions[...], from `nsswitch.conf`
&& nss_next_action (*ni, NSS_STATUS_UNAVAIL) == NSS_ACTION_CONTINUE
&& (*ni)->next != NULL)
{
*ni = (*ni)->next; // advance to next service
// try resolve again in the new node
*fctp = __nss_lookup_function (*ni, fct_name);
if (*fctp == NULL && fct2_name != NULL)
*fctp = __nss_lookup_function (*ni, fct2_name);
}
// Return func ptr by `__libc_dlsym` via `__nss_lookup_function`
return *fctp != NULL ? 0 : (*ni)->next == NULL ? 1 : -1;
}
libc_hidden_def (__nss_lookup)Given a current NSS service node (service_user **ni) and one (or two) target symbol names (e.g., "getpwuid_r"), this function tries to resolve a function pointer in the current service's module. If not available, consult the policy (ni->actions[...]) for that service and possibly advance to the next service in the chain.
Inputs
service_user **ni: current node (service_user) in the per-DB chain (e.g.,files,compat,systemd, …).const char *fct_name: the symbol suffix to look up (e.g.,"getpwuid_r").const char *fct2_name: optional secondary name (oftenNULL; used by some lookups that have two acceptable symbol names).void **fctp: out-param for the resolved function pointer.- returns: a function pointer (or
NULLon failure).__nss_lookupuses this to decide whether to continue toni->next.
Side effects
- Updates
*nito the last service examined (head, middle, or tail). - For the current service, calls
__nss_lookup_functionto resolve_nss_<service>_<fct>.
Then we will enter its callee __nss_lookup_function, who triggers our exploit target nss_load_library.
7.5.4. __nss_lookup_function
The __nss_lookup_function function is called internally inside __nss_lookup. Given a single NSS service node (service_user *ni, e.g., for "files" or "systemd") and a function name (e.g., "getpwuid_r"), it:
void *
__nss_lookup_function (
service_user *ni, // e.g., "file", "compat", "systemd"
const char *fct_name // e.g., "getpwuid_r"
)
{
void **found, *result;
// 1) Acquires a global lock (NSS state is shared process-wide).
__libc_lock_lock (lock);
// 2) Looks up the function in a per-service cache (ni->known),
// implemented as a binary tree via tsearch(3)
found = __tsearch (&fct_name, &ni->known, &known_compare); // ni->known is a tsearch(3) tree keyed by function name
if (found == NULL)
result = NULL; // out-of-memory
else if (*found != &fct_name)
{
// Cache hit: node already exists; retrieve the stored function ptr
result = ((known_function *) *found)->fct_ptr;
#ifdef PTR_DEMANGLE
PTR_DEMANGLE (result);
#endif
}
else
{
// Cache miss: we just inserted a placeholder that points to &fct_name
known_function *known = malloc (sizeof *known);
if (! known)
{ // Could not allocate the cache node:
#if !defined DO_STATIC_NSS || defined SHARED
remove_from_tree:
#endif
// delete the placeholder entry
__tdelete (&fct_name, &ni->known, &known_compare);
free (known);
result = NULL;
}
else
{
// Install the real cache node
*found = known;
known->fct_name = fct_name;
#if !defined DO_STATIC_NSS || defined SHARED
// 3) Ensure a `service_library` exists and the module is loaded
// `nss_new_service()` is called inside `nss_load_library()` if needed
// lib_handle == NULL → attempt `dlopen("libnss_<name>.so.<rev>")`
// lib_handle == (void*)-1 → previous load failed; skip dlsym
if (nss_load_library (ni) != 0) // [!] Cound load external libraries
goto remove_from_tree; // out of memory
if (ni->library->lib_handle == (void *) -1l)
result = NULL; // Cached load failure: treat as “function not found”
else
{
// Build symbol: "_nss_<service>_<fct_name>"
size_t namlen = (5 + strlen (ni->name) + 1
+ strlen (fct_name) + 1);
char name[namlen];
/* Construct the function name. */
__stpcpy (__stpcpy (__stpcpy (__stpcpy (name, "_nss_"),
ni->name),
"_"),
fct_name);
// Resolve the backend entry in the loaded module
result = __libc_dlsym (ni->library->lib_handle, name);
}
#else
// 4) Static libc case: resolve from a built-in table instead of dlsym
...This is the entry point calling our final target nss_load_library().
7.5.5. nss_load_library
We have already discussed nss_load_library earlier as the critical attack entry point. Here we recap its workflow briefly, emphasizing why we care:
static int nss_load_library(service_user *ni) {
// 1) The entry point for `nss_new_service()`
if (ni->library == NULL) {
static name_database default_table;
// [!] `nss_new_service()`
// 2) Place zero out `ni->library->lib_handle`
ni->library = nss_new_service(service_table ?: &default_table, ni->name);
if (!ni->library) return -1; // library != 0, library->lib_handle == 0
}
// 3) After initializing `lib_handle = 0`:
// Lazy-load libnss_<name>.so.<rev> on first use
if (ni->library->lib_handle == NULL) {
int saved_errno = errno;
char shlib_name[/* 7 + |name| + 3 + |rev| + 1 */];
// "libnss_" + name + ".so" + __nss_shlib_revision
__stpcpy(__stpcpy(__stpcpy(__stpcpy(shlib_name,
"libnss_"),
ni->name), ".so"),
__nss_shlib_revision);
// 4) Load library
ni->library->lib_handle = __libc_dlopen(shlib_name);
if (!ni->library->lib_handle) {
ni->library->lib_handle = (void*)-1l; // Cache failure: sentinel prevents auto-retry
__set_errno(saved_errno);
}
...nss_load_library(ni) is invoked only on a cache miss for fct_name in ni->known (the tsearch placeholder path):
void *
__nss_lookup_function (service_user *ni, const char *fct_name)
├─ found = tsearch(&fct_name, &ni->known, known_compare)
├─ if (found == NULL) → OOM → return NULL
├─ if (*found != &fct_name) // CACHE HIT
│ → result = ((known_function*)*found)->fct_ptr (demangle) → return
└─ else // CACHE MISS: placeholder just inserted
known = malloc(...)
if (!known) { tdelete(...); return NULL; }
*found = known; known->fct_name = fct_name
if (nss_load_library(ni) != 0) { tdelete(...); return NULL; }
...If it's a cache hit, __nss_lookup_function returns the cached pointer and never calls the loader. So to force nss_load_library, For our intention he comes:
#Requirement 4:
Cache miss for the target symbol on this service:
ni->knownmust not already contain an entry forfct_name(e.g.,"getpwuid_r").
At this point we now have:
- #Requirement 1:
ni->library->lib_handle == 0 - #Requirement 2:
ni->library == NULL(forces new allocation) - #Requirement 3:
ni->namemust be replaced with a nonexistent service name - #Requirement 4: Ensure a cache miss so that
nss_load_library()executes thedlopen()path.
Together, these requirements form the precise preconditions for steering nss_load_library into loading an attacker-controlled shared object under root.
7.6. Target Structures
7.6.1. Overview
In the previous sections, we frequently mentioned several heap-resident structures (service_user, service_library, etc.). During the attack chain, these objects matter greatly: some are global roots, while others are heap-allocated nodes created and managed dynamically by glibc's allocator.
All are defined in nss/nsswitch.h:
typedef struct name_database
{
/* List of all known databases. */
name_database_entry *entry;
/* List of libraries with service implementation. */
service_library *library;
} name_database;
typedef struct name_database_entry
{
/* And the link to the next entry. */
struct name_database_entry *next;
/* List of service to be used. */
service_user *service;
/* Name of the database. */
char name[0];
} name_database_entry;
typedef struct service_user
{
/* And the link to the next entry. */
struct service_user *next;
/* Action according to result. */
lookup_actions actions[5];
/* Link to the underlying library object. */
service_library *library;
/* Collection of known functions. */
void *known;
/* Name of the service (`files', `dns', `nis', ...). */
char name[0];
} service_user;
typedef struct service_library
{
/* Name of service (`files', `dns', `nis', ...). */
const char *name;
/* Pointer to the loaded shared library. */
void *lib_handle;
/* And the link to the next entry. */
struct service_library *next;
} service_library;Their relationship can be illustrated as:

All of them are heap allocated objects. And we can identify three types of linked lists:
- Entry list (
name_database_entry) - Service-user list (
service_user) - Service-library list (
service_library)
Example overview with two databases:
(global, once per process)
service_table : name_database*
┌──────────────────────────────────────────────────────────┐
│ .entry ──► [name_database_entry "passwd"] ──► [...] │
│ .library ──► [service_library "files"] ──► ["dns"] ─► … │
└──────────────────────────────────────────────────────────┘
.entry ──► [name_database_entry "passwd"]
.next ──► [name_database_entry "group"] ──► …
.service──► SU("files") ──► SU("db") ──► …
│ │
│ └─ .library ─► SL("db") (shared)
│
├─ .actions[5] (policy)
├─ .known (tsearch cache; not a list)
└─ .library ───► SL("files") (shared)
.library ──► [service_library "files"] (dedup across all DBs)
.name = "files"
.lib_handle = NULL | handle | (void*)-1l
.next ──► [service_library "dns"] ──► …7.6.1. Global Root
As we can see, the name_database structure members are process-wide global objects:
/* The root of the whole data base. */
static name_database *service_table;
static name_database default_table;This is a static global. It is initialized once (per process) when __nss_database_lookup() first parses /etc/nsswitch.conf via nss_parse_file().
name_database itself owns two heads:
.entry→ the database list (passwd/group/hosts/…).library→ the global list ofservice_librarynodes (one per service name like"files","dns","db", …)
And its member—the service_library list is process-global (per name_database) as well, and each service_user->library points into that shared list.
The service_user->library is resolved by searching that shared global list. Here's the code path that binds a service_user to a service_library:
// nss_load_library(...)
if (ni->library == NULL) {
static name_database default_table;
ni->library = nss_new_service(service_table ?: &default_table, ni->name);
...If this service_user hasn't been bound yet (ni->library == NULL), glibc calls nss_new_service to searches/extends the service_library list hanging off the global name_database:
- If
service_tableexists (usual case), use it. - Else use a function-static
default_table(also one per process).
So either way, the list is shared process-wide. And insidenss_new_service, this global list got dedup + sharing:
static service_library *
nss_new_service (name_database *database, const char *name)
{
service_library **currentp = &database->library;
while (*currentp != NULL) {
if (strcmp ((*currentp)->name, name) == 0)
return *currentp; // ← return existing node
currentp = &(*currentp)->next;
}
// Not found: append a new node
*currentp = malloc(sizeof(service_library));
if (*currentp == NULL)
return NULL;
(*currentp)->name = name;
(*currentp)->lib_handle = NULL;
(*currentp)->next = NULL;
return *currentp;
}- It walks
database->library(the global list) and returns an existing node ifnamematches. - Only if not found does it append a new
service_libraryto that global list and return it. - Therefore, every
service_userwith the samename[]will get the sameservice_library(deduplicated by name).
7.6.2. Heap Objects
All these NSS chunks are heap-allocated objects in glibc's NSS implementation.
service_table is a process-wide global pointer variable:
/* The root of the whole data base. */
static name_database *service_table; // global (static storage), holds a pointerThat variable lives in static storage and is visible process-wide (inside libc). It points to a name_database object that is allocated on the heap the first time NSS is initialized via nss_parse_file():
name_database *result; // trampoline var
result = result = (name_database *) malloc (sizeof (name_database)); // heap allocation
service_table = result; // global pointer now points to itname_database_entry nodes (one per DB like passwd, hosts) are heap objects linked from service_table->entry, initialized via nss_getline():
name_database_entry *result;
len = strlen (name) + 1;
result = (name_database_entry *) malloc (sizeof (name_database_entry) + len); // heap allocation
// linked into service_table->entry listservice_user nodes (one per service token like files, dns) are heap objects linked from each entry's .service chain, allocated via nss_parse_service_list() :
new_service = (service_user *) malloc (sizeof (service_user)
+ (line - name + 1));
// linked under entry->serviceservice_library nodes (one per service name, deduped and shared) are also heap objects, linked from service_table->library and referenced by each service_user->library:
// in nss_new_service()
service_library *library = malloc(sizeof(service_library)); // heap allocation
// appended to database->library (i.e., service_table->library)All of these structures are heap objects allocated early in sudo's lifetime. This means that if we can maneuver them beneath our vulnerable chunk, the overflow primitive can poison their fields and bend NSS logic to our will.
7.7. Target Object
Now that we've mapped the structures we're after — the NSS chunks — the next question is: which one do we actually strike?
From our earlier backtrace, we know these chunks are set up right from the start, during get_user_info which invokes glibc's getpwuid for example:
main
└─ get_user_info (sudo.c: ~541)
└─ getpwuid(uid_t uid) // trampoline to glibc API
└─ __getpwuid_r(...)
└─ ...
└─ nss_load_library(...) // target finishes initializingBut the real prize comes later. The second invocation of nss_load_library (loop) is triggered when NSS resolves the initgroups entry point via glibc's getgrouplist:
main
└─ policy_check(...)
└─ sudoers_policy_check(...)
└─ sudoers_policy_main(...)
├─ set_cmnd(...) // heap overflow entry
│ ...
└─ sudoers_lookup(...)
└─ ...
└─ getgrouplist() // trampoline to glibc API
└─ nss_load_library(...) // load library from NSS chunks GDB confirms this stage skips the “passwd” DB and instead queries the "group" and "netgroup" databases:

So the takeaway is simple but crucial: we don't need to smash every NSS structure — we could precisely hijack the right service_user nodes (the ones for group or netgroup) with our overflow primitive from set_cmnd.
7.8. Challenges
Since our targets are heap objects (NSS chunks), exploiting them naturally comes down to heap overflow techniques.
The classic play is simple: place the target chunk directly below the overflowing vuln chunk, then blast through the boundary:

But in sudo, the allocation order is inverted:
target heap objects initialized ⟶ heap overflows vuln chunk ⟶ target objects later reusedWhich means the NSS chunks are allocated before the overflow entry point:

So we can't just “smash downward.” The vuln chunk lives after our targets, while the objects we want are sitting above it in memory.
That leaves us with two requirements to turn this into a workable exploit:
- Dissect NSS allocation
- Understand the order, exact sizes, and allocator bins used by
service_user,service_library, etc. - Map how they land in the heap arena during program startup.
- Understand the order, exact sizes, and allocator bins used by
- Shape the heap pre-overflow
- Identify heap allocations made before
get_user_info(). - Look for opportunities to
malloc+freechunks into the right bins, so we can later “recycle” those slots whensudosets up NSS structures. - This gives us control over where our vuln chunk lands, and whether the NSS targets can be maneuvered below it.
- Identify heap allocations made before
Only with this allocator choreography can we realistically overwrite the NSS chunks after the vuln is triggered.
Comments | 1 comment
what is the password for writeup